Skip to content

Commit 7f35c1b

Browse files
zabarnZach Barnett
andauthored
fix: Add defaults to Optional FeatureViewProjectionModel fields (#337)
* fix: Add defaults to Optional FeatureViewProjectionModel fields * fix: make format-python * fix: operator test * revert get_project_metadata change * revert get_project_metadata change but where its called * fix: update tests --------- Co-authored-by: Zach Barnett <zbarnett@expediagroup.com>
1 parent c370981 commit 7f35c1b

7 files changed

Lines changed: 190 additions & 28 deletions

File tree

infra/feast-operator/config/crd/bases/feast.dev_featurestores.yaml

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1292,12 +1292,14 @@ spec:
12921292
enum:
12931293
- snowflake.online
12941294
- redis
1295+
- eg-valkey
12951296
- ikv
12961297
- datastore
12971298
- dynamodb
12981299
- bigtable
12991300
- postgres
13001301
- cassandra
1302+
- scylladb
13011303
- mysql
13021304
- hazelcast
13031305
- singlestore
@@ -1306,6 +1308,7 @@ spec:
13061308
- qdrant
13071309
- couchbase.online
13081310
- milvus
1311+
- eg-milvus
13091312
type: string
13101313
required:
13111314
- secretRef
@@ -1767,7 +1770,9 @@ spec:
17671770
want to use.
17681771
enum:
17691772
- sql
1773+
- sql-fallback
17701774
- snowflake.registry
1775+
- http
17711776
type: string
17721777
required:
17731778
- secretRef
@@ -5285,12 +5290,14 @@ spec:
52855290
enum:
52865291
- snowflake.online
52875292
- redis
5293+
- eg-valkey
52885294
- ikv
52895295
- datastore
52905296
- dynamodb
52915297
- bigtable
52925298
- postgres
52935299
- cassandra
5300+
- scylladb
52945301
- mysql
52955302
- hazelcast
52965303
- singlestore
@@ -5299,6 +5306,7 @@ spec:
52995306
- qdrant
53005307
- couchbase.online
53015308
- milvus
5309+
- eg-milvus
53025310
type: string
53035311
required:
53045312
- secretRef
@@ -5772,7 +5780,9 @@ spec:
57725780
you want to use.
57735781
enum:
57745782
- sql
5783+
- sql-fallback
57755784
- snowflake.registry
5785+
- http
57765786
type: string
57775787
required:
57785788
- secretRef

infra/feast-operator/dist/install.yaml

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1300,12 +1300,14 @@ spec:
13001300
enum:
13011301
- snowflake.online
13021302
- redis
1303+
- eg-valkey
13031304
- ikv
13041305
- datastore
13051306
- dynamodb
13061307
- bigtable
13071308
- postgres
13081309
- cassandra
1310+
- scylladb
13091311
- mysql
13101312
- hazelcast
13111313
- singlestore
@@ -1314,6 +1316,7 @@ spec:
13141316
- qdrant
13151317
- couchbase.online
13161318
- milvus
1319+
- eg-milvus
13171320
type: string
13181321
required:
13191322
- secretRef
@@ -1775,7 +1778,9 @@ spec:
17751778
want to use.
17761779
enum:
17771780
- sql
1781+
- sql-fallback
17781782
- snowflake.registry
1783+
- http
17791784
type: string
17801785
required:
17811786
- secretRef
@@ -5293,12 +5298,14 @@ spec:
52935298
enum:
52945299
- snowflake.online
52955300
- redis
5301+
- eg-valkey
52965302
- ikv
52975303
- datastore
52985304
- dynamodb
52995305
- bigtable
53005306
- postgres
53015307
- cassandra
5308+
- scylladb
53025309
- mysql
53035310
- hazelcast
53045311
- singlestore
@@ -5307,6 +5314,7 @@ spec:
53075314
- qdrant
53085315
- couchbase.online
53095316
- milvus
5317+
- eg-milvus
53105318
type: string
53115319
required:
53125320
- secretRef
@@ -5780,7 +5788,9 @@ spec:
57805788
you want to use.
57815789
enum:
57825790
- sql
5791+
- sql-fallback
57835792
- snowflake.registry
5793+
- http
57845794
type: string
57855795
required:
57865796
- secretRef

sdk/python/feast/expediagroup/pydantic_models/feature_view_model.py

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -210,14 +210,14 @@ class FeatureViewProjectionModel(BaseModel):
210210
"""
211211

212212
name: str
213-
name_alias: Optional[str]
213+
name_alias: Optional[str] = None
214214
desired_features: List[str]
215215
features: List[FieldModel]
216216
join_key_map: Dict[str, str]
217-
timestamp_field: Optional[str]
218-
date_partition_column: Optional[str]
219-
created_timestamp_column: Optional[str]
220-
batch_source: Optional[AnyBatchDataSource]
217+
timestamp_field: Optional[str] = None
218+
date_partition_column: Optional[str] = None
219+
created_timestamp_column: Optional[str] = None
220+
batch_source: Optional[AnyBatchDataSource] = None
221221

222222
def to_feature_view_projection(self) -> FeatureViewProjection:
223223
return FeatureViewProjection(

sdk/python/feast/infra/registry/http.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -959,6 +959,7 @@ def list_project_metadata( # type: ignore[return]
959959
try:
960960
url = f"{self.base_url}/projects/{project}"
961961
response_data = self._send_request("GET", url)
962+
logger.info(f"ProjectMetadata response data: {response_data}")
962963
return [
963964
ProjectMetadataModel.model_validate(response_data).to_project_metadata()
964965
]

sdk/python/feast/infra/registry/sql.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -444,7 +444,7 @@ def apply_project_metadata(
444444
self, project: str, commit: bool
445445
) -> ProjectMetadataModel:
446446
self._maybe_init_project_metadata(project)
447-
return self._get_project_metadata_model(project)
447+
return self.get_project_metadata_model(project)
448448

449449
def _get_entity(self, name: str, project: str) -> Entity:
450450
return self._get_object(
@@ -1531,12 +1531,13 @@ def get_project_metadata(self, project: str, key: str) -> Optional[str]:
15311531
return row._mapping["metadata_value"]
15321532
return None
15331533

1534-
def _get_project_metadata_model(
1534+
def get_project_metadata_model(
15351535
self,
15361536
project: str,
15371537
allow_cache: bool = False,
15381538
) -> ProjectMetadataModel:
15391539
"""
1540+
Expedia specific function used in eg-feature-store-registry to get project metadata model.
15401541
Returns given project metdata. No supporting function in SQL Registry so implemented this here rather than using _get_last_updated_metadata and list_project_metadata.
15411542
"""
15421543

sdk/python/tests/unit/infra/registry/test_sql_registry.py

Lines changed: 62 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -35,24 +35,65 @@ def sqlite_registry():
3535
registry.teardown()
3636

3737

38-
def test_sql_registry(sqlite_registry):
39-
"""
40-
Test the SQL registry
41-
"""
42-
entity = Entity(
43-
name="test_entity",
44-
description="Test entity for testing",
45-
tags={"test": "transaction"},
46-
)
47-
sqlite_registry.apply_entity(entity, "test_project")
48-
retrieved_entity = sqlite_registry.get_entity("test_entity", "test_project")
49-
assert retrieved_entity.name == "test_entity"
50-
assert retrieved_entity.description == "Test entity for testing"
51-
52-
sqlite_registry.set_project_metadata("test_project", "test_key", "test_value")
53-
value = sqlite_registry.get_project_metadata("test_project", "test_key")
54-
assert value == "test_value"
55-
56-
sqlite_registry.delete_entity("test_entity", "test_project")
57-
with pytest.raises(Exception):
58-
sqlite_registry.get_entity("test_entity", "test_project")
38+
class TestSqlRegistry:
39+
"""Test class for SqlRegistry"""
40+
41+
def test_apply_and_retrieve_entity(self, sqlite_registry):
42+
"""Test applying and retrieving an entity from the SQL registry."""
43+
entity = Entity(
44+
name="test_entity",
45+
description="Test entity for testing",
46+
tags={"test": "transaction"},
47+
)
48+
sqlite_registry.apply_entity(entity, "test_project")
49+
50+
retrieved_entity = sqlite_registry.get_entity("test_entity", "test_project")
51+
assert retrieved_entity.name == "test_entity"
52+
assert retrieved_entity.description == "Test entity for testing"
53+
54+
def test_delete_entity(self, sqlite_registry):
55+
"""Test deleting an entity from the SQL registry."""
56+
entity = Entity(name="test_entity", description="Test entity")
57+
sqlite_registry.apply_entity(entity, "test_project")
58+
59+
sqlite_registry.delete_entity("test_entity", "test_project")
60+
61+
with pytest.raises(Exception):
62+
sqlite_registry.get_entity("test_entity", "test_project")
63+
64+
def test_get_project_metadata_model_returns_initialized_metadata(
65+
self, sqlite_registry
66+
):
67+
"""Test that get_project_metadata_model returns metadata after applying an entity."""
68+
entity = Entity(name="test_entity", description="Test entity")
69+
sqlite_registry.apply_entity(entity, "test_project")
70+
71+
project_metadata = sqlite_registry.get_project_metadata_model("test_project")
72+
73+
assert project_metadata.project_name == "test_project"
74+
assert project_metadata.project_uuid is not None
75+
assert project_metadata.last_updated_timestamp is not None
76+
77+
def test_get_project_metadata_model_nonexistent_project(self, sqlite_registry):
78+
"""Test that get_project_metadata_model handles non-existent projects gracefully."""
79+
project_metadata = sqlite_registry.get_project_metadata_model(
80+
"nonexistent_project"
81+
)
82+
83+
assert project_metadata.project_name == "nonexistent_project"
84+
assert project_metadata is not None
85+
86+
def test_get_all_project_metadata_multiple_projects(self, sqlite_registry):
87+
"""Test that get_all_project_metadata returns metadata for all projects."""
88+
entity1 = Entity(name="entity1", description="Entity 1")
89+
entity2 = Entity(name="entity2", description="Entity 2")
90+
sqlite_registry.apply_entity(entity1, "project_1")
91+
sqlite_registry.apply_entity(entity2, "project_2")
92+
93+
all_metadata = sqlite_registry.get_all_project_metadata()
94+
95+
project_names = [m.project_name for m in all_metadata]
96+
assert "project_1" in project_names
97+
assert "project_2" in project_names
98+
for metadata in all_metadata:
99+
assert metadata.project_uuid is not None

sdk/python/tests/unit/test_pydantic_models.py

Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -581,6 +581,44 @@ def test_idempotent_feature_view_projection_conversion():
581581
assert pydantic_obj == FeatureViewProjectionModel.model_validate(pydantic_json)
582582

583583

584+
def test_feature_view_projection_backwards_compatibility():
585+
# Test deserialization with minimal fields (missing new optional fields)
586+
# https://github.com/ExpediaGroup/feast/pull/295/files#diff-5ad1ae3dd32afd2194e090c33d3661dcc68de8a49b1358f2a7c2796394e1f2fc
587+
minimal_dict = {
588+
"name": "test_projection",
589+
"desired_features": [],
590+
"features": [],
591+
"join_key_map": {},
592+
}
593+
pydantic_obj = FeatureViewProjectionModel.model_validate(minimal_dict)
594+
assert pydantic_obj.name == "test_projection"
595+
assert pydantic_obj.name_alias is None
596+
assert pydantic_obj.desired_features == []
597+
assert pydantic_obj.features == []
598+
assert pydantic_obj.join_key_map == {}
599+
assert pydantic_obj.timestamp_field is None
600+
assert pydantic_obj.date_partition_column is None
601+
assert pydantic_obj.created_timestamp_column is None
602+
assert pydantic_obj.batch_source is None
603+
604+
minimal_json = '{"name": "test_projection_json", "desired_features": [], "features": [], "join_key_map": {}}'
605+
pydantic_obj_from_json = FeatureViewProjectionModel.model_validate_json(
606+
minimal_json
607+
)
608+
assert pydantic_obj_from_json.name == "test_projection_json"
609+
assert pydantic_obj_from_json.timestamp_field is None
610+
assert pydantic_obj_from_json.date_partition_column is None
611+
assert pydantic_obj_from_json.created_timestamp_column is None
612+
assert pydantic_obj_from_json.batch_source is None
613+
614+
converted_python_obj = pydantic_obj.to_feature_view_projection()
615+
assert converted_python_obj.name == "test_projection"
616+
assert converted_python_obj.timestamp_field is None
617+
assert converted_python_obj.date_partition_column is None
618+
assert converted_python_obj.created_timestamp_column is None
619+
assert converted_python_obj.batch_source is None
620+
621+
584622
def test_idempotent_on_demand_feature_view_conversion():
585623
tags = {
586624
"tag1": "val1",
@@ -831,6 +869,67 @@ def test_idempotent_feature_service_conversion():
831869
assert pydantic_obj == FeatureServiceModel.model_validate(pydantic_json)
832870

833871

872+
def test_feature_service_backwards_compatibility():
873+
# Test deserialization with feature_view_projections missing optional fields
874+
# https://github.com/ExpediaGroup/feast/pull/295/files#diff-5ad1ae3dd32afd2194e090c33d3661dcc68de8a49b1358f2a7c2796394e1f2fc
875+
field1 = Field(name="feature1", dtype=Float32)
876+
field2 = Field(name="feature2", dtype=String)
877+
field1_model = FieldModel.from_field(field1)
878+
field2_model = FieldModel.from_field(field2)
879+
880+
minimal_dict = {
881+
"name": "test_feature_service",
882+
"features": [],
883+
"feature_view_projections": [
884+
{
885+
"name": "projection1",
886+
"desired_features": ["feature1", "feature2"],
887+
"features": [
888+
field1_model.model_dump(),
889+
field2_model.model_dump(),
890+
],
891+
"join_key_map": {"entity_id": "entity_id"},
892+
},
893+
{
894+
"name": "projection2",
895+
"desired_features": [],
896+
"features": [],
897+
"join_key_map": {},
898+
},
899+
],
900+
"description": "Test feature service",
901+
"tags": {"env": "test"},
902+
"owner": "test@example.com",
903+
"created_timestamp": None,
904+
"last_updated_timestamp": None,
905+
}
906+
pydantic_obj = FeatureServiceModel.model_validate(minimal_dict)
907+
assert pydantic_obj.name == "test_feature_service"
908+
assert len(pydantic_obj.feature_view_projections) == 2
909+
910+
proj1 = pydantic_obj.feature_view_projections[0]
911+
assert proj1.name == "projection1"
912+
assert proj1.timestamp_field is None
913+
assert proj1.date_partition_column is None
914+
assert proj1.created_timestamp_column is None
915+
assert proj1.batch_source is None
916+
assert len(proj1.features) == 2
917+
918+
proj2 = pydantic_obj.feature_view_projections[1]
919+
assert proj2.name == "projection2"
920+
assert proj2.timestamp_field is None
921+
assert proj2.date_partition_column is None
922+
assert proj2.created_timestamp_column is None
923+
assert proj2.batch_source is None
924+
925+
pydantic_json = pydantic_obj.model_dump_json()
926+
pydantic_obj_from_json = FeatureServiceModel.model_validate_json(pydantic_json)
927+
assert pydantic_obj_from_json.name == "test_feature_service"
928+
assert len(pydantic_obj_from_json.feature_view_projections) == 2
929+
assert pydantic_obj_from_json.feature_view_projections[0].timestamp_field is None
930+
assert pydantic_obj_from_json.feature_view_projections[1].batch_source is None
931+
932+
834933
def test_idempotent_project_metadata_conversion():
835934
python_obj = ProjectMetadata(
836935
project_name="test_project",

0 commit comments

Comments
 (0)