Skip to content

Commit 0c74a9c

Browse files
committed
Merge remote-tracking branch 'upstream/master'
2 parents 7411adf + 37628d9 commit 0c74a9c

16 files changed

+559
-15
lines changed
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
import logging
2+
from pathlib import Path
3+
4+
from jinja2 import Environment, FileSystemLoader
5+
6+
7+
def render_template(template_name, context):
8+
template_dir = Path(__file__).parent / "templates"
9+
env = Environment(
10+
loader=FileSystemLoader(str(template_dir)), trim_blocks=True, lstrip_blocks=True
11+
)
12+
template = env.get_template(template_name)
13+
try:
14+
return template.render(**context)
15+
except Exception as e:
16+
logging.warning(
17+
f"Failed to render template {template_name} for {context.get('name', 'unknown')}: {e}"
18+
)
19+
return ""
20+
21+
22+
def render_feature_view_code(context):
23+
return render_template("feature_view_template.jinja2", context)
24+
25+
26+
def render_entity_code(context):
27+
return render_template("entity_template.jinja2", context)
28+
29+
30+
def render_data_source_code(context):
31+
return render_template("data_source_template.jinja2", context)
32+
33+
34+
def render_feature_service_code(context):
35+
return render_template("feature_service_template.jinja2", context)
36+
37+
38+
def render_feature_code(context):
39+
return render_template("feature_template.jinja2", context)
40+
41+
42+
def render_saved_dataset_code(context):
43+
return render_template("saved_dataset_template.jinja2", context)
44+
45+
46+
def render_request_source_code(context):
47+
return render_template("request_source_template.jinja2", context)
48+
49+
50+
def render_push_source_code(context):
51+
return render_template("push_source_template.jinja2", context)

sdk/python/feast/api/registry/rest/data_sources.py

Lines changed: 52 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,11 @@
33

44
from fastapi import APIRouter, Depends, Query
55

6+
from feast.api.registry.rest.codegen_utils import (
7+
render_data_source_code,
8+
render_push_source_code,
9+
render_request_source_code,
10+
)
611
from feast.api.registry.rest.rest_utils import (
712
aggregate_across_projects,
813
create_grpc_pagination_params,
@@ -15,6 +20,8 @@
1520
parse_tags,
1621
)
1722
from feast.protos.feast.registry import RegistryServer_pb2
23+
from feast.type_map import _convert_value_type_str_to_value_type
24+
from feast.types import from_value_type
1825

1926
logger = logging.getLogger(__name__)
2027

