diff --git a/sdk/python/feast/feature_service.py b/sdk/python/feast/feature_service.py index bfa48b3bf4..9490de38c9 100644 --- a/sdk/python/feast/feature_service.py +++ b/sdk/python/feast/feature_service.py @@ -39,6 +39,7 @@ class FeatureService: """ name: str + _features: List[Union[FeatureView, OnDemandFeatureView]] feature_view_projections: List[FeatureViewProjection] description: str tags: Dict[str, str] @@ -93,24 +94,36 @@ def __init__( _features = [] self.name = _name + self._features = _features self.feature_view_projections = [] + self.description = description + self.tags = tags or {} + self.owner = owner + self.created_timestamp = None + self.last_updated_timestamp = None + self.logging_config = logging_config + self.infer_features() - for feature_grouping in _features: + def infer_features(self, fvs_to_update: Optional[Dict[str, FeatureView]] = None): + self.feature_view_projections = [] + for feature_grouping in self._features: if isinstance(feature_grouping, BaseFeatureView): + # For feature services that depend on an unspecified feature view, apply inferred schema + if ( + fvs_to_update is not None + and len(feature_grouping.projection.features) == 0 + and feature_grouping.name in fvs_to_update + ): + feature_grouping.projection.features = fvs_to_update[ + feature_grouping.name + ].features self.feature_view_projections.append(feature_grouping.projection) else: raise ValueError( - f"The feature service {name} has been provided with an invalid type " + f"The feature service {self.name} has been provided with an invalid type " f'{type(feature_grouping)} as part of the "features" argument.)' ) - self.description = description - self.tags = tags or {} - self.owner = owner - self.created_timestamp = None - self.last_updated_timestamp = None - self.logging_config = logging_config - def __repr__(self): items = (f"{k} = {v}" for k, v in self.__dict__.items()) return f"<{self.__class__.__name__}({', '.join(items)})>" @@ -119,7 +132,7 @@ def __str__(self): return str(MessageToJson(self.to_proto())) def __hash__(self): - return hash((self.name)) + return hash(self.name) def __eq__(self, other): if not isinstance(other, FeatureService): diff --git a/sdk/python/feast/feature_store.py b/sdk/python/feast/feature_store.py index 4b015e8ab8..115f0fd971 100644 --- a/sdk/python/feast/feature_store.py +++ b/sdk/python/feast/feature_store.py @@ -475,8 +475,9 @@ def _make_inferences( entities_to_update: List[Entity], views_to_update: List[FeatureView], odfvs_to_update: List[OnDemandFeatureView], + feature_services_to_update: List[FeatureService], ): - """Makes inferences for entities, feature views, and odfvs.""" + """Makes inferences for entities, feature views, odfvs, and feature services.""" update_entities_with_inferred_types_from_feature_views( entities_to_update, views_to_update, self.config ) @@ -498,6 +499,10 @@ def _make_inferences( for odfv in odfvs_to_update: odfv.infer_features() + fvs_to_update_map = {view.name: view for view in views_to_update} + for feature_service in feature_services_to_update: + feature_service.infer_features(fvs_to_update=fvs_to_update_map) + @log_exceptions_and_usage def _plan( self, desired_repo_contents: RepoContents @@ -553,6 +558,7 @@ def _plan( desired_repo_contents.entities, desired_repo_contents.feature_views, desired_repo_contents.on_demand_feature_views, + desired_repo_contents.feature_services, ) # Compute the desired difference between the current objects in the registry and @@ -692,7 +698,11 @@ def apply( views_to_update, odfvs_to_update, request_views_to_update ) self._make_inferences( - data_sources_to_update, entities_to_update, views_to_update, odfvs_to_update + data_sources_to_update, + entities_to_update, + views_to_update, + odfvs_to_update, + services_to_update, ) # Handle all entityless feature views by using DUMMY_ENTITY as a placeholder entity. diff --git a/sdk/python/tests/integration/registration/test_inference.py b/sdk/python/tests/integration/registration/test_inference.py index 8b719eb733..b1fb509d75 100644 --- a/sdk/python/tests/integration/registration/test_inference.py +++ b/sdk/python/tests/integration/registration/test_inference.py @@ -7,6 +7,7 @@ BigQuerySource, Entity, Feature, + FeatureService, FileSource, RedshiftSource, RepoConfig, @@ -332,3 +333,25 @@ def test_update_feature_views_with_inferred_features(): ) assert len(feature_view_2.schema) == 1 assert len(feature_view_2.features) == 1 + + +def test_update_feature_services_with_inferred_features(simple_dataset_1): + with prep_file_source(df=simple_dataset_1, timestamp_field="ts_1") as file_source: + entity1 = Entity(name="test1", join_keys=["id_join_key"]) + feature_view_1 = FeatureView( + name="test1", entities=[entity1], source=file_source, + ) + feature_service = FeatureService(name="fs_1", features=[feature_view_1]) + assert len(feature_service.feature_view_projections) == 1 + assert len(feature_service.feature_view_projections[0].features) == 0 + + update_feature_views_with_inferred_features( + [feature_view_1], [entity1], RepoConfig(provider="local", project="test") + ) + feature_service.infer_features( + fvs_to_update={feature_view_1.name: feature_view_1} + ) + + assert len(feature_view_1.schema) == 3 + assert len(feature_view_1.features) == 3 + assert len(feature_service.feature_view_projections[0].features) == 3