Skip to content

Commit 5d0f7d6

Browse files
committed
fix: Fixed data source creation form gaps
Signed-off-by: ntkathole <[email protected]>
1 parent 2b41ded commit 5d0f7d6

14 files changed

Lines changed: 1467 additions & 54 deletions

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

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,8 @@
3131

3232
class FileOptionsModel(BaseModel):
3333
uri: str = ""
34+
file_format: Optional[str] = "parquet"
35+
s3_endpoint_override: Optional[str] = ""
3436

3537

3638
class BigQueryOptionsModel(BaseModel):
@@ -42,22 +44,59 @@ class SnowflakeOptionsModel(BaseModel):
4244
table: str = ""
4345
database: str = ""
4446
schema_: str = ""
47+
query: Optional[str] = ""
48+
warehouse: Optional[str] = ""
4549

4650

4751
class RedshiftOptionsModel(BaseModel):
4852
table: str = ""
4953
database: str = ""
5054
schema_: str = ""
55+
query: Optional[str] = ""
5156

5257

5358
class KafkaOptionsModel(BaseModel):
5459
kafka_bootstrap_servers: str = ""
5560
topic: str = ""
61+
message_format: Optional[str] = "json"
62+
watermark_delay_threshold: Optional[str] = ""
5663

5764

5865
class SparkOptionsModel(BaseModel):
5966
table: str = ""
6067
path: str = ""
68+
query: Optional[str] = ""
69+
file_format: Optional[str] = ""
70+
table_format: Optional[str] = ""
71+
table_format_catalog: Optional[str] = ""
72+
table_format_namespace: Optional[str] = ""
73+
table_format_properties: Optional[str] = ""
74+
date_partition_column: Optional[str] = ""
75+
date_partition_column_format: Optional[str] = ""
76+
77+
78+
class KinesisOptionsModel(BaseModel):
79+
region: str = ""
80+
stream_name: str = ""
81+
record_format: Optional[str] = "json"
82+
83+
84+
class TrinoOptionsModel(BaseModel):
85+
table: str = ""
86+
query: str = ""
87+
88+
89+
class AthenaOptionsModel(BaseModel):
90+
table: str = ""
91+
query: str = ""
92+
database: str = ""
93+
data_source: Optional[str] = ""
94+
95+
96+
class CustomOptionsModel(BaseModel):
97+
configuration: Optional[str] = ""
98+
class_name: Optional[str] = ""
99+
config: Optional[str] = ""
61100

62101

63102
class ApplyDataSourceRequestBody(BaseModel):
@@ -66,6 +105,7 @@ class ApplyDataSourceRequestBody(BaseModel):
66105
type: Optional[int] = None
67106
timestamp_field: Optional[str] = ""
68107
created_timestamp_column: Optional[str] = ""
108+
date_partition_column: Optional[str] = ""
69109
description: Optional[str] = ""
70110
tags: Optional[Dict[str, str]] = {}
71111
owner: Optional[str] = ""
@@ -75,6 +115,11 @@ class ApplyDataSourceRequestBody(BaseModel):
75115
redshift_options: Optional[RedshiftOptionsModel] = None
76116
kafka_options: Optional[KafkaOptionsModel] = None
77117
spark_options: Optional[SparkOptionsModel] = None
118+
trino_options: Optional[TrinoOptionsModel] = None
119+
athena_options: Optional[AthenaOptionsModel] = None
120+
kinesis_options: Optional[KinesisOptionsModel] = None
121+
custom_options: Optional[CustomOptionsModel] = None
122+
data_source_class_type: Optional[str] = None
78123

79124

80125
def get_data_source_router(grpc_handler) -> APIRouter:
@@ -221,19 +266,30 @@ def apply_data_source(body: ApplyDataSourceRequestBody):
221266
if body.type is not None:
222267
ds_proto.type = body.type # type: ignore[assignment]
223268

