Skip to content

Commit 2d7a43b

Browse files
updated
Signed-off-by: Francisco Javier Arceo <farceo@redhat.com>
1 parent 5b759ed commit 2d7a43b

6 files changed

Lines changed: 176 additions & 111 deletions

sdk/python/feast/feature_store.py

Lines changed: 17 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -912,23 +912,23 @@ def apply(
912912
"Use FeatureView with feature_transformation parameters instead. "
913913
"See documentation for migration guide.",
914914
DeprecationWarning,
915-
stacklevel=2
915+
stacklevel=2,
916916
)
917917
elif isinstance(ob, StreamFeatureView):
918918
warnings.warn(
919919
f"StreamFeatureView '{ob.name}' is deprecated. "
920920
"Use FeatureView with feature_transformation parameters instead. "
921921
"See documentation for migration guide.",
922922
DeprecationWarning,
923-
stacklevel=2
923+
stacklevel=2,
924924
)
925925
elif isinstance(ob, OnDemandFeatureView):
926926
warnings.warn(
927927
f"OnDemandFeatureView '{ob.name}' is deprecated. "
928928
"Use FeatureView with feature_transformation parameters instead. "
929929
"See documentation for migration guide.",
930930
DeprecationWarning,
931-
stacklevel=2
931+
stacklevel=2,
932932
)
933933