@@ -44,7 +51,7 @@ def list_data_sources(
4451
data_sources = response.get("dataSources", [])
4552

4653
result = {
47-
"data_sources": data_sources,
54+
"dataSources": data_sources,
4855
"pagination": response.get("pagination", {}),
4956
}
5057

@@ -105,6 +112,49 @@ def get_data_source(
105112
)
106113
result["relationships"] = relationships
107114

108-
return result
115+
if result:
116+
spec = result.get("spec", result)
117+
name = spec.get("name", result.get("name", "default_source"))
118+
source_type = spec.get("type", "").upper()
119+
if source_type == "REQUEST_SOURCE":
120+
schema_fields = []
121+
feast_types = set()
122+
for field in spec.get("requestDataOptions", {}).get("schema", []):
123+
value_type_enum = _convert_value_type_str_to_value_type(
124+
field.get("valueType", "").upper()
125+
)
126+
feast_type = from_value_type(value_type_enum)
127+
dtype = getattr(feast_type, "__name__", str(feast_type))
128+
schema_fields.append(
129+
f' Field(name="{field["name"]}", dtype={dtype}),'
130+
)
131+
feast_types.add(dtype)
132+
context = dict(
133+
name=name,
134+
schema_lines=schema_fields,
135+
feast_types=list(feast_types),
136+
)
137+
result["featureDefinition"] = render_request_source_code(context)
138+
return result
139+
elif source_type == "PUSH_SOURCE" and "batchSource" in spec:
140+
batch_source_name = spec["batchSource"].get("name", "batch_source")
141+
context = dict(
142+
name=name,
143+
batch_source_name=batch_source_name,
144+
)
145+
result["featureDefinition"] = render_push_source_code(context)
146+
return result
147+
else:
148+
path = spec.get("path") or spec.get("fileOptions", {}).get("uri", "")
149+
timestamp_field = spec.get("timestampField", "event_timestamp")
150+
created_timestamp_column = spec.get("createdTimestampColumn", "")
151+
context = dict(
152+
name=name,
153+
path=path,
154+
timestamp_field=timestamp_field,
155+
created_timestamp_column=created_timestamp_column,
156+
)
157+
result["featureDefinition"] = render_data_source_code(context)
158+
return result
109159

110160
return router

sdk/python/feast/api/registry/rest/entities.py

Lines changed: 36 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
from fastapi import APIRouter, Depends, Query
44

5+
from feast.api.registry.rest.codegen_utils import render_entity_code
56
from feast.api.registry.rest.rest_utils import (
67
aggregate_across_projects,
78
create_grpc_pagination_params,
@@ -95,12 +96,44 @@ def get_entity(
9596

9697
result = entity
9798

99+
relationships = get_object_relationships(
100+
grpc_handler, "entity", name, project, allow_cache
101+
)
102+
ds_list_req = RegistryServer_pb2.ListDataSourcesRequest(
103+
project=project,
104+
allow_cache=allow_cache,
105+
)
106+
ds_list_resp = grpc_call(grpc_handler.ListDataSources, ds_list_req)
107+
ds_map = {ds["name"]: ds for ds in ds_list_resp.get("dataSources", [])}
108+
data_source_objs = []
109+
seen_ds_names = set()
110+
for rel in relationships:
111+
if rel.get("target", {}).get("type") == "dataSource":
112+
ds_name = rel["target"]["name"]
113+
if ds_name not in seen_ds_names:
114+
ds_obj = ds_map.get(ds_name)
115+
if ds_obj:
116+
data_source_objs.append(ds_obj)
117+
seen_ds_names.add(ds_name)
118+
result["dataSources"] = data_source_objs
119+
98120
if include_relationships:
99-
relationships = get_object_relationships(
100-
grpc_handler, "entity", name, project, allow_cache
101-
)
102121
result["relationships"] = relationships
103122

123+
if result:
124+
spec = result.get("spec", result)
125+
name = spec.get("name") or result.get("name") or "default_entity"
126+
join_keys = spec.get("joinKeys") or (
127+
[spec["joinKey"]] if "joinKey" in spec else []
128+
)
129+
130+
context = {
131+
"name": name,
132+
"join_keys": join_keys,
133+
"description": spec.get("description", ""),
134+
"tags": spec.get("tags", {}),
135+
}
136+
result["featureDefinition"] = render_entity_code(context)
104137
return result
105138

106139
return router

sdk/python/feast/api/registry/rest/feature_services.py

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
from fastapi import APIRouter, Depends, Query
44

5+
from feast.api.registry.rest.codegen_utils import render_feature_service_code
56
from feast.api.registry.rest.rest_utils import (
67
aggregate_across_projects,
78
create_grpc_pagination_params,
@@ -102,6 +103,42 @@ def get_feature_service(
102103
)
103104
result["relationships"] = relationships
104105

106+
if result:
107+
spec = result.get("spec", result)
108+
name = spec.get("name") or result.get("name") or "default_feature_service"
109+
projections = spec.get("features", [])
110+
if not isinstance(projections, list):
111+
projections = []
112+
113+
features_exprs = []
114+
for proj in projections:
115+
if isinstance(proj, dict):
116+
view_name = proj.get("name")
117+
feature_names = proj.get("features", [])
118+
else:
119+
view_name = str(proj)
120+
feature_names = []
121+
122+
if not view_name:
123+
continue
124+
125+
if feature_names:
126+
feature_list = ", ".join([repr(f) for f in feature_names])
127+
features_exprs.append(f"{view_name}[[{feature_list}]]")
128+
else:
129+
features_exprs.append(view_name)
130+
131+
features_str = ", ".join(features_exprs)
132+
133+
context = {
134+
"name": name,
135+
"features": features_str,
136+
"tags": spec.get("tags", {}),
137+
"description": spec.get("description", ""),
138+
"logging_config": spec.get("loggingConfig"),
139+
}
140+
141+
result["featureDefinition"] = render_feature_service_code(context)
105142
return result
106143

107144
return router

sdk/python/feast/api/registry/rest/feature_views.py

Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
from fastapi import APIRouter, Depends, Query
44

5+
from feast.api.registry.rest.codegen_utils import render_feature_view_code
56
from feast.api.registry.rest.rest_utils import (
67
create_grpc_pagination_params,
78
create_grpc_sorting_params,
@@ -14,6 +15,8 @@
1415
parse_tags,
1516
)
1617
from feast.registry_server import RegistryServer_pb2
18+
from feast.type_map import _convert_value_type_str_to_value_type
19+
from feast.types import from_value_type
1720

1821

1922
def _extract_feature_view_from_any(any_feature_view: dict) -> dict:
@@ -32,6 +35,20 @@ def _extract_feature_view_from_any(any_feature_view: dict) -> dict:
3235
return {}
3336

3437

38+
def extract_feast_types_from_fields(fields):
39+
types = set()
40+
for field in fields:
41+
value_type_enum = _convert_value_type_str_to_value_type(
42+
field.get("valueType", "").upper()
43+
)
44+
feast_type = from_value_type(value_type_enum)
45+
dtype = (
46+
feast_type.__name__ if hasattr(feast_type, "__name__") else str(feast_type)
47+
)
48+
types.add(dtype)
49+
return list(types)
50+
51+
3552
def get_feature_view_router(grpc_handler) -> APIRouter:
3653
router = APIRouter()
3754

@@ -116,6 +133,93 @@ def get_any_feature_view(
116133
)
117134
result["relationships"] = relationships
118135

136+
if result and "spec" in result:
137+
spec = result["spec"]
138+
fv_type = result.get("type", "featureView")
139+
features = spec.get("features", [])
140+
if not isinstance(features, list):
141+
features = []
142+
if fv_type == "onDemandFeatureView":
143+
class_name = "OnDemandFeatureView"
144+
sources = spec.get("sources", {})
145+
if sources:
146+
source_exprs = []
147+
for k, v in sources.items():
148+
var_name = v.get("name", k) if isinstance(v, dict) else str(v)
149+
source_exprs.append(f'"{k}": {var_name}')
150+
source_name = "{" + ", ".join(source_exprs) + "}"
151+
else:
152+
source_name = "source_feature_view"
153+
elif fv_type == "streamFeatureView":
154+
class_name = "StreamFeatureView"
155+
stream_source = spec.get("streamSource", {})
156+
source_name = stream_source.get("name", "stream_source")
157+
else:
158+
class_name = "FeatureView"
159+
source_views = spec.get("source_views") or spec.get("sourceViews")
160+
if source_views and isinstance(source_views, list):
161+
source_vars = [sv.get("name", "source_view") for sv in source_views]
162+
source_name = "[" + ", ".join(source_vars) + "]"
163+
else:
164+
source = spec.get("source")
165+
if isinstance(source, dict) and source.get("name"):
166+
source_name = source["name"]
167+
else:
168+
batch_source = spec.get("batchSource", {})
169+
source_name = batch_source.get("name", "driver_stats_source")
170+
171+
# Entities
172+
entities = spec.get("entities") or []
173+
entities_str = ", ".join(entities)
174+
175+
# Feature schema
176+
schema_lines = []
177+
for field in features:
178+
value_type_enum = _convert_value_type_str_to_value_type(
179+
field.get("valueType", "").upper()
180+
)
181+
feast_type = from_value_type(value_type_enum)
182+
dtype = getattr(feast_type, "__name__", str(feast_type))
183+
desc = field.get("description")
184+
desc_str = f', description="{desc}"' if desc else ""
185+
schema_lines.append(
186+
f' Field(name="{field["name"]}", dtype={dtype}{desc_str}),'
187+
)
188+
189+
# Feast types
190+
feast_types = extract_feast_types_from_fields(features)
191+
192+
# Tags
193+
tags = spec.get("tags", {})
194+
tags_str = f"tags={tags}," if tags else ""
195+
196+
# TTL
197+
ttl = spec.get("ttl")
198+
ttl_str = "timedelta(days=1)"
199+
if ttl:
200+
if isinstance(ttl, int):
201+
ttl_str = f"timedelta(seconds={ttl})"
202+
elif isinstance(ttl, str) and ttl.endswith("s") and ttl[:-1].isdigit():
203+
ttl_str = f"timedelta(seconds={int(ttl[:-1])})"
204+
205+
# Online
206+
online = spec.get("online", True)
207+
208+
# Build context
209+
context = dict(
210+
class_name=class_name,
211+
name=spec.get("name", "example"),
212+
entities_str=entities_str,
213+
ttl_str=ttl_str,
214+
schema_lines=schema_lines,
215+
online=online,
216+
source_name=source_name,
217+
tags_str=tags_str,
218+
feast_types=feast_types,
219+
)
220+
221+
result["featureDefinition"] = render_feature_view_code(context)
222+
119223
return result
120224

121225
@router.get("/feature_views")

0 commit comments

Comments
 (0)