From 066880f5fb4db64afe2198ef708401ce4fda0471 Mon Sep 17 00:00:00 2001 From: Francisco Javier Arceo Date: Wed, 3 Apr 2024 00:14:57 -0400 Subject: [PATCH 1/9] checking in progress...trying to fix tests Signed-off-by: Francisco Javier Arceo --- .../example_repos/example_feature_repo_1.py | 15 +++ .../online_store/test_online_retrieval.py | 7 ++ .../tests/unit/test_on_demand_feature_view.py | 2 + .../test_on_demand_python_transformation.py | 117 ++++++++++++++++++ 4 files changed, 141 insertions(+) create mode 100644 sdk/python/tests/unit/test_on_demand_python_transformation.py diff --git a/sdk/python/tests/example_repos/example_feature_repo_1.py b/sdk/python/tests/example_repos/example_feature_repo_1.py index eca9aee57c9..1eb27147d50 100644 --- a/sdk/python/tests/example_repos/example_feature_repo_1.py +++ b/sdk/python/tests/example_repos/example_feature_repo_1.py @@ -1,7 +1,10 @@ from datetime import timedelta +import pandas as pd + from feast import Entity, FeatureService, FeatureView, Field, FileSource, PushSource from feast.types import Float32, Int64, String +from feast.on_demand_feature_view import on_demand_feature_view # Note that file source paths are not validated, so there doesn't actually need to be any data # at the paths for these file sources. Since these paths are effectively fake, this example @@ -98,6 +101,18 @@ tags={}, ) +@on_demand_feature_view( + sources=[customer_driver_combined_source], + schema=[ + Field(name='on_demand_feature', dtype=Int64) + ], + mode="pandas", +) +def customer_driver_combined_pandas_odfv(inputs: pd.DataFrame) -> pd.DataFrame: + outputs = pd.DataFrame() + outputs['on_demand_feature'] = inputs['trips'] + 1 + return outputs + all_drivers_feature_service = FeatureService( name="driver_locations_service", diff --git a/sdk/python/tests/unit/online_store/test_online_retrieval.py b/sdk/python/tests/unit/online_store/test_online_retrieval.py index 926c7226fc8..dfd4e75e98a 100644 --- a/sdk/python/tests/unit/online_store/test_online_retrieval.py +++ b/sdk/python/tests/unit/online_store/test_online_retrieval.py @@ -124,6 +124,13 @@ def test_online() -> None: assert "trips" in result + result = store.get_online_features( + features=["customer_driver_combined_pandas_odfv:on_demand_feature"], + entity_rows=[{"driver_id": 0, "customer_id": 0}], + full_feature_names=False, + ).to_dict() + print(result) + assert 1 == 2 # invalid table reference with pytest.raises(FeatureViewNotFoundException): store.get_online_features( diff --git a/sdk/python/tests/unit/test_on_demand_feature_view.py b/sdk/python/tests/unit/test_on_demand_feature_view.py index cf4afa94228..9161876cb55 100644 --- a/sdk/python/tests/unit/test_on_demand_feature_view.py +++ b/sdk/python/tests/unit/test_on_demand_feature_view.py @@ -211,6 +211,8 @@ def test_python_native_transformation_mode(): } ) == {"feature1": 0, "feature2": 1, "output1": 100, "output2": 102} +# def test_get_online_features_on_demand(): + @pytest.mark.filterwarnings("ignore:udf and udf_string parameters are deprecated") def test_from_proto_backwards_compatible_udf(): diff --git a/sdk/python/tests/unit/test_on_demand_python_transformation.py b/sdk/python/tests/unit/test_on_demand_python_transformation.py new file mode 100644 index 00000000000..8ab07e7c830 --- /dev/null +++ b/sdk/python/tests/unit/test_on_demand_python_transformation.py @@ -0,0 +1,117 @@ +import os +import tempfile +from datetime import datetime, timedelta + +import pandas as pd + +from feast import Entity, FeatureStore, FeatureView, FileSource, RepoConfig +from feast.driver_test_data import create_driver_hourly_stats_df +from feast.field import Field +from feast.infra.online_stores.sqlite import SqliteOnlineStoreConfig +from feast.on_demand_feature_view import on_demand_feature_view +from feast.types import Float32, Float64, Int64 +from typing import Dict, Any + + +def test_python_pandas_parity(): + with tempfile.TemporaryDirectory() as data_dir: + store = FeatureStore( + config=RepoConfig( + project="test_on_demand_python_transformation", + registry=os.path.join(data_dir, "registry.db"), + provider="local", + entity_key_serialization_version=2, + online_store=SqliteOnlineStoreConfig( + path=os.path.join(data_dir, "online.db") + ), + ) + ) + + # Generate test data. + end_date = datetime.now().replace(microsecond=0, second=0, minute=0) + 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_stats_path = os.path.join(data_dir, "driver_stats.parquet") + driver_df.to_parquet(path=driver_stats_path, allow_truncated_timestamps=True) + + driver = Entity(name="driver", join_keys=["driver_id"]) + + driver_stats_source = FileSource( + name="driver_hourly_stats_source", + path=driver_stats_path, + timestamp_field="event_timestamp", + created_timestamp_column="created", + ) + + driver_stats_fv = FeatureView( + name="driver_hourly_stats", + entities=[driver], + ttl=timedelta(days=1), + schema=[ + Field(name="conv_rate", dtype=Float32), + Field(name="acc_rate", dtype=Float32), + Field(name="avg_daily_trips", dtype=Int64), + ], + online=True, + source=driver_stats_source, + ) + + @on_demand_feature_view( + sources=[driver_stats_fv], + schema=[Field(name="conv_rate_plus_acc", dtype=Float64)], + mode="pandas", + ) + def pandas_view(inputs: pd.DataFrame) -> pd.DataFrame: + df = pd.DataFrame() + df["conv_rate_plus_acc"] = inputs["conv_rate"] + inputs["acc_rate"] + return df + + # @on_demand_feature_view( + # sources=[driver_stats_fv[["conv_rate", "acc_rate"]]], + # schema=[Field(name="conv_rate_plus_acc_python", dtype=Float64)], + # mode="python", + # ) + # def python_view(inputs: Dict[str, Any]) -> Dict[str, Any]: + # output: Dict[str, Any] = {'conv_rate_plus_acc_python': inputs['conv_rate'] + inputs['acc_rate']} + # return output + + store.apply( + [driver, driver_stats_source, driver_stats_fv, pandas_view] + ) + + entity_rows = [ + { + # entity's join key -> entity values + "driver_id": 1001, + # "event_timestamp" (reserved key) -> timestamps + "event_timestamp": datetime(2021, 4, 12, 10, 59, 42), + } + ] + entity_df = pd.DataFrame.from_dict( + { + # entity's join key -> entity values + "driver_id": [1001], + # "event_timestamp" (reserved key) -> timestamps + "event_timestamp": [ + datetime(2021, 4, 12, 10, 59, 42), + ], + } + ) + + training_df = store.get_online_features( + # entity_rows=entity_rows, + entity_rows=entity_df, + features=[ + "driver_hourly_stats:conv_rate", + "driver_hourly_stats:acc_rate", + "driver_hourly_stats:avg_daily_trips", + # "python_view:conv_rate_plus_acc_python", + "pandas_view:conv_rate_plus_acc", + ], + ).to_df() + + assert training_df["conv_rate_plus_acc"].equals( + training_df["conv_rate_plus_acc_python"] + ) From 79e6edcd8d1f46b54ce7e7f4286ba79e2bb00b0b Mon Sep 17 00:00:00 2001 From: Francisco Javier Arceo Date: Wed, 3 Apr 2024 08:31:22 -0400 Subject: [PATCH 2/9] testing more... Signed-off-by: Francisco Javier Arceo --- .../test_on_demand_pandas_transformation.py | 90 +++++++++++++++++++ .../test_on_demand_python_transformation.py | 27 ++---- 2 files changed, 97 insertions(+), 20 deletions(-) create mode 100644 sdk/python/tests/unit/test_on_demand_pandas_transformation.py diff --git a/sdk/python/tests/unit/test_on_demand_pandas_transformation.py b/sdk/python/tests/unit/test_on_demand_pandas_transformation.py new file mode 100644 index 00000000000..3ace59fa0de --- /dev/null +++ b/sdk/python/tests/unit/test_on_demand_pandas_transformation.py @@ -0,0 +1,90 @@ +import os +import tempfile +from datetime import datetime, timedelta + +import pandas as pd + +from feast import Entity, FeatureStore, FeatureView, FileSource, RepoConfig +from feast.driver_test_data import create_driver_hourly_stats_df +from feast.field import Field +from feast.infra.online_stores.sqlite import SqliteOnlineStoreConfig +from feast.on_demand_feature_view import on_demand_feature_view +from feast.types import Float32, Float64, Int64 + + +def test_pandas_transformation(): + with tempfile.TemporaryDirectory() as data_dir: + store = FeatureStore( + config=RepoConfig( + project="test_on_demand_python_transformation", + registry=os.path.join(data_dir, "registry.db"), + provider="local", + entity_key_serialization_version=2, + online_store=SqliteOnlineStoreConfig( + path=os.path.join(data_dir, "online.db") + ), + ) + ) + + # Generate test data. + end_date = datetime.now().replace(microsecond=0, second=0, minute=0) + 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_stats_path = os.path.join(data_dir, "driver_stats.parquet") + driver_df.to_parquet(path=driver_stats_path, allow_truncated_timestamps=True) + + driver = Entity(name="driver", join_keys=["driver_id"]) + + driver_stats_source = FileSource( + name="driver_hourly_stats_source", + path=driver_stats_path, + timestamp_field="event_timestamp", + created_timestamp_column="created", + ) + + driver_stats_fv = FeatureView( + name="driver_hourly_stats", + entities=[driver], + ttl=timedelta(days=0), + schema=[ + Field(name="conv_rate", dtype=Float32), + Field(name="acc_rate", dtype=Float32), + Field(name="avg_daily_trips", dtype=Int64), + ], + online=True, + source=driver_stats_source, + ) + + @on_demand_feature_view( + sources=[driver_stats_fv], + schema=[Field(name="conv_rate_plus_acc", dtype=Float64)], + mode="pandas", + ) + def pandas_view(inputs: pd.DataFrame) -> pd.DataFrame: + df = pd.DataFrame() + df["conv_rate_plus_acc"] = inputs["conv_rate"] + inputs["acc_rate"] + return df + + store.apply( + [driver, driver_stats_source, driver_stats_fv, pandas_view] + ) + + entity_rows = [ + { + "driver_id": 1001, + } + ] + + online_response = store.get_online_features( + entity_rows=entity_rows, + features=[ + "driver_hourly_stats:conv_rate", + "driver_hourly_stats:acc_rate", + "driver_hourly_stats:avg_daily_trips", + "pandas_view:conv_rate_plus_acc", + ], + ).to_df() + + assert online_response["conv_rate_plus_acc"].equals(1) diff --git a/sdk/python/tests/unit/test_on_demand_python_transformation.py b/sdk/python/tests/unit/test_on_demand_python_transformation.py index 8ab07e7c830..80842f06bec 100644 --- a/sdk/python/tests/unit/test_on_demand_python_transformation.py +++ b/sdk/python/tests/unit/test_on_demand_python_transformation.py @@ -48,7 +48,7 @@ def test_python_pandas_parity(): driver_stats_fv = FeatureView( name="driver_hourly_stats", entities=[driver], - ttl=timedelta(days=1), + ttl=0, schema=[ Field(name="conv_rate", dtype=Float32), Field(name="acc_rate", dtype=Float32), @@ -83,35 +83,22 @@ def pandas_view(inputs: pd.DataFrame) -> pd.DataFrame: entity_rows = [ { - # entity's join key -> entity values "driver_id": 1001, - # "event_timestamp" (reserved key) -> timestamps - "event_timestamp": datetime(2021, 4, 12, 10, 59, 42), + # "event_timestamp": datetime(2021, 4, 12, 10, 59, 42), } ] - entity_df = pd.DataFrame.from_dict( - { - # entity's join key -> entity values - "driver_id": [1001], - # "event_timestamp" (reserved key) -> timestamps - "event_timestamp": [ - datetime(2021, 4, 12, 10, 59, 42), - ], - } - ) - training_df = store.get_online_features( - # entity_rows=entity_rows, - entity_rows=entity_df, + online_response = store.get_online_features( + entity_rows=entity_rows, features=[ "driver_hourly_stats:conv_rate", "driver_hourly_stats:acc_rate", "driver_hourly_stats:avg_daily_trips", - # "python_view:conv_rate_plus_acc_python", + "python_view:conv_rate_plus_acc_python", "pandas_view:conv_rate_plus_acc", ], ).to_df() - assert training_df["conv_rate_plus_acc"].equals( - training_df["conv_rate_plus_acc_python"] + assert online_response["conv_rate_plus_acc"].equals( + online_response["conv_rate_plus_acc_python"] ) From 262c58600d91ff6e01804185cf5401d5d9ff05e8 Mon Sep 17 00:00:00 2001 From: Francisco Javier Arceo Date: Wed, 3 Apr 2024 11:03:35 -0400 Subject: [PATCH 3/9] fixed Signed-off-by: Francisco Javier Arceo --- .../tests/data/driver_hourly_stats.parquet | Bin 0 -> 35177 bytes .../example_repos/example_feature_repo_1.py | 8 ++++---- .../unit/online_store/test_online_retrieval.py | 12 ++++++++---- 3 files changed, 12 insertions(+), 8 deletions(-) create mode 100644 sdk/python/tests/data/driver_hourly_stats.parquet diff --git a/sdk/python/tests/data/driver_hourly_stats.parquet b/sdk/python/tests/data/driver_hourly_stats.parquet new file mode 100644 index 0000000000000000000000000000000000000000..9efec515877340e367a5668e73c2d3751a71594a GIT binary patch literal 35177 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 literal 0 HcmV?d00001 diff --git a/sdk/python/tests/example_repos/example_feature_repo_1.py b/sdk/python/tests/example_repos/example_feature_repo_1.py index 1eb27147d50..fefc75dc4f8 100644 --- a/sdk/python/tests/example_repos/example_feature_repo_1.py +++ b/sdk/python/tests/example_repos/example_feature_repo_1.py @@ -102,15 +102,15 @@ ) @on_demand_feature_view( - sources=[customer_driver_combined_source], + sources=[customer_profile], schema=[ - Field(name='on_demand_feature', dtype=Int64) + Field(name='on_demand_age', dtype=Int64) ], mode="pandas", ) -def customer_driver_combined_pandas_odfv(inputs: pd.DataFrame) -> pd.DataFrame: +def customer_profile_pandas_odfv(inputs: pd.DataFrame) -> pd.DataFrame: outputs = pd.DataFrame() - outputs['on_demand_feature'] = inputs['trips'] + 1 + outputs['on_demand_age'] = inputs['age'] + 1 return outputs diff --git a/sdk/python/tests/unit/online_store/test_online_retrieval.py b/sdk/python/tests/unit/online_store/test_online_retrieval.py index dfd4e75e98a..6b8c8b0b981 100644 --- a/sdk/python/tests/unit/online_store/test_online_retrieval.py +++ b/sdk/python/tests/unit/online_store/test_online_retrieval.py @@ -125,12 +125,16 @@ def test_online() -> None: assert "trips" in result result = store.get_online_features( - features=["customer_driver_combined_pandas_odfv:on_demand_feature"], - entity_rows=[{"driver_id": 0, "customer_id": 0}], + features=["customer_profile_pandas_odfv:on_demand_age"], + entity_rows=[{"driver_id": 1, "customer_id": "5"}], full_feature_names=False, ).to_dict() - print(result) - assert 1 == 2 + + assert "on_demand_age" in result + assert result["driver_id"] == [1] + assert result["customer_id"] == ["5"] + assert result["on_demand_age"] == [4] + # invalid table reference with pytest.raises(FeatureViewNotFoundException): store.get_online_features( From 0ebfe7d5f3192c197dfa0e50e9fa8cf091eb208a Mon Sep 17 00:00:00 2001 From: Francisco Javier Arceo Date: Wed, 3 Apr 2024 12:52:07 -0400 Subject: [PATCH 4/9] fixed some tests Signed-off-by: Francisco Javier Arceo --- .../example_repos/example_feature_repo_1.py | 9 +- .../tests/unit/test_on_demand_feature_view.py | 1 + .../test_on_demand_pandas_transformation.py | 11 +- .../test_on_demand_python_transformation.py | 164 ++++++++++-------- 4 files changed, 107 insertions(+), 78 deletions(-) diff --git a/sdk/python/tests/example_repos/example_feature_repo_1.py b/sdk/python/tests/example_repos/example_feature_repo_1.py index fefc75dc4f8..fbf1fbb9b07 100644 --- a/sdk/python/tests/example_repos/example_feature_repo_1.py +++ b/sdk/python/tests/example_repos/example_feature_repo_1.py @@ -3,8 +3,8 @@ import pandas as pd from feast import Entity, FeatureService, FeatureView, Field, FileSource, PushSource -from feast.types import Float32, Int64, String from feast.on_demand_feature_view import on_demand_feature_view +from feast.types import Float32, Int64, String # Note that file source paths are not validated, so there doesn't actually need to be any data # at the paths for these file sources. Since these paths are effectively fake, this example @@ -101,16 +101,15 @@ tags={}, ) + @on_demand_feature_view( sources=[customer_profile], - schema=[ - Field(name='on_demand_age', dtype=Int64) - ], + schema=[Field(name="on_demand_age", dtype=Int64)], mode="pandas", ) def customer_profile_pandas_odfv(inputs: pd.DataFrame) -> pd.DataFrame: outputs = pd.DataFrame() - outputs['on_demand_age'] = inputs['age'] + 1 + outputs["on_demand_age"] = inputs["age"] + 1 return outputs diff --git a/sdk/python/tests/unit/test_on_demand_feature_view.py b/sdk/python/tests/unit/test_on_demand_feature_view.py index 9161876cb55..c9c46275ea2 100644 --- a/sdk/python/tests/unit/test_on_demand_feature_view.py +++ b/sdk/python/tests/unit/test_on_demand_feature_view.py @@ -211,6 +211,7 @@ def test_python_native_transformation_mode(): } ) == {"feature1": 0, "feature2": 1, "output1": 100, "output2": 102} + # def test_get_online_features_on_demand(): diff --git a/sdk/python/tests/unit/test_on_demand_pandas_transformation.py b/sdk/python/tests/unit/test_on_demand_pandas_transformation.py index 3ace59fa0de..c5f066dd83d 100644 --- a/sdk/python/tests/unit/test_on_demand_pandas_transformation.py +++ b/sdk/python/tests/unit/test_on_demand_pandas_transformation.py @@ -67,15 +67,16 @@ def pandas_view(inputs: pd.DataFrame) -> pd.DataFrame: df["conv_rate_plus_acc"] = inputs["conv_rate"] + inputs["acc_rate"] return df - store.apply( - [driver, driver_stats_source, driver_stats_fv, pandas_view] - ) + store.apply([driver, driver_stats_source, driver_stats_fv, pandas_view]) entity_rows = [ { "driver_id": 1001, } ] + store.write_to_online_store( + feature_view_name="driver_hourly_stats", df=driver_df + ) online_response = store.get_online_features( entity_rows=entity_rows, @@ -87,4 +88,6 @@ def pandas_view(inputs: pd.DataFrame) -> pd.DataFrame: ], ).to_df() - assert online_response["conv_rate_plus_acc"].equals(1) + assert online_response["conv_rate_plus_acc"].equals( + online_response["conv_rate"] + online_response["acc_rate"] + ) diff --git a/sdk/python/tests/unit/test_on_demand_python_transformation.py b/sdk/python/tests/unit/test_on_demand_python_transformation.py index 80842f06bec..4c5ffbfcd04 100644 --- a/sdk/python/tests/unit/test_on_demand_python_transformation.py +++ b/sdk/python/tests/unit/test_on_demand_python_transformation.py @@ -1,6 +1,8 @@ import os import tempfile +import unittest from datetime import datetime, timedelta +from typing import Any, Dict import pandas as pd @@ -10,95 +12,119 @@ from feast.infra.online_stores.sqlite import SqliteOnlineStoreConfig from feast.on_demand_feature_view import on_demand_feature_view from feast.types import Float32, Float64, Int64 -from typing import Dict, Any - - -def test_python_pandas_parity(): - with tempfile.TemporaryDirectory() as data_dir: - store = FeatureStore( - config=RepoConfig( - project="test_on_demand_python_transformation", - registry=os.path.join(data_dir, "registry.db"), - provider="local", - entity_key_serialization_version=2, - online_store=SqliteOnlineStoreConfig( - path=os.path.join(data_dir, "online.db") - ), + + +class TestOnDemandPythonTransformation(unittest.TestCase): + def setUp(self): + # data_dir = tempfile.TemporaryDirectory() + with tempfile.TemporaryDirectory() as data_dir: + self.store = FeatureStore( + config=RepoConfig( + project="test_on_demand_python_transformation", + registry=os.path.join(data_dir, "registry.db"), + provider="local", + entity_key_serialization_version=2, + online_store=SqliteOnlineStoreConfig( + path=os.path.join(data_dir, "online.db") + ), + ) ) - ) - # Generate test data. - end_date = datetime.now().replace(microsecond=0, second=0, minute=0) - start_date = end_date - timedelta(days=15) + # Generate test data. + end_date = datetime.now().replace(microsecond=0, second=0, minute=0) + 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_stats_path = os.path.join(data_dir, "driver_stats.parquet") - driver_df.to_parquet(path=driver_stats_path, allow_truncated_timestamps=True) + driver_entities = [1001, 1002, 1003, 1004, 1005] + 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 = Entity(name="driver", join_keys=["driver_id"]) + driver = Entity(name="driver", join_keys=["driver_id"]) - driver_stats_source = FileSource( - name="driver_hourly_stats_source", - path=driver_stats_path, - timestamp_field="event_timestamp", - created_timestamp_column="created", - ) + driver_stats_source = FileSource( + name="driver_hourly_stats_source", + path=driver_stats_path, + timestamp_field="event_timestamp", + created_timestamp_column="created", + ) - driver_stats_fv = FeatureView( - name="driver_hourly_stats", - entities=[driver], - ttl=0, - schema=[ - Field(name="conv_rate", dtype=Float32), - Field(name="acc_rate", dtype=Float32), - Field(name="avg_daily_trips", dtype=Int64), - ], - online=True, - source=driver_stats_source, - ) + driver_stats_fv = FeatureView( + name="driver_hourly_stats", + entities=[driver], + ttl=timedelta(days=0), + schema=[ + Field(name="conv_rate", dtype=Float32), + Field(name="acc_rate", dtype=Float32), + Field(name="avg_daily_trips", dtype=Int64), + ], + online=True, + source=driver_stats_source, + ) - @on_demand_feature_view( - sources=[driver_stats_fv], - schema=[Field(name="conv_rate_plus_acc", dtype=Float64)], - mode="pandas", - ) - def pandas_view(inputs: pd.DataFrame) -> pd.DataFrame: - df = pd.DataFrame() - df["conv_rate_plus_acc"] = inputs["conv_rate"] + inputs["acc_rate"] - return df - - # @on_demand_feature_view( - # sources=[driver_stats_fv[["conv_rate", "acc_rate"]]], - # schema=[Field(name="conv_rate_plus_acc_python", dtype=Float64)], - # mode="python", - # ) - # def python_view(inputs: Dict[str, Any]) -> Dict[str, Any]: - # output: Dict[str, Any] = {'conv_rate_plus_acc_python': inputs['conv_rate'] + inputs['acc_rate']} - # return output - - store.apply( - [driver, driver_stats_source, driver_stats_fv, pandas_view] - ) + @on_demand_feature_view( + sources=[driver_stats_fv], + schema=[Field(name="conv_rate_plus_acc_pandas", dtype=Float64)], + mode="pandas", + ) + def pandas_view(inputs: pd.DataFrame) -> pd.DataFrame: + df = pd.DataFrame() + df["conv_rate_plus_acc_pandas"] = ( + inputs["conv_rate"] + inputs["acc_rate"] + ) + return df + @on_demand_feature_view( + sources=[driver_stats_fv[["conv_rate", "acc_rate"]]], + schema=[Field(name="conv_rate_plus_acc_python", dtype=Float64)], + mode="python", + ) + def python_view(inputs: Dict[str, Any]) -> Dict[str, Any]: + output: Dict[str, Any] = { + "conv_rate_plus_acc_python": inputs["conv_rate"] + + inputs["acc_rate"] + } + return output + + self.store.apply( + [driver, driver_stats_source, driver_stats_fv, pandas_view, python_view] + ) + self.store.write_to_online_store( + feature_view_name="driver_hourly_stats", df=driver_df + ) + + def test_python_pandas_parity(self): entity_rows = [ { "driver_id": 1001, - # "event_timestamp": datetime(2021, 4, 12, 10, 59, 42), } ] - online_response = store.get_online_features( + online_python_response = self.store.get_online_features( entity_rows=entity_rows, features=[ "driver_hourly_stats:conv_rate", "driver_hourly_stats:acc_rate", - "driver_hourly_stats:avg_daily_trips", "python_view:conv_rate_plus_acc_python", - "pandas_view:conv_rate_plus_acc", ], ).to_df() - assert online_response["conv_rate_plus_acc"].equals( - online_response["conv_rate_plus_acc_python"] + online_pandas_response = self.store.get_online_features( + entity_rows=entity_rows, + features=[ + "driver_hourly_stats:conv_rate", + "driver_hourly_stats:acc_rate", + "pandas_view:conv_rate_plus_acc_pandas", + ], + ).to_df() + + assert ( + online_python_response["conv_rate_plus_acc_python"] + .equals(online_pandas_response["conv_rate_plus_acc_pandas"]) + .equals( + online_python_response["conv_rate"] + online_python_response["acc_rate"] + ) ) From 270f6c0d30ac77c5f52deda9b3b62b682c730cef Mon Sep 17 00:00:00 2001 From: Francisco Javier Arceo Date: Wed, 3 Apr 2024 23:47:29 -0400 Subject: [PATCH 5/9] fixed test and serialization Signed-off-by: Francisco Javier Arceo --- sdk/python/feast/on_demand_feature_view.py | 13 ++++++++ .../test_on_demand_python_transformation.py | 31 ++++++++++++------- 2 files changed, 33 insertions(+), 11 deletions(-) diff --git a/sdk/python/feast/on_demand_feature_view.py b/sdk/python/feast/on_demand_feature_view.py index f83500cbc9b..00e94f612ee 100644 --- a/sdk/python/feast/on_demand_feature_view.py +++ b/sdk/python/feast/on_demand_feature_view.py @@ -300,10 +300,23 @@ def from_proto( == "user_defined_function" and on_demand_feature_view_proto.spec.feature_transformation.user_defined_function.body_text != "" + and on_demand_feature_view_proto.spec.mode == "pandas" ): transformation = PandasTransformation.from_proto( on_demand_feature_view_proto.spec.feature_transformation.user_defined_function ) + elif ( + on_demand_feature_view_proto.spec.feature_transformation.WhichOneof( + "transformation" + ) + == "user_defined_function" + and on_demand_feature_view_proto.spec.feature_transformation.user_defined_function.body_text + != "" + and on_demand_feature_view_proto.spec.mode == "python" + ): + transformation = PythonTransformation.from_proto( + on_demand_feature_view_proto.spec.feature_transformation.user_defined_function + ) elif ( on_demand_feature_view_proto.spec.feature_transformation.WhichOneof( "transformation" diff --git a/sdk/python/tests/unit/test_on_demand_python_transformation.py b/sdk/python/tests/unit/test_on_demand_python_transformation.py index 4c5ffbfcd04..cd27b7a94b6 100644 --- a/sdk/python/tests/unit/test_on_demand_python_transformation.py +++ b/sdk/python/tests/unit/test_on_demand_python_transformation.py @@ -16,7 +16,6 @@ class TestOnDemandPythonTransformation(unittest.TestCase): def setUp(self): - # data_dir = tempfile.TemporaryDirectory() with tempfile.TemporaryDirectory() as data_dir: self.store = FeatureStore( config=RepoConfig( @@ -83,10 +82,10 @@ def pandas_view(inputs: pd.DataFrame) -> pd.DataFrame: mode="python", ) def python_view(inputs: Dict[str, Any]) -> Dict[str, Any]: - output: Dict[str, Any] = { - "conv_rate_plus_acc_python": inputs["conv_rate"] - + inputs["acc_rate"] - } + output: Dict[str, Any] = {"conv_rate_plus_acc_python": []} + output["conv_rate_plus_acc_python"].append( + inputs["conv_rate"][0] + inputs["acc_rate"][0] + ) return output self.store.apply( @@ -110,7 +109,7 @@ def test_python_pandas_parity(self): "driver_hourly_stats:acc_rate", "python_view:conv_rate_plus_acc_python", ], - ).to_df() + ).to_dict() online_pandas_response = self.store.get_online_features( entity_rows=entity_rows, @@ -121,10 +120,20 @@ def test_python_pandas_parity(self): ], ).to_df() + assert len(online_python_response) == 4 + assert all( + key in online_python_response.keys() + for key in [ + "driver_id", + "acc_rate", + "conv_rate", + "conv_rate_plus_acc_python", + ] + ) + assert len(online_python_response["conv_rate_plus_acc_python"]) == 1 assert ( - online_python_response["conv_rate_plus_acc_python"] - .equals(online_pandas_response["conv_rate_plus_acc_pandas"]) - .equals( - online_python_response["conv_rate"] + online_python_response["acc_rate"] - ) + online_python_response["conv_rate_plus_acc_python"][0] + == online_pandas_response["conv_rate_plus_acc_pandas"][0] + == online_python_response["conv_rate"][0] + + online_python_response["acc_rate"][0] ) From 3574bcc1c95d6f30c34f150c8696604affb70926 Mon Sep 17 00:00:00 2001 From: Francisco Javier Arceo Date: Wed, 3 Apr 2024 23:53:48 -0400 Subject: [PATCH 6/9] removed commented out code Signed-off-by: Francisco Javier Arceo --- .../tests/unit/test_on_demand_feature_view.py | 19 ++++++++----------- 1 file changed, 8 insertions(+), 11 deletions(-) diff --git a/sdk/python/tests/unit/test_on_demand_feature_view.py b/sdk/python/tests/unit/test_on_demand_feature_view.py index c9c46275ea2..7b4b25593bb 100644 --- a/sdk/python/tests/unit/test_on_demand_feature_view.py +++ b/sdk/python/tests/unit/test_on_demand_feature_view.py @@ -185,14 +185,14 @@ def test_python_native_transformation_mode(): ) assert ( - on_demand_feature_view_python_native.feature_transformation - == PythonTransformation(python_native_udf, "python native udf source code") + on_demand_feature_view_python_native.feature_transformation + == PythonTransformation(python_native_udf, "python native udf source code") ) with pytest.raises(TypeError): assert ( - on_demand_feature_view_python_native_err.feature_transformation - == PythonTransformation(python_native_udf, "python native udf source code") + on_demand_feature_view_python_native_err.feature_transformation + == PythonTransformation(python_native_udf, "python native udf source code") ) with pytest.raises(TypeError): @@ -212,9 +212,6 @@ def test_python_native_transformation_mode(): ) == {"feature1": 0, "feature2": 1, "output1": 100, "output2": 102} -# def test_get_online_features_on_demand(): - - @pytest.mark.filterwarnings("ignore:udf and udf_string parameters are deprecated") def test_from_proto_backwards_compatible_udf(): file_source = FileSource(name="my-file-source", path="test.parquet") @@ -244,8 +241,8 @@ def test_from_proto_backwards_compatible_udf(): # and to populate it in feature_transformation proto = on_demand_feature_view.to_proto() assert ( - on_demand_feature_view.feature_transformation.udf_string - == proto.spec.feature_transformation.user_defined_function.body_text + on_demand_feature_view.feature_transformation.udf_string + == proto.spec.feature_transformation.user_defined_function.body_text ) # Because of the current set of code this is just confirming it is empty assert proto.spec.user_defined_function.body_text == "" @@ -272,6 +269,6 @@ def test_from_proto_backwards_compatible_udf(): # And now we expect the to get the same object back under feature_transformation reserialized_proto = OnDemandFeatureView.from_proto(proto) assert ( - reserialized_proto.feature_transformation.udf_string - == on_demand_feature_view.feature_transformation.udf_string + reserialized_proto.feature_transformation.udf_string + == on_demand_feature_view.feature_transformation.udf_string ) From 1d3745d6f0d1833295187db15763685b70b8cb1c Mon Sep 17 00:00:00 2001 From: Francisco Javier Arceo Date: Thu, 4 Apr 2024 00:03:53 -0400 Subject: [PATCH 7/9] lint Signed-off-by: Francisco Javier Arceo --- .../tests/unit/test_on_demand_feature_view.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/sdk/python/tests/unit/test_on_demand_feature_view.py b/sdk/python/tests/unit/test_on_demand_feature_view.py index 7b4b25593bb..cf4afa94228 100644 --- a/sdk/python/tests/unit/test_on_demand_feature_view.py +++ b/sdk/python/tests/unit/test_on_demand_feature_view.py @@ -185,14 +185,14 @@ def test_python_native_transformation_mode(): ) assert ( - on_demand_feature_view_python_native.feature_transformation - == PythonTransformation(python_native_udf, "python native udf source code") + on_demand_feature_view_python_native.feature_transformation + == PythonTransformation(python_native_udf, "python native udf source code") ) with pytest.raises(TypeError): assert ( - on_demand_feature_view_python_native_err.feature_transformation - == PythonTransformation(python_native_udf, "python native udf source code") + on_demand_feature_view_python_native_err.feature_transformation + == PythonTransformation(python_native_udf, "python native udf source code") ) with pytest.raises(TypeError): @@ -241,8 +241,8 @@ def test_from_proto_backwards_compatible_udf(): # and to populate it in feature_transformation proto = on_demand_feature_view.to_proto() assert ( - on_demand_feature_view.feature_transformation.udf_string - == proto.spec.feature_transformation.user_defined_function.body_text + on_demand_feature_view.feature_transformation.udf_string + == proto.spec.feature_transformation.user_defined_function.body_text ) # Because of the current set of code this is just confirming it is empty assert proto.spec.user_defined_function.body_text == "" @@ -269,6 +269,6 @@ def test_from_proto_backwards_compatible_udf(): # And now we expect the to get the same object back under feature_transformation reserialized_proto = OnDemandFeatureView.from_proto(proto) assert ( - reserialized_proto.feature_transformation.udf_string - == on_demand_feature_view.feature_transformation.udf_string + reserialized_proto.feature_transformation.udf_string + == on_demand_feature_view.feature_transformation.udf_string ) From 255ba05c7218a27a6685971bcb4b1c0e7d4390ba Mon Sep 17 00:00:00 2001 From: Francisco Javier Arceo Date: Thu, 4 Apr 2024 09:15:06 -0400 Subject: [PATCH 8/9] added a test to make it explicit that feature calculation must happen on a list Signed-off-by: Francisco Javier Arceo --- .../tests/data/driver_hourly_stats.parquet | Bin 35177 -> 0 bytes .../test_on_demand_python_transformation.py | 18 ++++++++++++++++++ 2 files changed, 18 insertions(+) delete mode 100644 sdk/python/tests/data/driver_hourly_stats.parquet diff --git a/sdk/python/tests/data/driver_hourly_stats.parquet b/sdk/python/tests/data/driver_hourly_stats.parquet deleted file mode 100644 index 9efec515877340e367a5668e73c2d3751a71594a..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 35177 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 diff --git a/sdk/python/tests/unit/test_on_demand_python_transformation.py b/sdk/python/tests/unit/test_on_demand_python_transformation.py index cd27b7a94b6..ac89ccb5c55 100644 --- a/sdk/python/tests/unit/test_on_demand_python_transformation.py +++ b/sdk/python/tests/unit/test_on_demand_python_transformation.py @@ -5,6 +5,7 @@ from typing import Any, Dict import pandas as pd +import pytest from feast import Entity, FeatureStore, FeatureView, FileSource, RepoConfig from feast.driver_test_data import create_driver_hourly_stats_df @@ -88,6 +89,23 @@ def python_view(inputs: Dict[str, Any]) -> Dict[str, Any]: ) return output + @on_demand_feature_view( + sources=[driver_stats_fv[["conv_rate", "acc_rate"]]], + schema=[Field(name="conv_rate_plus_acc_python_singleton", dtype=Float64)], + mode="python", + ) + def python_singleton_view(inputs: Dict[str, Any]) -> Dict[str, Any]: + output: Dict[str, Any] = dict(conv_rate_plus_acc_python=float('-inf')) + output["conv_rate_plus_acc_python_singleton"] = inputs["conv_rate"] + inputs["acc_rate"] + return output + + with pytest.raises(TypeError): + # Note the singleton view will fail as the type is + # expected to be a List which can be confirmed in _infer_features_dict + self.store.apply( + [driver, driver_stats_source, driver_stats_fv, pandas_view, python_view, python_singleton_view] + ) + self.store.apply( [driver, driver_stats_source, driver_stats_fv, pandas_view, python_view] ) From 924ebd5d7ef6472009df8b08f77b8b5294ac2598 Mon Sep 17 00:00:00 2001 From: Francisco Javier Arceo Date: Thu, 4 Apr 2024 09:25:41 -0400 Subject: [PATCH 9/9] linter Signed-off-by: Francisco Javier Arceo --- .../test_on_demand_python_transformation.py | 31 ++++++++++++++----- 1 file changed, 23 insertions(+), 8 deletions(-) diff --git a/sdk/python/tests/unit/test_on_demand_python_transformation.py b/sdk/python/tests/unit/test_on_demand_python_transformation.py index ac89ccb5c55..4913b6c1b1d 100644 --- a/sdk/python/tests/unit/test_on_demand_python_transformation.py +++ b/sdk/python/tests/unit/test_on_demand_python_transformation.py @@ -83,27 +83,42 @@ def pandas_view(inputs: pd.DataFrame) -> pd.DataFrame: mode="python", ) def python_view(inputs: Dict[str, Any]) -> Dict[str, Any]: - output: Dict[str, Any] = {"conv_rate_plus_acc_python": []} - output["conv_rate_plus_acc_python"].append( - inputs["conv_rate"][0] + inputs["acc_rate"][0] - ) + 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"] + ) + ] + } return output @on_demand_feature_view( sources=[driver_stats_fv[["conv_rate", "acc_rate"]]], - schema=[Field(name="conv_rate_plus_acc_python_singleton", dtype=Float64)], + schema=[ + Field(name="conv_rate_plus_acc_python_singleton", dtype=Float64) + ], mode="python", ) def python_singleton_view(inputs: Dict[str, Any]) -> Dict[str, Any]: - output: Dict[str, Any] = dict(conv_rate_plus_acc_python=float('-inf')) - output["conv_rate_plus_acc_python_singleton"] = inputs["conv_rate"] + inputs["acc_rate"] + output: Dict[str, Any] = dict(conv_rate_plus_acc_python=float("-inf")) + output["conv_rate_plus_acc_python_singleton"] = ( + inputs["conv_rate"] + inputs["acc_rate"] + ) return output with pytest.raises(TypeError): # Note the singleton view will fail as the type is # expected to be a List which can be confirmed in _infer_features_dict self.store.apply( - [driver, driver_stats_source, driver_stats_fv, pandas_view, python_view, python_singleton_view] + [ + driver, + driver_stats_source, + driver_stats_fv, + pandas_view, + python_view, + python_singleton_view, + ] ) self.store.apply(