934934
services_to_update = [ob for ob in objects if isinstance(ob, FeatureService)]
@@ -1961,7 +1961,9 @@ def _apply_unified_transformation(
19611961
transformed_dict = transformation.udf(input_dict)
19621962
return pd.DataFrame(transformed_dict)
19631963
else:
1964-
raise Exception(f"Unsupported transformation mode: {transformation.mode.value}")
1964+
raise Exception(
1965+
f"Unsupported transformation mode: {transformation.mode.value}"
1966+
)
19651967

19661968
def _validate_transformed_schema(
19671969
self, feature_view: FeatureView, df: pd.DataFrame
@@ -1977,7 +1979,7 @@ def _validate_transformed_schema(
19771979
Raises:
19781980
ValueError: If schema validation fails
19791981
"""
1980-
if not hasattr(feature_view, 'schema') or not feature_view.schema:
1982+
if not hasattr(feature_view, "schema") or not feature_view.schema:
19811983
return # No schema to validate against
19821984

19831985
expected_columns = {field.name for field in feature_view.schema}
@@ -2052,11 +2054,19 @@ def _get_feature_view_and_df_for_online_write(
20522054
):
20532055
df = self._transform_on_demand_feature_view_df(feature_view, df)
20542056
# Handle unified FeatureView with feature_transformation
2055-
elif hasattr(feature_view, 'feature_transformation') and feature_view.feature_transformation:
2057+
elif (
2058+
hasattr(feature_view, "feature_transformation")
2059+
and feature_view.feature_transformation
2060+
):
20562061
df = self._apply_unified_transformation(feature_view, df)
20572062

20582063
# Schema validation when transform=False
2059-
elif not transform_on_write and df is not None and hasattr(feature_view, 'feature_transformation') and feature_view.feature_transformation:
2064+
elif (
2065+
not transform_on_write
2066+
and df is not None
2067+
and hasattr(feature_view, "feature_transformation")
2068+
and feature_view.feature_transformation
2069+
):
20602070
self._validate_transformed_schema(feature_view, df)
20612071

20622072
return feature_view, df

sdk/python/feast/utils.py

Lines changed: 40 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -500,9 +500,9 @@ def _group_feature_refs(
500500
# on demand view to on demand view proto
501501
on_demand_view_index: Dict[str, "OnDemandFeatureView"] = {}
502502
for view in all_on_demand_feature_views:
503-
if view.projection and not getattr(view, 'write_to_online_store', True):
503+
if view.projection and not getattr(view, "write_to_online_store", True):
504504
on_demand_view_index[view.projection.name_to_use()] = view
505-
elif view.projection and getattr(view, 'write_to_online_store', True):
505+
elif view.projection and getattr(view, "write_to_online_store", True):
506506
# we insert the ODFV view to FVs for ones that are written to the online store
507507
view_index[view.projection.name_to_use()] = view
508508

@@ -690,16 +690,17 @@ def _augment_response_with_on_demand_transforms(
690690
# For unified FeatureViews with transformations, always execute transforms
691691
# For OnDemandFeatureViews, check write_to_online_store setting
692692
should_transform = (
693-
hasattr(odfv, 'feature_transformation') and odfv.feature_transformation is not None
694-
) or not getattr(odfv, 'write_to_online_store', True)
693+
hasattr(odfv, "feature_transformation")
694+
and odfv.feature_transformation is not None
695+
) or not getattr(odfv, "write_to_online_store", True)
695696

696697
if should_transform:
697698
# Apply aggregations if configured.
698-
aggregations = getattr(odfv, 'aggregations', [])
699-
mode_attr = getattr(odfv, 'mode', 'pandas')
699+
aggregations = getattr(odfv, "aggregations", [])
700+
mode_attr = getattr(odfv, "mode", "pandas")
700701
# Handle TransformationMode enum values
701-
mode = mode_attr.value if hasattr(mode_attr, 'value') else mode_attr
702-
entities = getattr(odfv, 'entities', [])
702+
mode = mode_attr.value if hasattr(mode_attr, "value") else mode_attr
703+
entities = getattr(odfv, "entities", [])
703704
if aggregations:
704705
if mode == "python":
705706
if initial_response_dict is None:
@@ -727,23 +728,34 @@ def _augment_response_with_on_demand_transforms(
727728
if initial_response_dict is None:
728729
initial_response_dict = initial_response.to_dict()
729730
# Use feature_transformation for unified FeatureViews
730-
if hasattr(odfv, 'feature_transformation') and odfv.feature_transformation:
731-
transformed_features_dict = odfv.feature_transformation.udf(initial_response_dict)
731+
if (
732+
hasattr(odfv, "feature_transformation")
733+
and odfv.feature_transformation
734+
):
735+
transformed_features_dict = odfv.feature_transformation.udf(
736+
initial_response_dict
737+
)
732738
else:
733739
# Fallback to OnDemandFeatureView method
734-
transformed_features_dict: Dict[str, List[Any]] = odfv.transform_dict(
735-
initial_response_dict
740+
transformed_features_dict: Dict[str, List[Any]] = (
741+
odfv.transform_dict(initial_response_dict)
736742
)
737743
elif mode in {"pandas", "substrait"}:
738744
if initial_response_arrow is None:
739745
initial_response_arrow = initial_response.to_arrow()
740746
# Use feature_transformation for unified FeatureViews
741-
if hasattr(odfv, 'feature_transformation') and odfv.feature_transformation:
747+
if (
748+
hasattr(odfv, "feature_transformation")
749+
and odfv.feature_transformation
750+
):
742751
if mode == "pandas":
743752
df = initial_response_arrow.to_pandas()
744753
transformed_df = odfv.feature_transformation.udf(df)
745754
import pyarrow as pa
746-
transformed_features_arrow = pa.Table.from_pandas(transformed_df)
755+
756+
transformed_features_arrow = pa.Table.from_pandas(
757+
transformed_df
758+
)
747759
else:
748760
# For substrait mode, fallback to OnDemandFeatureView method
749761
transformed_features_arrow = odfv.transform_arrow(
@@ -772,7 +784,7 @@ def _augment_response_with_on_demand_transforms(
772784
selected_subset = [f for f in transformed_columns if f in _feature_refs]
773785

774786
proto_values = []
775-
schema_dict = {k.name: k.dtype for k in getattr(odfv, 'schema', [])}
787+
schema_dict = {k.name: k.dtype for k in getattr(odfv, "schema", [])}
776788
for selected_feature in selected_subset:
777789
feature_vector = transformed_features[selected_feature]
778790
selected_feature_type = schema_dict.get(selected_feature, None)
@@ -1215,17 +1227,24 @@ def _get_feature_views_to_use(
12151227
od_fvs_to_use.append(
12161228
fv.with_projection(copy.copy(projection)) if projection else fv
12171229
)
1218-
elif hasattr(fv, 'feature_transformation') and fv.feature_transformation is not None:
1230+
elif (
1231+
hasattr(fv, "feature_transformation")
1232+
and fv.feature_transformation is not None
1233+
):
12191234
# Handle unified FeatureViews with transformations like OnDemandFeatureViews
12201235
od_fvs_to_use.append(
12211236
fv.with_projection(copy.copy(projection)) if projection else fv
12221237
)
12231238

12241239
# For unified FeatureViews, source FeatureViews are stored in source_views property
1225-
source_views = fv.source_views if hasattr(fv, 'source_views') and fv.source_views else []
1240+
source_views = (
1241+
fv.source_views
1242+
if hasattr(fv, "source_views") and fv.source_views
1243+
else []
1244+
)
12261245
for source_fv in source_views:
12271246
# source_fv is already a FeatureView object for unified FeatureViews
1228-
if hasattr(source_fv, 'name'):
1247+
if hasattr(source_fv, "name"):
12291248
# If it's a FeatureView, get it from registry to ensure it's up to date
12301249
source_fv = registry.get_any_feature_view(
12311250
source_fv.name, project, allow_cache
@@ -1380,7 +1399,9 @@ def _prepare_entities_to_read_from_online_store(
13801399
]
13811400
odfv_entities.extend(entities_for_odfv)
13821401
# Check if the feature view has source_request_sources (OnDemandFeatureView attribute)
1383-
source_request_sources = getattr(on_demand_feature_view, 'source_request_sources', {})
1402+
source_request_sources = getattr(
1403+
on_demand_feature_view, "source_request_sources", {}
1404+
)
13841405
for source in source_request_sources:
13851406
source_schema = source_request_sources[source].schema
13861407
for column in source_schema:

sdk/python/tests/unit/test_unified_aggregation_functionality.py

Lines changed: 23 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -5,18 +5,18 @@
55
aggregation functionality working with the new unified transformation system.
66
"""
77

8-
import pyarrow as pa
9-
import pandas as pd
10-
import pytest
118
from typing import Any, Dict
129

10+
import pandas as pd
11+
import pyarrow as pa
12+
1313
from feast.aggregation import Aggregation
14-
from feast.utils import _apply_aggregations_to_response
15-
from feast.transformation.base import transformation
1614
from feast.feature_view import FeatureView
1715
from feast.field import Field
1816
from feast.infra.offline_stores.file_source import FileSource
17+
from feast.transformation.base import transformation
1918
from feast.types import Float32, Int64
19+
from feast.utils import _apply_aggregations_to_response
2020

2121

2222
def test_aggregation_python_mode():
@@ -113,9 +113,7 @@ def aggregation_transform(inputs: Dict[str, Any]) -> Dict[str, Any]:
113113
]
114114

115115
# Apply aggregations using the utility function
116-
result = _apply_aggregations_to_response(
117-
inputs, aggs, ["driver_id"], "python"
118-
)
116+
result = _apply_aggregations_to_response(inputs, aggs, ["driver_id"], "python")
119117
return result
120118

121119
# Create unified FeatureView with aggregation transformation
@@ -168,16 +166,16 @@ def test_unified_transformation_with_aggregation_pandas():
168166
def pandas_aggregation_transform(inputs: pd.DataFrame) -> pd.DataFrame:
169167
"""Pandas transformation that performs aggregation using groupby."""
170168
# Perform aggregation using pandas groupby
171-
result = inputs.groupby("driver_id").agg({
172-
"trips": "sum",
173-
"revenue": "mean"
174-
}).reset_index()
169+
result = (
170+
inputs.groupby("driver_id")
171+
.agg({"trips": "sum", "revenue": "mean"})
172+
.reset_index()
173+
)
175174

176175
# Rename columns to match expected output
177-
result = result.rename(columns={
178-
"trips": "sum_trips",
179-
"revenue": "mean_revenue"
180-
})
176+
result = result.rename(
177+
columns={"trips": "sum_trips", "revenue": "mean_revenue"}
178+
)
181179

182180
return result
183181

@@ -195,11 +193,13 @@ def pandas_aggregation_transform(inputs: pd.DataFrame) -> pd.DataFrame:
195193
)
196194

197195
# Test the transformation directly
198-
test_data = pd.DataFrame({
199-
"driver_id": [1, 1, 2, 2],
200-
"trips": [10, 20, 15, 25],
201-
"revenue": [100.0, 200.0, 150.0, 250.0],
202-
})
196+
test_data = pd.DataFrame(
197+
{
198+
"driver_id": [1, 1, 2, 2],
199+
"trips": [10, 20, 15, 25],
200+
"revenue": [100.0, 200.0, 150.0, 250.0],
201+
}
202+
)
203203

204204
result = unified_pandas_aggregation_view.feature_transformation.udf(test_data)
205205

@@ -325,7 +325,7 @@ def write_aggregation_transform(inputs: Dict[str, Any]) -> Dict[str, Any]:
325325
)
326326

