From 2db0c9a4c73f2715287cb9f506cdfe09c51d35dd Mon Sep 17 00:00:00 2001 From: Danny Chiao Date: Wed, 16 Mar 2022 17:57:12 -0700 Subject: [PATCH 01/15] fix: Print more warning statements on requirement for data sources to have name in future, but don't prevent apply from running if there are duplicate empty data sources. Also attach class type when applying data sources so repeated feast apply commands properly work for Spark Signed-off-by: Danny Chiao --- sdk/python/feast/cli.py | 11 +++++++++++ sdk/python/feast/diff/registry_diff.py | 3 +++ sdk/python/feast/feature_store.py | 17 ++++++++++------- .../infra/offline_stores/bigquery_source.py | 2 +- .../infra/offline_stores/redshift_source.py | 3 ++- .../infra/offline_stores/snowflake_source.py | 3 ++- sdk/python/feast/registry.py | 5 ++++- 7 files changed, 33 insertions(+), 11 deletions(-) diff --git a/sdk/python/feast/cli.py b/sdk/python/feast/cli.py index d2a71bc561b..9d75e14c939 100644 --- a/sdk/python/feast/cli.py +++ b/sdk/python/feast/cli.py @@ -13,6 +13,7 @@ # limitations under the License. import logging +import warnings from datetime import datetime from pathlib import Path from typing import List, Optional @@ -151,6 +152,11 @@ def data_source_describe(ctx: click.Context, name: str): print(e) exit(1) + warnings.warn( + "Describing data sources will only work properly if all data sources have names or table names specified. " + "Starting Feast 0.21, data source unique names will be required to encourage data source discovery.", + RuntimeWarning, + ) print( yaml.dump( yaml.safe_load(str(data_source)), default_flow_style=False, sort_keys=False @@ -173,6 +179,11 @@ def data_source_list(ctx: click.Context): from tabulate import tabulate + warnings.warn( + "Listing data sources will only work properly if all data sources have names or table names specified. " + "Starting Feast 0.21, data source unique names will be required to encourage data source discovery", + RuntimeWarning, + ) print(tabulate(table, headers=["NAME", "CLASS"], tablefmt="plain")) diff --git a/sdk/python/feast/diff/registry_diff.py b/sdk/python/feast/diff/registry_diff.py index 4558a149a5c..3ad846f1248 100644 --- a/sdk/python/feast/diff/registry_diff.py +++ b/sdk/python/feast/diff/registry_diff.py @@ -60,6 +60,9 @@ def to_string(self): continue if feast_object_diff.transition_type == TransitionType.UNCHANGED: continue + if feast_object_diff.feast_object_type == FeastObjectType.DATA_SOURCE: + # TODO(adchia): Print statements out starting in Feast 0.21 + continue action, color = message_action_map[feast_object_diff.transition_type] log_string += f"{action} {feast_object_diff.feast_object_type.value} {Style.BRIGHT + color}{feast_object_diff.name}{Style.RESET_ALL}\n" if feast_object_diff.transition_type == TransitionType.UPDATE: diff --git a/sdk/python/feast/feature_store.py b/sdk/python/feast/feature_store.py index 73c2f14a638..bce495c474d 100644 --- a/sdk/python/feast/feature_store.py +++ b/sdk/python/feast/feature_store.py @@ -2063,14 +2063,17 @@ def _validate_feature_views(feature_views: List[BaseFeatureView]): def _validate_data_sources(data_sources: List[DataSource]): """ Verify data sources have case-insensitively unique names""" ds_names = set() - for fv in data_sources: - case_insensitive_ds_name = fv.name.lower() + for ds in data_sources: + case_insensitive_ds_name = ds.name.lower() if case_insensitive_ds_name in ds_names: - raise ValueError( - f"More than one data source with name {case_insensitive_ds_name} found. " - f"Please ensure that all data source names are case-insensitively unique. " - f"It may be necessary to ignore certain files in your feature repository by using a .feastignore file." - ) + if case_insensitive_ds_name.strip(): + warnings.warning( + f"More than one data source with name {case_insensitive_ds_name} found. " + f"Please ensure that all data source names are case-insensitively unique. " + f"It may be necessary to ignore certain files in your feature repository by using a .feastignore " + f"file. Starting in Feast 0.21, unique names (perhaps inferred from the table name) will be " + f"required in data sources to encourage data source discovery" + ) else: ds_names.add(case_insensitive_ds_name) diff --git a/sdk/python/feast/infra/offline_stores/bigquery_source.py b/sdk/python/feast/infra/offline_stores/bigquery_source.py index 92b6939fc3a..1d797077f07 100644 --- a/sdk/python/feast/infra/offline_stores/bigquery_source.py +++ b/sdk/python/feast/infra/offline_stores/bigquery_source.py @@ -64,7 +64,7 @@ def __init__( else: warnings.warn( ( - "Starting in Feast 0.21, Feast will require either a name for a data source (if using query) or `table`." + f"Starting in Feast 0.21, Feast will require either a name for a data source (if using query) or `table`: {self.query}" ), DeprecationWarning, ) diff --git a/sdk/python/feast/infra/offline_stores/redshift_source.py b/sdk/python/feast/infra/offline_stores/redshift_source.py index 8573396aca4..149fee76c43 100644 --- a/sdk/python/feast/infra/offline_stores/redshift_source.py +++ b/sdk/python/feast/infra/offline_stores/redshift_source.py @@ -50,7 +50,8 @@ def __init__( else: warnings.warn( ( - "Starting in Feast 0.21, Feast will require either a name for a data source (if using query) or `table`." + f"Starting in Feast 0.21, Feast will require either a name for a data source (if using query) " + f"or `table`: {self.query}" ), DeprecationWarning, ) diff --git a/sdk/python/feast/infra/offline_stores/snowflake_source.py b/sdk/python/feast/infra/offline_stores/snowflake_source.py index 6ca5df7d6fd..f7375b15438 100644 --- a/sdk/python/feast/infra/offline_stores/snowflake_source.py +++ b/sdk/python/feast/infra/offline_stores/snowflake_source.py @@ -53,7 +53,8 @@ def __init__( else: warnings.warn( ( - "Starting in Feast 0.21, Feast will require either a name for a data source (if using query) or `table`." + f"Starting in Feast 0.21, Feast will require either a name for a data source (if using query) " + f"or `table`: {self.query}" ), DeprecationWarning, ) diff --git a/sdk/python/feast/registry.py b/sdk/python/feast/registry.py index 0bf73fcd24c..3996ed163cb 100644 --- a/sdk/python/feast/registry.py +++ b/sdk/python/feast/registry.py @@ -13,6 +13,7 @@ # limitations under the License. import json import logging +import warnings from collections import defaultdict from datetime import datetime, timedelta from enum import Enum @@ -314,11 +315,13 @@ def apply_data_source( commit: Whether to immediately commit to the registry """ registry = self._prepare_registry_for_changes() - for idx, existing_data_source_proto in enumerate(registry.data_sources): if existing_data_source_proto.name == data_source.name: del registry.data_sources[idx] data_source_proto = data_source.to_proto() + data_source_proto.data_source_class_type = ( + f"{data_source.__class__.__module__}.{data_source.__class__.__name__}" + ) data_source_proto.project = project data_source_proto.data_source_class_type = ( f"{data_source.__class__.__module__}.{data_source.__class__.__name__}" From 2d4fc844b62378815db1ff283e06ee54cf2e6653 Mon Sep 17 00:00:00 2001 From: Danny Chiao Date: Wed, 16 Mar 2022 17:58:40 -0700 Subject: [PATCH 02/15] typo Signed-off-by: Danny Chiao --- sdk/python/feast/feature_store.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sdk/python/feast/feature_store.py b/sdk/python/feast/feature_store.py index bce495c474d..4871fe2f6a5 100644 --- a/sdk/python/feast/feature_store.py +++ b/sdk/python/feast/feature_store.py @@ -2067,7 +2067,7 @@ def _validate_data_sources(data_sources: List[DataSource]): case_insensitive_ds_name = ds.name.lower() if case_insensitive_ds_name in ds_names: if case_insensitive_ds_name.strip(): - warnings.warning( + warnings.warn( f"More than one data source with name {case_insensitive_ds_name} found. " f"Please ensure that all data source names are case-insensitively unique. " f"It may be necessary to ignore certain files in your feature repository by using a .feastignore " From 88a7434338febf3867502d0a3e2d0cce0e112f31 Mon Sep 17 00:00:00 2001 From: Danny Chiao Date: Wed, 16 Mar 2022 17:59:06 -0700 Subject: [PATCH 03/15] typo Signed-off-by: Danny Chiao --- sdk/python/feast/registry.py | 1 - 1 file changed, 1 deletion(-) diff --git a/sdk/python/feast/registry.py b/sdk/python/feast/registry.py index 3996ed163cb..da9c6c6b217 100644 --- a/sdk/python/feast/registry.py +++ b/sdk/python/feast/registry.py @@ -13,7 +13,6 @@ # limitations under the License. import json import logging -import warnings from collections import defaultdict from datetime import datetime, timedelta from enum import Enum From ba9719082839c0c54c70b77182930ae5f55c1b43 Mon Sep 17 00:00:00 2001 From: Danny Chiao Date: Wed, 16 Mar 2022 18:06:01 -0700 Subject: [PATCH 04/15] fix Signed-off-by: Danny Chiao --- protos/feast/core/SavedDataset.proto | 1 - .../feast/infra/offline_stores/redshift_source.py | 13 ++++++------- .../feast/infra/offline_stores/snowflake_source.py | 13 ++++++------- .../tests/example_repos/example_feature_repo_1.py | 12 ++++++++++++ 4 files changed, 24 insertions(+), 15 deletions(-) diff --git a/protos/feast/core/SavedDataset.proto b/protos/feast/core/SavedDataset.proto index fd8fdecde06..bc783d5b9e7 100644 --- a/protos/feast/core/SavedDataset.proto +++ b/protos/feast/core/SavedDataset.proto @@ -24,7 +24,6 @@ option go_package = "github.com/feast-dev/feast/go/protos/feast/core"; import "google/protobuf/timestamp.proto"; import "feast/core/DataSource.proto"; -import "feast/core/FeatureService.proto"; message SavedDatasetSpec { // Name of the dataset. Must be unique since it's possible to overwrite dataset by name diff --git a/sdk/python/feast/infra/offline_stores/redshift_source.py b/sdk/python/feast/infra/offline_stores/redshift_source.py index 149fee76c43..df42e18910c 100644 --- a/sdk/python/feast/infra/offline_stores/redshift_source.py +++ b/sdk/python/feast/infra/offline_stores/redshift_source.py @@ -41,6 +41,12 @@ def __init__( query (optional): The query to be executed to obtain the features. name (optional): Name for the source. Defaults to the table_ref if not specified. """ + # The default Redshift schema is named "public". + _schema = "public" if table and not schema else schema + self.redshift_options = RedshiftOptions( + table=table, schema=_schema, query=query + ) + if table is None and query is None: raise ValueError('No "table" argument provided.') _name = name @@ -64,13 +70,6 @@ def __init__( date_partition_column, ) - # The default Redshift schema is named "public". - _schema = "public" if table and not schema else schema - - self.redshift_options = RedshiftOptions( - table=table, schema=_schema, query=query - ) - @staticmethod def from_proto(data_source: DataSourceProto): """ diff --git a/sdk/python/feast/infra/offline_stores/snowflake_source.py b/sdk/python/feast/infra/offline_stores/snowflake_source.py index f7375b15438..a972df191b1 100644 --- a/sdk/python/feast/infra/offline_stores/snowflake_source.py +++ b/sdk/python/feast/infra/offline_stores/snowflake_source.py @@ -44,6 +44,12 @@ def __init__( """ if table is None and query is None: raise ValueError('No "table" argument provided.') + # The default Snowflake schema is named "PUBLIC". + _schema = "PUBLIC" if (database and table and not schema) else schema + + self.snowflake_options = SnowflakeOptions( + database=database, schema=_schema, table=table, query=query + ) # If no name, use the table as the default name _name = name @@ -67,13 +73,6 @@ def __init__( date_partition_column, ) - # The default Snowflake schema is named "PUBLIC". - _schema = "PUBLIC" if (database and table and not schema) else schema - - self.snowflake_options = SnowflakeOptions( - database=database, schema=_schema, table=table, query=query - ) - @staticmethod def from_proto(data_source: DataSourceProto): """ 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 bb9bc0f1a1a..2b39cb421f0 100644 --- a/sdk/python/tests/example_repos/example_feature_repo_1.py +++ b/sdk/python/tests/example_repos/example_feature_repo_1.py @@ -16,6 +16,18 @@ created_timestamp_column="created_timestamp", ) +driver_locations_source_query = BigQuerySource( + query="SELECT * from feast-oss.public.drivers", + event_timestamp_column="event_timestamp", + created_timestamp_column="created_timestamp", +) + +driver_locations_source_query_2 = BigQuerySource( + query="SELECT lat * 2 FROM feast-oss.public.drivers", + event_timestamp_column="event_timestamp", + created_timestamp_column="created_timestamp", +) + customer_profile_source = BigQuerySource( name="customer_profile_source", table_ref="feast-oss.public.customers", From cdee46c89009032e16d8543c9ffdcd12b90e7383 Mon Sep 17 00:00:00 2001 From: Danny Chiao Date: Wed, 16 Mar 2022 18:14:36 -0700 Subject: [PATCH 05/15] fix Signed-off-by: Danny Chiao --- .../feature_repos/repo_configuration.py | 52 +++++++++---------- 1 file changed, 26 insertions(+), 26 deletions(-) diff --git a/sdk/python/tests/integration/feature_repos/repo_configuration.py b/sdk/python/tests/integration/feature_repos/repo_configuration.py index 9cbe11f86b9..c7b993c813a 100644 --- a/sdk/python/tests/integration/feature_repos/repo_configuration.py +++ b/sdk/python/tests/integration/feature_repos/repo_configuration.py @@ -66,42 +66,42 @@ # module will be imported and FULL_REPO_CONFIGS will be extracted from the file. DEFAULT_FULL_REPO_CONFIGS: List[IntegrationTestRepoConfig] = [ # Local configurations - IntegrationTestRepoConfig(), - IntegrationTestRepoConfig(python_feature_server=True), + # IntegrationTestRepoConfig(), + # IntegrationTestRepoConfig(python_feature_server=True), ] if os.getenv("FEAST_IS_LOCAL_TEST", "False") != "True": DEFAULT_FULL_REPO_CONFIGS.extend( [ - IntegrationTestRepoConfig(online_store=REDIS_CONFIG), + # IntegrationTestRepoConfig(online_store=REDIS_CONFIG), # GCP configurations IntegrationTestRepoConfig( provider="gcp", offline_store_creator=BigQueryDataSourceCreator, online_store="datastore", ), - IntegrationTestRepoConfig( - provider="gcp", - offline_store_creator=BigQueryDataSourceCreator, - online_store=REDIS_CONFIG, - ), - # AWS configurations - IntegrationTestRepoConfig( - provider="aws", - offline_store_creator=RedshiftDataSourceCreator, - online_store=DYNAMO_CONFIG, - python_feature_server=True, - ), - IntegrationTestRepoConfig( - provider="aws", - offline_store_creator=RedshiftDataSourceCreator, - online_store=REDIS_CONFIG, - ), - # Snowflake configurations - IntegrationTestRepoConfig( - provider="aws", # no list features, no feature server - offline_store_creator=SnowflakeDataSourceCreator, - online_store=REDIS_CONFIG, - ), + # IntegrationTestRepoConfig( + # provider="gcp", + # offline_store_creator=BigQueryDataSourceCreator, + # online_store=REDIS_CONFIG, + # ), + # # AWS configurations + # IntegrationTestRepoConfig( + # provider="aws", + # offline_store_creator=RedshiftDataSourceCreator, + # online_store=DYNAMO_CONFIG, + # python_feature_server=True, + # ), + # IntegrationTestRepoConfig( + # provider="aws", + # offline_store_creator=RedshiftDataSourceCreator, + # online_store=REDIS_CONFIG, + # ), + # # Snowflake configurations + # IntegrationTestRepoConfig( + # provider="aws", # no list features, no feature server + # offline_store_creator=SnowflakeDataSourceCreator, + # online_store=REDIS_CONFIG, + # ), ] ) full_repo_configs_module = os.environ.get(FULL_REPO_CONFIGS_MODULE_ENV_NAME) From 3a4b4cdd0ccd2701c6898b52ce397eea66a3124d Mon Sep 17 00:00:00 2001 From: Danny Chiao Date: Wed, 16 Mar 2022 18:14:47 -0700 Subject: [PATCH 06/15] fix Signed-off-by: Danny Chiao --- .../feature_repos/repo_configuration.py | 52 +++++++++---------- 1 file changed, 26 insertions(+), 26 deletions(-) diff --git a/sdk/python/tests/integration/feature_repos/repo_configuration.py b/sdk/python/tests/integration/feature_repos/repo_configuration.py index c7b993c813a..9cbe11f86b9 100644 --- a/sdk/python/tests/integration/feature_repos/repo_configuration.py +++ b/sdk/python/tests/integration/feature_repos/repo_configuration.py @@ -66,42 +66,42 @@ # module will be imported and FULL_REPO_CONFIGS will be extracted from the file. DEFAULT_FULL_REPO_CONFIGS: List[IntegrationTestRepoConfig] = [ # Local configurations - # IntegrationTestRepoConfig(), - # IntegrationTestRepoConfig(python_feature_server=True), + IntegrationTestRepoConfig(), + IntegrationTestRepoConfig(python_feature_server=True), ] if os.getenv("FEAST_IS_LOCAL_TEST", "False") != "True": DEFAULT_FULL_REPO_CONFIGS.extend( [ - # IntegrationTestRepoConfig(online_store=REDIS_CONFIG), + IntegrationTestRepoConfig(online_store=REDIS_CONFIG), # GCP configurations IntegrationTestRepoConfig( provider="gcp", offline_store_creator=BigQueryDataSourceCreator, online_store="datastore", ), - # IntegrationTestRepoConfig( - # provider="gcp", - # offline_store_creator=BigQueryDataSourceCreator, - # online_store=REDIS_CONFIG, - # ), - # # AWS configurations - # IntegrationTestRepoConfig( - # provider="aws", - # offline_store_creator=RedshiftDataSourceCreator, - # online_store=DYNAMO_CONFIG, - # python_feature_server=True, - # ), - # IntegrationTestRepoConfig( - # provider="aws", - # offline_store_creator=RedshiftDataSourceCreator, - # online_store=REDIS_CONFIG, - # ), - # # Snowflake configurations - # IntegrationTestRepoConfig( - # provider="aws", # no list features, no feature server - # offline_store_creator=SnowflakeDataSourceCreator, - # online_store=REDIS_CONFIG, - # ), + IntegrationTestRepoConfig( + provider="gcp", + offline_store_creator=BigQueryDataSourceCreator, + online_store=REDIS_CONFIG, + ), + # AWS configurations + IntegrationTestRepoConfig( + provider="aws", + offline_store_creator=RedshiftDataSourceCreator, + online_store=DYNAMO_CONFIG, + python_feature_server=True, + ), + IntegrationTestRepoConfig( + provider="aws", + offline_store_creator=RedshiftDataSourceCreator, + online_store=REDIS_CONFIG, + ), + # Snowflake configurations + IntegrationTestRepoConfig( + provider="aws", # no list features, no feature server + offline_store_creator=SnowflakeDataSourceCreator, + online_store=REDIS_CONFIG, + ), ] ) full_repo_configs_module = os.environ.get(FULL_REPO_CONFIGS_MODULE_ENV_NAME) From 7add51af32a53d3ef92c6656862b0ce15cf6ada7 Mon Sep 17 00:00:00 2001 From: Danny Chiao Date: Mon, 21 Mar 2022 11:29:11 -0700 Subject: [PATCH 07/15] More tests Signed-off-by: Danny Chiao --- sdk/python/feast/base_feature_view.py | 3 +++ sdk/python/feast/data_source.py | 8 ++++++ sdk/python/feast/diff/registry_diff.py | 27 ++++++++++--------- sdk/python/feast/entity.py | 3 +++ sdk/python/feast/feature_store.py | 2 +- sdk/python/feast/registry.py | 4 +-- sdk/python/feast/repo_operations.py | 6 ++--- .../integration/registration/test_cli.py | 6 ----- 8 files changed, 34 insertions(+), 25 deletions(-) diff --git a/sdk/python/feast/base_feature_view.py b/sdk/python/feast/base_feature_view.py index 609feab6414..ffd069cd94d 100644 --- a/sdk/python/feast/base_feature_view.py +++ b/sdk/python/feast/base_feature_view.py @@ -107,6 +107,9 @@ def __repr__(self): def __str__(self): return str(MessageToJson(self.to_proto())) + def __lt__(self, other): + return self.name < other.name + def __hash__(self): return hash((id(self), self.name)) diff --git a/sdk/python/feast/data_source.py b/sdk/python/feast/data_source.py index 2f66f846bcb..813218e13c6 100644 --- a/sdk/python/feast/data_source.py +++ b/sdk/python/feast/data_source.py @@ -17,6 +17,8 @@ from abc import ABC, abstractmethod from typing import Any, Callable, Dict, Iterable, Optional, Tuple +from google.protobuf.json_format import MessageToJson + from feast import type_map from feast.data_format import StreamFormat from feast.protos.feast.core.DataSource_pb2 import DataSource as DataSourceProto @@ -192,6 +194,12 @@ def __init__( def __hash__(self): return hash((id(self), self.name)) + def __str__(self): + return str(MessageToJson(self.to_proto())) + + def __lt__(self, other): + return str(self) < str(other) + def __eq__(self, other): if not isinstance(other, DataSource): raise TypeError("Comparisons should only involve DataSource class objects.") diff --git a/sdk/python/feast/diff/registry_diff.py b/sdk/python/feast/diff/registry_diff.py index 3ad846f1248..619b7d9c22c 100644 --- a/sdk/python/feast/diff/registry_diff.py +++ b/sdk/python/feast/diff/registry_diff.py @@ -1,6 +1,7 @@ from dataclasses import dataclass from typing import Any, Dict, Iterable, List, Optional, Set, Tuple, TypeVar, cast +from feast import BigQuerySource from feast.base_feature_view import BaseFeatureView from feast.data_source import DataSource from feast.diff.property_diff import PropertyDiff, TransitionType @@ -60,9 +61,9 @@ def to_string(self): continue if feast_object_diff.transition_type == TransitionType.UNCHANGED: continue - if feast_object_diff.feast_object_type == FeastObjectType.DATA_SOURCE: - # TODO(adchia): Print statements out starting in Feast 0.21 - continue + # if feast_object_diff.feast_object_type == FeastObjectType.DATA_SOURCE: + # # TODO(adchia): Print statements out starting in Feast 0.21 + # continue action, color = message_action_map[feast_object_diff.transition_type] log_string += f"{action} {feast_object_diff.feast_object_type.value} {Style.BRIGHT + color}{feast_object_diff.name}{Style.RESET_ALL}\n" if feast_object_diff.transition_type == TransitionType.UPDATE: @@ -80,14 +81,16 @@ def to_string(self): def tag_objects_for_keep_delete_update_add( existing_objs: Iterable[FeastObject], desired_objs: Iterable[FeastObject] -) -> Tuple[Set[FeastObject], Set[FeastObject], Set[FeastObject], Set[FeastObject]]: +) -> Tuple[List[FeastObject], List[FeastObject], List[FeastObject], List[FeastObject]]: existing_obj_names = {e.name for e in existing_objs} + desired_objs = sorted(list(desired_objs)) + existing_objs = sorted(list(existing_objs)) desired_obj_names = {e.name for e in desired_objs} - objs_to_add = {e for e in desired_objs if e.name not in existing_obj_names} - objs_to_update = {e for e in desired_objs if e.name in existing_obj_names} - objs_to_keep = {e for e in existing_objs if e.name in desired_obj_names} - objs_to_delete = {e for e in existing_objs if e.name not in desired_obj_names} + objs_to_add = [e for e in desired_objs if e.name not in existing_obj_names] + objs_to_update = [e for e in desired_objs if e.name in existing_obj_names] + objs_to_keep = [e for e in existing_objs if e.name in desired_obj_names] + objs_to_delete = [e for e in existing_objs if e.name not in desired_obj_names] return objs_to_keep, objs_to_delete, objs_to_update, objs_to_add @@ -152,10 +155,10 @@ def diff_registry_objects( def extract_objects_for_keep_delete_update_add( registry: Registry, current_project: str, desired_repo_contents: RepoContents, ) -> Tuple[ - Dict[FeastObjectType, Set[FeastObject]], - Dict[FeastObjectType, Set[FeastObject]], - Dict[FeastObjectType, Set[FeastObject]], - Dict[FeastObjectType, Set[FeastObject]], + Dict[FeastObjectType, List[FeastObject]], + Dict[FeastObjectType, List[FeastObject]], + Dict[FeastObjectType, List[FeastObject]], + Dict[FeastObjectType, List[FeastObject]], ]: """ Returns the objects in the registry that must be modified to achieve the desired repo state. diff --git a/sdk/python/feast/entity.py b/sdk/python/feast/entity.py index 817d884a658..965ef507b0e 100644 --- a/sdk/python/feast/entity.py +++ b/sdk/python/feast/entity.py @@ -73,6 +73,9 @@ def __init__( def __hash__(self) -> int: return hash((id(self), self.name)) + def __lt__(self, other): + return self.name < other.name + def __eq__(self, other): if not isinstance(other, Entity): raise TypeError("Comparisons should only involve Entity class objects.") diff --git a/sdk/python/feast/feature_store.py b/sdk/python/feast/feature_store.py index 4871fe2f6a5..53d1be86bf3 100644 --- a/sdk/python/feast/feature_store.py +++ b/sdk/python/feast/feature_store.py @@ -675,7 +675,7 @@ def apply( for v in odfv.input_request_data_sources.values(): data_sources_set_to_update.add(v) - data_sources_to_update = list(data_sources_set_to_update) + data_sources_to_update = sorted(list(data_sources_set_to_update)) # Validate all feature views and make inferences. self._validate_all_feature_views( diff --git a/sdk/python/feast/registry.py b/sdk/python/feast/registry.py index da9c6c6b217..429ef8749bf 100644 --- a/sdk/python/feast/registry.py +++ b/sdk/python/feast/registry.py @@ -81,7 +81,7 @@ def get_objects_from_registry( registry: "Registry", project: str ) -> Dict["FeastObjectType", List[Any]]: return { - FeastObjectType.DATA_SOURCE: registry.list_data_sources(project=project), + FeastObjectType.DATA_SOURCE: sorted(registry.list_data_sources(project=project)), FeastObjectType.ENTITY: registry.list_entities(project=project), FeastObjectType.FEATURE_VIEW: registry.list_feature_views(project=project), FeastObjectType.ON_DEMAND_FEATURE_VIEW: registry.list_on_demand_feature_views( @@ -100,7 +100,7 @@ def get_objects_from_repo_contents( repo_contents: RepoContents, ) -> Dict["FeastObjectType", Set[Any]]: return { - FeastObjectType.DATA_SOURCE: repo_contents.data_sources, + FeastObjectType.DATA_SOURCE: sorted(repo_contents.data_sources), FeastObjectType.ENTITY: repo_contents.entities, FeastObjectType.FEATURE_VIEW: repo_contents.feature_views, FeastObjectType.ON_DEMAND_FEATURE_VIEW: repo_contents.on_demand_feature_views, diff --git a/sdk/python/feast/repo_operations.py b/sdk/python/feast/repo_operations.py index 3457aa48866..b847f927be6 100644 --- a/sdk/python/feast/repo_operations.py +++ b/sdk/python/feast/repo_operations.py @@ -188,10 +188,8 @@ def extract_objects_for_apply_delete(project, registry, repo): return ( all_to_apply, all_to_delete, - set( - objs_to_add[FeastObjectType.FEATURE_VIEW].union( - objs_to_update[FeastObjectType.FEATURE_VIEW] - ) + set(objs_to_add[FeastObjectType.FEATURE_VIEW]).union( + set(objs_to_update[FeastObjectType.FEATURE_VIEW]) ), objs_to_delete[FeastObjectType.FEATURE_VIEW], ) diff --git a/sdk/python/tests/integration/registration/test_cli.py b/sdk/python/tests/integration/registration/test_cli.py index 655e53e7593..1e1ab3e7198 100644 --- a/sdk/python/tests/integration/registration/test_cli.py +++ b/sdk/python/tests/integration/registration/test_cli.py @@ -104,12 +104,6 @@ def test_universal_cli(environment: Environment): # Doing another apply should be a no op, and should not cause errors result = runner.run(["apply"], cwd=repo_path) assertpy.assert_that(result.returncode).is_equal_to(0) - basic_rw_test( - FeatureStore(repo_path=str(repo_path), config=None), - view_name="driver_locations", - ) - - # Confirm that registry contents have not changed. registry_dict = fs.registry.to_dict(project=project) assertpy.assert_that(registry_specs).is_equal_to( { From bc9d3ae8d3910edd05ebdd4da84a686a36652049 Mon Sep 17 00:00:00 2001 From: Danny Chiao Date: Tue, 29 Mar 2022 12:06:16 -0400 Subject: [PATCH 08/15] fix Signed-off-by: Danny Chiao --- sdk/python/feast/go_server.py | 2 +- sdk/python/feast/registry.py | 4 +++- sdk/python/feast/transformation_server.py | 2 +- 3 files changed, 5 insertions(+), 3 deletions(-) diff --git a/sdk/python/feast/go_server.py b/sdk/python/feast/go_server.py index 1fcbab61f08..38a350fd6d2 100644 --- a/sdk/python/feast/go_server.py +++ b/sdk/python/feast/go_server.py @@ -25,10 +25,10 @@ from subprocess import Popen from typing import Any, Dict, List, Optional, Union -import grpc from tenacity import retry, stop_after_attempt, stop_after_delay, wait_exponential import feast +import grpc from feast.errors import FeatureNameCollisionError, InvalidFeaturesParameterType from feast.feature_service import FeatureService from feast.flags_helper import is_test diff --git a/sdk/python/feast/registry.py b/sdk/python/feast/registry.py index 429ef8749bf..de65bb4441a 100644 --- a/sdk/python/feast/registry.py +++ b/sdk/python/feast/registry.py @@ -81,7 +81,9 @@ def get_objects_from_registry( registry: "Registry", project: str ) -> Dict["FeastObjectType", List[Any]]: return { - FeastObjectType.DATA_SOURCE: sorted(registry.list_data_sources(project=project)), + FeastObjectType.DATA_SOURCE: sorted( + registry.list_data_sources(project=project) + ), FeastObjectType.ENTITY: registry.list_entities(project=project), FeastObjectType.FEATURE_VIEW: registry.list_feature_views(project=project), FeastObjectType.ON_DEMAND_FEATURE_VIEW: registry.list_on_demand_feature_views( diff --git a/sdk/python/feast/transformation_server.py b/sdk/python/feast/transformation_server.py index 83f4af749e3..8e0efd73137 100644 --- a/sdk/python/feast/transformation_server.py +++ b/sdk/python/feast/transformation_server.py @@ -2,10 +2,10 @@ import sys from concurrent import futures -import grpc import pyarrow as pa from grpc_reflection.v1alpha import reflection +import grpc from feast.errors import OnDemandFeatureViewNotFoundException from feast.feature_store import FeatureStore from feast.protos.feast.serving.TransformationService_pb2 import ( From 9d2c5f135dc716092a81dc1aa8904db9409d89e3 Mon Sep 17 00:00:00 2001 From: Danny Chiao Date: Tue, 29 Mar 2022 12:08:55 -0400 Subject: [PATCH 09/15] fix Signed-off-by: Danny Chiao --- sdk/python/feast/diff/registry_diff.py | 4 ++-- sdk/python/feast/registry.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/sdk/python/feast/diff/registry_diff.py b/sdk/python/feast/diff/registry_diff.py index 619b7d9c22c..34cd0b0d623 100644 --- a/sdk/python/feast/diff/registry_diff.py +++ b/sdk/python/feast/diff/registry_diff.py @@ -83,8 +83,8 @@ def tag_objects_for_keep_delete_update_add( existing_objs: Iterable[FeastObject], desired_objs: Iterable[FeastObject] ) -> Tuple[List[FeastObject], List[FeastObject], List[FeastObject], List[FeastObject]]: existing_obj_names = {e.name for e in existing_objs} - desired_objs = sorted(list(desired_objs)) - existing_objs = sorted(list(existing_objs)) + desired_objs = list(desired_objs) + existing_objs = list(existing_objs) desired_obj_names = {e.name for e in desired_objs} objs_to_add = [e for e in desired_objs if e.name not in existing_obj_names] diff --git a/sdk/python/feast/registry.py b/sdk/python/feast/registry.py index de65bb4441a..6ea123b18b3 100644 --- a/sdk/python/feast/registry.py +++ b/sdk/python/feast/registry.py @@ -102,7 +102,7 @@ def get_objects_from_repo_contents( repo_contents: RepoContents, ) -> Dict["FeastObjectType", Set[Any]]: return { - FeastObjectType.DATA_SOURCE: sorted(repo_contents.data_sources), + FeastObjectType.DATA_SOURCE: repo_contents.data_sources, FeastObjectType.ENTITY: repo_contents.entities, FeastObjectType.FEATURE_VIEW: repo_contents.feature_views, FeastObjectType.ON_DEMAND_FEATURE_VIEW: repo_contents.on_demand_feature_views, From bae2e37340db070223829aa15a95472f335ebe4a Mon Sep 17 00:00:00 2001 From: Danny Chiao Date: Tue, 29 Mar 2022 12:09:13 -0400 Subject: [PATCH 10/15] fix Signed-off-by: Danny Chiao --- sdk/python/feast/diff/registry_diff.py | 1 - sdk/python/tests/integration/registration/test_cli.py | 1 - 2 files changed, 2 deletions(-) diff --git a/sdk/python/feast/diff/registry_diff.py b/sdk/python/feast/diff/registry_diff.py index 34cd0b0d623..248c0596ccd 100644 --- a/sdk/python/feast/diff/registry_diff.py +++ b/sdk/python/feast/diff/registry_diff.py @@ -1,7 +1,6 @@ from dataclasses import dataclass from typing import Any, Dict, Iterable, List, Optional, Set, Tuple, TypeVar, cast -from feast import BigQuerySource from feast.base_feature_view import BaseFeatureView from feast.data_source import DataSource from feast.diff.property_diff import PropertyDiff, TransitionType diff --git a/sdk/python/tests/integration/registration/test_cli.py b/sdk/python/tests/integration/registration/test_cli.py index 1e1ab3e7198..e441d831d95 100644 --- a/sdk/python/tests/integration/registration/test_cli.py +++ b/sdk/python/tests/integration/registration/test_cli.py @@ -28,7 +28,6 @@ RedshiftDataSourceCreator, ) from tests.utils.cli_utils import CliRunner, get_example_repo -from tests.utils.online_read_write_test import basic_rw_test @pytest.mark.integration From 41ff4d9249b9f899427293a5099ebb3171f050e4 Mon Sep 17 00:00:00 2001 From: Danny Chiao Date: Tue, 29 Mar 2022 12:14:31 -0400 Subject: [PATCH 11/15] fix Signed-off-by: Danny Chiao --- sdk/python/feast/diff/registry_diff.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/sdk/python/feast/diff/registry_diff.py b/sdk/python/feast/diff/registry_diff.py index 248c0596ccd..f39a237fd46 100644 --- a/sdk/python/feast/diff/registry_diff.py +++ b/sdk/python/feast/diff/registry_diff.py @@ -60,9 +60,9 @@ def to_string(self): continue if feast_object_diff.transition_type == TransitionType.UNCHANGED: continue - # if feast_object_diff.feast_object_type == FeastObjectType.DATA_SOURCE: - # # TODO(adchia): Print statements out starting in Feast 0.21 - # continue + if feast_object_diff.feast_object_type == FeastObjectType.DATA_SOURCE: + # TODO(adchia): Print statements out starting in Feast 0.21 + continue action, color = message_action_map[feast_object_diff.transition_type] log_string += f"{action} {feast_object_diff.feast_object_type.value} {Style.BRIGHT + color}{feast_object_diff.name}{Style.RESET_ALL}\n" if feast_object_diff.transition_type == TransitionType.UPDATE: From 70d4b8d24ee9406bd96e9bf6f0f05c8ae871beec Mon Sep 17 00:00:00 2001 From: Danny Chiao Date: Tue, 29 Mar 2022 12:17:32 -0400 Subject: [PATCH 12/15] revert Signed-off-by: Danny Chiao --- sdk/python/tests/integration/registration/test_cli.py | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/sdk/python/tests/integration/registration/test_cli.py b/sdk/python/tests/integration/registration/test_cli.py index e441d831d95..c79d672fb94 100644 --- a/sdk/python/tests/integration/registration/test_cli.py +++ b/sdk/python/tests/integration/registration/test_cli.py @@ -28,6 +28,7 @@ RedshiftDataSourceCreator, ) from tests.utils.cli_utils import CliRunner, get_example_repo +from tests.utils.online_read_write_test import basic_rw_test @pytest.mark.integration @@ -83,12 +84,12 @@ def test_universal_cli(environment: Environment): cwd=repo_path, ) assertpy.assert_that(result.returncode).is_equal_to(0) - assertpy.assert_that(fs.list_feature_views()).is_length(4) + assertpy.assert_that(fs.list_feature_views()).is_length(3) result = runner.run( ["data-sources", "describe", "customer_profile_source"], cwd=repo_path, ) assertpy.assert_that(result.returncode).is_equal_to(0) - assertpy.assert_that(fs.list_data_sources()).is_length(4) + assertpy.assert_that(fs.list_data_sources()).is_length(3) # entity & feature view describe commands should fail when objects don't exist result = runner.run(["entities", "describe", "foo"], cwd=repo_path) @@ -103,6 +104,12 @@ def test_universal_cli(environment: Environment): # Doing another apply should be a no op, and should not cause errors result = runner.run(["apply"], cwd=repo_path) assertpy.assert_that(result.returncode).is_equal_to(0) + basic_rw_test( + FeatureStore(repo_path=str(repo_path), config=None), + view_name="driver_locations", + ) + + # Confirm that registry contents have not changed. registry_dict = fs.registry.to_dict(project=project) assertpy.assert_that(registry_specs).is_equal_to( { From e17973f8cefd82ed5d35ca424dae6cef55a8b25a Mon Sep 17 00:00:00 2001 From: Danny Chiao Date: Tue, 29 Mar 2022 12:34:09 -0400 Subject: [PATCH 13/15] fix Signed-off-by: Danny Chiao --- sdk/python/tests/integration/registration/test_cli.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/sdk/python/tests/integration/registration/test_cli.py b/sdk/python/tests/integration/registration/test_cli.py index c79d672fb94..456baed2603 100644 --- a/sdk/python/tests/integration/registration/test_cli.py +++ b/sdk/python/tests/integration/registration/test_cli.py @@ -84,12 +84,12 @@ def test_universal_cli(environment: Environment): cwd=repo_path, ) assertpy.assert_that(result.returncode).is_equal_to(0) - assertpy.assert_that(fs.list_feature_views()).is_length(3) + assertpy.assert_that(fs.list_feature_views()).is_length(4) result = runner.run( ["data-sources", "describe", "customer_profile_source"], cwd=repo_path, ) assertpy.assert_that(result.returncode).is_equal_to(0) - assertpy.assert_that(fs.list_data_sources()).is_length(3) + assertpy.assert_that(fs.list_data_sources()).is_length(5) # entity & feature view describe commands should fail when objects don't exist result = runner.run(["entities", "describe", "foo"], cwd=repo_path) From 0fe2df8fdc99d6550471f6ad4ca424d472c02bd3 Mon Sep 17 00:00:00 2001 From: Danny Chiao Date: Tue, 29 Mar 2022 14:30:40 -0400 Subject: [PATCH 14/15] fix Signed-off-by: Danny Chiao --- sdk/python/feast/diff/registry_diff.py | 9 +++++---- sdk/python/tests/integration/registration/test_cli.py | 2 +- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/sdk/python/feast/diff/registry_diff.py b/sdk/python/feast/diff/registry_diff.py index f39a237fd46..b78087a63bb 100644 --- a/sdk/python/feast/diff/registry_diff.py +++ b/sdk/python/feast/diff/registry_diff.py @@ -81,10 +81,11 @@ def to_string(self): def tag_objects_for_keep_delete_update_add( existing_objs: Iterable[FeastObject], desired_objs: Iterable[FeastObject] ) -> Tuple[List[FeastObject], List[FeastObject], List[FeastObject], List[FeastObject]]: - existing_obj_names = {e.name for e in existing_objs} - desired_objs = list(desired_objs) - existing_objs = list(existing_objs) - desired_obj_names = {e.name for e in desired_objs} + # TODO(adchia): Remove the "if X.name" condition when data sources are forced to have names + existing_obj_names = {e.name for e in existing_objs if e.name} + desired_objs = [obj for obj in desired_objs if obj.name] + existing_objs = [obj for obj in existing_objs if obj.name] + desired_obj_names = {e.name for e in desired_objs if e.name} objs_to_add = [e for e in desired_objs if e.name not in existing_obj_names] objs_to_update = [e for e in desired_objs if e.name in existing_obj_names] diff --git a/sdk/python/tests/integration/registration/test_cli.py b/sdk/python/tests/integration/registration/test_cli.py index 456baed2603..655e53e7593 100644 --- a/sdk/python/tests/integration/registration/test_cli.py +++ b/sdk/python/tests/integration/registration/test_cli.py @@ -89,7 +89,7 @@ def test_universal_cli(environment: Environment): ["data-sources", "describe", "customer_profile_source"], cwd=repo_path, ) assertpy.assert_that(result.returncode).is_equal_to(0) - assertpy.assert_that(fs.list_data_sources()).is_length(5) + assertpy.assert_that(fs.list_data_sources()).is_length(4) # entity & feature view describe commands should fail when objects don't exist result = runner.run(["entities", "describe", "foo"], cwd=repo_path) From f854de47a76aec13df470e59e91d7e0f01c595bd Mon Sep 17 00:00:00 2001 From: Danny Chiao Date: Tue, 29 Mar 2022 15:37:35 -0400 Subject: [PATCH 15/15] fix Signed-off-by: Danny Chiao --- sdk/python/feast/base_feature_view.py | 3 --- sdk/python/feast/data_source.py | 3 --- sdk/python/feast/diff/registry_diff.py | 18 +++++++++--------- sdk/python/feast/entity.py | 3 --- sdk/python/feast/feature_store.py | 2 +- sdk/python/feast/go_server.py | 2 +- sdk/python/feast/registry.py | 4 +--- sdk/python/feast/transformation_server.py | 2 +- 8 files changed, 13 insertions(+), 24 deletions(-) diff --git a/sdk/python/feast/base_feature_view.py b/sdk/python/feast/base_feature_view.py index ffd069cd94d..609feab6414 100644 --- a/sdk/python/feast/base_feature_view.py +++ b/sdk/python/feast/base_feature_view.py @@ -107,9 +107,6 @@ def __repr__(self): def __str__(self): return str(MessageToJson(self.to_proto())) - def __lt__(self, other): - return self.name < other.name - def __hash__(self): return hash((id(self), self.name)) diff --git a/sdk/python/feast/data_source.py b/sdk/python/feast/data_source.py index 813218e13c6..1e0cb47caa9 100644 --- a/sdk/python/feast/data_source.py +++ b/sdk/python/feast/data_source.py @@ -197,9 +197,6 @@ def __hash__(self): def __str__(self): return str(MessageToJson(self.to_proto())) - def __lt__(self, other): - return str(self) < str(other) - def __eq__(self, other): if not isinstance(other, DataSource): raise TypeError("Comparisons should only involve DataSource class objects.") diff --git a/sdk/python/feast/diff/registry_diff.py b/sdk/python/feast/diff/registry_diff.py index b78087a63bb..10bd88c56f8 100644 --- a/sdk/python/feast/diff/registry_diff.py +++ b/sdk/python/feast/diff/registry_diff.py @@ -80,17 +80,17 @@ def to_string(self): def tag_objects_for_keep_delete_update_add( existing_objs: Iterable[FeastObject], desired_objs: Iterable[FeastObject] -) -> Tuple[List[FeastObject], List[FeastObject], List[FeastObject], List[FeastObject]]: +) -> Tuple[Set[FeastObject], Set[FeastObject], Set[FeastObject], Set[FeastObject]]: # TODO(adchia): Remove the "if X.name" condition when data sources are forced to have names existing_obj_names = {e.name for e in existing_objs if e.name} desired_objs = [obj for obj in desired_objs if obj.name] existing_objs = [obj for obj in existing_objs if obj.name] desired_obj_names = {e.name for e in desired_objs if e.name} - objs_to_add = [e for e in desired_objs if e.name not in existing_obj_names] - objs_to_update = [e for e in desired_objs if e.name in existing_obj_names] - objs_to_keep = [e for e in existing_objs if e.name in desired_obj_names] - objs_to_delete = [e for e in existing_objs if e.name not in desired_obj_names] + objs_to_add = {e for e in desired_objs if e.name not in existing_obj_names} + objs_to_update = {e for e in desired_objs if e.name in existing_obj_names} + objs_to_keep = {e for e in existing_objs if e.name in desired_obj_names} + objs_to_delete = {e for e in existing_objs if e.name not in desired_obj_names} return objs_to_keep, objs_to_delete, objs_to_update, objs_to_add @@ -155,10 +155,10 @@ def diff_registry_objects( def extract_objects_for_keep_delete_update_add( registry: Registry, current_project: str, desired_repo_contents: RepoContents, ) -> Tuple[ - Dict[FeastObjectType, List[FeastObject]], - Dict[FeastObjectType, List[FeastObject]], - Dict[FeastObjectType, List[FeastObject]], - Dict[FeastObjectType, List[FeastObject]], + Dict[FeastObjectType, Set[FeastObject]], + Dict[FeastObjectType, Set[FeastObject]], + Dict[FeastObjectType, Set[FeastObject]], + Dict[FeastObjectType, Set[FeastObject]], ]: """ Returns the objects in the registry that must be modified to achieve the desired repo state. diff --git a/sdk/python/feast/entity.py b/sdk/python/feast/entity.py index 965ef507b0e..817d884a658 100644 --- a/sdk/python/feast/entity.py +++ b/sdk/python/feast/entity.py @@ -73,9 +73,6 @@ def __init__( def __hash__(self) -> int: return hash((id(self), self.name)) - def __lt__(self, other): - return self.name < other.name - def __eq__(self, other): if not isinstance(other, Entity): raise TypeError("Comparisons should only involve Entity class objects.") diff --git a/sdk/python/feast/feature_store.py b/sdk/python/feast/feature_store.py index 53d1be86bf3..4871fe2f6a5 100644 --- a/sdk/python/feast/feature_store.py +++ b/sdk/python/feast/feature_store.py @@ -675,7 +675,7 @@ def apply( for v in odfv.input_request_data_sources.values(): data_sources_set_to_update.add(v) - data_sources_to_update = sorted(list(data_sources_set_to_update)) + data_sources_to_update = list(data_sources_set_to_update) # Validate all feature views and make inferences. self._validate_all_feature_views( diff --git a/sdk/python/feast/go_server.py b/sdk/python/feast/go_server.py index 38a350fd6d2..1fcbab61f08 100644 --- a/sdk/python/feast/go_server.py +++ b/sdk/python/feast/go_server.py @@ -25,10 +25,10 @@ from subprocess import Popen from typing import Any, Dict, List, Optional, Union +import grpc from tenacity import retry, stop_after_attempt, stop_after_delay, wait_exponential import feast -import grpc from feast.errors import FeatureNameCollisionError, InvalidFeaturesParameterType from feast.feature_service import FeatureService from feast.flags_helper import is_test diff --git a/sdk/python/feast/registry.py b/sdk/python/feast/registry.py index 6ea123b18b3..da9c6c6b217 100644 --- a/sdk/python/feast/registry.py +++ b/sdk/python/feast/registry.py @@ -81,9 +81,7 @@ def get_objects_from_registry( registry: "Registry", project: str ) -> Dict["FeastObjectType", List[Any]]: return { - FeastObjectType.DATA_SOURCE: sorted( - registry.list_data_sources(project=project) - ), + FeastObjectType.DATA_SOURCE: registry.list_data_sources(project=project), FeastObjectType.ENTITY: registry.list_entities(project=project), FeastObjectType.FEATURE_VIEW: registry.list_feature_views(project=project), FeastObjectType.ON_DEMAND_FEATURE_VIEW: registry.list_on_demand_feature_views( diff --git a/sdk/python/feast/transformation_server.py b/sdk/python/feast/transformation_server.py index 8e0efd73137..83f4af749e3 100644 --- a/sdk/python/feast/transformation_server.py +++ b/sdk/python/feast/transformation_server.py @@ -2,10 +2,10 @@ import sys from concurrent import futures +import grpc import pyarrow as pa from grpc_reflection.v1alpha import reflection -import grpc from feast.errors import OnDemandFeatureViewNotFoundException from feast.feature_store import FeatureStore from feast.protos.feast.serving.TransformationService_pb2 import (