From ee8a7d0ad58ec4c646276534a0c6528a964af611 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 23 Jan 2026 06:34:38 +0000 Subject: [PATCH 1/8] Initial plan From 6b2bf7fce4f504a9a5f2ebc179b53b03332a122f Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 23 Jan 2026 06:39:21 +0000 Subject: [PATCH 2/8] Add skip_validation parameter to push method and related methods Co-authored-by: franciscojavierarceo <4163062+franciscojavierarceo@users.noreply.github.com> --- sdk/python/feast/feature_store.py | 34 +++++++++++++++---- .../infra/registry/proto_registry_utils.py | 14 +++++--- sdk/python/feast/infra/registry/registry.py | 3 +- sdk/python/feast/on_demand_feature_view.py | 15 ++++---- .../transformation/pandas_transformation.py | 12 ++++++- .../transformation/python_transformation.py | 12 ++++++- 6 files changed, 70 insertions(+), 20 deletions(-) diff --git a/sdk/python/feast/feature_store.py b/sdk/python/feast/feature_store.py index fc4517281d3..d63cd70a19f 100644 --- a/sdk/python/feast/feature_store.py +++ b/sdk/python/feast/feature_store.py @@ -267,11 +267,14 @@ def list_feature_services( return self._registry.list_feature_services(self.project, tags=tags) def _list_all_feature_views( - self, allow_cache: bool = False, tags: Optional[dict[str, str]] = None + self, + allow_cache: bool = False, + tags: Optional[dict[str, str]] = None, + skip_validation: bool = False, ) -> List[BaseFeatureView]: feature_views = [] for fv in self.registry.list_all_feature_views( - self.project, allow_cache=allow_cache, tags=tags + self.project, allow_cache=allow_cache, tags=tags, skip_udf=skip_validation ): if ( isinstance(fv, FeatureView) @@ -284,18 +287,23 @@ def _list_all_feature_views( return feature_views def list_all_feature_views( - self, allow_cache: bool = False, tags: Optional[dict[str, str]] = None + self, + allow_cache: bool = False, + tags: Optional[dict[str, str]] = None, + skip_validation: bool = False, ) -> List[BaseFeatureView]: """ Retrieves the list of feature views from the registry. Args: allow_cache: Whether to allow returning entities from a cached registry. + tags: Filter by tags. + skip_validation: Whether to skip validation of feature views (e.g., UDF deserialization). Returns: A list of feature views. """ - return self._list_all_feature_views(allow_cache, tags=tags) + return self._list_all_feature_views(allow_cache, tags=tags, skip_validation=skip_validation) def list_feature_views( self, allow_cache: bool = False, tags: Optional[dict[str, str]] = None @@ -1729,6 +1737,7 @@ def push( allow_registry_cache: bool = True, to: PushMode = PushMode.ONLINE, transform_on_write: bool = True, + skip_validation: bool = False, ): """ Push features to a push source. This updates all the feature views that have the push source as stream source. @@ -1739,6 +1748,7 @@ def push( allow_registry_cache: Whether to allow cached versions of the registry. to: Whether to push to online or offline store. Defaults to online store only. transform_on_write: Whether to transform the data before pushing. + skip_validation: Whether to skip validation of feature views (e.g., UDF deserialization). """ for fv in self._fvs_for_push_source_or_raise( push_source_name, allow_registry_cache @@ -1749,6 +1759,7 @@ def push( df, allow_registry_cache=allow_registry_cache, transform_on_write=transform_on_write, + skip_validation=skip_validation, ) if to == PushMode.OFFLINE or to == PushMode.ONLINE_AND_OFFLINE: self.write_to_offline_store( @@ -1761,6 +1772,7 @@ async def push_async( df: pd.DataFrame, allow_registry_cache: bool = True, to: PushMode = PushMode.ONLINE, + skip_validation: bool = False, **kwargs, ): fvs = self._fvs_for_push_source_or_raise(push_source_name, allow_registry_cache) @@ -1769,7 +1781,10 @@ async def push_async( _ = await asyncio.gather( *[ self.write_to_online_store_async( - fv.name, df, allow_registry_cache=allow_registry_cache + fv.name, + df, + allow_registry_cache=allow_registry_cache, + skip_validation=skip_validation, ) for fv in fvs ] @@ -1947,10 +1962,11 @@ def _get_feature_view_and_df_for_online_write( inputs: Optional[Union[Dict[str, List[Any]], pd.DataFrame]] = None, allow_registry_cache: bool = True, transform_on_write: bool = True, + skip_validation: bool = False, ): feature_view_dict = { fv_proto.name: fv_proto - for fv_proto in self.list_all_feature_views(allow_registry_cache) + for fv_proto in self.list_all_feature_views(allow_registry_cache, skip_validation=skip_validation) } try: feature_view = feature_view_dict[feature_view_name] @@ -1980,6 +1996,7 @@ def write_to_online_store( inputs: Optional[Union[Dict[str, List[Any]], pd.DataFrame]] = None, allow_registry_cache: bool = True, transform_on_write: bool = True, + skip_validation: bool = False, ): """ Persists a dataframe to the online store. @@ -1990,6 +2007,7 @@ def write_to_online_store( inputs: Optional the dictionary object to be written allow_registry_cache (optional): Whether to allow retrieving feature views from a cached registry. transform_on_write (optional): Whether to transform the data before pushing. + skip_validation (optional): Whether to skip validation of feature views (e.g., UDF deserialization). """ feature_view, df = self._get_feature_view_and_df_for_online_write( @@ -1998,6 +2016,7 @@ def write_to_online_store( inputs=inputs, allow_registry_cache=allow_registry_cache, transform_on_write=transform_on_write, + skip_validation=skip_validation, ) # Validate that the dataframe has meaningful feature data @@ -2025,6 +2044,7 @@ async def write_to_online_store_async( df: Optional[pd.DataFrame] = None, inputs: Optional[Union[Dict[str, List[Any]], pd.DataFrame]] = None, allow_registry_cache: bool = True, + skip_validation: bool = False, ): """ Persists a dataframe to the online store asynchronously. @@ -2034,6 +2054,7 @@ async def write_to_online_store_async( df: The dataframe to be persisted. inputs: Optional the dictionary object to be written allow_registry_cache (optional): Whether to allow retrieving feature views from a cached registry. + skip_validation (optional): Whether to skip validation of feature views (e.g., UDF deserialization). """ feature_view, df = self._get_feature_view_and_df_for_online_write( @@ -2041,6 +2062,7 @@ async def write_to_online_store_async( df=df, inputs=inputs, allow_registry_cache=allow_registry_cache, + skip_validation=skip_validation, ) # Validate that the dataframe has meaningful feature data diff --git a/sdk/python/feast/infra/registry/proto_registry_utils.py b/sdk/python/feast/infra/registry/proto_registry_utils.py index 26a5b7e1689..5e52ebdfea4 100644 --- a/sdk/python/feast/infra/registry/proto_registry_utils.py +++ b/sdk/python/feast/infra/registry/proto_registry_utils.py @@ -235,12 +235,15 @@ def list_feature_services( @registry_proto_cache_with_tags def list_all_feature_views( - registry_proto: RegistryProto, project: str, tags: Optional[dict[str, str]] + registry_proto: RegistryProto, + project: str, + tags: Optional[dict[str, str]], + skip_udf: bool = False, ) -> List[BaseFeatureView]: return ( list_feature_views(registry_proto, project, tags) + list_stream_feature_views(registry_proto, project, tags) - + list_on_demand_feature_views(registry_proto, project, tags) + + list_on_demand_feature_views(registry_proto, project, tags, skip_udf=skip_udf) ) @@ -274,7 +277,10 @@ def list_stream_feature_views( @registry_proto_cache_with_tags def list_on_demand_feature_views( - registry_proto: RegistryProto, project: str, tags: Optional[dict[str, str]] + registry_proto: RegistryProto, + project: str, + tags: Optional[dict[str, str]], + skip_udf: bool = False, ) -> List[OnDemandFeatureView]: on_demand_feature_views = [] for on_demand_feature_view in registry_proto.on_demand_feature_views: @@ -282,7 +288,7 @@ def list_on_demand_feature_views( on_demand_feature_view.spec.tags, tags ): on_demand_feature_views.append( - OnDemandFeatureView.from_proto(on_demand_feature_view) + OnDemandFeatureView.from_proto(on_demand_feature_view, skip_udf=skip_udf) ) return on_demand_feature_views diff --git a/sdk/python/feast/infra/registry/registry.py b/sdk/python/feast/infra/registry/registry.py index ff9c1f405a1..60e51573f6d 100644 --- a/sdk/python/feast/infra/registry/registry.py +++ b/sdk/python/feast/infra/registry/registry.py @@ -640,12 +640,13 @@ def list_all_feature_views( project: str, allow_cache: bool = False, tags: Optional[dict[str, str]] = None, + skip_udf: bool = False, ) -> List[BaseFeatureView]: registry_proto = self._get_registry_proto( project=project, allow_cache=allow_cache ) return proto_registry_utils.list_all_feature_views( - registry_proto, project, tags + registry_proto, project, tags, skip_udf=skip_udf ) def get_any_feature_view( diff --git a/sdk/python/feast/on_demand_feature_view.py b/sdk/python/feast/on_demand_feature_view.py index 7ead26cb984..6d775e44136 100644 --- a/sdk/python/feast/on_demand_feature_view.py +++ b/sdk/python/feast/on_demand_feature_view.py @@ -531,7 +531,7 @@ def from_proto( # Parse transformation from proto transformation = cls._parse_transformation_from_proto( - on_demand_feature_view_proto + on_demand_feature_view_proto, skip_udf=skip_udf ) # Parse optional fields with defaults @@ -603,7 +603,7 @@ def _parse_sources_from_proto( @classmethod def _parse_transformation_from_proto( - cls, proto: OnDemandFeatureViewProto + cls, proto: OnDemandFeatureViewProto, skip_udf: bool = False ) -> Transformation: """Parse and convert the transformation from the protobuf representation.""" feature_transformation = proto.spec.feature_transformation @@ -616,14 +616,14 @@ def _parse_transformation_from_proto( # Check for non-empty UDF body if udf_proto.body_text: if mode == "pandas": - return PandasTransformation.from_proto(udf_proto) + return PandasTransformation.from_proto(udf_proto, skip_udf=skip_udf) elif mode == "python": - return PythonTransformation.from_proto(udf_proto) + return PythonTransformation.from_proto(udf_proto, skip_udf=skip_udf) else: raise ValueError(ODFVErrorMessages.unsupported_mode_for_udf(mode)) else: # Handle backward compatibility case with empty body_text - return cls._handle_backward_compatible_udf(proto) + return cls._handle_backward_compatible_udf(proto, skip_udf=skip_udf) elif transformation_type == "substrait_transformation": return SubstraitTransformation.from_proto( @@ -631,7 +631,7 @@ def _parse_transformation_from_proto( ) elif transformation_type is None: # Handle backward compatibility case where feature_transformation is cleared - return cls._handle_backward_compatible_udf(proto) + return cls._handle_backward_compatible_udf(proto, skip_udf=skip_udf) else: raise ValueError( ODFVErrorMessages.unsupported_transformation_type(transformation_type) @@ -639,7 +639,7 @@ def _parse_transformation_from_proto( @classmethod def _handle_backward_compatible_udf( - cls, proto: OnDemandFeatureViewProto + cls, proto: OnDemandFeatureViewProto, skip_udf: bool = False ) -> Transformation: """Handle backward compatibility for UDFs with empty body_text.""" if not hasattr(proto.spec, "user_defined_function"): @@ -653,6 +653,7 @@ def _handle_backward_compatible_udf( ) return PandasTransformation.from_proto( user_defined_function_proto=backwards_compatible_udf, + skip_udf=skip_udf, ) @classmethod diff --git a/sdk/python/feast/transformation/pandas_transformation.py b/sdk/python/feast/transformation/pandas_transformation.py index 6e073c30100..07a00500e43 100644 --- a/sdk/python/feast/transformation/pandas_transformation.py +++ b/sdk/python/feast/transformation/pandas_transformation.py @@ -145,7 +145,17 @@ def __eq__(self, other): return True @classmethod - def from_proto(cls, user_defined_function_proto: UserDefinedFunctionProto): + def from_proto( + cls, + user_defined_function_proto: UserDefinedFunctionProto, + skip_udf: bool = False, + ): + if skip_udf: + # Return a dummy transformation when skipping UDF deserialization + return PandasTransformation( + udf=lambda x: x, # Identity function as placeholder + udf_string=user_defined_function_proto.body_text, + ) return PandasTransformation( udf=dill.loads(user_defined_function_proto.body), udf_string=user_defined_function_proto.body_text, diff --git a/sdk/python/feast/transformation/python_transformation.py b/sdk/python/feast/transformation/python_transformation.py index 68e9eee95f6..30384414938 100644 --- a/sdk/python/feast/transformation/python_transformation.py +++ b/sdk/python/feast/transformation/python_transformation.py @@ -163,7 +163,17 @@ def __reduce__(self): ) @classmethod - def from_proto(cls, user_defined_function_proto: UserDefinedFunctionProto): + def from_proto( + cls, + user_defined_function_proto: UserDefinedFunctionProto, + skip_udf: bool = False, + ): + if skip_udf: + # Return a dummy transformation when skipping UDF deserialization + return PythonTransformation( + udf=lambda x: x, # Identity function as placeholder + udf_string=user_defined_function_proto.body_text, + ) return PythonTransformation( udf=dill.loads(user_defined_function_proto.body), udf_string=user_defined_function_proto.body_text, From f9542e5436f46f1bd645997b1c39a932cb97f46f Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 23 Jan 2026 06:43:21 +0000 Subject: [PATCH 3/8] Fix caching decorator compatibility with skip_udf parameter Co-authored-by: franciscojavierarceo <4163062+franciscojavierarceo@users.noreply.github.com> --- .../infra/registry/proto_registry_utils.py | 40 ++++- sdk/python/tests/unit/test_skip_validation.py | 144 ++++++++++++++++++ 2 files changed, 180 insertions(+), 4 deletions(-) diff --git a/sdk/python/feast/infra/registry/proto_registry_utils.py b/sdk/python/feast/infra/registry/proto_registry_utils.py index 5e52ebdfea4..48ef4e763bf 100644 --- a/sdk/python/feast/infra/registry/proto_registry_utils.py +++ b/sdk/python/feast/infra/registry/proto_registry_utils.py @@ -233,17 +233,31 @@ def list_feature_services( return feature_services -@registry_proto_cache_with_tags def list_all_feature_views( registry_proto: RegistryProto, project: str, tags: Optional[dict[str, str]], skip_udf: bool = False, +) -> List[BaseFeatureView]: + # Skip caching when skip_udf is True to avoid cache pollution + if skip_udf: + return ( + list_feature_views(registry_proto, project, tags) + + list_stream_feature_views(registry_proto, project, tags) + + list_on_demand_feature_views(registry_proto, project, tags, skip_udf=skip_udf) + ) + else: + return _list_all_feature_views_cached(registry_proto, project, tags) + + +@registry_proto_cache_with_tags +def _list_all_feature_views_cached( + registry_proto: RegistryProto, project: str, tags: Optional[dict[str, str]] ) -> List[BaseFeatureView]: return ( list_feature_views(registry_proto, project, tags) + list_stream_feature_views(registry_proto, project, tags) - + list_on_demand_feature_views(registry_proto, project, tags, skip_udf=skip_udf) + + list_on_demand_feature_views(registry_proto, project, tags, skip_udf=False) ) @@ -275,12 +289,30 @@ def list_stream_feature_views( return stream_feature_views -@registry_proto_cache_with_tags def list_on_demand_feature_views( registry_proto: RegistryProto, project: str, tags: Optional[dict[str, str]], skip_udf: bool = False, +) -> List[OnDemandFeatureView]: + # Skip caching when skip_udf is True to avoid cache pollution with dummy UDFs + if skip_udf: + on_demand_feature_views = [] + for on_demand_feature_view in registry_proto.on_demand_feature_views: + if on_demand_feature_view.spec.project == project and utils.has_all_tags( + on_demand_feature_view.spec.tags, tags + ): + on_demand_feature_views.append( + OnDemandFeatureView.from_proto(on_demand_feature_view, skip_udf=skip_udf) + ) + return on_demand_feature_views + else: + return _list_on_demand_feature_views_cached(registry_proto, project, tags) + + +@registry_proto_cache_with_tags +def _list_on_demand_feature_views_cached( + registry_proto: RegistryProto, project: str, tags: Optional[dict[str, str]] ) -> List[OnDemandFeatureView]: on_demand_feature_views = [] for on_demand_feature_view in registry_proto.on_demand_feature_views: @@ -288,7 +320,7 @@ def list_on_demand_feature_views( on_demand_feature_view.spec.tags, tags ): on_demand_feature_views.append( - OnDemandFeatureView.from_proto(on_demand_feature_view, skip_udf=skip_udf) + OnDemandFeatureView.from_proto(on_demand_feature_view, skip_udf=False) ) return on_demand_feature_views diff --git a/sdk/python/tests/unit/test_skip_validation.py b/sdk/python/tests/unit/test_skip_validation.py index 8fb916e3776..82d68ac3860 100644 --- a/sdk/python/tests/unit/test_skip_validation.py +++ b/sdk/python/tests/unit/test_skip_validation.py @@ -7,11 +7,20 @@ - Cases where the type/validation system is being too restrictive Users should be encouraged to report issues on GitHub when they need to use this flag. + +Also tests skip_validation parameter in push() and related methods to handle +On-Demand Feature Views with UDFs that reference modules not available in the +current environment. """ import inspect +import dill +import pandas as pd + from feast.feature_store import FeatureStore +from feast.on_demand_feature_view import PandasTransformation, PythonTransformation +from feast.protos.feast.core.Transformation_pb2 import UserDefinedFunctionV2 as UserDefinedFunctionProto def test_apply_has_skip_feature_view_validation_parameter(): @@ -48,6 +57,108 @@ def test_plan_has_skip_feature_view_validation_parameter(): assert param.annotation == bool +def test_push_has_skip_validation_parameter(): + """Test that FeatureStore.push() method has skip_validation parameter""" + # Get the signature of the push method + sig = inspect.signature(FeatureStore.push) + + # Check that skip_validation parameter exists + assert "skip_validation" in sig.parameters + + # Check that it has a default value of False + param = sig.parameters["skip_validation"] + assert param.default is False + + # Check that it's a boolean type hint (if type hints are present) + if param.annotation != inspect.Parameter.empty: + assert param.annotation == bool + + +def test_push_async_has_skip_validation_parameter(): + """Test that FeatureStore.push_async() method has skip_validation parameter""" + # Get the signature of the push_async method + sig = inspect.signature(FeatureStore.push_async) + + # Check that skip_validation parameter exists + assert "skip_validation" in sig.parameters + + # Check that it has a default value of False + param = sig.parameters["skip_validation"] + assert param.default is False + + # Check that it's a boolean type hint (if type hints are present) + if param.annotation != inspect.Parameter.empty: + assert param.annotation == bool + + +def test_pandas_transformation_from_proto_with_skip_udf(): + """Test that PandasTransformation.from_proto works with skip_udf=True.""" + + # Create a UDF that would reference a non-existent module + def udf_with_missing_module(df: pd.DataFrame) -> pd.DataFrame: + # This would normally fail if a module is missing during deserialization + import nonexistent_module # noqa: F401 + return df + + # Serialize the UDF + serialized_udf = dill.dumps(udf_with_missing_module) + udf_string = "import nonexistent_module\ndef udf(df): return df" + + # Create proto + udf_proto = UserDefinedFunctionProto( + name="test_udf", + body=serialized_udf, + body_text=udf_string, + ) + + # Test that skip_udf=True doesn't try to deserialize the UDF + # This would normally fail with ModuleNotFoundError + transformation = PandasTransformation.from_proto(udf_proto, skip_udf=True) + + # Should get a dummy transformation with identity function + assert transformation is not None + assert transformation.udf_string == udf_string + + # The dummy UDF should be callable and act as identity + test_df = pd.DataFrame({"col1": [1, 2, 3]}) + result = transformation.udf(test_df) + assert result.equals(test_df) + + +def test_python_transformation_from_proto_with_skip_udf(): + """Test that PythonTransformation.from_proto works with skip_udf=True.""" + + # Create a UDF that would reference a non-existent module + def udf_with_missing_module(features_dict): + # This would normally fail if a module is missing during deserialization + import nonexistent_module # noqa: F401 + return features_dict + + # Serialize the UDF + serialized_udf = dill.dumps(udf_with_missing_module) + udf_string = "import nonexistent_module\ndef udf(d): return d" + + # Create proto + udf_proto = UserDefinedFunctionProto( + name="test_udf", + body=serialized_udf, + body_text=udf_string, + ) + + # Test that skip_udf=True doesn't try to deserialize the UDF + # This would normally fail with ModuleNotFoundError + transformation = PythonTransformation.from_proto(udf_proto, skip_udf=True) + + # Should get a dummy transformation with identity function + assert transformation is not None + assert transformation.udf_string == udf_string + + # The dummy UDF should be callable and act as identity + test_dict = {"col1": 1} + result = transformation.udf(test_dict) + assert result == test_dict + + def test_skip_feature_view_validation_use_case_documentation(): """ Documentation test: This test documents the key use case for skip_feature_view_validation. @@ -69,3 +180,36 @@ def test_skip_feature_view_validation_use_case_documentation(): can improve the validation system. """ pass # This is a documentation test + + +def test_skip_validation_use_case_documentation(): + """ + Documentation test: This test documents the key use case for skip_validation in push(). + + The skip_validation flag in push() addresses the ModuleNotFoundError issue when: + 1. An OnDemandFeatureView with a UDF is defined in an environment with specific modules + 2. The UDF references functions, classes, or constants from those modules (e.g., 'training') + 3. feast.apply() is run to save the definition to the remote registry + 4. store.push() is called from a different environment without those modules + + Without skip_validation: + - push() calls list_all_feature_views() which deserializes ODFVs + - Deserialization uses dill.loads() which fails if referenced modules are missing + - Results in: ModuleNotFoundError: No module named 'training' + + With skip_validation=True: + - push() calls list_all_feature_views(skip_validation=True) + - ODFVs are loaded with dummy UDFs (identity functions) + - No deserialization of the actual UDF happens + - push() can proceed successfully + + Example usage: + store.push("my_push_source", df, skip_validation=True) + + This is particularly useful in production environments where: + - Data ingestion services don't need the training/modeling code + - The UDF logic isn't needed during push operations + - Different teams manage training vs. serving infrastructure + """ + pass # This is a documentation test + From 79d67447277f25e926626ecc5f90a560edb15e52 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 23 Jan 2026 06:44:11 +0000 Subject: [PATCH 4/8] Apply code formatting with ruff Co-authored-by: franciscojavierarceo <4163062+franciscojavierarceo@users.noreply.github.com> --- sdk/python/feast/feature_store.py | 8 +++-- .../infra/registry/proto_registry_utils.py | 8 +++-- sdk/python/tests/unit/test_skip_validation.py | 31 ++++++++++--------- 3 files changed, 29 insertions(+), 18 deletions(-) diff --git a/sdk/python/feast/feature_store.py b/sdk/python/feast/feature_store.py index d63cd70a19f..9e0215fea75 100644 --- a/sdk/python/feast/feature_store.py +++ b/sdk/python/feast/feature_store.py @@ -303,7 +303,9 @@ def list_all_feature_views( Returns: A list of feature views. """ - return self._list_all_feature_views(allow_cache, tags=tags, skip_validation=skip_validation) + return self._list_all_feature_views( + allow_cache, tags=tags, skip_validation=skip_validation + ) def list_feature_views( self, allow_cache: bool = False, tags: Optional[dict[str, str]] = None @@ -1966,7 +1968,9 @@ def _get_feature_view_and_df_for_online_write( ): feature_view_dict = { fv_proto.name: fv_proto - for fv_proto in self.list_all_feature_views(allow_registry_cache, skip_validation=skip_validation) + for fv_proto in self.list_all_feature_views( + allow_registry_cache, skip_validation=skip_validation + ) } try: feature_view = feature_view_dict[feature_view_name] diff --git a/sdk/python/feast/infra/registry/proto_registry_utils.py b/sdk/python/feast/infra/registry/proto_registry_utils.py index 48ef4e763bf..9f300c6246d 100644 --- a/sdk/python/feast/infra/registry/proto_registry_utils.py +++ b/sdk/python/feast/infra/registry/proto_registry_utils.py @@ -244,7 +244,9 @@ def list_all_feature_views( return ( list_feature_views(registry_proto, project, tags) + list_stream_feature_views(registry_proto, project, tags) - + list_on_demand_feature_views(registry_proto, project, tags, skip_udf=skip_udf) + + list_on_demand_feature_views( + registry_proto, project, tags, skip_udf=skip_udf + ) ) else: return _list_all_feature_views_cached(registry_proto, project, tags) @@ -303,7 +305,9 @@ def list_on_demand_feature_views( on_demand_feature_view.spec.tags, tags ): on_demand_feature_views.append( - OnDemandFeatureView.from_proto(on_demand_feature_view, skip_udf=skip_udf) + OnDemandFeatureView.from_proto( + on_demand_feature_view, skip_udf=skip_udf + ) ) return on_demand_feature_views else: diff --git a/sdk/python/tests/unit/test_skip_validation.py b/sdk/python/tests/unit/test_skip_validation.py index 82d68ac3860..1de3f0e7fdd 100644 --- a/sdk/python/tests/unit/test_skip_validation.py +++ b/sdk/python/tests/unit/test_skip_validation.py @@ -20,7 +20,9 @@ from feast.feature_store import FeatureStore from feast.on_demand_feature_view import PandasTransformation, PythonTransformation -from feast.protos.feast.core.Transformation_pb2 import UserDefinedFunctionV2 as UserDefinedFunctionProto +from feast.protos.feast.core.Transformation_pb2 import ( + UserDefinedFunctionV2 as UserDefinedFunctionProto, +) def test_apply_has_skip_feature_view_validation_parameter(): @@ -93,32 +95,33 @@ def test_push_async_has_skip_validation_parameter(): def test_pandas_transformation_from_proto_with_skip_udf(): """Test that PandasTransformation.from_proto works with skip_udf=True.""" - + # Create a UDF that would reference a non-existent module def udf_with_missing_module(df: pd.DataFrame) -> pd.DataFrame: # This would normally fail if a module is missing during deserialization import nonexistent_module # noqa: F401 + return df - + # Serialize the UDF serialized_udf = dill.dumps(udf_with_missing_module) udf_string = "import nonexistent_module\ndef udf(df): return df" - + # Create proto udf_proto = UserDefinedFunctionProto( name="test_udf", body=serialized_udf, body_text=udf_string, ) - + # Test that skip_udf=True doesn't try to deserialize the UDF # This would normally fail with ModuleNotFoundError transformation = PandasTransformation.from_proto(udf_proto, skip_udf=True) - + # Should get a dummy transformation with identity function assert transformation is not None assert transformation.udf_string == udf_string - + # The dummy UDF should be callable and act as identity test_df = pd.DataFrame({"col1": [1, 2, 3]}) result = transformation.udf(test_df) @@ -127,32 +130,33 @@ def udf_with_missing_module(df: pd.DataFrame) -> pd.DataFrame: def test_python_transformation_from_proto_with_skip_udf(): """Test that PythonTransformation.from_proto works with skip_udf=True.""" - + # Create a UDF that would reference a non-existent module def udf_with_missing_module(features_dict): # This would normally fail if a module is missing during deserialization import nonexistent_module # noqa: F401 + return features_dict - + # Serialize the UDF serialized_udf = dill.dumps(udf_with_missing_module) udf_string = "import nonexistent_module\ndef udf(d): return d" - + # Create proto udf_proto = UserDefinedFunctionProto( name="test_udf", body=serialized_udf, body_text=udf_string, ) - + # Test that skip_udf=True doesn't try to deserialize the UDF # This would normally fail with ModuleNotFoundError transformation = PythonTransformation.from_proto(udf_proto, skip_udf=True) - + # Should get a dummy transformation with identity function assert transformation is not None assert transformation.udf_string == udf_string - + # The dummy UDF should be callable and act as identity test_dict = {"col1": 1} result = transformation.udf(test_dict) @@ -212,4 +216,3 @@ def test_skip_validation_use_case_documentation(): - Different teams manage training vs. serving infrastructure """ pass # This is a documentation test - From a735bfbd541de2b4c19abc1413863ad9a88cc5f5 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 23 Jan 2026 06:48:04 +0000 Subject: [PATCH 5/8] Use explicit identity functions instead of lambda for better type safety Co-authored-by: franciscojavierarceo <4163062+franciscojavierarceo@users.noreply.github.com> --- sdk/python/feast/transformation/pandas_transformation.py | 6 +++++- sdk/python/feast/transformation/python_transformation.py | 6 +++++- 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/sdk/python/feast/transformation/pandas_transformation.py b/sdk/python/feast/transformation/pandas_transformation.py index 07a00500e43..47b0503a2c7 100644 --- a/sdk/python/feast/transformation/pandas_transformation.py +++ b/sdk/python/feast/transformation/pandas_transformation.py @@ -152,8 +152,12 @@ def from_proto( ): if skip_udf: # Return a dummy transformation when skipping UDF deserialization + # Identity function that preserves DataFrame structure + def identity_udf(df: pd.DataFrame) -> pd.DataFrame: + return df + return PandasTransformation( - udf=lambda x: x, # Identity function as placeholder + udf=identity_udf, udf_string=user_defined_function_proto.body_text, ) return PandasTransformation( diff --git a/sdk/python/feast/transformation/python_transformation.py b/sdk/python/feast/transformation/python_transformation.py index 30384414938..d94063b928f 100644 --- a/sdk/python/feast/transformation/python_transformation.py +++ b/sdk/python/feast/transformation/python_transformation.py @@ -170,8 +170,12 @@ def from_proto( ): if skip_udf: # Return a dummy transformation when skipping UDF deserialization + # Identity function that preserves dictionary structure + def identity_udf(features_dict: Dict[str, Any]) -> Dict[str, Any]: + return features_dict + return PythonTransformation( - udf=lambda x: x, # Identity function as placeholder + udf=identity_udf, udf_string=user_defined_function_proto.body_text, ) return PythonTransformation( From 2433df6cd68e89d2e24e8d1cfb9b002e178c6b63 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 23 Jan 2026 18:48:24 +0000 Subject: [PATCH 6/8] Only skip UDF validation for ODFVs without write_to_online_store Co-authored-by: franciscojavierarceo <4163062+franciscojavierarceo@users.noreply.github.com> --- .../infra/registry/proto_registry_utils.py | 8 +- sdk/python/tests/unit/test_skip_validation.py | 109 +++++++++++++++++- 2 files changed, 114 insertions(+), 3 deletions(-) diff --git a/sdk/python/feast/infra/registry/proto_registry_utils.py b/sdk/python/feast/infra/registry/proto_registry_utils.py index 9f300c6246d..141ed36e18f 100644 --- a/sdk/python/feast/infra/registry/proto_registry_utils.py +++ b/sdk/python/feast/infra/registry/proto_registry_utils.py @@ -304,9 +304,15 @@ def list_on_demand_feature_views( if on_demand_feature_view.spec.project == project and utils.has_all_tags( on_demand_feature_view.spec.tags, tags ): + # Only skip UDF deserialization for ODFVs that don't write to online store + # ODFVs with write_to_online_store=True need the actual UDF loaded + # because it will be executed during push operations + should_skip_udf = ( + skip_udf and not on_demand_feature_view.spec.write_to_online_store + ) on_demand_feature_views.append( OnDemandFeatureView.from_proto( - on_demand_feature_view, skip_udf=skip_udf + on_demand_feature_view, skip_udf=should_skip_udf ) ) return on_demand_feature_views diff --git a/sdk/python/tests/unit/test_skip_validation.py b/sdk/python/tests/unit/test_skip_validation.py index 1de3f0e7fdd..ee219d6f8e9 100644 --- a/sdk/python/tests/unit/test_skip_validation.py +++ b/sdk/python/tests/unit/test_skip_validation.py @@ -203,10 +203,15 @@ def test_skip_validation_use_case_documentation(): With skip_validation=True: - push() calls list_all_feature_views(skip_validation=True) - - ODFVs are loaded with dummy UDFs (identity functions) - - No deserialization of the actual UDF happens + - ODFVs WITHOUT write_to_online_store=True are loaded with dummy UDFs (identity functions) + - ODFVs WITH write_to_online_store=True are loaded normally (UDF is deserialized) + - No deserialization of the actual UDF happens for ODFVs that won't execute transformations - push() can proceed successfully + IMPORTANT: ODFVs with write_to_online_store=True will have their UDFs executed during + push operations, so their UDFs MUST be properly deserialized even when skip_validation=True. + Only ODFVs that don't execute transformations during push can safely skip UDF loading. + Example usage: store.push("my_push_source", df, skip_validation=True) @@ -216,3 +221,103 @@ def test_skip_validation_use_case_documentation(): - Different teams manage training vs. serving infrastructure """ pass # This is a documentation test + + +def test_skip_validation_only_applies_to_non_writing_odfvs(): + """ + Test that skip_validation only skips UDF loading for ODFVs that don't write to online store. + + ODFVs with write_to_online_store=True need their UDFs loaded because they will be executed + during push operations. Only ODFVs with write_to_online_store=False can safely skip UDF loading. + """ + from feast.infra.registry.proto_registry_utils import list_on_demand_feature_views + from feast.protos.feast.core.OnDemandFeatureView_pb2 import ( + OnDemandFeatureView as OnDemandFeatureViewProto, + ) + from feast.protos.feast.core.OnDemandFeatureView_pb2 import ( + OnDemandFeatureViewSpec, + ) + from feast.protos.feast.core.Registry_pb2 import Registry as RegistryProto + from feast.protos.feast.core.Transformation_pb2 import ( + FeatureTransformationV2, + ) + from feast.protos.feast.core.Transformation_pb2 import ( + UserDefinedFunctionV2 as UserDefinedFunctionProto, + ) + + # Create a UDF that doesn't reference any modules (will work fine) + def simple_udf(df): + return df + + serialized_udf = dill.dumps(simple_udf) + udf_string = "def simple_udf(df): return df" + + udf_proto = UserDefinedFunctionProto( + name="test_udf", + body=serialized_udf, + body_text=udf_string, + ) + + feature_transformation = FeatureTransformationV2(user_defined_function=udf_proto) + + # Create two ODFVs: one with write_to_online_store=True, one with False + odfv_with_write_spec = OnDemandFeatureViewSpec( + name="odfv_with_write", + project="test_project", + mode="pandas", + feature_transformation=feature_transformation, + write_to_online_store=True, + ) + odfv_with_write_proto = OnDemandFeatureViewProto(spec=odfv_with_write_spec) + + odfv_without_write_spec = OnDemandFeatureViewSpec( + name="odfv_without_write", + project="test_project", + mode="pandas", + feature_transformation=feature_transformation, + write_to_online_store=False, + ) + odfv_without_write_proto = OnDemandFeatureViewProto(spec=odfv_without_write_spec) + + # Create a registry with both ODFVs + registry_proto = RegistryProto( + on_demand_feature_views=[odfv_with_write_proto, odfv_without_write_proto] + ) + + # Test with skip_udf=True + odfvs = list_on_demand_feature_views( + registry_proto, "test_project", None, skip_udf=True + ) + + # We should get exactly 2 ODFVs back + assert len(odfvs) == 2 + + # Find each ODFV + odfv_with_write = next(fv for fv in odfvs if fv.name == "odfv_with_write") + odfv_without_write = next(fv for fv in odfvs if fv.name == "odfv_without_write") + + # Verify write_to_online_store flags are correct + assert odfv_with_write.write_to_online_store is True + assert odfv_without_write.write_to_online_store is False + + # The key test: Check if the UDFs behave correctly + # The ODFV with write_to_online_store=True should have the REAL UDF + # The ODFV with write_to_online_store=False should have a DUMMY UDF (identity function) + + test_df = pd.DataFrame({"col1": [1, 2, 3]}) + + # Test the ODFV with write_to_online_store=False - should have dummy UDF + # The dummy UDF is an identity function, so output equals input + result_without_write = odfv_without_write.feature_transformation.udf(test_df) + assert result_without_write.equals(test_df), ( + "ODFV without write_to_online_store should have identity UDF" + ) + + # Test the ODFV with write_to_online_store=True - should have real UDF + # The real UDF is also an identity function in this test, but it's the ACTUAL deserialized UDF + # We can't easily distinguish between real and dummy identity functions in this test + # But the important thing is that it loaded without error + result_with_write = odfv_with_write.feature_transformation.udf(test_df) + assert result_with_write.equals(test_df), ( + "ODFV with write_to_online_store should have real UDF" + ) From 72c471eb84ad83690f7d91133e209b679a6533d0 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 23 Jan 2026 18:49:47 +0000 Subject: [PATCH 7/8] Minor wording improvement in test documentation Co-authored-by: franciscojavierarceo <4163062+franciscojavierarceo@users.noreply.github.com> --- sdk/python/tests/unit/test_skip_validation.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/sdk/python/tests/unit/test_skip_validation.py b/sdk/python/tests/unit/test_skip_validation.py index ee219d6f8e9..725e3ccce06 100644 --- a/sdk/python/tests/unit/test_skip_validation.py +++ b/sdk/python/tests/unit/test_skip_validation.py @@ -203,8 +203,8 @@ def test_skip_validation_use_case_documentation(): With skip_validation=True: - push() calls list_all_feature_views(skip_validation=True) - - ODFVs WITHOUT write_to_online_store=True are loaded with dummy UDFs (identity functions) - - ODFVs WITH write_to_online_store=True are loaded normally (UDF is deserialized) + - ODFVs with write_to_online_store=False are loaded with dummy UDFs (identity functions) + - ODFVs with write_to_online_store=True are loaded normally (UDF is deserialized) - No deserialization of the actual UDF happens for ODFVs that won't execute transformations - push() can proceed successfully From 885536e29d8e5585d863b4a7f749c69ab029e91e Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 24 Jan 2026 05:19:25 +0000 Subject: [PATCH 8/8] Rename skip_validation to skip_feature_view_validation for API consistency Co-authored-by: franciscojavierarceo <4163062+franciscojavierarceo@users.noreply.github.com> --- sdk/python/feast/feature_store.py | 42 +++++++++-------- sdk/python/tests/unit/test_skip_validation.py | 46 +++++++++---------- 2 files changed, 47 insertions(+), 41 deletions(-) diff --git a/sdk/python/feast/feature_store.py b/sdk/python/feast/feature_store.py index 9e0215fea75..6e6d0a78e3a 100644 --- a/sdk/python/feast/feature_store.py +++ b/sdk/python/feast/feature_store.py @@ -270,11 +270,14 @@ def _list_all_feature_views( self, allow_cache: bool = False, tags: Optional[dict[str, str]] = None, - skip_validation: bool = False, + skip_feature_view_validation: bool = False, ) -> List[BaseFeatureView]: feature_views = [] for fv in self.registry.list_all_feature_views( - self.project, allow_cache=allow_cache, tags=tags, skip_udf=skip_validation + self.project, + allow_cache=allow_cache, + tags=tags, + skip_udf=skip_feature_view_validation, ): if ( isinstance(fv, FeatureView) @@ -290,7 +293,7 @@ def list_all_feature_views( self, allow_cache: bool = False, tags: Optional[dict[str, str]] = None, - skip_validation: bool = False, + skip_feature_view_validation: bool = False, ) -> List[BaseFeatureView]: """ Retrieves the list of feature views from the registry. @@ -298,13 +301,15 @@ def list_all_feature_views( Args: allow_cache: Whether to allow returning entities from a cached registry. tags: Filter by tags. - skip_validation: Whether to skip validation of feature views (e.g., UDF deserialization). + skip_feature_view_validation: Whether to skip validation of feature views (e.g., UDF deserialization). Returns: A list of feature views. """ return self._list_all_feature_views( - allow_cache, tags=tags, skip_validation=skip_validation + allow_cache, + tags=tags, + skip_feature_view_validation=skip_feature_view_validation, ) def list_feature_views( @@ -1739,7 +1744,7 @@ def push( allow_registry_cache: bool = True, to: PushMode = PushMode.ONLINE, transform_on_write: bool = True, - skip_validation: bool = False, + skip_feature_view_validation: bool = False, ): """ Push features to a push source. This updates all the feature views that have the push source as stream source. @@ -1750,7 +1755,7 @@ def push( allow_registry_cache: Whether to allow cached versions of the registry. to: Whether to push to online or offline store. Defaults to online store only. transform_on_write: Whether to transform the data before pushing. - skip_validation: Whether to skip validation of feature views (e.g., UDF deserialization). + skip_feature_view_validation: Whether to skip validation of feature views (e.g., UDF deserialization). """ for fv in self._fvs_for_push_source_or_raise( push_source_name, allow_registry_cache @@ -1761,7 +1766,7 @@ def push( df, allow_registry_cache=allow_registry_cache, transform_on_write=transform_on_write, - skip_validation=skip_validation, + skip_feature_view_validation=skip_feature_view_validation, ) if to == PushMode.OFFLINE or to == PushMode.ONLINE_AND_OFFLINE: self.write_to_offline_store( @@ -1774,7 +1779,7 @@ async def push_async( df: pd.DataFrame, allow_registry_cache: bool = True, to: PushMode = PushMode.ONLINE, - skip_validation: bool = False, + skip_feature_view_validation: bool = False, **kwargs, ): fvs = self._fvs_for_push_source_or_raise(push_source_name, allow_registry_cache) @@ -1786,7 +1791,7 @@ async def push_async( fv.name, df, allow_registry_cache=allow_registry_cache, - skip_validation=skip_validation, + skip_feature_view_validation=skip_feature_view_validation, ) for fv in fvs ] @@ -1964,12 +1969,13 @@ def _get_feature_view_and_df_for_online_write( inputs: Optional[Union[Dict[str, List[Any]], pd.DataFrame]] = None, allow_registry_cache: bool = True, transform_on_write: bool = True, - skip_validation: bool = False, + skip_feature_view_validation: bool = False, ): feature_view_dict = { fv_proto.name: fv_proto for fv_proto in self.list_all_feature_views( - allow_registry_cache, skip_validation=skip_validation + allow_registry_cache, + skip_feature_view_validation=skip_feature_view_validation, ) } try: @@ -2000,7 +2006,7 @@ def write_to_online_store( inputs: Optional[Union[Dict[str, List[Any]], pd.DataFrame]] = None, allow_registry_cache: bool = True, transform_on_write: bool = True, - skip_validation: bool = False, + skip_feature_view_validation: bool = False, ): """ Persists a dataframe to the online store. @@ -2011,7 +2017,7 @@ def write_to_online_store( inputs: Optional the dictionary object to be written allow_registry_cache (optional): Whether to allow retrieving feature views from a cached registry. transform_on_write (optional): Whether to transform the data before pushing. - skip_validation (optional): Whether to skip validation of feature views (e.g., UDF deserialization). + skip_feature_view_validation (optional): Whether to skip validation of feature views (e.g., UDF deserialization). """ feature_view, df = self._get_feature_view_and_df_for_online_write( @@ -2020,7 +2026,7 @@ def write_to_online_store( inputs=inputs, allow_registry_cache=allow_registry_cache, transform_on_write=transform_on_write, - skip_validation=skip_validation, + skip_feature_view_validation=skip_feature_view_validation, ) # Validate that the dataframe has meaningful feature data @@ -2048,7 +2054,7 @@ async def write_to_online_store_async( df: Optional[pd.DataFrame] = None, inputs: Optional[Union[Dict[str, List[Any]], pd.DataFrame]] = None, allow_registry_cache: bool = True, - skip_validation: bool = False, + skip_feature_view_validation: bool = False, ): """ Persists a dataframe to the online store asynchronously. @@ -2058,7 +2064,7 @@ async def write_to_online_store_async( df: The dataframe to be persisted. inputs: Optional the dictionary object to be written allow_registry_cache (optional): Whether to allow retrieving feature views from a cached registry. - skip_validation (optional): Whether to skip validation of feature views (e.g., UDF deserialization). + skip_feature_view_validation (optional): Whether to skip validation of feature views (e.g., UDF deserialization). """ feature_view, df = self._get_feature_view_and_df_for_online_write( @@ -2066,7 +2072,7 @@ async def write_to_online_store_async( df=df, inputs=inputs, allow_registry_cache=allow_registry_cache, - skip_validation=skip_validation, + skip_feature_view_validation=skip_feature_view_validation, ) # Validate that the dataframe has meaningful feature data diff --git a/sdk/python/tests/unit/test_skip_validation.py b/sdk/python/tests/unit/test_skip_validation.py index 725e3ccce06..ed749d897ea 100644 --- a/sdk/python/tests/unit/test_skip_validation.py +++ b/sdk/python/tests/unit/test_skip_validation.py @@ -8,7 +8,7 @@ Users should be encouraged to report issues on GitHub when they need to use this flag. -Also tests skip_validation parameter in push() and related methods to handle +Also tests skip_feature_view_validation parameter in push() and related methods to handle On-Demand Feature Views with UDFs that reference modules not available in the current environment. """ @@ -59,16 +59,16 @@ def test_plan_has_skip_feature_view_validation_parameter(): assert param.annotation == bool -def test_push_has_skip_validation_parameter(): - """Test that FeatureStore.push() method has skip_validation parameter""" +def test_push_has_skip_feature_view_validation_parameter(): + """Test that FeatureStore.push() method has skip_feature_view_validation parameter""" # Get the signature of the push method sig = inspect.signature(FeatureStore.push) - # Check that skip_validation parameter exists - assert "skip_validation" in sig.parameters + # Check that skip_feature_view_validation parameter exists + assert "skip_feature_view_validation" in sig.parameters # Check that it has a default value of False - param = sig.parameters["skip_validation"] + param = sig.parameters["skip_feature_view_validation"] assert param.default is False # Check that it's a boolean type hint (if type hints are present) @@ -76,16 +76,16 @@ def test_push_has_skip_validation_parameter(): assert param.annotation == bool -def test_push_async_has_skip_validation_parameter(): - """Test that FeatureStore.push_async() method has skip_validation parameter""" +def test_push_async_has_skip_feature_view_validation_parameter(): + """Test that FeatureStore.push_async() method has skip_feature_view_validation parameter""" # Get the signature of the push_async method sig = inspect.signature(FeatureStore.push_async) - # Check that skip_validation parameter exists - assert "skip_validation" in sig.parameters + # Check that skip_feature_view_validation parameter exists + assert "skip_feature_view_validation" in sig.parameters # Check that it has a default value of False - param = sig.parameters["skip_validation"] + param = sig.parameters["skip_feature_view_validation"] assert param.default is False # Check that it's a boolean type hint (if type hints are present) @@ -163,9 +163,9 @@ def udf_with_missing_module(features_dict): assert result == test_dict -def test_skip_feature_view_validation_use_case_documentation(): +def test_skip_feature_view_validation_in_apply_use_case_documentation(): """ - Documentation test: This test documents the key use case for skip_feature_view_validation. + Documentation test: This test documents the key use case for skip_feature_view_validation in apply(). The skip_feature_view_validation flag is particularly important for On-Demand Feature Views (ODFVs) that use feature transformations. During the apply() process, ODFVs call infer_features() @@ -186,34 +186,34 @@ def test_skip_feature_view_validation_use_case_documentation(): pass # This is a documentation test -def test_skip_validation_use_case_documentation(): +def test_skip_feature_view_validation_in_push_use_case_documentation(): """ - Documentation test: This test documents the key use case for skip_validation in push(). + Documentation test: This test documents the key use case for skip_feature_view_validation in push(). - The skip_validation flag in push() addresses the ModuleNotFoundError issue when: + The skip_feature_view_validation flag in push() addresses the ModuleNotFoundError issue when: 1. An OnDemandFeatureView with a UDF is defined in an environment with specific modules 2. The UDF references functions, classes, or constants from those modules (e.g., 'training') 3. feast.apply() is run to save the definition to the remote registry 4. store.push() is called from a different environment without those modules - Without skip_validation: + Without skip_feature_view_validation: - push() calls list_all_feature_views() which deserializes ODFVs - Deserialization uses dill.loads() which fails if referenced modules are missing - Results in: ModuleNotFoundError: No module named 'training' - With skip_validation=True: - - push() calls list_all_feature_views(skip_validation=True) + With skip_feature_view_validation=True: + - push() calls list_all_feature_views(skip_feature_view_validation=True) - ODFVs with write_to_online_store=False are loaded with dummy UDFs (identity functions) - ODFVs with write_to_online_store=True are loaded normally (UDF is deserialized) - No deserialization of the actual UDF happens for ODFVs that won't execute transformations - push() can proceed successfully IMPORTANT: ODFVs with write_to_online_store=True will have their UDFs executed during - push operations, so their UDFs MUST be properly deserialized even when skip_validation=True. + push operations, so their UDFs MUST be properly deserialized even when skip_feature_view_validation=True. Only ODFVs that don't execute transformations during push can safely skip UDF loading. Example usage: - store.push("my_push_source", df, skip_validation=True) + store.push("my_push_source", df, skip_feature_view_validation=True) This is particularly useful in production environments where: - Data ingestion services don't need the training/modeling code @@ -223,9 +223,9 @@ def test_skip_validation_use_case_documentation(): pass # This is a documentation test -def test_skip_validation_only_applies_to_non_writing_odfvs(): +def test_skip_feature_view_validation_only_applies_to_non_writing_odfvs(): """ - Test that skip_validation only skips UDF loading for ODFVs that don't write to online store. + Test that skip_feature_view_validation only skips UDF loading for ODFVs that don't write to online store. ODFVs with write_to_online_store=True need their UDFs loaded because they will be executed during push operations. Only ODFVs with write_to_online_store=False can safely skip UDF loading.