Visitar URL original
feat: Unify Feature Transformations and Feature Views by franciscojavierarceo · Pull Request #5747 · feast-dev/feast · GitHub
Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
Show all changes
34 commits
Select commit Hold shift + click to select a range
837e34f
feat: Add unified transformation
franciscojavierarceo Nov 26, 2025
d12fbfd
feat: Unify transformations
franciscojavierarceo Nov 27, 2025
9a2df49
feat: Unify Transformations
franciscojavierarceo Nov 28, 2025
9aceb7f
feat: Unify Transformations
franciscojavierarceo Nov 28, 2025
5c8b93c
updated docs
franciscojavierarceo Dec 4, 2025
6d5ce47
refactor: separate transformation logic from execution decisions with…
franciscojavierarceo Dec 23, 2025
9380cf9
format
franciscojavierarceo Dec 23, 2025
5b759ed
incorporaitng feedback
franciscojavierarceo Dec 24, 2025
2d7a43b
updated
franciscojavierarceo Dec 25, 2025
b6299d2
fix
franciscojavierarceo Dec 26, 2025
3726733
linter
franciscojavierarceo Dec 26, 2025
f55d4b4
cleanup
franciscojavierarceo Dec 26, 2025
716e692
cleanup
franciscojavierarceo Dec 26, 2025
a4f2e0a
more fix
franciscojavierarceo Dec 29, 2025
02ae40b
more fix
franciscojavierarceo Dec 29, 2025
74de467
updated
franciscojavierarceo Dec 31, 2025
c360aef
fix
franciscojavierarceo Jan 5, 2026
f73431c
fix
franciscojavierarceo Jan 5, 2026
50e536a
fix
franciscojavierarceo Jan 5, 2026
a87c4b4
fix
franciscojavierarceo Jan 5, 2026
e2f722c
Merge branch 'master' into refactor-odfv
franciscojavierarceo Jan 5, 2026
dde05bd
lint
franciscojavierarceo Jan 5, 2026
0e1f037
fix
franciscojavierarceo Jan 6, 2026
662e21b
linter
franciscojavierarceo Jan 6, 2026
9f48c75
fix linter
franciscojavierarceo Jan 6, 2026
a058aac
fix
franciscojavierarceo Jan 6, 2026
b8771c9
fix(redis): Preserve millisecond timestamp precision for Redis online…
jatin5251 Jan 6, 2026
34d9b52
feat: Add GCS registry store in Go feature server (#5818)
samuelkim7 Jan 6, 2026
6c35d45
chore: Refactor some unit tests into integration tests (#5820)
franciscojavierarceo Jan 6, 2026
5bcd6e6
test: Remove e2e_rhoai package tests
Srihari1192 Jan 5, 2026
61812ba
fix
franciscojavierarceo Jan 7, 2026
d4adcd5
fix
franciscojavierarceo Jan 7, 2026
a007be3
fix
franciscojavierarceo Jan 8, 2026
416b15c
uploading progress
franciscojavierarceo Jan 13, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Prev Previous commit
Next Next commit
fix
Signed-off-by: Francisco Javier Arceo <farceo@redhat.com>
  • Loading branch information
franciscojavierarceo committed Jan 7, 2026
commit d4adcd5ba0fea352762d8b0b3bc53663c0e9b12f
26 changes: 20 additions & 6 deletions sdk/python/feast/feature_store.py
Original file line number Diff line number Diff line change
Expand Up @@ -1387,11 +1387,13 @@ def get_historical_features(
# EXCEPT for remote providers where the server needs to handle OnDemandFeatureViews
provider_feature_refs = []
transformation_view_names = [fv.name for fv, _ in unified_transformation_views]
odfv_names = [odfv.name for odfv in on_demand_feature_views]

# Check if using remote offline store
from feast.infra.offline_stores.remote import RemoteOfflineStoreConfig
is_remote_provider = isinstance(self.config.offline_store, RemoteOfflineStoreConfig)

is_remote_provider = isinstance(
self.config.offline_store, RemoteOfflineStoreConfig
)

# For remote providers, send ALL feature references to the server
# The server has access to the full registry and can handle OnDemandFeatureViews
Expand Down Expand Up @@ -2278,11 +2280,17 @@ def write_to_online_store(

# If transform_on_write is not explicitly set, use auto-detection
if transform_on_write is None:
feature_view = self._registry.get_any_feature_view(
fv_for_detection = self._registry.get_any_feature_view(
feature_view_name, self.project, allow_cache=allow_registry_cache
)
input_data = df if df is not None else inputs
transform_on_write = should_apply_transformation(feature_view, input_data)
# Only apply transformation detection for FeatureView or OnDemandFeatureView
if isinstance(fv_for_detection, (FeatureView, OnDemandFeatureView)):
transform_on_write = should_apply_transformation(
fv_for_detection, input_data
)
else:
transform_on_write = True # Default for other types
if transform_on_write is None:
transform_on_write = True # Default fallback

Expand All @@ -2305,8 +2313,14 @@ def write_to_online_store(
if feature_column_names:
# For OnDemandFeatureViews with transform_on_write=False, skip feature column validation
# since the dataframe contains raw input data, not computed output features
missing_columns = [col for col in feature_column_names if col not in df.columns]
if missing_columns and isinstance(feature_view, OnDemandFeatureView) and not transform_on_write:
missing_columns = [
col for col in feature_column_names if col not in df.columns
]
if (
missing_columns
and isinstance(feature_view, OnDemandFeatureView)
and not transform_on_write
):
# Raw input data for OnDemandFeatureView - computed features not present yet, skip validation
pass
elif not missing_columns:
Expand Down
6 changes: 4 additions & 2 deletions sdk/python/feast/stream_feature_view.py
Original file line number Diff line number Diff line change
Expand Up @@ -226,8 +226,10 @@ def __eq__(self, other):

# Only compare bytecode if both UDFs have the same name and string representation
# This makes serialization/deserialization more robust
if (self.udf.__name__ != other.udf.__name__ or
self.udf_string != other.udf_string):
if (
self.udf.__name__ != other.udf.__name__
or self.udf_string != other.udf_string
):
return False

return True
Expand Down
10 changes: 5 additions & 5 deletions sdk/python/feast/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -794,9 +794,7 @@ def _augment_response_with_on_demand_transforms(
if initial_response_dict is None:
initial_response_dict = initial_response.to_dict()
# Always use transform_dict for OnDemandFeatureViews - it handles singleton mode properly
transformed_features_dict = odfv.transform_dict(
initial_response_dict
)
transformed_features_dict = odfv.transform_dict(initial_response_dict)
transformed_features = transformed_features_dict
elif mode in {"pandas", "substrait"}:
if initial_response_arrow is None:
Expand Down Expand Up @@ -1359,11 +1357,13 @@ def _get_feature_views_to_use(

# Ensure OnDemandFeatureView source dependencies are included
for odfv in od_fvs_to_use:
if hasattr(odfv, 'source_feature_view_projections'):
if hasattr(odfv, "source_feature_view_projections"):
for source_fv_projection in odfv.source_feature_view_projections.values():
# Get the actual feature view from registry
try:
source_fv = registry.get_any_feature_view(source_fv_projection.name, project, allow_cache)
source_fv = registry.get_any_feature_view(
source_fv_projection.name, project, allow_cache
)
if source_fv and source_fv not in fvs_to_use:
fvs_to_use.append(source_fv)
except Exception:
Expand Down
10 changes: 3 additions & 7 deletions sdk/python/tests/unit/online_store/test_online_writes.py
Original file line number Diff line number Diff line change
Expand Up @@ -60,13 +60,9 @@ def setUp(self):
start_date = end_date - timedelta(days=15)

driver_entities = [1001, 1002, 1003, 1004, 1005]
driver_df = create_driver_hourly_stats_df(
driver_entities, start_date, end_date
)
driver_df = create_driver_hourly_stats_df(driver_entities, start_date, end_date)
driver_stats_path = os.path.join(data_dir, "driver_stats.parquet")
driver_df.to_parquet(
path=driver_stats_path, allow_truncated_timestamps=True
)
driver_df.to_parquet(path=driver_stats_path, allow_truncated_timestamps=True)

driver = Entity(name="driver", join_keys=["driver_id"])

Expand Down Expand Up @@ -133,7 +129,7 @@ def test_view(inputs: dict[str, Any]) -> dict[str, Any]:
)

def tearDown(self):
if hasattr(self, 'temp_dir'):
if hasattr(self, "temp_dir"):
shutil.rmtree(self.temp_dir, ignore_errors=True)

def test_online_retrieval(self):
Expand Down
36 changes: 12 additions & 24 deletions sdk/python/tests/unit/test_on_demand_python_transformation.py
Original file line number Diff line number Diff line change
Expand Up @@ -64,13 +64,9 @@ def setUp(self):
start_date = end_date - timedelta(days=15)

driver_entities = [1001, 1002, 1003, 1004, 1005]
driver_df = create_driver_hourly_stats_df(
driver_entities, start_date, end_date
)
driver_df = create_driver_hourly_stats_df(driver_entities, start_date, end_date)
driver_stats_path = os.path.join(data_dir, "driver_stats.parquet")
driver_df.to_parquet(
path=driver_stats_path, allow_truncated_timestamps=True
)
driver_df.to_parquet(path=driver_stats_path, allow_truncated_timestamps=True)

driver = Entity(
name="driver", join_keys=["driver_id"], value_type=ValueType.INT64
Expand Down Expand Up @@ -123,9 +119,7 @@ def setUp(self):
)
def pandas_view(inputs: pd.DataFrame) -> pd.DataFrame:
df = pd.DataFrame()
df["conv_rate_plus_acc_pandas"] = (
inputs["conv_rate"] + inputs["acc_rate"]
)
df["conv_rate_plus_acc_pandas"] = inputs["conv_rate"] + inputs["acc_rate"]
return df

@on_demand_feature_view(
Expand All @@ -136,9 +130,7 @@ def pandas_view(inputs: pd.DataFrame) -> pd.DataFrame:
def python_view(inputs: dict[str, Any]) -> dict[str, Any]:
output: dict[str, Any] = {
"conv_rate_plus_acc_python": conv_rate + acc_rate
for conv_rate, acc_rate in zip(
inputs["conv_rate"], inputs["acc_rate"]
)
for conv_rate, acc_rate in zip(inputs["conv_rate"], inputs["acc_rate"])
}
return output

Expand Down Expand Up @@ -244,8 +236,9 @@ def python_stored_writes_feature_view(
assert len(self.store.list_stream_feature_views()) == 0

def tearDown(self):
if hasattr(self, 'temp_dir'):
if hasattr(self, "temp_dir"):
import shutil

shutil.rmtree(self.temp_dir, ignore_errors=True)

def test_setup(self):
Expand Down Expand Up @@ -424,13 +417,9 @@ def setUp(self):
start_date = end_date - timedelta(days=15)

driver_entities = [1001, 1002, 1003, 1004, 1005]
driver_df = create_driver_hourly_stats_df(
driver_entities, start_date, end_date
)
driver_df = create_driver_hourly_stats_df(driver_entities, start_date, end_date)
driver_stats_path = os.path.join(data_dir, "driver_stats.parquet")
driver_df.to_parquet(
path=driver_stats_path, allow_truncated_timestamps=True
)
driver_df.to_parquet(path=driver_stats_path, allow_truncated_timestamps=True)

driver = Entity(name="driver", join_keys=["driver_id"])

Expand Down Expand Up @@ -648,9 +637,7 @@ def pandas_view(features_df: pd.DataFrame) -> pd.DataFrame:
"val_to_add_2",
]
resp_online_missing_entity = self.store.get_online_features(
entity_rows=[
{"driver_id": 1234567890, "val_to_add": 0, "val_to_add_2": 1}
],
entity_rows=[{"driver_id": 1234567890, "val_to_add": 0, "val_to_add_2": 1}],
features=[
"driver_hourly_stats:conv_rate",
"driver_hourly_stats:acc_rate",
Expand Down Expand Up @@ -693,8 +680,9 @@ def pandas_view(features_df: pd.DataFrame) -> pd.DataFrame:
assert sorted(resp_online.columns) != sorted(resp_offline.columns)

def tearDown(self):
if hasattr(self, 'temp_dir'):
if hasattr(self, "temp_dir"):
import shutil

shutil.rmtree(self.temp_dir, ignore_errors=True)

def test_setup(self):
Expand Down Expand Up @@ -1451,7 +1439,7 @@ def python_stored_writes_feature_view_explode_singleton(

assert odfv_applied.entities == [chunk.name, document.name]

# Note here that after apply() is called, the entity_columns are populated with the join_key
# Note here that after apply() is called, the entity_columns are populated with the join_key
assert odfv_applied.entity_columns[1].name == chunk.join_key
assert odfv_applied.entity_columns[0].name == document.join_key

Expand Down
Loading