327327
# Verify online setting
328-
assert unified_write_aggregation_view.online == True
328+
assert unified_write_aggregation_view.online
329329

330330
# Test the transformation
331331
test_data = {
@@ -340,4 +340,4 @@ def write_aggregation_transform(inputs: Dict[str, Any]) -> Dict[str, Any]:
340340
"sum_trips": [30, 40],
341341
}
342342

343-
assert result == expected
343+
assert result == expected

sdk/python/tests/unit/test_unified_feature_view_functionality.py

Lines changed: 11 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,11 @@
55
unified transformation system with FeatureView + feature_transformation
66
instead of OnDemandFeatureView.
77
"""
8+
89
import datetime
910
from typing import Any, Dict, List
1011

1112
import pandas as pd
12-
import pytest
1313

1414
from feast.feature_view import FeatureView
1515
from feast.field import Field
@@ -51,8 +51,9 @@ def python_writes_test_udf(features_dict: Dict[str, Any]) -> Dict[str, Any]:
5151

5252
def test_hash():
5353
"""Test that unified FeatureViews with same transformations hash the same way."""
54-
import tempfile
5554
import os
55+
import tempfile
56+
5657
with tempfile.TemporaryDirectory() as temp_dir:
5758
test_path = os.path.join(temp_dir, "test.parquet")
5859
sink_path = os.path.join(temp_dir, "sink.parquet")
@@ -189,7 +190,9 @@ def python_native_transform(features_dict: Dict[str, Any]) -> Dict[str, Any]:
189190
)
190191

191192
assert unified_feature_view_python_native.feature_transformation is not None
192-
assert unified_feature_view_python_native.feature_transformation.mode.value == "python"
193+
assert (
194+
unified_feature_view_python_native.feature_transformation.mode.value == "python"
195+
)
193196

194197
# Test that transformation works
195198
test_input = {"feature1": [0], "feature2": [1]}
@@ -274,15 +277,15 @@ def pandas_transform_writes(features_df: pd.DataFrame) -> pd.DataFrame:
274277
)
275278

276279
# Test that online setting is preserved
277-
assert unified_feature_view.online == True
280+
assert unified_feature_view.online
278281

279282
# Test proto serialization preserves this setting
280283
proto = unified_feature_view.to_proto()
281-
assert proto.spec.online == True
284+
assert proto.spec.online
282285

283286
try:
284287
reserialized_proto = FeatureView.from_proto(proto)
285-
assert reserialized_proto.online == True
288+
assert reserialized_proto.online
286289
print("✅ Write functionality test completed successfully")
287290
except Exception as e:
288291
print(f"Proto write functionality behavior may vary: {e}")
@@ -370,7 +373,7 @@ def transform_features(features_df: pd.DataFrame) -> pd.DataFrame:
370373
assert unified_fv.feature_transformation is not None
371374

372375
# Test that transformation has the expected name (if set)
373-
if hasattr(transform_features, 'name'):
376+
if hasattr(transform_features, "name"):
374377
assert transform_features.name == "transform_features"
375378

376379
# Test proto serialization
@@ -416,4 +419,4 @@ def another_transform(features_df: pd.DataFrame) -> pd.DataFrame:
416419
assert deserialized.name == CUSTOM_FUNCTION_NAME
417420
print("✅ Custom name test completed successfully")
418421
except Exception as e:
419-
print(f"Custom name behavior may vary: {e}")
422+
print(f"Custom name behavior may vary: {e}")

0 commit comments

Comments
 (0)