269+
if body.date_partition_column:
270+
ds_proto.date_partition_column = body.date_partition_column
271+
224272
if body.file_options:
225273
ds_proto.file_options.uri = body.file_options.uri
274+
if body.file_options.s3_endpoint_override:
275+
ds_proto.file_options.s3_endpoint_override = (
276+
body.file_options.s3_endpoint_override
277+
)
226278
elif body.bigquery_options:
227279
ds_proto.bigquery_options.table = body.bigquery_options.table
228280
ds_proto.bigquery_options.query = body.bigquery_options.query
229281
elif body.snowflake_options:
230282
ds_proto.snowflake_options.table = body.snowflake_options.table
231283
ds_proto.snowflake_options.database = body.snowflake_options.database
232284
ds_proto.snowflake_options.schema = body.snowflake_options.schema_
285+
if body.snowflake_options.query:
286+
ds_proto.snowflake_options.query = body.snowflake_options.query
233287
elif body.redshift_options:
234288
ds_proto.redshift_options.table = body.redshift_options.table
235289
ds_proto.redshift_options.database = body.redshift_options.database
236290
ds_proto.redshift_options.schema = body.redshift_options.schema_
291+
if body.redshift_options.query:
292+
ds_proto.redshift_options.query = body.redshift_options.query
237293
elif body.kafka_options:
238294
ds_proto.kafka_options.kafka_bootstrap_servers = (
239295
body.kafka_options.kafka_bootstrap_servers
@@ -242,6 +298,34 @@ def apply_data_source(body: ApplyDataSourceRequestBody):
242298
elif body.spark_options:
243299
ds_proto.spark_options.table = body.spark_options.table
244300
ds_proto.spark_options.path = body.spark_options.path
301+
if body.spark_options.query:
302+
ds_proto.spark_options.query = body.spark_options.query
303+
if body.spark_options.file_format:
304+
ds_proto.spark_options.file_format = body.spark_options.file_format
305+
if body.spark_options.date_partition_column_format:
306+
ds_proto.spark_options.date_partition_column_format = (
307+
body.spark_options.date_partition_column_format
308+
)
309+
elif body.trino_options:
310+
ds_proto.trino_options.table = body.trino_options.table
311+
ds_proto.trino_options.query = body.trino_options.query
312+
elif body.athena_options:
313+
ds_proto.athena_options.table = body.athena_options.table
314+
ds_proto.athena_options.query = body.athena_options.query
315+
ds_proto.athena_options.database = body.athena_options.database
316+
if body.athena_options.data_source:
317+
ds_proto.athena_options.data_source = body.athena_options.data_source
318+
elif body.kinesis_options:
319+
ds_proto.kinesis_options.region = body.kinesis_options.region
320+
ds_proto.kinesis_options.stream_name = body.kinesis_options.stream_name
321+
elif body.custom_options:
322+
if body.custom_options.configuration:
323+
ds_proto.custom_options.configuration = (
324+
body.custom_options.configuration.encode("utf-8")
325+
)
326+
327+
if body.data_source_class_type:
328+
ds_proto.data_source_class_type = body.data_source_class_type
245329

246330
req = RegistryServer_pb2.ApplyDataSourceRequest(
247331
data_source=ds_proto,

‎sdk/python/feast/api/registry/rest/saved_datasets.py‎

Lines changed: 117 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import json
12
import logging
23
from typing import Dict, List, Optional
34

@@ -32,13 +33,58 @@
3233
logger = logging.getLogger(__name__)
3334

3435

36+
def _build_feature_view_to_datasource_map(
37+
grpc_handler, project: str, allow_cache: bool = True
38+
) -> Dict[str, str]:
39+
"""Build a map of feature_view_name → data_source_name for a project."""
40+
try:
41+
fv_req = RegistryServer_pb2.ListAllFeatureViewsRequest(
42+
project=project, allow_cache=allow_cache
43+
)
44+
fv_response = grpc_call(grpc_handler.ListAllFeatureViews, fv_req)
45+
feature_views = fv_response.get("featureViews", [])
46+
except Exception:
47+
return {}
48+
49+
fv_ds_map: Dict[str, str] = {}
50+
for any_fv in feature_views:
51+
# AnyFeatureView wraps under "featureView", "onDemandFeatureView", etc.
52+
fv_data = any_fv.get("featureView") or any_fv.get("streamFeatureView") or {}
53+
spec = fv_data.get("spec", {})
54+
fv_name = spec.get("name", "")
55+
if not fv_name:
56+
continue
57+
batch_source = spec.get("batchSource", {})
58+
ds_name = batch_source.get("name", "") if batch_source else ""
59+
if ds_name:
60+
fv_ds_map[fv_name] = ds_name
61+
return fv_ds_map
62+
63+
64+
def _get_dataset_data_sources(dataset: dict, fv_ds_map: Dict[str, str]) -> List[str]:
65+
"""Get data source names for a dataset via its features' feature view lineage."""
66+
spec = dataset.get("spec", dataset)
67+
features = spec.get("features", [])
68+
data_sources: List[str] = []
69+
seen: set = set()
70+
for feature_ref in features:
71+
if isinstance(feature_ref, str) and ":" in feature_ref:
72+
fv_name = feature_ref.split(":")[0]
73+
ds_name = fv_ds_map.get(fv_name)
74+
if ds_name and ds_name not in seen:
75+
seen.add(ds_name)
76+
data_sources.append(ds_name)
77+
return data_sources
78+
79+
3580
class RegisterDatasetRequest(BaseModel):
3681
name: str
3782
project: str
3883
features: List[str] = []
3984
join_keys: List[str] = []
4085
storage_path: Optional[str] = None
4186
storage_type: str = "file"
87+
storage_file_format: Optional[str] = "parquet"
4288
tags: Dict[str, str] = {}
4389
full_feature_names: bool = False
4490
feature_service_name: Optional[str] = None
@@ -64,6 +110,7 @@ class CreateDatasetRequest(BaseModel):
64110
# Storage
65111
storage_type: str = "file"
66112
storage_path: str = ""
113+
storage_file_format: Optional[str] = "parquet"
67114
tags: Dict[str, str] = {}
68115
allow_overwrite: bool = False
69116

@@ -94,7 +141,7 @@ def list_saved_datasets_all(
94141
if collection is not None:
95142
extra_params["collection"] = collection
96143

97-
return aggregate_across_projects(
144+
result = aggregate_across_projects(
98145
grpc_handler=grpc_handler,
99146
list_method=grpc_handler.ListSavedDatasets,
100147
request_cls=RegistryServer_pb2.ListSavedDatasetsRequest,
@@ -109,6 +156,21 @@ def list_saved_datasets_all(
109156
extra_request_params=extra_params or None,
110157
)
111158

159+
saved_datasets = result.get("savedDatasets", [])
160+
projects_seen: Dict[str, Dict[str, str]] = {}
161+
for sd in saved_datasets:
162+
proj = sd.get("project", "")
163+
if proj not in projects_seen:
164+
projects_seen[proj] = _build_feature_view_to_datasource_map(
165+
grpc_handler, proj, allow_cache
166+
)
167+
ds_names = _get_dataset_data_sources(sd, projects_seen[proj])
168+
if ds_names:
169+
spec = sd.get("spec", sd)
170+
spec["dataSources"] = ds_names
171+
172+
return result
173+
112174
@router.get("/saved_datasets/data/{name}")
113175
def get_dataset_data(
114176
name: str,
@@ -209,6 +271,14 @@ def get_saved_dataset(
209271

210272
result = saved_dataset
211273

274+
fv_ds_map = _build_feature_view_to_datasource_map(
275+
grpc_handler, project, allow_cache
276+
)
277+
ds_names = _get_dataset_data_sources(result, fv_ds_map)
278+
if ds_names:
279+
spec = result.get("spec", result)
280+
spec["dataSources"] = ds_names
281+
212282
if include_relationships:
213283
relationships = get_object_relationships(
214284
grpc_handler, "savedDataset", name, project, allow_cache
@@ -270,6 +340,15 @@ def list_saved_datasets(
270340
response = grpc_call(grpc_handler.ListSavedDatasets, req)
271341
saved_datasets = response.get("savedDatasets", [])
272342

343+
fv_ds_map = _build_feature_view_to_datasource_map(
344+
grpc_handler, project, allow_cache
345+
)
346+
for sd in saved_datasets:
347+
ds_names = _get_dataset_data_sources(sd, fv_ds_map)
348+
if ds_names:
349+
spec = sd.get("spec", sd)
350+
spec["dataSources"] = ds_names
351+
273352
result = {
274353
"savedDatasets": saved_datasets,
275354
"pagination": response.get("pagination", {}),
@@ -332,12 +411,48 @@ def register_saved_dataset(payload: RegisterDatasetRequest = Body(...)):
332411
)
333412
elif payload.storage_type == "spark":
334413
storage_proto.spark_storage.CopyFrom(
335-
DataSourceProto.SparkOptions(path=path)
414+
DataSourceProto.SparkOptions(
415+
path=path,
416+
file_format=payload.storage_file_format or "parquet",
417+
)
336418
)
337419
elif payload.storage_type == "athena":
338420
storage_proto.athena_storage.CopyFrom(
339421
DataSourceProto.AthenaOptions(table=path)
340422
)
423+
elif payload.storage_type == "postgres":
424+
config = json.dumps({"name": None, "query": None, "table": path})
425+
storage_proto.custom_storage.CopyFrom(
426+
DataSourceProto.CustomSourceOptions(
427+
configuration=config.encode("utf-8")
428+
)
429+
)
430+
elif payload.storage_type == "clickhouse":
431+
config = json.dumps({"name": None, "query": None, "table": path})
432+
storage_proto.custom_storage.CopyFrom(
433+
DataSourceProto.CustomSourceOptions(
434+
configuration=config.encode("utf-8")
435+
)
436+
)
437+
elif payload.storage_type == "couchbase":
438+
parts = path.split(".", 2)
439+
database = parts[0] if len(parts) > 0 else ""
440+
scope = parts[1] if len(parts) > 1 else ""
441+
collection = parts[2] if len(parts) > 2 else ""
442+
config = json.dumps(
443+
{
444+
"name": None,
445+
"query": None,
446+
"database": database,
447+
"scope": scope,
448+
"collection": collection,
449+
}
450+
)
451+
storage_proto.custom_storage.CopyFrom(
452+
DataSourceProto.CustomSourceOptions(
453+
configuration=config.encode("utf-8")
454+
)
455+
)
341456
elif payload.storage_type == "custom":
342457
storage_proto.custom_storage.CopyFrom(
343458
DataSourceProto.CustomSourceOptions(configuration=path.encode("utf-8"))

0 commit comments

Comments
 (0)