From f1df40a0747da3fa244ebb30dfe08824ba7cf1ab Mon Sep 17 00:00:00 2001 From: codyjlin <31944154+codyjlin@users.noreply.github.com> Date: Mon, 11 Oct 2021 15:51:04 -0700 Subject: [PATCH 01/50] Implement entity aliasing for feature retrival (#1868) * Initial commit for implementing shadow entities 8b Signed-off-by: Cody Lin * fix small bug failing unit tests Signed-off-by: Cody Lin * revert changes using FVProjection Signed-off-by: Cody Lin * initial design for testing Signed-off-by: Cody Lin * add join_key_map to FV proto definition Signed-off-by: Cody Lin * handle join_key_map for proto conversions Signed-off-by: Cody Lin * try to grab underlying changes Signed-off-by: Cody Lin * Tests fixed after rebasing with dependencies Signed-off-by: Cody Lin * Remove small unnecessary changes Signed-off-by: Cody Lin * Remove small unnecessary changes Signed-off-by: Cody Lin * Fix lint issues Signed-off-by: Cody Lin * Fix redshift query error Signed-off-by: Cody Lin * Fix cache error Signed-off-by: Cody Lin * Fix benchmark test error Signed-off-by: Cody Lin * remove unnecessary changes Signed-off-by: Cody Lin * respond to David's comments Signed-off-by: Cody Lin * Improve docstring, revert to entity_name_to_join_key_map Signed-off-by: Cody Lin * Fix docstring test Signed-off-by: Cody Lin * Remove code from previous design Signed-off-by: Cody Lin * Improve comments and variable naming Signed-off-by: Cody Lin * Use get with default Signed-off-by: Cody Lin * Fix use get with default Signed-off-by: Cody Lin Co-authored-by: David Y Liu --- protos/feast/core/FeatureViewProjection.proto | 3 + sdk/python/feast/driver_test_data.py | 64 +++++- sdk/python/feast/feature_store.py | 42 +++- sdk/python/feast/feature_view.py | 40 +++- sdk/python/feast/feature_view_projection.py | 8 +- sdk/python/feast/infra/offline_stores/file.py | 14 +- .../infra/offline_stores/offline_utils.py | 11 +- .../feast/infra/offline_stores/redshift.py | 2 +- ...st_benchmark_universal_online_retrieval.py | 8 +- .../feature_repos/repo_configuration.py | 31 ++- .../feature_repos/universal/entities.py | 4 + .../feature_repos/universal/feature_views.py | 13 ++ .../test_universal_historical_retrieval.py | 145 ++++++++++++- .../online_store/test_universal_online.py | 197 ++++++++++++++++-- 14 files changed, 528 insertions(+), 54 deletions(-) diff --git a/protos/feast/core/FeatureViewProjection.proto b/protos/feast/core/FeatureViewProjection.proto index 2ede5ebc362..e81d8dad01e 100644 --- a/protos/feast/core/FeatureViewProjection.proto +++ b/protos/feast/core/FeatureViewProjection.proto @@ -19,4 +19,7 @@ message FeatureViewProjection { // The features of the feature view that are a part of the feature reference. repeated FeatureSpecV2 feature_columns = 2; + + // Map for entity join_key overrides of feature data entity join_key to entity data join_key + map join_key_map = 4; } diff --git a/sdk/python/feast/driver_test_data.py b/sdk/python/feast/driver_test_data.py index 6aa98b3d064..1c9a1dd20bc 100644 --- a/sdk/python/feast/driver_test_data.py +++ b/sdk/python/feast/driver_test_data.py @@ -1,4 +1,5 @@ # This module generates dummy data to be used for tests and examples. +import itertools from enum import Enum import numpy as np @@ -29,22 +30,29 @@ def _convert_event_timestamp(event_timestamp: pd.Timestamp, t: EventTimestampTyp def create_orders_df( - customers, drivers, start_date, end_date, order_count, + customers, drivers, start_date, end_date, order_count, locations=None, ) -> pd.DataFrame: """ - Example df generated by this function: + Example df generated by this function (if locations): - | order_id | driver_id | customer_id | order_is_success | event_timestamp | - +----------+-----------+-------------+------------------+---------------------+ - | 100 | 5004 | 1007 | 0 | 2021-03-10 19:31:15 | - | 101 | 5003 | 1006 | 0 | 2021-03-11 22:02:50 | - | 102 | 5010 | 1005 | 0 | 2021-03-13 00:34:24 | - | 103 | 5010 | 1001 | 1 | 2021-03-14 03:05:59 | + | order_id | driver_id | customer_id | origin_id | destination_id | order_is_success | event_timestamp | + +----------+-----------+-------------+-----------+----------------+------------------+---------------------+ + | 100 | 5004 | 1007 | 1 | 18 | 0 | 2021-03-10 19:31:15 | + | 101 | 5003 | 1006 | 24 | 42 | 0 | 2021-03-11 22:02:50 | + | 102 | 5010 | 1005 | 19 | 12 | 0 | 2021-03-13 00:34:24 | + | 103 | 5010 | 1001 | 35 | 8 | 1 | 2021-03-14 03:05:59 | """ df = pd.DataFrame() df["order_id"] = [order_id for order_id in range(100, 100 + order_count)] df["driver_id"] = np.random.choice(drivers, order_count) df["customer_id"] = np.random.choice(customers, order_count) + if locations: + location_pairs = np.array(list(itertools.permutations(locations, 2))) + locations_sample = location_pairs[ + np.random.choice(len(location_pairs), order_count) + ].T + df["origin_id"] = locations_sample[0] + df["destination_id"] = locations_sample[1] df["order_is_success"] = np.random.randint(0, 2, size=order_count).astype(np.int32) df[DEFAULT_ENTITY_DF_EVENT_TIMESTAMP_COL] = [ _convert_event_timestamp( @@ -180,6 +188,46 @@ def create_customer_daily_profile_df(customers, start_date, end_date) -> pd.Data return df_all_customers +def create_location_stats_df(locations, start_date, end_date) -> pd.DataFrame: + """ + Example df generated by this function: + + | event_timestamp | location_id | temperature | created | + +------------------+-------------+-------------+------------------+ + | 2021-03-17 19:31 | 1 | 74 | 2021-03-24 19:38 | + | 2021-03-17 20:31 | 24 | 63 | 2021-03-24 19:38 | + | 2021-03-17 21:31 | 19 | 65 | 2021-03-24 19:38 | + | 2021-03-17 22:31 | 35 | 86 | 2021-03-24 19:38 | + """ + df_hourly = pd.DataFrame( + { + "event_timestamp": [ + pd.Timestamp(dt, unit="ms", tz="UTC").round("ms") + for dt in pd.date_range( + start=start_date, end=end_date, freq="1H", closed="left" + ) + ] + } + ) + df_all_locations = pd.DataFrame() + + for location in locations: + df_hourly_copy = df_hourly.copy() + df_hourly_copy["location_id"] = location + df_all_locations = pd.concat([df_hourly_copy, df_all_locations]) + + df_all_locations.reset_index(drop=True, inplace=True) + rows = df_all_locations["event_timestamp"].count() + + df_all_locations["temperature"] = np.random.randint(50, 100, size=rows).astype( + np.int32 + ) + + # TODO: Remove created timestamp in order to test whether its really optional + df_all_locations["created"] = pd.to_datetime(pd.Timestamp.now(tz=None).round("ms")) + return df_all_locations + + def create_global_daily_stats_df(start_date, end_date) -> pd.DataFrame: """ Example df generated by this function: diff --git a/sdk/python/feast/feature_store.py b/sdk/python/feast/feature_store.py index 7a175bb5b56..d6d0953d4cb 100644 --- a/sdk/python/feast/feature_store.py +++ b/sdk/python/feast/feature_store.py @@ -839,6 +839,20 @@ def get_online_features( entity_name_to_join_key_map = {} for entity in entities: entity_name_to_join_key_map[entity.name] = entity.join_key + for feature_view in all_feature_views: + for entity_name in feature_view.entities: + entity = self._registry.get_entity( + entity_name, self.project, allow_cache=True + ) + # User directly uses join_key as the entity reference in the entity_rows for the + # entity mapping case. + entity_name = feature_view.projection.join_key_map.get( + entity.join_key, entity.name + ) + join_key = feature_view.projection.join_key_map.get( + entity.join_key, entity.join_key + ) + entity_name_to_join_key_map[entity_name] = join_key needed_request_data_features = self._get_needed_request_data_features( grouped_odfv_refs @@ -895,8 +909,12 @@ def get_online_features( ] = GetOnlineFeaturesResponse.FieldStatus.PRESENT for table, requested_features in grouped_refs: + table_join_keys = [ + entity_name_to_join_key_map[entity_name] + for entity_name in table.entities + ] self._populate_result_rows_from_feature_view( - entity_name_to_join_key_map, + table_join_keys, full_feature_names, provider, requested_features, @@ -918,7 +936,7 @@ def get_online_features( def _populate_result_rows_from_feature_view( self, - entity_name_to_join_key_map: Dict[str, str], + table_join_keys: List[str], full_feature_names: bool, provider: Provider, requested_features: List[str], @@ -927,7 +945,7 @@ def _populate_result_rows_from_feature_view( union_of_entity_keys: List[EntityKeyProto], ): entity_keys = _get_table_entity_keys( - table, union_of_entity_keys, entity_name_to_join_key_map + table, union_of_entity_keys, table_join_keys ) read_rows = provider.online_read( config=self.config, @@ -1045,8 +1063,8 @@ def _get_feature_views_to_use( ) } - fvs_to_use, od_fvs_to_use = [], [] if isinstance(features, FeatureService): + fvs_to_use, od_fvs_to_use = [], [] for fv_name, projection in [ (projection.name, projection) for projection in features.feature_view_projections @@ -1137,10 +1155,12 @@ def _group_feature_refs( """ Get list of feature views and corresponding feature names based on feature references""" # view name to view proto - view_index = {view.name: view for view in all_feature_views} + view_index = {view.projection.name_to_use(): view for view in all_feature_views} # on demand view to on demand view proto - on_demand_view_index = {view.name: view for view in all_on_demand_feature_views} + on_demand_view_index = { + view.projection.name_to_use(): view for view in all_on_demand_feature_views + } # view name to feature names views_features = defaultdict(list) @@ -1168,15 +1188,19 @@ def _group_feature_refs( def _get_table_entity_keys( - table: FeatureView, entity_keys: List[EntityKeyProto], join_key_map: Dict[str, str], + table: FeatureView, entity_keys: List[EntityKeyProto], table_join_keys: List[str] ) -> List[EntityKeyProto]: - table_join_keys = [join_key_map[entity_name] for entity_name in table.entities] + reverse_join_key_map = { + alias: original for original, alias in table.projection.join_key_map.items() + } required_entities = OrderedDict.fromkeys(sorted(table_join_keys)) entity_key_protos = [] for entity_key in entity_keys: required_entities_to_values = required_entities.copy() for i in range(len(entity_key.join_keys)): - entity_name = entity_key.join_keys[i] + entity_name = reverse_join_key_map.get( + entity_key.join_keys[i], entity_key.join_keys[i] + ) entity_value = entity_key.entity_values[i] if entity_name in required_entities_to_values: diff --git a/sdk/python/feast/feature_view.py b/sdk/python/feast/feature_view.py index 3a5e78d83f4..68fec061ac3 100644 --- a/sdk/python/feast/feature_view.py +++ b/sdk/python/feast/feature_view.py @@ -126,7 +126,7 @@ def __init__( self.name = name self.entities = entities if entities else [DUMMY_ENTITY_NAME] self.features = _features - self.tags = tags if tags is not None else {} + self.tags = tags if tags else {} if isinstance(ttl, Duration): self.ttl = timedelta(seconds=int(ttl.seconds)) @@ -238,6 +238,44 @@ def with_name(self, name: str): return cp + def with_join_key_map(self, join_key_map: Dict[str, str]): + """ + Sets the join_key_map by returning a copy of this feature view with that field set. + This join_key mapping operation is only used as part of query operations and will + not modify the underlying FeatureView. + + Args: + join_key_map: A map of join keys in which the left is the join_key that + corresponds with the feature data and the right corresponds with the entity data. + + Returns: + A copy of this FeatureView with the join_key_map replaced with the 'join_key_map' input. + + Examples: + Join a location feature data table to both the origin column and destination + column of the entity data. + + temperatures_feature_service = FeatureService( + name="temperatures", + features=[ + location_stats_feature_view + .with_name("origin_stats") + .with_join_key_map( + {"location_id": "origin_id"} + ), + location_stats_feature_view + .with_name("destination_stats") + .with_join_key_map( + {"location_id": "destination_id"} + ), + ], + ) + """ + cp = self.__copy__() + cp.projection.join_key_map = join_key_map + + return cp + def with_projection(self, feature_view_projection: FeatureViewProjection): """ Sets the feature view projection by returning a copy of this feature view diff --git a/sdk/python/feast/feature_view_projection.py b/sdk/python/feast/feature_view_projection.py index e271937e0ed..97b3b0ab577 100644 --- a/sdk/python/feast/feature_view_projection.py +++ b/sdk/python/feast/feature_view_projection.py @@ -1,4 +1,4 @@ -from typing import List, Optional +from typing import Dict, List, Optional from attr import dataclass @@ -13,13 +13,16 @@ class FeatureViewProjection: name: str name_alias: Optional[str] features: List[Feature] + join_key_map: Dict[str, str] = {} def name_to_use(self): return self.name_alias or self.name def to_proto(self): feature_reference_proto = FeatureViewProjectionProto( - feature_view_name=self.name, feature_view_name_alias=self.name_alias + feature_view_name=self.name, + feature_view_name_alias=self.name_alias, + join_key_map=self.join_key_map, ) for feature in self.features: feature_reference_proto.feature_columns.append(feature.to_proto()) @@ -32,6 +35,7 @@ def from_proto(proto: FeatureViewProjectionProto): name=proto.feature_view_name, name_alias=proto.feature_view_name_alias, features=[], + join_key_map=dict(proto.join_key_map), ) for feature_column in proto.feature_columns: ref.features.append(Feature.from_proto(feature_column)) diff --git a/sdk/python/feast/infra/offline_stores/file.py b/sdk/python/feast/infra/offline_stores/file.py index 17c3ebfad67..a38d175b116 100644 --- a/sdk/python/feast/infra/offline_stores/file.py +++ b/sdk/python/feast/infra/offline_stores/file.py @@ -143,6 +143,11 @@ def evaluate_historical_retrieval(): table = _run_field_mapping( table, feature_view.batch_source.field_mapping ) + # Rename entity columns by the join_key_map dictionary if it exists + if feature_view.projection.join_key_map: + table = _run_field_mapping( + table, feature_view.projection.join_key_map + ) # Convert pyarrow table to pandas dataframe. Note, if the underlying data has missing values, # pandas will convert those values to np.nan if the dtypes are numerical (floats, ints, etc.) or boolean @@ -176,7 +181,9 @@ def evaluate_historical_retrieval(): # double underscore as separator for consistency with other databases like BigQuery, # where there are very few characters available for use as separators if full_feature_names: - formatted_feature_name = f"{feature_view.name}__{feature}" + formatted_feature_name = ( + f"{feature_view.projection.name_to_use()}__{feature}" + ) else: formatted_feature_name = feature # Add the feature name to the list of columns @@ -191,7 +198,10 @@ def evaluate_historical_retrieval(): join_keys = [] for entity_name in feature_view.entities: entity = registry.get_entity(entity_name, project) - join_keys.append(entity.join_key) + join_key = feature_view.projection.join_key_map.get( + entity.join_key, entity.join_key + ) + join_keys.append(join_key) right_entity_columns = join_keys right_entity_key_columns = [ event_timestamp_column diff --git a/sdk/python/feast/infra/offline_stores/offline_utils.py b/sdk/python/feast/infra/offline_stores/offline_utils.py index 05a6486293d..1e23ca54fb2 100644 --- a/sdk/python/feast/infra/offline_stores/offline_utils.py +++ b/sdk/python/feast/infra/offline_stores/offline_utils.py @@ -64,7 +64,10 @@ def get_expected_join_keys( entities = feature_view.entities for entity_name in entities: entity = registry.get_entity(entity_name, project) - join_keys.add(entity.join_key) + join_key = feature_view.projection.join_key_map.get( + entity.join_key, entity.join_key + ) + join_keys.add(join_key) return join_keys @@ -113,11 +116,11 @@ def get_feature_view_query_context( } for entity_name in feature_view.entities: entity = registry.get_entity(entity_name, project) - join_keys.append(entity.join_key) - join_key_column = reverse_field_mapping.get( + join_key = feature_view.projection.join_key_map.get( entity.join_key, entity.join_key ) - entity_selections.append(f"{join_key_column} AS {entity.join_key}") + join_keys.append(join_key) + entity_selections.append(f"{entity.join_key} AS {join_key}") if isinstance(feature_view.ttl, timedelta): ttl_seconds = int(feature_view.ttl.total_seconds()) diff --git a/sdk/python/feast/infra/offline_stores/redshift.py b/sdk/python/feast/infra/offline_stores/redshift.py index 1b17ae15b0a..40fa9cf94b6 100644 --- a/sdk/python/feast/infra/offline_stores/redshift.py +++ b/sdk/python/feast/infra/offline_stores/redshift.py @@ -176,7 +176,7 @@ def query_generator() -> Iterator[str]: ), drop_columns=["entity_timestamp"] + [ - f"{feature_view.name}__entity_row_unique_id" + f"{feature_view.projection.name_to_use()}__entity_row_unique_id" for feature_view in feature_views ], ) diff --git a/sdk/python/tests/benchmarks/test_benchmark_universal_online_retrieval.py b/sdk/python/tests/benchmarks/test_benchmark_universal_online_retrieval.py index b124cd5c762..0651383880f 100644 --- a/sdk/python/tests/benchmarks/test_benchmark_universal_online_retrieval.py +++ b/sdk/python/tests/benchmarks/test_benchmark_universal_online_retrieval.py @@ -6,7 +6,11 @@ from tests.integration.feature_repos.repo_configuration import ( construct_universal_feature_views, ) -from tests.integration.feature_repos.universal.entities import customer, driver +from tests.integration.feature_repos.universal.entities import ( + customer, + driver, + location, +) @pytest.mark.benchmark @@ -24,7 +28,7 @@ def test_online_retrieval(environment, universal_data_sources, benchmark): feast_objects = [] feast_objects.extend(feature_views.values()) - feast_objects.extend([driver(), customer(), feature_service]) + feast_objects.extend([driver(), customer(), location(), feature_service]) fs.apply(feast_objects) fs.materialize(environment.start_date, environment.end_date) diff --git a/sdk/python/tests/integration/feature_repos/repo_configuration.py b/sdk/python/tests/integration/feature_repos/repo_configuration.py index afb66ba7f2c..876b66ef4a4 100644 --- a/sdk/python/tests/integration/feature_repos/repo_configuration.py +++ b/sdk/python/tests/integration/feature_repos/repo_configuration.py @@ -28,6 +28,7 @@ create_customer_daily_profile_feature_view, create_driver_hourly_stats_feature_view, create_global_stats_feature_view, + create_location_stats_feature_view, create_order_feature_view, ) @@ -90,7 +91,11 @@ def __repr__(self) -> str: def construct_universal_entities() -> Dict[str, List[Any]]: - return {"customer": list(range(1001, 1020)), "driver": list(range(5001, 5020))} + return { + "customer": list(range(1001, 1020)), + "driver": list(range(5001, 5020)), + "location": list(range(1, 50)), + } def construct_universal_datasets( @@ -102,19 +107,33 @@ def construct_universal_datasets( driver_df = driver_test_data.create_driver_hourly_stats_df( entities["driver"], start_time, end_time ) + location_df = driver_test_data.create_location_stats_df( + entities["location"], start_time, end_time + ) orders_df = driver_test_data.create_orders_df( customers=entities["customer"], drivers=entities["driver"], + locations=entities["location"], start_date=start_time, end_date=end_time, order_count=20, ) global_df = driver_test_data.create_global_daily_stats_df(start_time, end_time) - entity_df = orders_df[["customer_id", "driver_id", "order_id", "event_timestamp"]] + entity_df = orders_df[ + [ + "customer_id", + "driver_id", + "order_id", + "origin_id", + "destination_id", + "event_timestamp", + ] + ] return { "customer": customer_df, "driver": driver_df, + "location": location_df, "orders": orders_df, "global": global_df, "entity": entity_df, @@ -136,6 +155,12 @@ def construct_universal_data_sources( event_timestamp_column="event_timestamp", created_timestamp_column="created", ) + location_ds = data_source_creator.create_data_source( + datasets["location"], + destination_name="location_hourly", + event_timestamp_column="event_timestamp", + created_timestamp_column="created", + ) orders_ds = data_source_creator.create_data_source( datasets["orders"], destination_name="orders", @@ -151,6 +176,7 @@ def construct_universal_data_sources( return { "customer": customer_ds, "driver": driver_ds, + "location": location_ds, "orders": orders_ds, "global": global_ds, } @@ -175,6 +201,7 @@ def construct_universal_feature_views( } ), "order": create_order_feature_view(data_sources["orders"]), + "location": create_location_stats_feature_view(data_sources["location"]), } diff --git a/sdk/python/tests/integration/feature_repos/universal/entities.py b/sdk/python/tests/integration/feature_repos/universal/entities.py index 8886e813d51..3b4ec05f5d9 100644 --- a/sdk/python/tests/integration/feature_repos/universal/entities.py +++ b/sdk/python/tests/integration/feature_repos/universal/entities.py @@ -12,3 +12,7 @@ def driver(value_type: ValueType = ValueType.INT64): def customer(): return Entity(name="customer_id", value_type=ValueType.INT64) + + +def location(): + return Entity(name="location_id", value_type=ValueType.INT64) diff --git a/sdk/python/tests/integration/feature_repos/universal/feature_views.py b/sdk/python/tests/integration/feature_repos/universal/feature_views.py index 1b701190581..f68d1a6555b 100644 --- a/sdk/python/tests/integration/feature_repos/universal/feature_views.py +++ b/sdk/python/tests/integration/feature_repos/universal/feature_views.py @@ -129,3 +129,16 @@ def create_order_feature_view(source, infer_features: bool = False): batch_source=source, ttl=timedelta(days=2), ) + + +def create_location_stats_feature_view(source, infer_features: bool = False): + location_stats_feature_view = FeatureView( + name="location_stats", + entities=["location_id"], + features=None + if infer_features + else [Feature(name="temperature", dtype=ValueType.INT32)], + batch_source=source, + ttl=timedelta(days=2), + ) + return location_stats_feature_view diff --git a/sdk/python/tests/integration/offline_store/test_universal_historical_retrieval.py b/sdk/python/tests/integration/offline_store/test_universal_historical_retrieval.py index 9ad2a276041..d4e81b30de8 100644 --- a/sdk/python/tests/integration/offline_store/test_universal_historical_retrieval.py +++ b/sdk/python/tests/integration/offline_store/test_universal_historical_retrieval.py @@ -8,7 +8,10 @@ from pytz import utc from feast import utils -from feast.errors import RequestDataNotFoundInEntityDfException +from feast.errors import ( + FeatureNameCollisionError, + RequestDataNotFoundInEntityDfException, +) from feast.feature_service import FeatureService from feast.feature_view import FeatureView from feast.infra.offline_stores.offline_utils import ( @@ -18,7 +21,11 @@ construct_universal_feature_views, table_name_from_data_source, ) -from tests.integration.feature_repos.universal.entities import customer, driver +from tests.integration.feature_repos.universal.entities import ( + customer, + driver, + location, +) np.random.seed(0) @@ -66,6 +73,8 @@ def get_expected_training_df( driver_fv: FeatureView, orders_df: pd.DataFrame, order_fv: FeatureView, + location_df: pd.DataFrame, + location_fv: FeatureView, global_df: pd.DataFrame, global_fv: FeatureView, entity_df: pd.DataFrame, @@ -82,6 +91,9 @@ def get_expected_training_df( order_records = convert_timestamp_records_to_utc( orders_df.to_dict("records"), event_timestamp ) + location_records = convert_timestamp_records_to_utc( + location_df.to_dict("records"), location_fv.batch_source.event_timestamp_column + ) global_records = convert_timestamp_records_to_utc( global_df.to_dict("records"), global_fv.batch_source.event_timestamp_column ) @@ -116,6 +128,22 @@ def get_expected_training_df( filter_keys=["customer_id", "driver_id"], filter_values=[entity_row["customer_id"], entity_row["driver_id"]], ) + origin_record = find_asof_record( + location_records, + ts_key=location_fv.batch_source.event_timestamp_column, + ts_start=order_record[event_timestamp] - location_fv.ttl, + ts_end=order_record[event_timestamp], + filter_keys=["location_id"], + filter_values=[order_record["origin_id"]], + ) + destination_record = find_asof_record( + location_records, + ts_key=location_fv.batch_source.event_timestamp_column, + ts_start=order_record[event_timestamp] - location_fv.ttl, + ts_end=order_record[event_timestamp], + filter_keys=["location_id"], + filter_values=[order_record["destination_id"]], + ) global_record = find_asof_record( global_records, ts_key=global_fv.batch_source.event_timestamp_column, @@ -149,6 +177,12 @@ def get_expected_training_df( for k in ("order_is_success",) } ) + entity_row.update( + { + "origin__temperature": origin_record.get("temperature", None), + "destination__temperature": destination_record.get("temperature", None), + } + ) entity_row.update( { (f"global_stats__{k}" if full_feature_names else k): global_record.get( @@ -204,9 +238,10 @@ def test_historical_features(environment, universal_data_sources, full_feature_n (entities, datasets, data_sources) = universal_data_sources feature_views = construct_universal_feature_views(data_sources) - customer_df, driver_df, orders_df, global_df, entity_df = ( + customer_df, driver_df, location_df, orders_df, global_df, entity_df = ( datasets["customer"], datasets["driver"], + datasets["location"], datasets["orders"], datasets["global"], datasets["entity"], @@ -216,18 +251,30 @@ def test_historical_features(environment, universal_data_sources, full_feature_n i for i in range(len(entity_df_with_request_data)) ] - customer_fv, driver_fv, driver_odfv, order_fv, global_fv = ( + customer_fv, driver_fv, driver_odfv, location_fv, order_fv, global_fv = ( feature_views["customer"], feature_views["driver"], feature_views["driver_odfv"], + feature_views["location"], feature_views["order"], feature_views["global"], ) feature_service = FeatureService( - "convrate_plus100", + name="convrate_plus100", features=[feature_views["driver"][["conv_rate"]], feature_views["driver_odfv"]], ) + feature_service_entity_mapping = FeatureService( + name="entity_mapping", + features=[ + location_fv.with_name("origin").with_join_key_map( + {"location_id": "origin_id"} + ), + location_fv.with_name("destination").with_join_key_map( + {"location_id": "destination_id"} + ), + ], + ) feast_objects = [] feast_objects.extend( @@ -235,11 +282,14 @@ def test_historical_features(environment, universal_data_sources, full_feature_n customer_fv, driver_fv, driver_odfv, + location_fv, order_fv, global_fv, driver(), customer(), + location(), feature_service, + feature_service_entity_mapping, ] ) store.apply(feast_objects) @@ -247,20 +297,22 @@ def test_historical_features(environment, universal_data_sources, full_feature_n entity_df_query = None orders_table = table_name_from_data_source(data_sources["orders"]) if orders_table: - entity_df_query = f"SELECT customer_id, driver_id, order_id, event_timestamp FROM {orders_table}" + entity_df_query = f"SELECT customer_id, driver_id, order_id, origin_id, destination_id, event_timestamp FROM {orders_table}" event_timestamp = ( DEFAULT_ENTITY_DF_EVENT_TIMESTAMP_COL if DEFAULT_ENTITY_DF_EVENT_TIMESTAMP_COL in orders_df.columns else "e_ts" ) - expected_df = get_expected_training_df( + full_expected_df = get_expected_training_df( customer_df, customer_fv, driver_df, driver_fv, orders_df, order_fv, + location_df, + location_fv, global_df, global_fv, entity_df_with_request_data, @@ -268,6 +320,11 @@ def test_historical_features(environment, universal_data_sources, full_feature_n full_feature_names, ) + # Only need the shadow entities features in the FeatureService test + expected_df = full_expected_df.drop( + columns=["origin__temperature", "destination__temperature"], + ) + if entity_df_query: job_from_sql = store.get_historical_features( entity_df=entity_df_query, @@ -382,6 +439,14 @@ def test_historical_features(environment, universal_data_sources, full_feature_n expected_df, event_timestamp, ) + assert_feature_service_entity_mapping_correctness( + store, + feature_service_entity_mapping, + full_feature_names, + entity_df_with_request_data, + full_expected_df, + event_timestamp, + ) table_from_df_entities: pd.DataFrame = job_from_df.to_arrow().to_pandas() @@ -463,3 +528,69 @@ def assert_feature_service_correctness( assert_frame_equal( expected_df, actual_df_from_df_entities, check_dtype=False, ) + + +def assert_feature_service_entity_mapping_correctness( + store, feature_service, full_feature_names, entity_df, expected_df, event_timestamp +): + if full_feature_names: + job_from_df = store.get_historical_features( + entity_df=entity_df, + features=feature_service, + full_feature_names=full_feature_names, + ) + actual_df_from_df_entities = job_from_df.to_df() + + expected_df: pd.DataFrame = ( + expected_df.sort_values( + by=[ + event_timestamp, + "order_id", + "driver_id", + "customer_id", + "origin_id", + "destination_id", + ] + ) + .drop_duplicates() + .reset_index(drop=True) + ) + expected_df = expected_df[ + [ + event_timestamp, + "order_id", + "driver_id", + "customer_id", + "origin_id", + "destination_id", + "origin__temperature", + "destination__temperature", + ] + ] + actual_df_from_df_entities = ( + actual_df_from_df_entities[expected_df.columns] + .sort_values( + by=[ + event_timestamp, + "order_id", + "driver_id", + "customer_id", + "origin_id", + "destination_id", + ] + ) + .drop_duplicates() + .reset_index(drop=True) + ) + + assert_frame_equal( + expected_df, actual_df_from_df_entities, check_dtype=False, + ) + else: + # using 2 of the same FeatureView without full_feature_names=True will result in collision + with pytest.raises(FeatureNameCollisionError): + job_from_df = store.get_historical_features( + entity_df=entity_df, + features=feature_service, + full_feature_names=full_feature_names, + ) diff --git a/sdk/python/tests/integration/online_store/test_universal_online.py b/sdk/python/tests/integration/online_store/test_universal_online.py index aeab91e5f88..689e8134777 100644 --- a/sdk/python/tests/integration/online_store/test_universal_online.py +++ b/sdk/python/tests/integration/online_store/test_universal_online.py @@ -1,15 +1,24 @@ +import itertools import unittest from datetime import timedelta +import numpy as np import pandas as pd import pytest from feast import FeatureService -from feast.errors import RequestDataNotFoundInEntityRowsException +from feast.errors import ( + FeatureNameCollisionError, + RequestDataNotFoundInEntityRowsException, +) from tests.integration.feature_repos.repo_configuration import ( construct_universal_feature_views, ) -from tests.integration.feature_repos.universal.entities import customer, driver +from tests.integration.feature_repos.universal.entities import ( + customer, + driver, + location, +) @pytest.mark.integration @@ -24,10 +33,29 @@ def test_online_retrieval(environment, universal_data_sources, full_feature_name "convrate_plus100", features=[feature_views["driver"][["conv_rate"]], feature_views["driver_odfv"]], ) + feature_service_entity_mapping = FeatureService( + name="entity_mapping", + features=[ + feature_views["location"] + .with_name("origin") + .with_join_key_map({"location_id": "origin_id"}), + feature_views["location"] + .with_name("destination") + .with_join_key_map({"location_id": "destination_id"}), + ], + ) feast_objects = [] feast_objects.extend(feature_views.values()) - feast_objects.extend([driver(), customer(), feature_service]) + feast_objects.extend( + [ + driver(), + customer(), + location(), + feature_service, + feature_service_entity_mapping, + ] + ) fs.apply(feast_objects) fs.materialize( environment.start_date - timedelta(days=1), @@ -54,6 +82,15 @@ def test_online_retrieval(environment, universal_data_sources, full_feature_name datasets["customer"]["customer_id"].isin(sample_customers) ] + location_pairs = np.array(list(itertools.permutations(entities["location"], 2))) + sample_location_pairs = location_pairs[np.random.choice(len(location_pairs), 10)].T + origins_df = datasets["location"][ + datasets["location"]["location_id"].isin(sample_location_pairs[0]) + ] + destinations_df = datasets["location"][ + datasets["location"]["location_id"].isin(sample_location_pairs[1]) + ] + global_df = datasets["global"] entity_rows = [ @@ -105,7 +142,11 @@ def test_online_retrieval(environment, universal_data_sources, full_feature_name tc = unittest.TestCase() for i, entity_row in enumerate(entity_rows): df_features = get_latest_feature_values_from_dataframes( - drivers_df, customers_df, orders_df, global_df, entity_row + driver_df=drivers_df, + customer_df=customers_df, + orders_df=orders_df, + global_df=global_df, + entity_row=entity_row, ) assert df_features["customer_id"] == online_features_dict["customer_id"][i] @@ -167,6 +208,29 @@ def test_online_retrieval(environment, universal_data_sources, full_feature_name global_df, ) + entity_rows = [ + { + "driver": driver, + "customer_id": customer, + "origin_id": origin, + "destination_id": destination, + } + for (driver, customer, origin, destination) in zip( + sample_drivers, sample_customers, *sample_location_pairs + ) + ] + assert_feature_service_entity_mapping_correctness( + fs, + feature_service_entity_mapping, + entity_rows, + full_feature_names, + drivers_df, + customers_df, + orders_df, + origins_df, + destinations_df, + ) + def response_feature_name(feature: str, full_feature_names: bool) -> str: if ( @@ -193,17 +257,24 @@ def response_feature_name(feature: str, full_feature_names: bool) -> str: return feature +def get_latest_row(entity_row, df, join_key, entity_key): + rows = df[df[join_key] == entity_row[entity_key]] + return rows.loc[rows["event_timestamp"].idxmax()].to_dict() + + def get_latest_feature_values_from_dataframes( - driver_df, customer_df, orders_df, global_df, entity_row + driver_df, + customer_df, + orders_df, + entity_row, + global_df=None, + origin_df=None, + destination_df=None, ): - driver_rows = driver_df[driver_df["driver_id"] == entity_row["driver"]] - latest_driver_row: pd.DataFrame = driver_rows.loc[ - driver_rows["event_timestamp"].idxmax() - ].to_dict() - customer_rows = customer_df[customer_df["customer_id"] == entity_row["customer_id"]] - latest_customer_row = customer_rows.loc[ - customer_rows["event_timestamp"].idxmax() - ].to_dict() + latest_driver_row = get_latest_row(entity_row, driver_df, "driver_id", "driver") + latest_customer_row = get_latest_row( + entity_row, customer_df, "customer_id", "customer_id" + ) # Since the event timestamp columns may contain timestamps of different timezones, # we must first convert the timestamps to UTC before we can compare them. @@ -218,14 +289,47 @@ def get_latest_feature_values_from_dataframes( max_index = timestamps["event_timestamp"].idxmax() latest_orders_row = order_rows.loc[max_index] - latest_global_row = global_df.loc[global_df["event_timestamp"].idxmax()].to_dict() + if global_df is not None: + latest_global_row = global_df.loc[ + global_df["event_timestamp"].idxmax() + ].to_dict() + if origin_df is not None: + latest_origin_row = get_latest_row( + entity_row, origin_df, "location_id", "origin_id" + ) + latest_destination_row = get_latest_row( + entity_row, destination_df, "location_id", "destination_id" + ) + # Need full feature names for shadow entities + latest_origin_row["origin__temperature"] = latest_origin_row.pop("temperature") + latest_destination_row["destination__temperature"] = latest_destination_row.pop( + "temperature" + ) request_data_features = entity_row.copy() request_data_features.pop("driver") request_data_features.pop("customer_id") + if global_df is not None: + return { + **latest_customer_row, + **latest_driver_row, + **latest_orders_row, + **latest_global_row, + **request_data_features, + } + if origin_df is not None: + request_data_features.pop("origin_id") + request_data_features.pop("destination_id") + return { + **latest_customer_row, + **latest_driver_row, + **latest_orders_row, + **latest_origin_row, + **latest_destination_row, + **request_data_features, + } return { **latest_customer_row, **latest_driver_row, - **latest_global_row, **latest_orders_row, **request_data_features, } @@ -265,7 +369,11 @@ def assert_feature_service_correctness( tc = unittest.TestCase() for i, entity_row in enumerate(entity_rows): df_features = get_latest_feature_values_from_dataframes( - drivers_df, customers_df, orders_df, global_df, entity_row + driver_df=drivers_df, + customer_df=customers_df, + orders_df=orders_df, + global_df=global_df, + entity_row=entity_row, ) tc.assertAlmostEqual( feature_service_online_features_dict[ @@ -274,3 +382,60 @@ def assert_feature_service_correctness( df_features["conv_rate"] + 100, delta=0.0001, ) + + +def assert_feature_service_entity_mapping_correctness( + fs, + feature_service, + entity_rows, + full_feature_names, + drivers_df, + customers_df, + orders_df, + origins_df, + destinations_df, +): + if full_feature_names: + feature_service_response = fs.get_online_features( + features=feature_service, + entity_rows=entity_rows, + full_feature_names=full_feature_names, + ) + assert feature_service_response is not None + + feature_service_online_features_dict = feature_service_response.to_dict() + feature_service_keys = feature_service_online_features_dict.keys() + + assert ( + len(feature_service_keys) + == sum( + [ + len(projection.features) + for projection in feature_service.feature_view_projections + ] + ) + + 4 + ) # Add 4 for the driver_id, customer_id, origin_id, and destination_id entity keys + + for i, entity_row in enumerate(entity_rows): + df_features = get_latest_feature_values_from_dataframes( + driver_df=drivers_df, + customer_df=customers_df, + orders_df=orders_df, + origin_df=origins_df, + destination_df=destinations_df, + entity_row=entity_row, + ) + for feature_name in ["origin__temperature", "destination__temperature"]: + assert ( + feature_service_online_features_dict[feature_name][i] + == df_features[feature_name] + ) + else: + # using 2 of the same FeatureView without full_feature_names=True will result in collision + with pytest.raises(FeatureNameCollisionError): + feature_service_response = fs.get_online_features( + features=feature_service, + entity_rows=entity_rows, + full_feature_names=full_feature_names, + ) From 94737f6462ce259ce2a786eaf3f4e5f7d56e23be Mon Sep 17 00:00:00 2001 From: David Y Liu <7172604+mavysavydav@users.noreply.github.com> Date: Wed, 13 Oct 2021 00:48:58 -0700 Subject: [PATCH 02/50] corrected BQ version that's needed for a contributor's merged PR 1844 (#1934) Signed-off-by: David Y Liu --- sdk/python/setup.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/sdk/python/setup.py b/sdk/python/setup.py index 08a0c1db008..ea4bb670b89 100644 --- a/sdk/python/setup.py +++ b/sdk/python/setup.py @@ -65,7 +65,7 @@ ] GCP_REQUIRED = [ - "google-cloud-bigquery>=2.0.*", + "google-cloud-bigquery>=2.14.0", "google-cloud-bigquery-storage >= 2.0.0", "google-cloud-datastore>=2.1.*", "google-cloud-storage>=1.34.*", @@ -111,7 +111,7 @@ "firebase-admin==4.5.2", "pre-commit", "assertpy==1.1", - "google-cloud-bigquery>=2.0.*", + "google-cloud-bigquery>=2.14.0", "google-cloud-bigquery-storage >= 2.0.0", "google-cloud-datastore>=2.1.*", "google-cloud-storage>=1.20.*", From 156d004672e73d27ddaa2408fff49d7e5f642a63 Mon Sep 17 00:00:00 2001 From: David Y Liu <7172604+mavysavydav@users.noreply.github.com> Date: Wed, 13 Oct 2021 15:19:00 -0700 Subject: [PATCH 03/50] Fix __getitem__ return for feature view and on-demand feature view (#1936) Signed-off-by: David Y Liu --- sdk/python/feast/feature_view.py | 2 +- sdk/python/feast/on_demand_feature_view.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/sdk/python/feast/feature_view.py b/sdk/python/feast/feature_view.py index 68fec061ac3..90a80a92335 100644 --- a/sdk/python/feast/feature_view.py +++ b/sdk/python/feast/feature_view.py @@ -181,7 +181,7 @@ def __getitem__(self, item): cp = self.__copy__() cp.projection.features = referenced_features - return self + return cp def __eq__(self, other): if not isinstance(other, FeatureView): diff --git a/sdk/python/feast/on_demand_feature_view.py b/sdk/python/feast/on_demand_feature_view.py index 4ec2b138b21..0516346986f 100644 --- a/sdk/python/feast/on_demand_feature_view.py +++ b/sdk/python/feast/on_demand_feature_view.py @@ -87,7 +87,7 @@ def __getitem__(self, item): cp = self.__copy__() cp.projection.features = referenced_features - return self + return cp def with_name(self, name: str): """ From e8a151941c2ea53ce72aadb254573cd6966e1a82 Mon Sep 17 00:00:00 2001 From: codyjlin <31944154+codyjlin@users.noreply.github.com> Date: Wed, 13 Oct 2021 18:15:17 -0700 Subject: [PATCH 04/50] Fix conditional statements for if OnDemandFVs exist (#1937) Signed-off-by: Cody Lin --- sdk/python/feast/infra/offline_stores/bigquery.py | 2 +- sdk/python/feast/infra/offline_stores/redshift.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/sdk/python/feast/infra/offline_stores/bigquery.py b/sdk/python/feast/infra/offline_stores/bigquery.py index aa4d43047c3..bb750a8c97c 100644 --- a/sdk/python/feast/infra/offline_stores/bigquery.py +++ b/sdk/python/feast/infra/offline_stores/bigquery.py @@ -255,7 +255,7 @@ def to_bigquery( path = f"{self.client.project}.{self.config.offline_store.dataset}.historical_{today}_{rand_id}" job_config = bigquery.QueryJobConfig(destination=path) - if not job_config.dry_run and self.on_demand_feature_views is not None: + if not job_config.dry_run and self.on_demand_feature_views: job = _write_pyarrow_table_to_bq( self.client, self.to_arrow(), job_config.destination ) diff --git a/sdk/python/feast/infra/offline_stores/redshift.py b/sdk/python/feast/infra/offline_stores/redshift.py index 40fa9cf94b6..c3cd461ce15 100644 --- a/sdk/python/feast/infra/offline_stores/redshift.py +++ b/sdk/python/feast/infra/offline_stores/redshift.py @@ -265,7 +265,7 @@ def _to_arrow_internal(self) -> pa.Table: def to_s3(self) -> str: """ Export dataset to S3 in Parquet format and return path """ - if self.on_demand_feature_views is not None: + if self.on_demand_feature_views: transformed_df = self.to_df() aws_utils.upload_df_to_s3(self._s3_resource, self._s3_path, transformed_df) return self._s3_path @@ -285,7 +285,7 @@ def to_s3(self) -> str: def to_redshift(self, table_name: str) -> None: """ Save dataset as a new Redshift table """ - if self.on_demand_feature_views is not None: + if self.on_demand_feature_views: transformed_df = self.to_df() aws_utils.upload_df_to_redshift( self._redshift_client, From 973f1c5db0947b1fafded63363b7f18995a9dc6e Mon Sep 17 00:00:00 2001 From: Felix Wang Date: Wed, 13 Oct 2021 23:36:16 -0700 Subject: [PATCH 05/50] Fix bug in feast alpha enable CLI command (#1940) * Fix bug in feast alpha enable CLI command Signed-off-by: Felix Wang * Lint Signed-off-by: Felix Wang --- sdk/python/feast/repo_config.py | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/sdk/python/feast/repo_config.py b/sdk/python/feast/repo_config.py index 3274e3b9a63..9a04723201a 100644 --- a/sdk/python/feast/repo_config.py +++ b/sdk/python/feast/repo_config.py @@ -277,14 +277,7 @@ def write_to_path(self, repo_path: Path): config_path = repo_path / "feature_store.yaml" with open(config_path, mode="w") as f: yaml.dump( - yaml.safe_load( - self.json( - exclude={"repo_path"}, - exclude_none=True, - exclude_unset=True, - exclude_defaults=True, - ) - ), + yaml.safe_load(self.json(exclude={"repo_path"}, exclude_unset=True,)), f, sort_keys=False, ) From c8e1e478737854982b79e6a744ff0607781cb833 Mon Sep 17 00:00:00 2001 From: Achal Shah Date: Thu, 14 Oct 2021 08:08:16 -0700 Subject: [PATCH 06/50] Add David and Matt as reviewers, and add actions for issue/PR assignment (#1942) * Add David and Matt as reviewers, and add actions for issue/PR assignment Signed-off-by: Achal Shah * remove with Signed-off-by: Achal Shah * More fixes Signed-off-by: Achal Shah * new line Signed-off-by: Achal Shah --- .github/auto_assign-issues.yml | 10 ---------- .github/auto_assign.yml | 19 +++++++++++++++++++ .github/workflows/assign_issues.yml | 15 +++++++++++++++ .github/workflows/assign_reviewers.yml | 12 ++++++++++++ OWNERS | 7 +++++-- 5 files changed, 51 insertions(+), 12 deletions(-) delete mode 100644 .github/auto_assign-issues.yml create mode 100644 .github/auto_assign.yml create mode 100644 .github/workflows/assign_issues.yml create mode 100644 .github/workflows/assign_reviewers.yml diff --git a/.github/auto_assign-issues.yml b/.github/auto_assign-issues.yml deleted file mode 100644 index cf2c7221317..00000000000 --- a/.github/auto_assign-issues.yml +++ /dev/null @@ -1,10 +0,0 @@ -# If enabled, auto-assigns users when a new issue is created -# Defaults to true, allows you to install the app globally, and disable on a per-repo basis -addAssignees: true - -# The list of users to assign to new issues. -# If empty or not provided, the repository owner is assigned -assignees: - - jklegar - - woop - - tsotnet diff --git a/.github/auto_assign.yml b/.github/auto_assign.yml new file mode 100644 index 00000000000..18151f74547 --- /dev/null +++ b/.github/auto_assign.yml @@ -0,0 +1,19 @@ +# Set to true to add reviewers to pull requests +addReviewers: false + +# Set to true to add assignees to pull requests +addAssignees: true + +# A list of assignees, overrides reviewers if set +assignees: + - woop + - tsotnet + - achals + - adchia + - felixwang9817 + +# A number of assignees to add to the pull request +# Set to 0 to add all of the assignees. +# Uses numberOfReviewers if unset. +numberOfAssignees: 1 + diff --git a/.github/workflows/assign_issues.yml b/.github/workflows/assign_issues.yml new file mode 100644 index 00000000000..f8702e2485c --- /dev/null +++ b/.github/workflows/assign_issues.yml @@ -0,0 +1,15 @@ +name: Issue assignment + +on: + issues: + types: [opened] + +jobs: + auto-assign: + runs-on: ubuntu-latest + steps: + - name: 'Auto-assign issue' + uses: pozil/auto-assign-issue@v1.1.0 + with: + assignees: achals, woop, tsotnet, felix, felixwang9817, adchia + numOfAssignee: 1 diff --git a/.github/workflows/assign_reviewers.yml b/.github/workflows/assign_reviewers.yml new file mode 100644 index 00000000000..ab776add0dc --- /dev/null +++ b/.github/workflows/assign_reviewers.yml @@ -0,0 +1,12 @@ +name: 'Assign Reviewers' +on: + pull_request_target: + types: + - opened + - ready_for_review + +jobs: + add-reviews: + runs-on: ubuntu-latest + steps: + - uses: kentaro-m/auto-assign-action@v1.2.0 diff --git a/OWNERS b/OWNERS index 3ed906d2e99..faa663d8e92 100644 --- a/OWNERS +++ b/OWNERS @@ -1,15 +1,18 @@ +# This file is different from the CODEOWNERS file. +# OWNERS is used by feast-ci-bot to accept commands like `/ok-to-test` and `/lgtm` +# More info at https://www.kubernetes.dev/docs/guide/owners/ approvers: - woop - - jklegar - tsotnet - achals - adchia - felixwang9817 reviewers: - woop - - jklegar - tsotnet - achals - tedhtchang - adchia - felixwang9817 + - mavysavydav + - MattDelac From cd0e0e087ba127bd354f626cf8acd1e22ecba14c Mon Sep 17 00:00:00 2001 From: Achal Shah Date: Thu, 14 Oct 2021 08:21:49 -0700 Subject: [PATCH 07/50] Add David and Matt as approvers as well (#1943) Signed-off-by: Achal Shah --- OWNERS | 2 ++ 1 file changed, 2 insertions(+) diff --git a/OWNERS b/OWNERS index faa663d8e92..199f9d19f23 100644 --- a/OWNERS +++ b/OWNERS @@ -7,6 +7,8 @@ approvers: - achals - adchia - felixwang9817 + - mavysavydav + - MattDelac reviewers: - woop - tsotnet From 0212728dc2251e8b9af3d0a94dc62797fe7c4567 Mon Sep 17 00:00:00 2001 From: Achal Shah Date: Thu, 14 Oct 2021 11:34:16 -0700 Subject: [PATCH 08/50] Remove some paths from unit test cache (#1944) * Remove some paths from unit test cache Signed-off-by: Achal Shah * Remove cache Signed-off-by: Achal Shah * Echo which pop Signed-off-by: Achal Shah * upgrade pip Signed-off-by: Achal Shah * downgrade pip Signed-off-by: Achal Shah * this is my ifnal form Signed-off-by: Achal Shah --- .github/workflows/unit_tests.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/workflows/unit_tests.yml b/.github/workflows/unit_tests.yml index eea9bc23679..5eeb4b27eab 100644 --- a/.github/workflows/unit_tests.yml +++ b/.github/workflows/unit_tests.yml @@ -20,6 +20,9 @@ jobs: with: python-version: ${{ matrix.python-version }} architecture: x64 + - name: Downgrade pip + run: | + pip install --upgrade "pip<21.3" - name: Get pip cache dir id: pip-cache run: | From e4559226678af07aeee9abd55a5e309e1dee19cd Mon Sep 17 00:00:00 2001 From: Felix Wang Date: Thu, 14 Oct 2021 14:28:17 -0700 Subject: [PATCH 09/50] Plugin repo universal tests (#1946) * Add universal pytest marker Signed-off-by: Felix Wang * Allow plugin repos to overwrite FULL_REPO_CONFIGS through environment variable Signed-off-by: Felix Wang --- Makefile | 3 +++ sdk/python/feast/constants.py | 3 +++ sdk/python/tests/conftest.py | 13 ++++++++++ .../integration/e2e/test_universal_e2e.py | 1 + .../feature_repos/repo_configuration.py | 24 ++++++++++++++++++- .../test_universal_historical_retrieval.py | 1 + .../online_store/test_universal_online.py | 1 + .../test_universal_odfv_feature_inference.py | 2 ++ .../registration/test_universal_types.py | 3 +++ 9 files changed, 50 insertions(+), 1 deletion(-) diff --git a/Makefile b/Makefile index 1f53c390666..0d1bc034c21 100644 --- a/Makefile +++ b/Makefile @@ -61,6 +61,9 @@ test-python: test-python-integration: FEAST_USAGE=False IS_TEST=True python -m pytest -n 8 --integration sdk/python/tests +test-python-universal: + FEAST_USAGE=False IS_TEST=True python -m pytest -n 8 --integration --universal sdk/python/tests + format-python: # Sort cd ${ROOT_DIR}/sdk/python; python -m isort feast/ tests/ diff --git a/sdk/python/feast/constants.py b/sdk/python/feast/constants.py index 4cdfd46d2d8..d96e05024fc 100644 --- a/sdk/python/feast/constants.py +++ b/sdk/python/feast/constants.py @@ -24,3 +24,6 @@ # Environment variable for toggling usage FEAST_USAGE = "FEAST_USAGE" + +# Environment variable for the path for overwriting universal test configs +FULL_REPO_CONFIGS_MODULE_ENV_NAME: str = "FULL_REPO_CONFIGS_MODULE" diff --git a/sdk/python/tests/conftest.py b/sdk/python/tests/conftest.py index df069775a1a..ce18cee7501 100644 --- a/sdk/python/tests/conftest.py +++ b/sdk/python/tests/conftest.py @@ -40,6 +40,9 @@ def pytest_configure(config): "markers", "integration: mark test that has external dependencies" ) config.addinivalue_line("markers", "benchmark: mark benchmarking tests") + config.addinivalue_line( + "markers", "universal: mark tests that use the universal feature repo" + ) def pytest_addoption(parser): @@ -52,11 +55,15 @@ def pytest_addoption(parser): parser.addoption( "--benchmark", action="store_true", default=False, help="Run benchmark tests", ) + parser.addoption( + "--universal", action="store_true", default=False, help="Run universal tests", + ) def pytest_collection_modifyitems(config, items: List[Item]): should_run_integration = config.getoption("--integration") is True should_run_benchmark = config.getoption("--benchmark") is True + should_run_universal = config.getoption("--universal") is True integration_tests = [t for t in items if "integration" in t.keywords] if not should_run_integration: @@ -76,6 +83,12 @@ def pytest_collection_modifyitems(config, items: List[Item]): for t in benchmark_tests: items.append(t) + universal_tests = [t for t in items if "universal" in t.keywords] + if should_run_universal: + items.clear() + for t in universal_tests: + items.append(t) + @pytest.fixture def simple_dataset_1() -> pd.DataFrame: diff --git a/sdk/python/tests/integration/e2e/test_universal_e2e.py b/sdk/python/tests/integration/e2e/test_universal_e2e.py index e985e8ed108..1512f41c0d6 100644 --- a/sdk/python/tests/integration/e2e/test_universal_e2e.py +++ b/sdk/python/tests/integration/e2e/test_universal_e2e.py @@ -12,6 +12,7 @@ @pytest.mark.integration +@pytest.mark.universal @pytest.mark.parametrize("infer_features", [True, False]) def test_e2e_consistency(environment, e2e_data_sources, infer_features): fs = environment.feature_store diff --git a/sdk/python/tests/integration/feature_repos/repo_configuration.py b/sdk/python/tests/integration/feature_repos/repo_configuration.py index 876b66ef4a4..b98de8a1422 100644 --- a/sdk/python/tests/integration/feature_repos/repo_configuration.py +++ b/sdk/python/tests/integration/feature_repos/repo_configuration.py @@ -1,3 +1,5 @@ +import importlib +import os import tempfile import uuid from contextlib import contextmanager @@ -9,6 +11,7 @@ import pandas as pd from feast import FeatureStore, FeatureView, RepoConfig, driver_test_data +from feast.constants import FULL_REPO_CONFIGS_MODULE_ENV_NAME from feast.data_source import DataSource from tests.integration.feature_repos.universal.data_source_creator import ( DataSourceCreator, @@ -61,7 +64,15 @@ def __repr__(self) -> str: DYNAMO_CONFIG = {"type": "dynamodb", "region": "us-west-2"} REDIS_CONFIG = {"type": "redis", "connection_string": "localhost:6379,db=0"} -FULL_REPO_CONFIGS: List[IntegrationTestRepoConfig] = [ + +# FULL_REPO_CONFIGS contains the repo configurations (e.g. provider, offline store, +# online store, test data, and more parameters) that most integration tests will test +# against. By default, FULL_REPO_CONFIGS uses the three providers (local, GCP, and AWS) +# with their default offline and online stores; it also tests the providers with the +# Redis online store. It can be overwritten by specifying a Python module through the +# FULL_REPO_CONFIGS_MODULE_ENV_NAME environment variable. In this case, that Python +# module will be imported and FULL_REPO_CONFIGS will be extracted from the file. +DEFAULT_FULL_REPO_CONFIGS: List[IntegrationTestRepoConfig] = [ # Local configurations IntegrationTestRepoConfig(), IntegrationTestRepoConfig(online_store=REDIS_CONFIG), @@ -88,6 +99,17 @@ def __repr__(self) -> str: online_store=REDIS_CONFIG, ), ] +full_repo_configs_module = os.environ.get(FULL_REPO_CONFIGS_MODULE_ENV_NAME) +if full_repo_configs_module is not None: + try: + module = importlib.import_module(full_repo_configs_module) + FULL_REPO_CONFIGS = getattr(module, "FULL_REPO_CONFIGS") + except Exception: + pass + finally: + FULL_REPO_CONFIGS = DEFAULT_FULL_REPO_CONFIGS +else: + FULL_REPO_CONFIGS = DEFAULT_FULL_REPO_CONFIGS def construct_universal_entities() -> Dict[str, List[Any]]: diff --git a/sdk/python/tests/integration/offline_store/test_universal_historical_retrieval.py b/sdk/python/tests/integration/offline_store/test_universal_historical_retrieval.py index d4e81b30de8..2f88f2194c1 100644 --- a/sdk/python/tests/integration/offline_store/test_universal_historical_retrieval.py +++ b/sdk/python/tests/integration/offline_store/test_universal_historical_retrieval.py @@ -231,6 +231,7 @@ def get_expected_training_df( @pytest.mark.integration +@pytest.mark.universal @pytest.mark.parametrize("full_feature_names", [True, False], ids=lambda v: str(v)) def test_historical_features(environment, universal_data_sources, full_feature_names): store = environment.feature_store diff --git a/sdk/python/tests/integration/online_store/test_universal_online.py b/sdk/python/tests/integration/online_store/test_universal_online.py index 689e8134777..381635061e7 100644 --- a/sdk/python/tests/integration/online_store/test_universal_online.py +++ b/sdk/python/tests/integration/online_store/test_universal_online.py @@ -22,6 +22,7 @@ @pytest.mark.integration +@pytest.mark.universal @pytest.mark.parametrize("full_feature_names", [True, False], ids=lambda v: str(v)) def test_online_retrieval(environment, universal_data_sources, full_feature_names): diff --git a/sdk/python/tests/integration/registration/test_universal_odfv_feature_inference.py b/sdk/python/tests/integration/registration/test_universal_odfv_feature_inference.py index f7d33a9e0a9..a2ff8b24ade 100644 --- a/sdk/python/tests/integration/registration/test_universal_odfv_feature_inference.py +++ b/sdk/python/tests/integration/registration/test_universal_odfv_feature_inference.py @@ -11,6 +11,7 @@ @pytest.mark.integration +@pytest.mark.universal @pytest.mark.parametrize("infer_features", [True, False], ids=lambda v: str(v)) def test_infer_odfv_features(environment, universal_data_sources, infer_features): store = environment.feature_store @@ -33,6 +34,7 @@ def test_infer_odfv_features(environment, universal_data_sources, infer_features @pytest.mark.integration +@pytest.mark.universal def test_infer_odfv_features_with_error(environment, universal_data_sources): store = environment.feature_store diff --git a/sdk/python/tests/integration/registration/test_universal_types.py b/sdk/python/tests/integration/registration/test_universal_types.py index fcd9c4fc520..7ef1c5c1e79 100644 --- a/sdk/python/tests/integration/registration/test_universal_types.py +++ b/sdk/python/tests/integration/registration/test_universal_types.py @@ -116,6 +116,7 @@ def get_fixtures(request): @pytest.mark.integration +@pytest.mark.universal def test_entity_inference_types_match(offline_types_test_fixtures): environment, config, data_source, fv = offline_types_test_fixtures fs = environment.feature_store @@ -139,6 +140,7 @@ def test_entity_inference_types_match(offline_types_test_fixtures): @pytest.mark.integration +@pytest.mark.universal def test_feature_get_historical_features_types_match(offline_types_test_fixtures): environment, config, data_source, fv = offline_types_test_fixtures fs = environment.feature_store @@ -185,6 +187,7 @@ def test_feature_get_historical_features_types_match(offline_types_test_fixtures @pytest.mark.integration +@pytest.mark.universal def test_feature_get_online_features_types_match(online_types_test_fixtures): environment, config, data_source, fv = online_types_test_fixtures fv = create_feature_view( From df724a8d36e917ea903d6e3f4a56744888830afe Mon Sep 17 00:00:00 2001 From: Achal Shah Date: Thu, 14 Oct 2021 15:12:17 -0700 Subject: [PATCH 10/50] Add an interface for TransformationService and a basic implementation (#1932) * rebase Signed-off-by: Achal Shah * upadtes Signed-off-by: Achal Shah * Update the protos Signed-off-by: Achal Shah * CR comments, change names Signed-off-by: Achal Shah * tests Signed-off-by: Achal Shah * tests Signed-off-by: Achal Shah * update comment Signed-off-by: Achal Shah * cr updates Signed-off-by: Achal Shah * kill config Signed-off-by: Achal Shah * revive config Signed-off-by: Achal Shah * revive config Signed-off-by: Achal Shah * kill config Signed-off-by: Achal Shah * kill config Signed-off-by: Achal Shah --- go.mod | 15 +- go.sum | 23 + .../feast/serving/TransformationService.proto | 67 +++ sdk/go/protos/feast/core/CoreService.pb.go | 11 +- sdk/go/protos/feast/core/DataFormat.pb.go | 9 +- sdk/go/protos/feast/core/DataSource.pb.go | 553 ++++++++++++++---- sdk/go/protos/feast/core/Entity.pb.go | 118 ++-- sdk/go/protos/feast/core/Feature.pb.go | 11 +- sdk/go/protos/feast/core/FeatureTable.pb.go | 142 ++--- sdk/go/protos/feast/core/Store.pb.go | 154 +++-- .../protos/feast/serving/ServingService.pb.go | 427 ++++++++++---- sdk/go/protos/feast/storage/Redis.pb.go | 9 +- sdk/go/protos/feast/types/Field.pb.go | 9 +- sdk/go/protos/feast/types/Value.pb.go | 450 +++++++++----- .../tensorflow_metadata/proto/v0/path.pb.go | 9 +- .../tensorflow_metadata/proto/v0/schema.pb.go | 21 +- .../proto/v0/statistics.pb.go | 9 +- sdk/python/feast/cli.py | 14 + sdk/python/feast/feature_store.py | 12 + sdk/python/feast/repo_config.py | 1 - sdk/python/feast/transformation_server.py | 74 +++ sdk/python/setup.py | 1 + 22 files changed, 1557 insertions(+), 582 deletions(-) create mode 100644 protos/feast/serving/TransformationService.proto create mode 100644 sdk/python/feast/transformation_server.py diff --git a/go.mod b/go.mod index 6e71e5b637e..f4a14550566 100644 --- a/go.mod +++ b/go.mod @@ -9,8 +9,8 @@ require ( github.com/gogo/protobuf v1.3.1 // indirect github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e // indirect github.com/golang/mock v1.2.0 - github.com/golang/protobuf v1.4.3 - github.com/google/go-cmp v0.5.0 + github.com/golang/protobuf v1.5.2 + github.com/google/go-cmp v0.5.5 github.com/huandu/xstrings v1.2.0 // indirect github.com/lyft/protoc-gen-validate v0.1.0 // indirect github.com/mitchellh/copystructure v1.0.0 // indirect @@ -18,15 +18,18 @@ require ( github.com/mwitkow/go-proto-validators v0.2.0 // indirect github.com/pseudomuto/protoc-gen-doc v1.3.0 // indirect github.com/pseudomuto/protokit v0.2.0 // indirect + github.com/russross/blackfriday/v2 v2.1.0 // indirect github.com/spf13/cobra v0.0.4 github.com/spf13/viper v1.4.0 github.com/woop/protoc-gen-doc v1.3.0 // indirect go.opencensus.io v0.22.3 // indirect - golang.org/x/lint v0.0.0-20200302205851-738671d3881b // indirect - golang.org/x/net v0.0.0-20201021035429-f5854403a974 - golang.org/x/tools v0.0.0-20201124005743-911501bfb504 // indirect + golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9 // indirect + golang.org/x/lint v0.0.0-20210508222113-6edffad5e616 // indirect + golang.org/x/net v0.0.0-20210805182204-aaa1db679c0d + golang.org/x/tools v0.1.7 // indirect + google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013 // indirect google.golang.org/grpc v1.29.1 - google.golang.org/protobuf v1.25.0 // indirect + google.golang.org/protobuf v1.27.1 // indirect gopkg.in/russross/blackfriday.v2 v2.0.0 // indirect gopkg.in/yaml.v2 v2.2.4 istio.io/gogo-genproto v0.0.0-20191212213402-78a529a42cd8 // indirect diff --git a/go.sum b/go.sum index bd68e3f8638..5e87ccf6dbf 100644 --- a/go.sum +++ b/go.sum @@ -156,6 +156,9 @@ github.com/golang/protobuf v1.4.2 h1:+Z5KGCizgyZCbGh1KZqA0fcLLkwbsjIzS4aV2v7wJX0 github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= github.com/golang/protobuf v1.4.3 h1:JjCZWpVbqXDqFVmTfYWEVTMIYrL/NPdPSCHPJ0T/raM= github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= +github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= +github.com/golang/protobuf v1.5.2 h1:ROPKBNFfQgOUMifHyP+KYbvpjbdoFNs+aK7DXlji0Tw= +github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= github.com/google/btree v0.0.0-20160524151835-7d79101e329e/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= @@ -168,6 +171,7 @@ github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMyw github.com/google/go-cmp v0.4.0 h1:xsAVV57WRhGj6kEIi8ReJzQlHHqcBYCElAvkovg3B/4= github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/gofuzz v0.0.0-20161122191042-44d81051d367/go.mod h1:HP5RmnzzSNb993RKQDq4+1A4ia9nllfqcQFTQJedwGI= github.com/google/gofuzz v0.0.0-20170612174753-24818f796faf/go.mod h1:HP5RmnzzSNb993RKQDq4+1A4ia9nllfqcQFTQJedwGI= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= @@ -293,6 +297,8 @@ github.com/russross/blackfriday v1.5.2 h1:HyvC0ARfnZBqnXwABFeSZHpKvJHJJfPz81GNue github.com/russross/blackfriday v1.5.2/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g= github.com/russross/blackfriday/v2 v2.0.1 h1:lPqVAte+HuHNfhJ/0LC98ESWRz8afy9tM/0RK8m9o+Q= github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk= +github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/shurcooL/sanitized_anchor_name v1.0.0 h1:PdmoCO6wvbs+7yrJyMORt4/BmY5IYyJwS/kOiWx8mHo= github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= @@ -338,6 +344,7 @@ github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1: github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.4.0/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= go.etcd.io/bbolt v1.3.2/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU= go.opencensus.io v0.21.0 h1:mU6zScU4U1YAFPHEHYk+3JC4SY7JxgkqS10ZOSyksNg= go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU= @@ -372,10 +379,13 @@ golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3 h1:XQyxROzUlZH+WIQwySDgnISg golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= golang.org/x/lint v0.0.0-20200302205851-738671d3881b h1:Wh+f8QHJXR411sJR8/vRBTZ7YapZaRvUcLFFJhusH0k= golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= +golang.org/x/lint v0.0.0-20210508222113-6edffad5e616 h1:VLliZ0d+/avPrXXH+OakdXhpJuEoBZuwh1m2j7U6Iug= +golang.org/x/lint v0.0.0-20210508222113-6edffad5e616/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE= golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/net v0.0.0-20170114055629-f2499483f923/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180826012351-8a410e7b638d h1:g9qWBGx4puODJTMVyoPrpoxPFgVGd+z1DZwjfRu4d0I= @@ -404,6 +414,7 @@ golang.org/x/net v0.0.0-20200513185701-a91f0712d120 h1:EZ3cVSzKOlJxAd8e8YAJ7no8n golang.org/x/net v0.0.0-20200513185701-a91f0712d120/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.0.0-20210805182204-aaa1db679c0d/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20190402181905-9f3314589c9a/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= @@ -416,6 +427,7 @@ golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJ golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sys v0.0.0-20170830134202-bb24a47a89ea/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= @@ -439,6 +451,10 @@ golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20200515095857-1151b9dac4a9 h1:YTzHMGlqJu67/uEo1lBv0n3wBXhXNeUbB1XfN2vmTm0= golang.org/x/sys v0.0.0-20200515095857-1151b9dac4a9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/text v0.0.0-20160726164857-2910a502d2bf/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= @@ -447,6 +463,7 @@ golang.org/x/text v0.3.1-0.20181227161524-e6919f6577db/go.mod h1:bEr9sfX3Q8Zfm5f golang.org/x/text v0.3.2 h1:tW2bmiBqwgJj/UpqtC8EpXEZVYOwU0yG4iWbprSVAcs= golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/time v0.0.0-20161028155119-f51c12702a4d/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= @@ -506,6 +523,8 @@ golang.org/x/tools v0.0.0-20201017001424-6003fad69a88 h1:ZB1XYzdDo7c/O48jzjMkvIj golang.org/x/tools v0.0.0-20201017001424-6003fad69a88/go.mod h1:z6u4i615ZeAfBE4XtMziQW1fSVJXACjjbWkB/mvPzlU= golang.org/x/tools v0.0.0-20201124005743-911501bfb504 h1:jOKV2ysikH1GANB7t2LotmhyvkkPvl7HQoEXkV6slJA= golang.org/x/tools v0.0.0-20201124005743-911501bfb504/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.1.7 h1:6j8CgantCy3yc8JGBqkDLMKWqZ0RDU2g1HVgacojGWQ= +golang.org/x/tools v0.1.7/go.mod h1:LGqMHiF4EqQNHR1JncWGqT5BVaXmza+X+BDGol+dOxo= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4= @@ -565,6 +584,10 @@ google.golang.org/protobuf v1.24.0 h1:UhZDfRO8JRQru4/+LlLE0BRKGF8L+PICnvYZmx/fEG google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGjtUeSXeh4= google.golang.org/protobuf v1.25.0 h1:Ejskq+SyPohKW+1uil0JJMtmHCgJPJ/qWTxr8qp+R4c= google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= +google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= +google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= +google.golang.org/protobuf v1.27.1 h1:SnqbnDw1V7RiZcXPx5MEeqPv2s79L9i7BJUlG/+RurQ= +google.golang.org/protobuf v1.27.1/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= diff --git a/protos/feast/serving/TransformationService.proto b/protos/feast/serving/TransformationService.proto new file mode 100644 index 00000000000..113bd120c8f --- /dev/null +++ b/protos/feast/serving/TransformationService.proto @@ -0,0 +1,67 @@ +/* + * Copyright 2021 The Feast Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +syntax = "proto3"; + +package feast.serving; + +option java_package = "feast.proto.serving"; +option java_outer_classname = "TransformationServiceAPIProto"; +option go_package = "github.com/feast-dev/feast/sdk/go/protos/feast/serving"; + +service TransformationService { + rpc GetTransformationServiceInfo (GetTransformationServiceInfoRequest) returns (GetTransformationServiceInfoResponse); + + rpc TransformFeatures (TransformFeaturesRequest) returns (TransformFeaturesResponse); +} + +message ValueType { + oneof value { + // Having a oneOf provides forward compatibility if we need to support compound types + // that are not supported by arrow natively. + bytes arrow_value = 1; + } +} + +message GetTransformationServiceInfoRequest {} + +message GetTransformationServiceInfoResponse { + // Feast version of this transformation service deployment. + string version = 1; + + // Type of transformation service deployment. This is either Python, or custom + TransformationServiceType type = 2; + + string transformation_service_type_details = 3; +} + +message TransformFeaturesRequest { + string on_demand_feature_view_name = 1; + string project = 2; + + ValueType transformation_input = 3; +} + +message TransformFeaturesResponse { + ValueType transformation_output = 3; +} + +enum TransformationServiceType { + TRANSFORMATION_SERVICE_TYPE_INVALID = 0; + TRANSFORMATION_SERVICE_TYPE_PYTHON = 1; + + TRANSFORMATION_SERVICE_TYPE_CUSTOM = 100; +} diff --git a/sdk/go/protos/feast/core/CoreService.pb.go b/sdk/go/protos/feast/core/CoreService.pb.go index 2d9b0a8e0ad..1978edce45a 100644 --- a/sdk/go/protos/feast/core/CoreService.pb.go +++ b/sdk/go/protos/feast/core/CoreService.pb.go @@ -16,8 +16,8 @@ // Code generated by protoc-gen-go. DO NOT EDIT. // versions: -// protoc-gen-go v1.25.0 -// protoc v3.12.4 +// protoc-gen-go v1.27.1 +// protoc v3.11.2 // source: feast/core/CoreService.proto package core @@ -25,13 +25,12 @@ package core import ( context "context" _ "github.com/feast-dev/feast/sdk/go/protos/tensorflow_metadata/proto/v0" - proto "github.com/golang/protobuf/proto" - _ "github.com/golang/protobuf/ptypes/timestamp" grpc "google.golang.org/grpc" codes "google.golang.org/grpc/codes" status "google.golang.org/grpc/status" protoreflect "google.golang.org/protobuf/reflect/protoreflect" protoimpl "google.golang.org/protobuf/runtime/protoimpl" + _ "google.golang.org/protobuf/types/known/timestamppb" reflect "reflect" sync "sync" ) @@ -43,10 +42,6 @@ const ( _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) ) -// This is a compile-time assertion that a sufficiently up-to-date version -// of the legacy proto package is being used. -const _ = proto.ProtoPackageIsVersion4 - type UpdateStoreResponse_Status int32 const ( diff --git a/sdk/go/protos/feast/core/DataFormat.pb.go b/sdk/go/protos/feast/core/DataFormat.pb.go index 4a766cc8f9a..13c6cdda989 100644 --- a/sdk/go/protos/feast/core/DataFormat.pb.go +++ b/sdk/go/protos/feast/core/DataFormat.pb.go @@ -16,14 +16,13 @@ // Code generated by protoc-gen-go. DO NOT EDIT. // versions: -// protoc-gen-go v1.25.0 -// protoc v3.12.4 +// protoc-gen-go v1.27.1 +// protoc v3.11.2 // source: feast/core/DataFormat.proto package core import ( - proto "github.com/golang/protobuf/proto" protoreflect "google.golang.org/protobuf/reflect/protoreflect" protoimpl "google.golang.org/protobuf/runtime/protoimpl" reflect "reflect" @@ -37,10 +36,6 @@ const ( _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) ) -// This is a compile-time assertion that a sufficiently up-to-date version -// of the legacy proto package is being used. -const _ = proto.ProtoPackageIsVersion4 - // Defines the file format encoding the features/entity data in files type FileFormat struct { state protoimpl.MessageState diff --git a/sdk/go/protos/feast/core/DataSource.pb.go b/sdk/go/protos/feast/core/DataSource.pb.go index 1b43972d327..8af638a834f 100644 --- a/sdk/go/protos/feast/core/DataSource.pb.go +++ b/sdk/go/protos/feast/core/DataSource.pb.go @@ -16,14 +16,14 @@ // Code generated by protoc-gen-go. DO NOT EDIT. // versions: -// protoc-gen-go v1.25.0 -// protoc v3.12.4 +// protoc-gen-go v1.27.1 +// protoc v3.11.2 // source: feast/core/DataSource.proto package core import ( - proto "github.com/golang/protobuf/proto" + types "github.com/feast-dev/feast/sdk/go/protos/feast/types" protoreflect "google.golang.org/protobuf/reflect/protoreflect" protoimpl "google.golang.org/protobuf/runtime/protoimpl" reflect "reflect" @@ -37,10 +37,6 @@ const ( _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) ) -// This is a compile-time assertion that a sufficiently up-to-date version -// of the legacy proto package is being used. -const _ = proto.ProtoPackageIsVersion4 - // Type of Data Source. type DataSource_SourceType int32 @@ -50,6 +46,9 @@ const ( DataSource_BATCH_BIGQUERY DataSource_SourceType = 2 DataSource_STREAM_KAFKA DataSource_SourceType = 3 DataSource_STREAM_KINESIS DataSource_SourceType = 4 + DataSource_BATCH_REDSHIFT DataSource_SourceType = 5 + DataSource_CUSTOM_SOURCE DataSource_SourceType = 6 + DataSource_REQUEST_SOURCE DataSource_SourceType = 7 ) // Enum value maps for DataSource_SourceType. @@ -60,6 +59,9 @@ var ( 2: "BATCH_BIGQUERY", 3: "STREAM_KAFKA", 4: "STREAM_KINESIS", + 5: "BATCH_REDSHIFT", + 6: "CUSTOM_SOURCE", + 7: "REQUEST_SOURCE", } DataSource_SourceType_value = map[string]int32{ "INVALID": 0, @@ -67,6 +69,9 @@ var ( "BATCH_BIGQUERY": 2, "STREAM_KAFKA": 3, "STREAM_KINESIS": 4, + "BATCH_REDSHIFT": 5, + "CUSTOM_SOURCE": 6, + "REQUEST_SOURCE": 7, } ) @@ -114,6 +119,9 @@ type DataSource struct { DatePartitionColumn string `protobuf:"bytes,4,opt,name=date_partition_column,json=datePartitionColumn,proto3" json:"date_partition_column,omitempty"` // Must specify creation timestamp column name CreatedTimestampColumn string `protobuf:"bytes,5,opt,name=created_timestamp_column,json=createdTimestampColumn,proto3" json:"created_timestamp_column,omitempty"` + // This is an internal field that is represents the python class for the data source object a proto object represents. + // This should be set by feast, and not by users. + DataSourceClassType string `protobuf:"bytes,17,opt,name=data_source_class_type,json=dataSourceClassType,proto3" json:"data_source_class_type,omitempty"` // DataSource options. // // Types that are assignable to Options: @@ -121,6 +129,9 @@ type DataSource struct { // *DataSource_BigqueryOptions // *DataSource_KafkaOptions_ // *DataSource_KinesisOptions_ + // *DataSource_RedshiftOptions_ + // *DataSource_RequestDataOptions_ + // *DataSource_CustomOptions Options isDataSource_Options `protobuf_oneof:"options"` } @@ -191,6 +202,13 @@ func (x *DataSource) GetCreatedTimestampColumn() string { return "" } +func (x *DataSource) GetDataSourceClassType() string { + if x != nil { + return x.DataSourceClassType + } + return "" +} + func (m *DataSource) GetOptions() isDataSource_Options { if m != nil { return m.Options @@ -226,6 +244,27 @@ func (x *DataSource) GetKinesisOptions() *DataSource_KinesisOptions { return nil } +func (x *DataSource) GetRedshiftOptions() *DataSource_RedshiftOptions { + if x, ok := x.GetOptions().(*DataSource_RedshiftOptions_); ok { + return x.RedshiftOptions + } + return nil +} + +func (x *DataSource) GetRequestDataOptions() *DataSource_RequestDataOptions { + if x, ok := x.GetOptions().(*DataSource_RequestDataOptions_); ok { + return x.RequestDataOptions + } + return nil +} + +func (x *DataSource) GetCustomOptions() *DataSource_CustomSourceOptions { + if x, ok := x.GetOptions().(*DataSource_CustomOptions); ok { + return x.CustomOptions + } + return nil +} + type isDataSource_Options interface { isDataSource_Options() } @@ -246,6 +285,18 @@ type DataSource_KinesisOptions_ struct { KinesisOptions *DataSource_KinesisOptions `protobuf:"bytes,14,opt,name=kinesis_options,json=kinesisOptions,proto3,oneof"` } +type DataSource_RedshiftOptions_ struct { + RedshiftOptions *DataSource_RedshiftOptions `protobuf:"bytes,15,opt,name=redshift_options,json=redshiftOptions,proto3,oneof"` +} + +type DataSource_RequestDataOptions_ struct { + RequestDataOptions *DataSource_RequestDataOptions `protobuf:"bytes,18,opt,name=request_data_options,json=requestDataOptions,proto3,oneof"` +} + +type DataSource_CustomOptions struct { + CustomOptions *DataSource_CustomSourceOptions `protobuf:"bytes,16,opt,name=custom_options,json=customOptions,proto3,oneof"` +} + func (*DataSource_FileOptions_) isDataSource_Options() {} func (*DataSource_BigqueryOptions) isDataSource_Options() {} @@ -254,6 +305,12 @@ func (*DataSource_KafkaOptions_) isDataSource_Options() {} func (*DataSource_KinesisOptions_) isDataSource_Options() {} +func (*DataSource_RedshiftOptions_) isDataSource_Options() {} + +func (*DataSource_RequestDataOptions_) isDataSource_Options() {} + +func (*DataSource_CustomOptions) isDataSource_Options() {} + // Defines options for DataSource that sources features from a file type DataSource_FileOptions struct { state protoimpl.MessageState @@ -266,6 +323,8 @@ type DataSource_FileOptions struct { // gs://path/to/file for GCP GCS storage // file:///path/to/file for local storage FileUrl string `protobuf:"bytes,2,opt,name=file_url,json=fileUrl,proto3" json:"file_url,omitempty"` + // override AWS S3 storage endpoint with custom S3 endpoint + S3EndpointOverride string `protobuf:"bytes,3,opt,name=s3_endpoint_override,json=s3EndpointOverride,proto3" json:"s3_endpoint_override,omitempty"` } func (x *DataSource_FileOptions) Reset() { @@ -314,6 +373,13 @@ func (x *DataSource_FileOptions) GetFileUrl() string { return "" } +func (x *DataSource_FileOptions) GetS3EndpointOverride() string { + if x != nil { + return x.S3EndpointOverride + } + return "" +} + // Defines options for DataSource that sources features from a BigQuery Query type DataSource_BigQueryOptions struct { state protoimpl.MessageState @@ -322,6 +388,9 @@ type DataSource_BigQueryOptions struct { // Full table reference in the form of [project:dataset.table] TableRef string `protobuf:"bytes,1,opt,name=table_ref,json=tableRef,proto3" json:"table_ref,omitempty"` + // SQL query that returns a table containing feature data. Must contain an event_timestamp column, and respective + // entity columns + Query string `protobuf:"bytes,2,opt,name=query,proto3" json:"query,omitempty"` } func (x *DataSource_BigQueryOptions) Reset() { @@ -363,6 +432,13 @@ func (x *DataSource_BigQueryOptions) GetTableRef() string { return "" } +func (x *DataSource_BigQueryOptions) GetQuery() string { + if x != nil { + return x.Query + } + return "" +} + // Defines options for DataSource that sources features from Kafka messages. // Each message should be a Protobuf that can be decoded with the generated // Java Protobuf class at the given class path @@ -502,6 +578,182 @@ func (x *DataSource_KinesisOptions) GetRecordFormat() *StreamFormat { return nil } +// Defines options for DataSource that sources features from a Redshift Query +type DataSource_RedshiftOptions struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + // Redshift table name + Table string `protobuf:"bytes,1,opt,name=table,proto3" json:"table,omitempty"` + // SQL query that returns a table containing feature data. Must contain an event_timestamp column, and respective + // entity columns + Query string `protobuf:"bytes,2,opt,name=query,proto3" json:"query,omitempty"` + // Redshift schema name + Schema string `protobuf:"bytes,3,opt,name=schema,proto3" json:"schema,omitempty"` +} + +func (x *DataSource_RedshiftOptions) Reset() { + *x = DataSource_RedshiftOptions{} + if protoimpl.UnsafeEnabled { + mi := &file_feast_core_DataSource_proto_msgTypes[6] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *DataSource_RedshiftOptions) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*DataSource_RedshiftOptions) ProtoMessage() {} + +func (x *DataSource_RedshiftOptions) ProtoReflect() protoreflect.Message { + mi := &file_feast_core_DataSource_proto_msgTypes[6] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use DataSource_RedshiftOptions.ProtoReflect.Descriptor instead. +func (*DataSource_RedshiftOptions) Descriptor() ([]byte, []int) { + return file_feast_core_DataSource_proto_rawDescGZIP(), []int{0, 5} +} + +func (x *DataSource_RedshiftOptions) GetTable() string { + if x != nil { + return x.Table + } + return "" +} + +func (x *DataSource_RedshiftOptions) GetQuery() string { + if x != nil { + return x.Query + } + return "" +} + +func (x *DataSource_RedshiftOptions) GetSchema() string { + if x != nil { + return x.Schema + } + return "" +} + +// Defines configuration for custom third-party data sources. +type DataSource_CustomSourceOptions struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + // Serialized configuration information for the data source. The implementer of the custom data source is + // responsible for serializing and deserializing data from bytes + Configuration []byte `protobuf:"bytes,1,opt,name=configuration,proto3" json:"configuration,omitempty"` +} + +func (x *DataSource_CustomSourceOptions) Reset() { + *x = DataSource_CustomSourceOptions{} + if protoimpl.UnsafeEnabled { + mi := &file_feast_core_DataSource_proto_msgTypes[7] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *DataSource_CustomSourceOptions) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*DataSource_CustomSourceOptions) ProtoMessage() {} + +func (x *DataSource_CustomSourceOptions) ProtoReflect() protoreflect.Message { + mi := &file_feast_core_DataSource_proto_msgTypes[7] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use DataSource_CustomSourceOptions.ProtoReflect.Descriptor instead. +func (*DataSource_CustomSourceOptions) Descriptor() ([]byte, []int) { + return file_feast_core_DataSource_proto_rawDescGZIP(), []int{0, 6} +} + +func (x *DataSource_CustomSourceOptions) GetConfiguration() []byte { + if x != nil { + return x.Configuration + } + return nil +} + +// Defines options for DataSource that sources features from request data +type DataSource_RequestDataOptions struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + // Name of the request data source + Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"` + // Mapping of feature name to type + Schema map[string]types.ValueType_Enum `protobuf:"bytes,2,rep,name=schema,proto3" json:"schema,omitempty" protobuf_key:"bytes,1,opt,name=key,proto3" protobuf_val:"varint,2,opt,name=value,proto3,enum=feast.types.ValueType_Enum"` +} + +func (x *DataSource_RequestDataOptions) Reset() { + *x = DataSource_RequestDataOptions{} + if protoimpl.UnsafeEnabled { + mi := &file_feast_core_DataSource_proto_msgTypes[8] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *DataSource_RequestDataOptions) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*DataSource_RequestDataOptions) ProtoMessage() {} + +func (x *DataSource_RequestDataOptions) ProtoReflect() protoreflect.Message { + mi := &file_feast_core_DataSource_proto_msgTypes[8] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use DataSource_RequestDataOptions.ProtoReflect.Descriptor instead. +func (*DataSource_RequestDataOptions) Descriptor() ([]byte, []int) { + return file_feast_core_DataSource_proto_rawDescGZIP(), []int{0, 7} +} + +func (x *DataSource_RequestDataOptions) GetName() string { + if x != nil { + return x.Name + } + return "" +} + +func (x *DataSource_RequestDataOptions) GetSchema() map[string]types.ValueType_Enum { + if x != nil { + return x.Schema + } + return nil +} + var File_feast_core_DataSource_proto protoreflect.FileDescriptor var file_feast_core_DataSource_proto_rawDesc = []byte{ @@ -509,59 +761,85 @@ var file_feast_core_DataSource_proto_rawDesc = []byte{ 0x61, 0x53, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x12, 0x0a, 0x66, 0x65, 0x61, 0x73, 0x74, 0x2e, 0x63, 0x6f, 0x72, 0x65, 0x1a, 0x1b, 0x66, 0x65, 0x61, 0x73, 0x74, 0x2f, 0x63, 0x6f, 0x72, 0x65, 0x2f, 0x44, 0x61, 0x74, 0x61, 0x46, 0x6f, 0x72, 0x6d, 0x61, 0x74, - 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x22, 0xd6, 0x09, 0x0a, 0x0a, 0x44, 0x61, 0x74, 0x61, 0x53, - 0x6f, 0x75, 0x72, 0x63, 0x65, 0x12, 0x35, 0x0a, 0x04, 0x74, 0x79, 0x70, 0x65, 0x18, 0x01, 0x20, - 0x01, 0x28, 0x0e, 0x32, 0x21, 0x2e, 0x66, 0x65, 0x61, 0x73, 0x74, 0x2e, 0x63, 0x6f, 0x72, 0x65, - 0x2e, 0x44, 0x61, 0x74, 0x61, 0x53, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x53, 0x6f, 0x75, 0x72, - 0x63, 0x65, 0x54, 0x79, 0x70, 0x65, 0x52, 0x04, 0x74, 0x79, 0x70, 0x65, 0x12, 0x4d, 0x0a, 0x0d, - 0x66, 0x69, 0x65, 0x6c, 0x64, 0x5f, 0x6d, 0x61, 0x70, 0x70, 0x69, 0x6e, 0x67, 0x18, 0x02, 0x20, - 0x03, 0x28, 0x0b, 0x32, 0x28, 0x2e, 0x66, 0x65, 0x61, 0x73, 0x74, 0x2e, 0x63, 0x6f, 0x72, 0x65, - 0x2e, 0x44, 0x61, 0x74, 0x61, 0x53, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x46, 0x69, 0x65, 0x6c, - 0x64, 0x4d, 0x61, 0x70, 0x70, 0x69, 0x6e, 0x67, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x52, 0x0c, 0x66, - 0x69, 0x65, 0x6c, 0x64, 0x4d, 0x61, 0x70, 0x70, 0x69, 0x6e, 0x67, 0x12, 0x34, 0x0a, 0x16, 0x65, - 0x76, 0x65, 0x6e, 0x74, 0x5f, 0x74, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x5f, 0x63, - 0x6f, 0x6c, 0x75, 0x6d, 0x6e, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x14, 0x65, 0x76, 0x65, - 0x6e, 0x74, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x43, 0x6f, 0x6c, 0x75, 0x6d, - 0x6e, 0x12, 0x32, 0x0a, 0x15, 0x64, 0x61, 0x74, 0x65, 0x5f, 0x70, 0x61, 0x72, 0x74, 0x69, 0x74, - 0x69, 0x6f, 0x6e, 0x5f, 0x63, 0x6f, 0x6c, 0x75, 0x6d, 0x6e, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, - 0x52, 0x13, 0x64, 0x61, 0x74, 0x65, 0x50, 0x61, 0x72, 0x74, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x43, - 0x6f, 0x6c, 0x75, 0x6d, 0x6e, 0x12, 0x38, 0x0a, 0x18, 0x63, 0x72, 0x65, 0x61, 0x74, 0x65, 0x64, - 0x5f, 0x74, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x5f, 0x63, 0x6f, 0x6c, 0x75, 0x6d, - 0x6e, 0x18, 0x05, 0x20, 0x01, 0x28, 0x09, 0x52, 0x16, 0x63, 0x72, 0x65, 0x61, 0x74, 0x65, 0x64, - 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x43, 0x6f, 0x6c, 0x75, 0x6d, 0x6e, 0x12, - 0x47, 0x0a, 0x0c, 0x66, 0x69, 0x6c, 0x65, 0x5f, 0x6f, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x18, - 0x0b, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x22, 0x2e, 0x66, 0x65, 0x61, 0x73, 0x74, 0x2e, 0x63, 0x6f, - 0x72, 0x65, 0x2e, 0x44, 0x61, 0x74, 0x61, 0x53, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x46, 0x69, - 0x6c, 0x65, 0x4f, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x48, 0x00, 0x52, 0x0b, 0x66, 0x69, 0x6c, - 0x65, 0x4f, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x12, 0x53, 0x0a, 0x10, 0x62, 0x69, 0x67, 0x71, - 0x75, 0x65, 0x72, 0x79, 0x5f, 0x6f, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x18, 0x0c, 0x20, 0x01, - 0x28, 0x0b, 0x32, 0x26, 0x2e, 0x66, 0x65, 0x61, 0x73, 0x74, 0x2e, 0x63, 0x6f, 0x72, 0x65, 0x2e, - 0x44, 0x61, 0x74, 0x61, 0x53, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x42, 0x69, 0x67, 0x51, 0x75, - 0x65, 0x72, 0x79, 0x4f, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x48, 0x00, 0x52, 0x0f, 0x62, 0x69, - 0x67, 0x71, 0x75, 0x65, 0x72, 0x79, 0x4f, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x12, 0x4a, 0x0a, - 0x0d, 0x6b, 0x61, 0x66, 0x6b, 0x61, 0x5f, 0x6f, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x18, 0x0d, - 0x20, 0x01, 0x28, 0x0b, 0x32, 0x23, 0x2e, 0x66, 0x65, 0x61, 0x73, 0x74, 0x2e, 0x63, 0x6f, 0x72, - 0x65, 0x2e, 0x44, 0x61, 0x74, 0x61, 0x53, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x4b, 0x61, 0x66, - 0x6b, 0x61, 0x4f, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x48, 0x00, 0x52, 0x0c, 0x6b, 0x61, 0x66, - 0x6b, 0x61, 0x4f, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x12, 0x50, 0x0a, 0x0f, 0x6b, 0x69, 0x6e, - 0x65, 0x73, 0x69, 0x73, 0x5f, 0x6f, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x18, 0x0e, 0x20, 0x01, - 0x28, 0x0b, 0x32, 0x25, 0x2e, 0x66, 0x65, 0x61, 0x73, 0x74, 0x2e, 0x63, 0x6f, 0x72, 0x65, 0x2e, - 0x44, 0x61, 0x74, 0x61, 0x53, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x4b, 0x69, 0x6e, 0x65, 0x73, - 0x69, 0x73, 0x4f, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x48, 0x00, 0x52, 0x0e, 0x6b, 0x69, 0x6e, - 0x65, 0x73, 0x69, 0x73, 0x4f, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x1a, 0x3f, 0x0a, 0x11, 0x46, - 0x69, 0x65, 0x6c, 0x64, 0x4d, 0x61, 0x70, 0x70, 0x69, 0x6e, 0x67, 0x45, 0x6e, 0x74, 0x72, 0x79, - 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x6b, - 0x65, 0x79, 0x12, 0x14, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, - 0x09, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x3a, 0x02, 0x38, 0x01, 0x1a, 0x61, 0x0a, 0x0b, - 0x46, 0x69, 0x6c, 0x65, 0x4f, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x12, 0x37, 0x0a, 0x0b, 0x66, - 0x69, 0x6c, 0x65, 0x5f, 0x66, 0x6f, 0x72, 0x6d, 0x61, 0x74, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, - 0x32, 0x16, 0x2e, 0x66, 0x65, 0x61, 0x73, 0x74, 0x2e, 0x63, 0x6f, 0x72, 0x65, 0x2e, 0x46, 0x69, - 0x6c, 0x65, 0x46, 0x6f, 0x72, 0x6d, 0x61, 0x74, 0x52, 0x0a, 0x66, 0x69, 0x6c, 0x65, 0x46, 0x6f, - 0x72, 0x6d, 0x61, 0x74, 0x12, 0x19, 0x0a, 0x08, 0x66, 0x69, 0x6c, 0x65, 0x5f, 0x75, 0x72, 0x6c, - 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x66, 0x69, 0x6c, 0x65, 0x55, 0x72, 0x6c, 0x1a, - 0x2e, 0x0a, 0x0f, 0x42, 0x69, 0x67, 0x51, 0x75, 0x65, 0x72, 0x79, 0x4f, 0x70, 0x74, 0x69, 0x6f, - 0x6e, 0x73, 0x12, 0x1b, 0x0a, 0x09, 0x74, 0x61, 0x62, 0x6c, 0x65, 0x5f, 0x72, 0x65, 0x66, 0x18, - 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x74, 0x61, 0x62, 0x6c, 0x65, 0x52, 0x65, 0x66, 0x1a, + 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x1a, 0x17, 0x66, 0x65, 0x61, 0x73, 0x74, 0x2f, 0x74, 0x79, + 0x70, 0x65, 0x73, 0x2f, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x22, + 0x85, 0x10, 0x0a, 0x0a, 0x44, 0x61, 0x74, 0x61, 0x53, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x12, 0x35, + 0x0a, 0x04, 0x74, 0x79, 0x70, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x21, 0x2e, 0x66, + 0x65, 0x61, 0x73, 0x74, 0x2e, 0x63, 0x6f, 0x72, 0x65, 0x2e, 0x44, 0x61, 0x74, 0x61, 0x53, 0x6f, + 0x75, 0x72, 0x63, 0x65, 0x2e, 0x53, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x54, 0x79, 0x70, 0x65, 0x52, + 0x04, 0x74, 0x79, 0x70, 0x65, 0x12, 0x4d, 0x0a, 0x0d, 0x66, 0x69, 0x65, 0x6c, 0x64, 0x5f, 0x6d, + 0x61, 0x70, 0x70, 0x69, 0x6e, 0x67, 0x18, 0x02, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x28, 0x2e, 0x66, + 0x65, 0x61, 0x73, 0x74, 0x2e, 0x63, 0x6f, 0x72, 0x65, 0x2e, 0x44, 0x61, 0x74, 0x61, 0x53, 0x6f, + 0x75, 0x72, 0x63, 0x65, 0x2e, 0x46, 0x69, 0x65, 0x6c, 0x64, 0x4d, 0x61, 0x70, 0x70, 0x69, 0x6e, + 0x67, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x52, 0x0c, 0x66, 0x69, 0x65, 0x6c, 0x64, 0x4d, 0x61, 0x70, + 0x70, 0x69, 0x6e, 0x67, 0x12, 0x34, 0x0a, 0x16, 0x65, 0x76, 0x65, 0x6e, 0x74, 0x5f, 0x74, 0x69, + 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x5f, 0x63, 0x6f, 0x6c, 0x75, 0x6d, 0x6e, 0x18, 0x03, + 0x20, 0x01, 0x28, 0x09, 0x52, 0x14, 0x65, 0x76, 0x65, 0x6e, 0x74, 0x54, 0x69, 0x6d, 0x65, 0x73, + 0x74, 0x61, 0x6d, 0x70, 0x43, 0x6f, 0x6c, 0x75, 0x6d, 0x6e, 0x12, 0x32, 0x0a, 0x15, 0x64, 0x61, + 0x74, 0x65, 0x5f, 0x70, 0x61, 0x72, 0x74, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x5f, 0x63, 0x6f, 0x6c, + 0x75, 0x6d, 0x6e, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x13, 0x64, 0x61, 0x74, 0x65, 0x50, + 0x61, 0x72, 0x74, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x43, 0x6f, 0x6c, 0x75, 0x6d, 0x6e, 0x12, 0x38, + 0x0a, 0x18, 0x63, 0x72, 0x65, 0x61, 0x74, 0x65, 0x64, 0x5f, 0x74, 0x69, 0x6d, 0x65, 0x73, 0x74, + 0x61, 0x6d, 0x70, 0x5f, 0x63, 0x6f, 0x6c, 0x75, 0x6d, 0x6e, 0x18, 0x05, 0x20, 0x01, 0x28, 0x09, + 0x52, 0x16, 0x63, 0x72, 0x65, 0x61, 0x74, 0x65, 0x64, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, + 0x6d, 0x70, 0x43, 0x6f, 0x6c, 0x75, 0x6d, 0x6e, 0x12, 0x33, 0x0a, 0x16, 0x64, 0x61, 0x74, 0x61, + 0x5f, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x5f, 0x63, 0x6c, 0x61, 0x73, 0x73, 0x5f, 0x74, 0x79, + 0x70, 0x65, 0x18, 0x11, 0x20, 0x01, 0x28, 0x09, 0x52, 0x13, 0x64, 0x61, 0x74, 0x61, 0x53, 0x6f, + 0x75, 0x72, 0x63, 0x65, 0x43, 0x6c, 0x61, 0x73, 0x73, 0x54, 0x79, 0x70, 0x65, 0x12, 0x47, 0x0a, + 0x0c, 0x66, 0x69, 0x6c, 0x65, 0x5f, 0x6f, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x18, 0x0b, 0x20, + 0x01, 0x28, 0x0b, 0x32, 0x22, 0x2e, 0x66, 0x65, 0x61, 0x73, 0x74, 0x2e, 0x63, 0x6f, 0x72, 0x65, + 0x2e, 0x44, 0x61, 0x74, 0x61, 0x53, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x46, 0x69, 0x6c, 0x65, + 0x4f, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x48, 0x00, 0x52, 0x0b, 0x66, 0x69, 0x6c, 0x65, 0x4f, + 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x12, 0x53, 0x0a, 0x10, 0x62, 0x69, 0x67, 0x71, 0x75, 0x65, + 0x72, 0x79, 0x5f, 0x6f, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x18, 0x0c, 0x20, 0x01, 0x28, 0x0b, + 0x32, 0x26, 0x2e, 0x66, 0x65, 0x61, 0x73, 0x74, 0x2e, 0x63, 0x6f, 0x72, 0x65, 0x2e, 0x44, 0x61, + 0x74, 0x61, 0x53, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x42, 0x69, 0x67, 0x51, 0x75, 0x65, 0x72, + 0x79, 0x4f, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x48, 0x00, 0x52, 0x0f, 0x62, 0x69, 0x67, 0x71, + 0x75, 0x65, 0x72, 0x79, 0x4f, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x12, 0x4a, 0x0a, 0x0d, 0x6b, + 0x61, 0x66, 0x6b, 0x61, 0x5f, 0x6f, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x18, 0x0d, 0x20, 0x01, + 0x28, 0x0b, 0x32, 0x23, 0x2e, 0x66, 0x65, 0x61, 0x73, 0x74, 0x2e, 0x63, 0x6f, 0x72, 0x65, 0x2e, + 0x44, 0x61, 0x74, 0x61, 0x53, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x4b, 0x61, 0x66, 0x6b, 0x61, + 0x4f, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x48, 0x00, 0x52, 0x0c, 0x6b, 0x61, 0x66, 0x6b, 0x61, + 0x4f, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x12, 0x50, 0x0a, 0x0f, 0x6b, 0x69, 0x6e, 0x65, 0x73, + 0x69, 0x73, 0x5f, 0x6f, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x18, 0x0e, 0x20, 0x01, 0x28, 0x0b, + 0x32, 0x25, 0x2e, 0x66, 0x65, 0x61, 0x73, 0x74, 0x2e, 0x63, 0x6f, 0x72, 0x65, 0x2e, 0x44, 0x61, + 0x74, 0x61, 0x53, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x4b, 0x69, 0x6e, 0x65, 0x73, 0x69, 0x73, + 0x4f, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x48, 0x00, 0x52, 0x0e, 0x6b, 0x69, 0x6e, 0x65, 0x73, + 0x69, 0x73, 0x4f, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x12, 0x53, 0x0a, 0x10, 0x72, 0x65, 0x64, + 0x73, 0x68, 0x69, 0x66, 0x74, 0x5f, 0x6f, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x18, 0x0f, 0x20, + 0x01, 0x28, 0x0b, 0x32, 0x26, 0x2e, 0x66, 0x65, 0x61, 0x73, 0x74, 0x2e, 0x63, 0x6f, 0x72, 0x65, + 0x2e, 0x44, 0x61, 0x74, 0x61, 0x53, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x52, 0x65, 0x64, 0x73, + 0x68, 0x69, 0x66, 0x74, 0x4f, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x48, 0x00, 0x52, 0x0f, 0x72, + 0x65, 0x64, 0x73, 0x68, 0x69, 0x66, 0x74, 0x4f, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x12, 0x5d, + 0x0a, 0x14, 0x72, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x5f, 0x64, 0x61, 0x74, 0x61, 0x5f, 0x6f, + 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x18, 0x12, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x29, 0x2e, 0x66, + 0x65, 0x61, 0x73, 0x74, 0x2e, 0x63, 0x6f, 0x72, 0x65, 0x2e, 0x44, 0x61, 0x74, 0x61, 0x53, 0x6f, + 0x75, 0x72, 0x63, 0x65, 0x2e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x44, 0x61, 0x74, 0x61, + 0x4f, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x48, 0x00, 0x52, 0x12, 0x72, 0x65, 0x71, 0x75, 0x65, + 0x73, 0x74, 0x44, 0x61, 0x74, 0x61, 0x4f, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x12, 0x53, 0x0a, + 0x0e, 0x63, 0x75, 0x73, 0x74, 0x6f, 0x6d, 0x5f, 0x6f, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x18, + 0x10, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x2a, 0x2e, 0x66, 0x65, 0x61, 0x73, 0x74, 0x2e, 0x63, 0x6f, + 0x72, 0x65, 0x2e, 0x44, 0x61, 0x74, 0x61, 0x53, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x43, 0x75, + 0x73, 0x74, 0x6f, 0x6d, 0x53, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x4f, 0x70, 0x74, 0x69, 0x6f, 0x6e, + 0x73, 0x48, 0x00, 0x52, 0x0d, 0x63, 0x75, 0x73, 0x74, 0x6f, 0x6d, 0x4f, 0x70, 0x74, 0x69, 0x6f, + 0x6e, 0x73, 0x1a, 0x3f, 0x0a, 0x11, 0x46, 0x69, 0x65, 0x6c, 0x64, 0x4d, 0x61, 0x70, 0x70, 0x69, + 0x6e, 0x67, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, + 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x14, 0x0a, 0x05, 0x76, 0x61, 0x6c, + 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x3a, + 0x02, 0x38, 0x01, 0x1a, 0x93, 0x01, 0x0a, 0x0b, 0x46, 0x69, 0x6c, 0x65, 0x4f, 0x70, 0x74, 0x69, + 0x6f, 0x6e, 0x73, 0x12, 0x37, 0x0a, 0x0b, 0x66, 0x69, 0x6c, 0x65, 0x5f, 0x66, 0x6f, 0x72, 0x6d, + 0x61, 0x74, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x16, 0x2e, 0x66, 0x65, 0x61, 0x73, 0x74, + 0x2e, 0x63, 0x6f, 0x72, 0x65, 0x2e, 0x46, 0x69, 0x6c, 0x65, 0x46, 0x6f, 0x72, 0x6d, 0x61, 0x74, + 0x52, 0x0a, 0x66, 0x69, 0x6c, 0x65, 0x46, 0x6f, 0x72, 0x6d, 0x61, 0x74, 0x12, 0x19, 0x0a, 0x08, + 0x66, 0x69, 0x6c, 0x65, 0x5f, 0x75, 0x72, 0x6c, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, + 0x66, 0x69, 0x6c, 0x65, 0x55, 0x72, 0x6c, 0x12, 0x30, 0x0a, 0x14, 0x73, 0x33, 0x5f, 0x65, 0x6e, + 0x64, 0x70, 0x6f, 0x69, 0x6e, 0x74, 0x5f, 0x6f, 0x76, 0x65, 0x72, 0x72, 0x69, 0x64, 0x65, 0x18, + 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x12, 0x73, 0x33, 0x45, 0x6e, 0x64, 0x70, 0x6f, 0x69, 0x6e, + 0x74, 0x4f, 0x76, 0x65, 0x72, 0x72, 0x69, 0x64, 0x65, 0x1a, 0x44, 0x0a, 0x0f, 0x42, 0x69, 0x67, + 0x51, 0x75, 0x65, 0x72, 0x79, 0x4f, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x12, 0x1b, 0x0a, 0x09, + 0x74, 0x61, 0x62, 0x6c, 0x65, 0x5f, 0x72, 0x65, 0x66, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, + 0x08, 0x74, 0x61, 0x62, 0x6c, 0x65, 0x52, 0x65, 0x66, 0x12, 0x14, 0x0a, 0x05, 0x71, 0x75, 0x65, + 0x72, 0x79, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x71, 0x75, 0x65, 0x72, 0x79, 0x1a, 0x92, 0x01, 0x0a, 0x0c, 0x4b, 0x61, 0x66, 0x6b, 0x61, 0x4f, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x12, 0x2b, 0x0a, 0x11, 0x62, 0x6f, 0x6f, 0x74, 0x73, 0x74, 0x72, 0x61, 0x70, 0x5f, 0x73, 0x65, 0x72, 0x76, 0x65, 0x72, 0x73, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x10, 0x62, 0x6f, 0x6f, @@ -579,21 +857,47 @@ var file_feast_core_DataSource_proto_rawDesc = []byte{ 0x12, 0x3d, 0x0a, 0x0d, 0x72, 0x65, 0x63, 0x6f, 0x72, 0x64, 0x5f, 0x66, 0x6f, 0x72, 0x6d, 0x61, 0x74, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x18, 0x2e, 0x66, 0x65, 0x61, 0x73, 0x74, 0x2e, 0x63, 0x6f, 0x72, 0x65, 0x2e, 0x53, 0x74, 0x72, 0x65, 0x61, 0x6d, 0x46, 0x6f, 0x72, 0x6d, 0x61, - 0x74, 0x52, 0x0c, 0x72, 0x65, 0x63, 0x6f, 0x72, 0x64, 0x46, 0x6f, 0x72, 0x6d, 0x61, 0x74, 0x22, - 0x63, 0x0a, 0x0a, 0x53, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x54, 0x79, 0x70, 0x65, 0x12, 0x0b, 0x0a, - 0x07, 0x49, 0x4e, 0x56, 0x41, 0x4c, 0x49, 0x44, 0x10, 0x00, 0x12, 0x0e, 0x0a, 0x0a, 0x42, 0x41, - 0x54, 0x43, 0x48, 0x5f, 0x46, 0x49, 0x4c, 0x45, 0x10, 0x01, 0x12, 0x12, 0x0a, 0x0e, 0x42, 0x41, - 0x54, 0x43, 0x48, 0x5f, 0x42, 0x49, 0x47, 0x51, 0x55, 0x45, 0x52, 0x59, 0x10, 0x02, 0x12, 0x10, - 0x0a, 0x0c, 0x53, 0x54, 0x52, 0x45, 0x41, 0x4d, 0x5f, 0x4b, 0x41, 0x46, 0x4b, 0x41, 0x10, 0x03, - 0x12, 0x12, 0x0a, 0x0e, 0x53, 0x54, 0x52, 0x45, 0x41, 0x4d, 0x5f, 0x4b, 0x49, 0x4e, 0x45, 0x53, - 0x49, 0x53, 0x10, 0x04, 0x42, 0x09, 0x0a, 0x07, 0x6f, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x42, - 0x58, 0x0a, 0x10, 0x66, 0x65, 0x61, 0x73, 0x74, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x63, - 0x6f, 0x72, 0x65, 0x42, 0x0f, 0x44, 0x61, 0x74, 0x61, 0x53, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x50, - 0x72, 0x6f, 0x74, 0x6f, 0x5a, 0x33, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, - 0x2f, 0x66, 0x65, 0x61, 0x73, 0x74, 0x2d, 0x64, 0x65, 0x76, 0x2f, 0x66, 0x65, 0x61, 0x73, 0x74, - 0x2f, 0x73, 0x64, 0x6b, 0x2f, 0x67, 0x6f, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x73, 0x2f, 0x66, - 0x65, 0x61, 0x73, 0x74, 0x2f, 0x63, 0x6f, 0x72, 0x65, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, - 0x33, + 0x74, 0x52, 0x0c, 0x72, 0x65, 0x63, 0x6f, 0x72, 0x64, 0x46, 0x6f, 0x72, 0x6d, 0x61, 0x74, 0x1a, + 0x55, 0x0a, 0x0f, 0x52, 0x65, 0x64, 0x73, 0x68, 0x69, 0x66, 0x74, 0x4f, 0x70, 0x74, 0x69, 0x6f, + 0x6e, 0x73, 0x12, 0x14, 0x0a, 0x05, 0x74, 0x61, 0x62, 0x6c, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, + 0x09, 0x52, 0x05, 0x74, 0x61, 0x62, 0x6c, 0x65, 0x12, 0x14, 0x0a, 0x05, 0x71, 0x75, 0x65, 0x72, + 0x79, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x71, 0x75, 0x65, 0x72, 0x79, 0x12, 0x16, + 0x0a, 0x06, 0x73, 0x63, 0x68, 0x65, 0x6d, 0x61, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, + 0x73, 0x63, 0x68, 0x65, 0x6d, 0x61, 0x1a, 0x3b, 0x0a, 0x13, 0x43, 0x75, 0x73, 0x74, 0x6f, 0x6d, + 0x53, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x4f, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x12, 0x24, 0x0a, + 0x0d, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x75, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x01, + 0x20, 0x01, 0x28, 0x0c, 0x52, 0x0d, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x75, 0x72, 0x61, 0x74, + 0x69, 0x6f, 0x6e, 0x1a, 0xcf, 0x01, 0x0a, 0x12, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x44, + 0x61, 0x74, 0x61, 0x4f, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, + 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x4d, + 0x0a, 0x06, 0x73, 0x63, 0x68, 0x65, 0x6d, 0x61, 0x18, 0x02, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x35, + 0x2e, 0x66, 0x65, 0x61, 0x73, 0x74, 0x2e, 0x63, 0x6f, 0x72, 0x65, 0x2e, 0x44, 0x61, 0x74, 0x61, + 0x53, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x44, 0x61, + 0x74, 0x61, 0x4f, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x2e, 0x53, 0x63, 0x68, 0x65, 0x6d, 0x61, + 0x45, 0x6e, 0x74, 0x72, 0x79, 0x52, 0x06, 0x73, 0x63, 0x68, 0x65, 0x6d, 0x61, 0x1a, 0x56, 0x0a, + 0x0b, 0x53, 0x63, 0x68, 0x65, 0x6d, 0x61, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x12, 0x10, 0x0a, 0x03, + 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x31, + 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x1b, 0x2e, + 0x66, 0x65, 0x61, 0x73, 0x74, 0x2e, 0x74, 0x79, 0x70, 0x65, 0x73, 0x2e, 0x56, 0x61, 0x6c, 0x75, + 0x65, 0x54, 0x79, 0x70, 0x65, 0x2e, 0x45, 0x6e, 0x75, 0x6d, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, + 0x65, 0x3a, 0x02, 0x38, 0x01, 0x22, 0x9e, 0x01, 0x0a, 0x0a, 0x53, 0x6f, 0x75, 0x72, 0x63, 0x65, + 0x54, 0x79, 0x70, 0x65, 0x12, 0x0b, 0x0a, 0x07, 0x49, 0x4e, 0x56, 0x41, 0x4c, 0x49, 0x44, 0x10, + 0x00, 0x12, 0x0e, 0x0a, 0x0a, 0x42, 0x41, 0x54, 0x43, 0x48, 0x5f, 0x46, 0x49, 0x4c, 0x45, 0x10, + 0x01, 0x12, 0x12, 0x0a, 0x0e, 0x42, 0x41, 0x54, 0x43, 0x48, 0x5f, 0x42, 0x49, 0x47, 0x51, 0x55, + 0x45, 0x52, 0x59, 0x10, 0x02, 0x12, 0x10, 0x0a, 0x0c, 0x53, 0x54, 0x52, 0x45, 0x41, 0x4d, 0x5f, + 0x4b, 0x41, 0x46, 0x4b, 0x41, 0x10, 0x03, 0x12, 0x12, 0x0a, 0x0e, 0x53, 0x54, 0x52, 0x45, 0x41, + 0x4d, 0x5f, 0x4b, 0x49, 0x4e, 0x45, 0x53, 0x49, 0x53, 0x10, 0x04, 0x12, 0x12, 0x0a, 0x0e, 0x42, + 0x41, 0x54, 0x43, 0x48, 0x5f, 0x52, 0x45, 0x44, 0x53, 0x48, 0x49, 0x46, 0x54, 0x10, 0x05, 0x12, + 0x11, 0x0a, 0x0d, 0x43, 0x55, 0x53, 0x54, 0x4f, 0x4d, 0x5f, 0x53, 0x4f, 0x55, 0x52, 0x43, 0x45, + 0x10, 0x06, 0x12, 0x12, 0x0a, 0x0e, 0x52, 0x45, 0x51, 0x55, 0x45, 0x53, 0x54, 0x5f, 0x53, 0x4f, + 0x55, 0x52, 0x43, 0x45, 0x10, 0x07, 0x42, 0x09, 0x0a, 0x07, 0x6f, 0x70, 0x74, 0x69, 0x6f, 0x6e, + 0x73, 0x4a, 0x04, 0x08, 0x06, 0x10, 0x0b, 0x42, 0x58, 0x0a, 0x10, 0x66, 0x65, 0x61, 0x73, 0x74, + 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x63, 0x6f, 0x72, 0x65, 0x42, 0x0f, 0x44, 0x61, 0x74, + 0x61, 0x53, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x50, 0x72, 0x6f, 0x74, 0x6f, 0x5a, 0x33, 0x67, 0x69, + 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x66, 0x65, 0x61, 0x73, 0x74, 0x2d, 0x64, + 0x65, 0x76, 0x2f, 0x66, 0x65, 0x61, 0x73, 0x74, 0x2f, 0x73, 0x64, 0x6b, 0x2f, 0x67, 0x6f, 0x2f, + 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x73, 0x2f, 0x66, 0x65, 0x61, 0x73, 0x74, 0x2f, 0x63, 0x6f, 0x72, + 0x65, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, } var ( @@ -609,33 +913,43 @@ func file_feast_core_DataSource_proto_rawDescGZIP() []byte { } var file_feast_core_DataSource_proto_enumTypes = make([]protoimpl.EnumInfo, 1) -var file_feast_core_DataSource_proto_msgTypes = make([]protoimpl.MessageInfo, 6) +var file_feast_core_DataSource_proto_msgTypes = make([]protoimpl.MessageInfo, 10) var file_feast_core_DataSource_proto_goTypes = []interface{}{ - (DataSource_SourceType)(0), // 0: feast.core.DataSource.SourceType - (*DataSource)(nil), // 1: feast.core.DataSource - nil, // 2: feast.core.DataSource.FieldMappingEntry - (*DataSource_FileOptions)(nil), // 3: feast.core.DataSource.FileOptions - (*DataSource_BigQueryOptions)(nil), // 4: feast.core.DataSource.BigQueryOptions - (*DataSource_KafkaOptions)(nil), // 5: feast.core.DataSource.KafkaOptions - (*DataSource_KinesisOptions)(nil), // 6: feast.core.DataSource.KinesisOptions - (*FileFormat)(nil), // 7: feast.core.FileFormat - (*StreamFormat)(nil), // 8: feast.core.StreamFormat + (DataSource_SourceType)(0), // 0: feast.core.DataSource.SourceType + (*DataSource)(nil), // 1: feast.core.DataSource + nil, // 2: feast.core.DataSource.FieldMappingEntry + (*DataSource_FileOptions)(nil), // 3: feast.core.DataSource.FileOptions + (*DataSource_BigQueryOptions)(nil), // 4: feast.core.DataSource.BigQueryOptions + (*DataSource_KafkaOptions)(nil), // 5: feast.core.DataSource.KafkaOptions + (*DataSource_KinesisOptions)(nil), // 6: feast.core.DataSource.KinesisOptions + (*DataSource_RedshiftOptions)(nil), // 7: feast.core.DataSource.RedshiftOptions + (*DataSource_CustomSourceOptions)(nil), // 8: feast.core.DataSource.CustomSourceOptions + (*DataSource_RequestDataOptions)(nil), // 9: feast.core.DataSource.RequestDataOptions + nil, // 10: feast.core.DataSource.RequestDataOptions.SchemaEntry + (*FileFormat)(nil), // 11: feast.core.FileFormat + (*StreamFormat)(nil), // 12: feast.core.StreamFormat + (types.ValueType_Enum)(0), // 13: feast.types.ValueType.Enum } var file_feast_core_DataSource_proto_depIdxs = []int32{ - 0, // 0: feast.core.DataSource.type:type_name -> feast.core.DataSource.SourceType - 2, // 1: feast.core.DataSource.field_mapping:type_name -> feast.core.DataSource.FieldMappingEntry - 3, // 2: feast.core.DataSource.file_options:type_name -> feast.core.DataSource.FileOptions - 4, // 3: feast.core.DataSource.bigquery_options:type_name -> feast.core.DataSource.BigQueryOptions - 5, // 4: feast.core.DataSource.kafka_options:type_name -> feast.core.DataSource.KafkaOptions - 6, // 5: feast.core.DataSource.kinesis_options:type_name -> feast.core.DataSource.KinesisOptions - 7, // 6: feast.core.DataSource.FileOptions.file_format:type_name -> feast.core.FileFormat - 8, // 7: feast.core.DataSource.KafkaOptions.message_format:type_name -> feast.core.StreamFormat - 8, // 8: feast.core.DataSource.KinesisOptions.record_format:type_name -> feast.core.StreamFormat - 9, // [9:9] is the sub-list for method output_type - 9, // [9:9] is the sub-list for method input_type - 9, // [9:9] is the sub-list for extension type_name - 9, // [9:9] is the sub-list for extension extendee - 0, // [0:9] is the sub-list for field type_name + 0, // 0: feast.core.DataSource.type:type_name -> feast.core.DataSource.SourceType + 2, // 1: feast.core.DataSource.field_mapping:type_name -> feast.core.DataSource.FieldMappingEntry + 3, // 2: feast.core.DataSource.file_options:type_name -> feast.core.DataSource.FileOptions + 4, // 3: feast.core.DataSource.bigquery_options:type_name -> feast.core.DataSource.BigQueryOptions + 5, // 4: feast.core.DataSource.kafka_options:type_name -> feast.core.DataSource.KafkaOptions + 6, // 5: feast.core.DataSource.kinesis_options:type_name -> feast.core.DataSource.KinesisOptions + 7, // 6: feast.core.DataSource.redshift_options:type_name -> feast.core.DataSource.RedshiftOptions + 9, // 7: feast.core.DataSource.request_data_options:type_name -> feast.core.DataSource.RequestDataOptions + 8, // 8: feast.core.DataSource.custom_options:type_name -> feast.core.DataSource.CustomSourceOptions + 11, // 9: feast.core.DataSource.FileOptions.file_format:type_name -> feast.core.FileFormat + 12, // 10: feast.core.DataSource.KafkaOptions.message_format:type_name -> feast.core.StreamFormat + 12, // 11: feast.core.DataSource.KinesisOptions.record_format:type_name -> feast.core.StreamFormat + 10, // 12: feast.core.DataSource.RequestDataOptions.schema:type_name -> feast.core.DataSource.RequestDataOptions.SchemaEntry + 13, // 13: feast.core.DataSource.RequestDataOptions.SchemaEntry.value:type_name -> feast.types.ValueType.Enum + 14, // [14:14] is the sub-list for method output_type + 14, // [14:14] is the sub-list for method input_type + 14, // [14:14] is the sub-list for extension type_name + 14, // [14:14] is the sub-list for extension extendee + 0, // [0:14] is the sub-list for field type_name } func init() { file_feast_core_DataSource_proto_init() } @@ -705,12 +1019,51 @@ func file_feast_core_DataSource_proto_init() { return nil } } + file_feast_core_DataSource_proto_msgTypes[6].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*DataSource_RedshiftOptions); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_feast_core_DataSource_proto_msgTypes[7].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*DataSource_CustomSourceOptions); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_feast_core_DataSource_proto_msgTypes[8].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*DataSource_RequestDataOptions); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } } file_feast_core_DataSource_proto_msgTypes[0].OneofWrappers = []interface{}{ (*DataSource_FileOptions_)(nil), (*DataSource_BigqueryOptions)(nil), (*DataSource_KafkaOptions_)(nil), (*DataSource_KinesisOptions_)(nil), + (*DataSource_RedshiftOptions_)(nil), + (*DataSource_RequestDataOptions_)(nil), + (*DataSource_CustomOptions)(nil), } type x struct{} out := protoimpl.TypeBuilder{ @@ -718,7 +1071,7 @@ func file_feast_core_DataSource_proto_init() { GoPackagePath: reflect.TypeOf(x{}).PkgPath(), RawDescriptor: file_feast_core_DataSource_proto_rawDesc, NumEnums: 1, - NumMessages: 6, + NumMessages: 10, NumExtensions: 0, NumServices: 0, }, diff --git a/sdk/go/protos/feast/core/Entity.pb.go b/sdk/go/protos/feast/core/Entity.pb.go index 0aed9133257..c6d9014791e 100644 --- a/sdk/go/protos/feast/core/Entity.pb.go +++ b/sdk/go/protos/feast/core/Entity.pb.go @@ -16,18 +16,17 @@ // Code generated by protoc-gen-go. DO NOT EDIT. // versions: -// protoc-gen-go v1.25.0 -// protoc v3.12.4 +// protoc-gen-go v1.27.1 +// protoc v3.11.2 // source: feast/core/Entity.proto package core import ( types "github.com/feast-dev/feast/sdk/go/protos/feast/types" - proto "github.com/golang/protobuf/proto" - timestamp "github.com/golang/protobuf/ptypes/timestamp" protoreflect "google.golang.org/protobuf/reflect/protoreflect" protoimpl "google.golang.org/protobuf/runtime/protoimpl" + timestamppb "google.golang.org/protobuf/types/known/timestamppb" reflect "reflect" sync "sync" ) @@ -39,10 +38,6 @@ const ( _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) ) -// This is a compile-time assertion that a sufficiently up-to-date version -// of the legacy proto package is being used. -const _ = proto.ProtoPackageIsVersion4 - type Entity struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache @@ -107,10 +102,14 @@ type EntitySpecV2 struct { // Name of the entity. Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"` + // Name of Feast project that this feature table belongs to. + Project string `protobuf:"bytes,9,opt,name=project,proto3" json:"project,omitempty"` // Type of the entity. ValueType types.ValueType_Enum `protobuf:"varint,2,opt,name=value_type,json=valueType,proto3,enum=feast.types.ValueType_Enum" json:"value_type,omitempty"` // Description of the entity. Description string `protobuf:"bytes,3,opt,name=description,proto3" json:"description,omitempty"` + // Join key for the entity (i.e. name of the column the entity maps to). + JoinKey string `protobuf:"bytes,4,opt,name=join_key,json=joinKey,proto3" json:"join_key,omitempty"` // User defined metadata Labels map[string]string `protobuf:"bytes,8,rep,name=labels,proto3" json:"labels,omitempty" protobuf_key:"bytes,1,opt,name=key,proto3" protobuf_val:"bytes,2,opt,name=value,proto3"` } @@ -154,11 +153,18 @@ func (x *EntitySpecV2) GetName() string { return "" } +func (x *EntitySpecV2) GetProject() string { + if x != nil { + return x.Project + } + return "" +} + func (x *EntitySpecV2) GetValueType() types.ValueType_Enum { if x != nil { return x.ValueType } - return types.ValueType_INVALID + return types.ValueType_Enum(0) } func (x *EntitySpecV2) GetDescription() string { @@ -168,6 +174,13 @@ func (x *EntitySpecV2) GetDescription() string { return "" } +func (x *EntitySpecV2) GetJoinKey() string { + if x != nil { + return x.JoinKey + } + return "" +} + func (x *EntitySpecV2) GetLabels() map[string]string { if x != nil { return x.Labels @@ -180,8 +193,8 @@ type EntityMeta struct { sizeCache protoimpl.SizeCache unknownFields protoimpl.UnknownFields - CreatedTimestamp *timestamp.Timestamp `protobuf:"bytes,1,opt,name=created_timestamp,json=createdTimestamp,proto3" json:"created_timestamp,omitempty"` - LastUpdatedTimestamp *timestamp.Timestamp `protobuf:"bytes,2,opt,name=last_updated_timestamp,json=lastUpdatedTimestamp,proto3" json:"last_updated_timestamp,omitempty"` + CreatedTimestamp *timestamppb.Timestamp `protobuf:"bytes,1,opt,name=created_timestamp,json=createdTimestamp,proto3" json:"created_timestamp,omitempty"` + LastUpdatedTimestamp *timestamppb.Timestamp `protobuf:"bytes,2,opt,name=last_updated_timestamp,json=lastUpdatedTimestamp,proto3" json:"last_updated_timestamp,omitempty"` } func (x *EntityMeta) Reset() { @@ -216,14 +229,14 @@ func (*EntityMeta) Descriptor() ([]byte, []int) { return file_feast_core_Entity_proto_rawDescGZIP(), []int{2} } -func (x *EntityMeta) GetCreatedTimestamp() *timestamp.Timestamp { +func (x *EntityMeta) GetCreatedTimestamp() *timestamppb.Timestamp { if x != nil { return x.CreatedTimestamp } return nil } -func (x *EntityMeta) GetLastUpdatedTimestamp() *timestamp.Timestamp { +func (x *EntityMeta) GetLastUpdatedTimestamp() *timestamppb.Timestamp { if x != nil { return x.LastUpdatedTimestamp } @@ -245,39 +258,42 @@ var file_feast_core_Entity_proto_rawDesc = []byte{ 0x32, 0x52, 0x04, 0x73, 0x70, 0x65, 0x63, 0x12, 0x2a, 0x0a, 0x04, 0x6d, 0x65, 0x74, 0x61, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x16, 0x2e, 0x66, 0x65, 0x61, 0x73, 0x74, 0x2e, 0x63, 0x6f, 0x72, 0x65, 0x2e, 0x45, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x4d, 0x65, 0x74, 0x61, 0x52, 0x04, 0x6d, - 0x65, 0x74, 0x61, 0x22, 0xf9, 0x01, 0x0a, 0x0c, 0x45, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x53, 0x70, + 0x65, 0x74, 0x61, 0x22, 0xae, 0x02, 0x0a, 0x0c, 0x45, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x53, 0x70, 0x65, 0x63, 0x56, 0x32, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, - 0x28, 0x09, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x3a, 0x0a, 0x0a, 0x76, 0x61, 0x6c, 0x75, - 0x65, 0x5f, 0x74, 0x79, 0x70, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x1b, 0x2e, 0x66, - 0x65, 0x61, 0x73, 0x74, 0x2e, 0x74, 0x79, 0x70, 0x65, 0x73, 0x2e, 0x56, 0x61, 0x6c, 0x75, 0x65, - 0x54, 0x79, 0x70, 0x65, 0x2e, 0x45, 0x6e, 0x75, 0x6d, 0x52, 0x09, 0x76, 0x61, 0x6c, 0x75, 0x65, - 0x54, 0x79, 0x70, 0x65, 0x12, 0x20, 0x0a, 0x0b, 0x64, 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, - 0x69, 0x6f, 0x6e, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0b, 0x64, 0x65, 0x73, 0x63, 0x72, - 0x69, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x3c, 0x0a, 0x06, 0x6c, 0x61, 0x62, 0x65, 0x6c, 0x73, - 0x18, 0x08, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x24, 0x2e, 0x66, 0x65, 0x61, 0x73, 0x74, 0x2e, 0x63, - 0x6f, 0x72, 0x65, 0x2e, 0x45, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x53, 0x70, 0x65, 0x63, 0x56, 0x32, - 0x2e, 0x4c, 0x61, 0x62, 0x65, 0x6c, 0x73, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x52, 0x06, 0x6c, 0x61, - 0x62, 0x65, 0x6c, 0x73, 0x1a, 0x39, 0x0a, 0x0b, 0x4c, 0x61, 0x62, 0x65, 0x6c, 0x73, 0x45, 0x6e, - 0x74, 0x72, 0x79, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, - 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x14, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, - 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x3a, 0x02, 0x38, 0x01, 0x22, - 0xa7, 0x01, 0x0a, 0x0a, 0x45, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x4d, 0x65, 0x74, 0x61, 0x12, 0x47, - 0x0a, 0x11, 0x63, 0x72, 0x65, 0x61, 0x74, 0x65, 0x64, 0x5f, 0x74, 0x69, 0x6d, 0x65, 0x73, 0x74, - 0x61, 0x6d, 0x70, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, - 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x54, 0x69, 0x6d, 0x65, - 0x73, 0x74, 0x61, 0x6d, 0x70, 0x52, 0x10, 0x63, 0x72, 0x65, 0x61, 0x74, 0x65, 0x64, 0x54, 0x69, - 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x12, 0x50, 0x0a, 0x16, 0x6c, 0x61, 0x73, 0x74, 0x5f, - 0x75, 0x70, 0x64, 0x61, 0x74, 0x65, 0x64, 0x5f, 0x74, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, - 0x70, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, - 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, - 0x61, 0x6d, 0x70, 0x52, 0x14, 0x6c, 0x61, 0x73, 0x74, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x64, - 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x42, 0x54, 0x0a, 0x10, 0x66, 0x65, 0x61, - 0x73, 0x74, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x63, 0x6f, 0x72, 0x65, 0x42, 0x0b, 0x45, - 0x6e, 0x74, 0x69, 0x74, 0x79, 0x50, 0x72, 0x6f, 0x74, 0x6f, 0x5a, 0x33, 0x67, 0x69, 0x74, 0x68, - 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x66, 0x65, 0x61, 0x73, 0x74, 0x2d, 0x64, 0x65, 0x76, - 0x2f, 0x66, 0x65, 0x61, 0x73, 0x74, 0x2f, 0x73, 0x64, 0x6b, 0x2f, 0x67, 0x6f, 0x2f, 0x70, 0x72, - 0x6f, 0x74, 0x6f, 0x73, 0x2f, 0x66, 0x65, 0x61, 0x73, 0x74, 0x2f, 0x63, 0x6f, 0x72, 0x65, 0x62, - 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, + 0x28, 0x09, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x18, 0x0a, 0x07, 0x70, 0x72, 0x6f, 0x6a, + 0x65, 0x63, 0x74, 0x18, 0x09, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x70, 0x72, 0x6f, 0x6a, 0x65, + 0x63, 0x74, 0x12, 0x3a, 0x0a, 0x0a, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x5f, 0x74, 0x79, 0x70, 0x65, + 0x18, 0x02, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x1b, 0x2e, 0x66, 0x65, 0x61, 0x73, 0x74, 0x2e, 0x74, + 0x79, 0x70, 0x65, 0x73, 0x2e, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x54, 0x79, 0x70, 0x65, 0x2e, 0x45, + 0x6e, 0x75, 0x6d, 0x52, 0x09, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x54, 0x79, 0x70, 0x65, 0x12, 0x20, + 0x0a, 0x0b, 0x64, 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x03, 0x20, + 0x01, 0x28, 0x09, 0x52, 0x0b, 0x64, 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, 0x6f, 0x6e, + 0x12, 0x19, 0x0a, 0x08, 0x6a, 0x6f, 0x69, 0x6e, 0x5f, 0x6b, 0x65, 0x79, 0x18, 0x04, 0x20, 0x01, + 0x28, 0x09, 0x52, 0x07, 0x6a, 0x6f, 0x69, 0x6e, 0x4b, 0x65, 0x79, 0x12, 0x3c, 0x0a, 0x06, 0x6c, + 0x61, 0x62, 0x65, 0x6c, 0x73, 0x18, 0x08, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x24, 0x2e, 0x66, 0x65, + 0x61, 0x73, 0x74, 0x2e, 0x63, 0x6f, 0x72, 0x65, 0x2e, 0x45, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x53, + 0x70, 0x65, 0x63, 0x56, 0x32, 0x2e, 0x4c, 0x61, 0x62, 0x65, 0x6c, 0x73, 0x45, 0x6e, 0x74, 0x72, + 0x79, 0x52, 0x06, 0x6c, 0x61, 0x62, 0x65, 0x6c, 0x73, 0x1a, 0x39, 0x0a, 0x0b, 0x4c, 0x61, 0x62, + 0x65, 0x6c, 0x73, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, + 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x14, 0x0a, 0x05, 0x76, 0x61, + 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, + 0x3a, 0x02, 0x38, 0x01, 0x22, 0xa7, 0x01, 0x0a, 0x0a, 0x45, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x4d, + 0x65, 0x74, 0x61, 0x12, 0x47, 0x0a, 0x11, 0x63, 0x72, 0x65, 0x61, 0x74, 0x65, 0x64, 0x5f, 0x74, + 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, + 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, + 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x52, 0x10, 0x63, 0x72, 0x65, 0x61, + 0x74, 0x65, 0x64, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x12, 0x50, 0x0a, 0x16, + 0x6c, 0x61, 0x73, 0x74, 0x5f, 0x75, 0x70, 0x64, 0x61, 0x74, 0x65, 0x64, 0x5f, 0x74, 0x69, 0x6d, + 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, + 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x54, + 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x52, 0x14, 0x6c, 0x61, 0x73, 0x74, 0x55, 0x70, + 0x64, 0x61, 0x74, 0x65, 0x64, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x42, 0x54, + 0x0a, 0x10, 0x66, 0x65, 0x61, 0x73, 0x74, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x63, 0x6f, + 0x72, 0x65, 0x42, 0x0b, 0x45, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x50, 0x72, 0x6f, 0x74, 0x6f, 0x5a, + 0x33, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x66, 0x65, 0x61, 0x73, + 0x74, 0x2d, 0x64, 0x65, 0x76, 0x2f, 0x66, 0x65, 0x61, 0x73, 0x74, 0x2f, 0x73, 0x64, 0x6b, 0x2f, + 0x67, 0x6f, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x73, 0x2f, 0x66, 0x65, 0x61, 0x73, 0x74, 0x2f, + 0x63, 0x6f, 0x72, 0x65, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, } var ( @@ -294,12 +310,12 @@ func file_feast_core_Entity_proto_rawDescGZIP() []byte { var file_feast_core_Entity_proto_msgTypes = make([]protoimpl.MessageInfo, 4) var file_feast_core_Entity_proto_goTypes = []interface{}{ - (*Entity)(nil), // 0: feast.core.Entity - (*EntitySpecV2)(nil), // 1: feast.core.EntitySpecV2 - (*EntityMeta)(nil), // 2: feast.core.EntityMeta - nil, // 3: feast.core.EntitySpecV2.LabelsEntry - (types.ValueType_Enum)(0), // 4: feast.types.ValueType.Enum - (*timestamp.Timestamp)(nil), // 5: google.protobuf.Timestamp + (*Entity)(nil), // 0: feast.core.Entity + (*EntitySpecV2)(nil), // 1: feast.core.EntitySpecV2 + (*EntityMeta)(nil), // 2: feast.core.EntityMeta + nil, // 3: feast.core.EntitySpecV2.LabelsEntry + (types.ValueType_Enum)(0), // 4: feast.types.ValueType.Enum + (*timestamppb.Timestamp)(nil), // 5: google.protobuf.Timestamp } var file_feast_core_Entity_proto_depIdxs = []int32{ 1, // 0: feast.core.Entity.spec:type_name -> feast.core.EntitySpecV2 diff --git a/sdk/go/protos/feast/core/Feature.pb.go b/sdk/go/protos/feast/core/Feature.pb.go index 1ad93ef8a1c..5d332dddff7 100644 --- a/sdk/go/protos/feast/core/Feature.pb.go +++ b/sdk/go/protos/feast/core/Feature.pb.go @@ -16,15 +16,14 @@ // Code generated by protoc-gen-go. DO NOT EDIT. // versions: -// protoc-gen-go v1.25.0 -// protoc v3.12.4 +// protoc-gen-go v1.27.1 +// protoc v3.11.2 // source: feast/core/Feature.proto package core import ( types "github.com/feast-dev/feast/sdk/go/protos/feast/types" - proto "github.com/golang/protobuf/proto" protoreflect "google.golang.org/protobuf/reflect/protoreflect" protoimpl "google.golang.org/protobuf/runtime/protoimpl" reflect "reflect" @@ -38,10 +37,6 @@ const ( _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) ) -// This is a compile-time assertion that a sufficiently up-to-date version -// of the legacy proto package is being used. -const _ = proto.ProtoPackageIsVersion4 - type FeatureSpecV2 struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache @@ -98,7 +93,7 @@ func (x *FeatureSpecV2) GetValueType() types.ValueType_Enum { if x != nil { return x.ValueType } - return types.ValueType_INVALID + return types.ValueType_Enum(0) } func (x *FeatureSpecV2) GetLabels() map[string]string { diff --git a/sdk/go/protos/feast/core/FeatureTable.pb.go b/sdk/go/protos/feast/core/FeatureTable.pb.go index 0c4fa1b1f0c..355ef50fb86 100644 --- a/sdk/go/protos/feast/core/FeatureTable.pb.go +++ b/sdk/go/protos/feast/core/FeatureTable.pb.go @@ -16,18 +16,17 @@ // Code generated by protoc-gen-go. DO NOT EDIT. // versions: -// protoc-gen-go v1.25.0 -// protoc v3.12.4 +// protoc-gen-go v1.27.1 +// protoc v3.11.2 // source: feast/core/FeatureTable.proto package core import ( - proto "github.com/golang/protobuf/proto" - duration "github.com/golang/protobuf/ptypes/duration" - timestamp "github.com/golang/protobuf/ptypes/timestamp" protoreflect "google.golang.org/protobuf/reflect/protoreflect" protoimpl "google.golang.org/protobuf/runtime/protoimpl" + durationpb "google.golang.org/protobuf/types/known/durationpb" + timestamppb "google.golang.org/protobuf/types/known/timestamppb" reflect "reflect" sync "sync" ) @@ -39,10 +38,6 @@ const ( _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) ) -// This is a compile-time assertion that a sufficiently up-to-date version -// of the legacy proto package is being used. -const _ = proto.ProtoPackageIsVersion4 - type FeatureTable struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache @@ -107,6 +102,8 @@ type FeatureTableSpec struct { // Name of the feature table. Must be unique. Not updated. Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"` + // Name of Feast project that this feature table belongs to. + Project string `protobuf:"bytes,9,opt,name=project,proto3" json:"project,omitempty"` // List names of entities to associate with the Features defined in this // Feature Table. Not updatable. Entities []string `protobuf:"bytes,3,rep,name=entities,proto3" json:"entities,omitempty"` @@ -118,7 +115,7 @@ type FeatureTableSpec struct { // younger than max age. Age is measured as the duration of time between // the feature's event timestamp and when the feature is retrieved // Feature values outside max age will be returned as unset values and indicated to end user - MaxAge *duration.Duration `protobuf:"bytes,6,opt,name=max_age,json=maxAge,proto3" json:"max_age,omitempty"` + MaxAge *durationpb.Duration `protobuf:"bytes,6,opt,name=max_age,json=maxAge,proto3" json:"max_age,omitempty"` // Batch/Offline DataSource to source batch/offline feature data. // Only batch DataSource can be specified // (ie source type should start with 'BATCH_') @@ -168,6 +165,13 @@ func (x *FeatureTableSpec) GetName() string { return "" } +func (x *FeatureTableSpec) GetProject() string { + if x != nil { + return x.Project + } + return "" +} + func (x *FeatureTableSpec) GetEntities() []string { if x != nil { return x.Entities @@ -189,7 +193,7 @@ func (x *FeatureTableSpec) GetLabels() map[string]string { return nil } -func (x *FeatureTableSpec) GetMaxAge() *duration.Duration { +func (x *FeatureTableSpec) GetMaxAge() *durationpb.Duration { if x != nil { return x.MaxAge } @@ -216,9 +220,9 @@ type FeatureTableMeta struct { unknownFields protoimpl.UnknownFields // Time where this Feature Table is created - CreatedTimestamp *timestamp.Timestamp `protobuf:"bytes,1,opt,name=created_timestamp,json=createdTimestamp,proto3" json:"created_timestamp,omitempty"` + CreatedTimestamp *timestamppb.Timestamp `protobuf:"bytes,1,opt,name=created_timestamp,json=createdTimestamp,proto3" json:"created_timestamp,omitempty"` // Time where this Feature Table is last updated - LastUpdatedTimestamp *timestamp.Timestamp `protobuf:"bytes,2,opt,name=last_updated_timestamp,json=lastUpdatedTimestamp,proto3" json:"last_updated_timestamp,omitempty"` + LastUpdatedTimestamp *timestamppb.Timestamp `protobuf:"bytes,2,opt,name=last_updated_timestamp,json=lastUpdatedTimestamp,proto3" json:"last_updated_timestamp,omitempty"` // Auto incrementing revision no. of this Feature Table Revision int64 `protobuf:"varint,3,opt,name=revision,proto3" json:"revision,omitempty"` // Hash entities, features, batch_source and stream_source to inform JobService if @@ -258,14 +262,14 @@ func (*FeatureTableMeta) Descriptor() ([]byte, []int) { return file_feast_core_FeatureTable_proto_rawDescGZIP(), []int{2} } -func (x *FeatureTableMeta) GetCreatedTimestamp() *timestamp.Timestamp { +func (x *FeatureTableMeta) GetCreatedTimestamp() *timestamppb.Timestamp { if x != nil { return x.CreatedTimestamp } return nil } -func (x *FeatureTableMeta) GetLastUpdatedTimestamp() *timestamp.Timestamp { +func (x *FeatureTableMeta) GetLastUpdatedTimestamp() *timestamppb.Timestamp { if x != nil { return x.LastUpdatedTimestamp } @@ -306,53 +310,55 @@ var file_feast_core_FeatureTable_proto_rawDesc = []byte{ 0x04, 0x73, 0x70, 0x65, 0x63, 0x12, 0x30, 0x0a, 0x04, 0x6d, 0x65, 0x74, 0x61, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1c, 0x2e, 0x66, 0x65, 0x61, 0x73, 0x74, 0x2e, 0x63, 0x6f, 0x72, 0x65, 0x2e, 0x46, 0x65, 0x61, 0x74, 0x75, 0x72, 0x65, 0x54, 0x61, 0x62, 0x6c, 0x65, 0x4d, 0x65, 0x74, - 0x61, 0x52, 0x04, 0x6d, 0x65, 0x74, 0x61, 0x22, 0xa2, 0x03, 0x0a, 0x10, 0x46, 0x65, 0x61, 0x74, + 0x61, 0x52, 0x04, 0x6d, 0x65, 0x74, 0x61, 0x22, 0xbc, 0x03, 0x0a, 0x10, 0x46, 0x65, 0x61, 0x74, 0x75, 0x72, 0x65, 0x54, 0x61, 0x62, 0x6c, 0x65, 0x53, 0x70, 0x65, 0x63, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, - 0x12, 0x1a, 0x0a, 0x08, 0x65, 0x6e, 0x74, 0x69, 0x74, 0x69, 0x65, 0x73, 0x18, 0x03, 0x20, 0x03, - 0x28, 0x09, 0x52, 0x08, 0x65, 0x6e, 0x74, 0x69, 0x74, 0x69, 0x65, 0x73, 0x12, 0x35, 0x0a, 0x08, - 0x66, 0x65, 0x61, 0x74, 0x75, 0x72, 0x65, 0x73, 0x18, 0x04, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x19, - 0x2e, 0x66, 0x65, 0x61, 0x73, 0x74, 0x2e, 0x63, 0x6f, 0x72, 0x65, 0x2e, 0x46, 0x65, 0x61, 0x74, - 0x75, 0x72, 0x65, 0x53, 0x70, 0x65, 0x63, 0x56, 0x32, 0x52, 0x08, 0x66, 0x65, 0x61, 0x74, 0x75, - 0x72, 0x65, 0x73, 0x12, 0x40, 0x0a, 0x06, 0x6c, 0x61, 0x62, 0x65, 0x6c, 0x73, 0x18, 0x05, 0x20, - 0x03, 0x28, 0x0b, 0x32, 0x28, 0x2e, 0x66, 0x65, 0x61, 0x73, 0x74, 0x2e, 0x63, 0x6f, 0x72, 0x65, - 0x2e, 0x46, 0x65, 0x61, 0x74, 0x75, 0x72, 0x65, 0x54, 0x61, 0x62, 0x6c, 0x65, 0x53, 0x70, 0x65, - 0x63, 0x2e, 0x4c, 0x61, 0x62, 0x65, 0x6c, 0x73, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x52, 0x06, 0x6c, - 0x61, 0x62, 0x65, 0x6c, 0x73, 0x12, 0x32, 0x0a, 0x07, 0x6d, 0x61, 0x78, 0x5f, 0x61, 0x67, 0x65, - 0x18, 0x06, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x19, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, - 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x44, 0x75, 0x72, 0x61, 0x74, 0x69, 0x6f, - 0x6e, 0x52, 0x06, 0x6d, 0x61, 0x78, 0x41, 0x67, 0x65, 0x12, 0x39, 0x0a, 0x0c, 0x62, 0x61, 0x74, - 0x63, 0x68, 0x5f, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x18, 0x07, 0x20, 0x01, 0x28, 0x0b, 0x32, - 0x16, 0x2e, 0x66, 0x65, 0x61, 0x73, 0x74, 0x2e, 0x63, 0x6f, 0x72, 0x65, 0x2e, 0x44, 0x61, 0x74, - 0x61, 0x53, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x52, 0x0b, 0x62, 0x61, 0x74, 0x63, 0x68, 0x53, 0x6f, - 0x75, 0x72, 0x63, 0x65, 0x12, 0x3b, 0x0a, 0x0d, 0x73, 0x74, 0x72, 0x65, 0x61, 0x6d, 0x5f, 0x73, - 0x6f, 0x75, 0x72, 0x63, 0x65, 0x18, 0x08, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x16, 0x2e, 0x66, 0x65, - 0x61, 0x73, 0x74, 0x2e, 0x63, 0x6f, 0x72, 0x65, 0x2e, 0x44, 0x61, 0x74, 0x61, 0x53, 0x6f, 0x75, - 0x72, 0x63, 0x65, 0x52, 0x0c, 0x73, 0x74, 0x72, 0x65, 0x61, 0x6d, 0x53, 0x6f, 0x75, 0x72, 0x63, - 0x65, 0x1a, 0x39, 0x0a, 0x0b, 0x4c, 0x61, 0x62, 0x65, 0x6c, 0x73, 0x45, 0x6e, 0x74, 0x72, 0x79, - 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x6b, - 0x65, 0x79, 0x12, 0x14, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, - 0x09, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x3a, 0x02, 0x38, 0x01, 0x22, 0xdd, 0x01, 0x0a, - 0x10, 0x46, 0x65, 0x61, 0x74, 0x75, 0x72, 0x65, 0x54, 0x61, 0x62, 0x6c, 0x65, 0x4d, 0x65, 0x74, - 0x61, 0x12, 0x47, 0x0a, 0x11, 0x63, 0x72, 0x65, 0x61, 0x74, 0x65, 0x64, 0x5f, 0x74, 0x69, 0x6d, - 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, - 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x54, - 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x52, 0x10, 0x63, 0x72, 0x65, 0x61, 0x74, 0x65, - 0x64, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x12, 0x50, 0x0a, 0x16, 0x6c, 0x61, - 0x73, 0x74, 0x5f, 0x75, 0x70, 0x64, 0x61, 0x74, 0x65, 0x64, 0x5f, 0x74, 0x69, 0x6d, 0x65, 0x73, - 0x74, 0x61, 0x6d, 0x70, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, - 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x54, 0x69, 0x6d, - 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x52, 0x14, 0x6c, 0x61, 0x73, 0x74, 0x55, 0x70, 0x64, 0x61, - 0x74, 0x65, 0x64, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x12, 0x1a, 0x0a, 0x08, - 0x72, 0x65, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x18, 0x03, 0x20, 0x01, 0x28, 0x03, 0x52, 0x08, - 0x72, 0x65, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x12, 0x12, 0x0a, 0x04, 0x68, 0x61, 0x73, 0x68, - 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x68, 0x61, 0x73, 0x68, 0x42, 0x5a, 0x0a, 0x10, - 0x66, 0x65, 0x61, 0x73, 0x74, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x63, 0x6f, 0x72, 0x65, - 0x42, 0x11, 0x46, 0x65, 0x61, 0x74, 0x75, 0x72, 0x65, 0x54, 0x61, 0x62, 0x6c, 0x65, 0x50, 0x72, - 0x6f, 0x74, 0x6f, 0x5a, 0x33, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, - 0x66, 0x65, 0x61, 0x73, 0x74, 0x2d, 0x64, 0x65, 0x76, 0x2f, 0x66, 0x65, 0x61, 0x73, 0x74, 0x2f, - 0x73, 0x64, 0x6b, 0x2f, 0x67, 0x6f, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x73, 0x2f, 0x66, 0x65, - 0x61, 0x73, 0x74, 0x2f, 0x63, 0x6f, 0x72, 0x65, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, + 0x12, 0x18, 0x0a, 0x07, 0x70, 0x72, 0x6f, 0x6a, 0x65, 0x63, 0x74, 0x18, 0x09, 0x20, 0x01, 0x28, + 0x09, 0x52, 0x07, 0x70, 0x72, 0x6f, 0x6a, 0x65, 0x63, 0x74, 0x12, 0x1a, 0x0a, 0x08, 0x65, 0x6e, + 0x74, 0x69, 0x74, 0x69, 0x65, 0x73, 0x18, 0x03, 0x20, 0x03, 0x28, 0x09, 0x52, 0x08, 0x65, 0x6e, + 0x74, 0x69, 0x74, 0x69, 0x65, 0x73, 0x12, 0x35, 0x0a, 0x08, 0x66, 0x65, 0x61, 0x74, 0x75, 0x72, + 0x65, 0x73, 0x18, 0x04, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x19, 0x2e, 0x66, 0x65, 0x61, 0x73, 0x74, + 0x2e, 0x63, 0x6f, 0x72, 0x65, 0x2e, 0x46, 0x65, 0x61, 0x74, 0x75, 0x72, 0x65, 0x53, 0x70, 0x65, + 0x63, 0x56, 0x32, 0x52, 0x08, 0x66, 0x65, 0x61, 0x74, 0x75, 0x72, 0x65, 0x73, 0x12, 0x40, 0x0a, + 0x06, 0x6c, 0x61, 0x62, 0x65, 0x6c, 0x73, 0x18, 0x05, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x28, 0x2e, + 0x66, 0x65, 0x61, 0x73, 0x74, 0x2e, 0x63, 0x6f, 0x72, 0x65, 0x2e, 0x46, 0x65, 0x61, 0x74, 0x75, + 0x72, 0x65, 0x54, 0x61, 0x62, 0x6c, 0x65, 0x53, 0x70, 0x65, 0x63, 0x2e, 0x4c, 0x61, 0x62, 0x65, + 0x6c, 0x73, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x52, 0x06, 0x6c, 0x61, 0x62, 0x65, 0x6c, 0x73, 0x12, + 0x32, 0x0a, 0x07, 0x6d, 0x61, 0x78, 0x5f, 0x61, 0x67, 0x65, 0x18, 0x06, 0x20, 0x01, 0x28, 0x0b, + 0x32, 0x19, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, + 0x75, 0x66, 0x2e, 0x44, 0x75, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x06, 0x6d, 0x61, 0x78, + 0x41, 0x67, 0x65, 0x12, 0x39, 0x0a, 0x0c, 0x62, 0x61, 0x74, 0x63, 0x68, 0x5f, 0x73, 0x6f, 0x75, + 0x72, 0x63, 0x65, 0x18, 0x07, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x16, 0x2e, 0x66, 0x65, 0x61, 0x73, + 0x74, 0x2e, 0x63, 0x6f, 0x72, 0x65, 0x2e, 0x44, 0x61, 0x74, 0x61, 0x53, 0x6f, 0x75, 0x72, 0x63, + 0x65, 0x52, 0x0b, 0x62, 0x61, 0x74, 0x63, 0x68, 0x53, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x12, 0x3b, + 0x0a, 0x0d, 0x73, 0x74, 0x72, 0x65, 0x61, 0x6d, 0x5f, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x18, + 0x08, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x16, 0x2e, 0x66, 0x65, 0x61, 0x73, 0x74, 0x2e, 0x63, 0x6f, + 0x72, 0x65, 0x2e, 0x44, 0x61, 0x74, 0x61, 0x53, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x52, 0x0c, 0x73, + 0x74, 0x72, 0x65, 0x61, 0x6d, 0x53, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x1a, 0x39, 0x0a, 0x0b, 0x4c, + 0x61, 0x62, 0x65, 0x6c, 0x73, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, + 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x14, 0x0a, 0x05, + 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x76, 0x61, 0x6c, + 0x75, 0x65, 0x3a, 0x02, 0x38, 0x01, 0x22, 0xdd, 0x01, 0x0a, 0x10, 0x46, 0x65, 0x61, 0x74, 0x75, + 0x72, 0x65, 0x54, 0x61, 0x62, 0x6c, 0x65, 0x4d, 0x65, 0x74, 0x61, 0x12, 0x47, 0x0a, 0x11, 0x63, + 0x72, 0x65, 0x61, 0x74, 0x65, 0x64, 0x5f, 0x74, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, + 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, + 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, + 0x6d, 0x70, 0x52, 0x10, 0x63, 0x72, 0x65, 0x61, 0x74, 0x65, 0x64, 0x54, 0x69, 0x6d, 0x65, 0x73, + 0x74, 0x61, 0x6d, 0x70, 0x12, 0x50, 0x0a, 0x16, 0x6c, 0x61, 0x73, 0x74, 0x5f, 0x75, 0x70, 0x64, + 0x61, 0x74, 0x65, 0x64, 0x5f, 0x74, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x18, 0x02, + 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, + 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, + 0x52, 0x14, 0x6c, 0x61, 0x73, 0x74, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x64, 0x54, 0x69, 0x6d, + 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x12, 0x1a, 0x0a, 0x08, 0x72, 0x65, 0x76, 0x69, 0x73, 0x69, + 0x6f, 0x6e, 0x18, 0x03, 0x20, 0x01, 0x28, 0x03, 0x52, 0x08, 0x72, 0x65, 0x76, 0x69, 0x73, 0x69, + 0x6f, 0x6e, 0x12, 0x12, 0x0a, 0x04, 0x68, 0x61, 0x73, 0x68, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, + 0x52, 0x04, 0x68, 0x61, 0x73, 0x68, 0x42, 0x5a, 0x0a, 0x10, 0x66, 0x65, 0x61, 0x73, 0x74, 0x2e, + 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x63, 0x6f, 0x72, 0x65, 0x42, 0x11, 0x46, 0x65, 0x61, 0x74, + 0x75, 0x72, 0x65, 0x54, 0x61, 0x62, 0x6c, 0x65, 0x50, 0x72, 0x6f, 0x74, 0x6f, 0x5a, 0x33, 0x67, + 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x66, 0x65, 0x61, 0x73, 0x74, 0x2d, + 0x64, 0x65, 0x76, 0x2f, 0x66, 0x65, 0x61, 0x73, 0x74, 0x2f, 0x73, 0x64, 0x6b, 0x2f, 0x67, 0x6f, + 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x73, 0x2f, 0x66, 0x65, 0x61, 0x73, 0x74, 0x2f, 0x63, 0x6f, + 0x72, 0x65, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, } var ( @@ -369,14 +375,14 @@ func file_feast_core_FeatureTable_proto_rawDescGZIP() []byte { var file_feast_core_FeatureTable_proto_msgTypes = make([]protoimpl.MessageInfo, 4) var file_feast_core_FeatureTable_proto_goTypes = []interface{}{ - (*FeatureTable)(nil), // 0: feast.core.FeatureTable - (*FeatureTableSpec)(nil), // 1: feast.core.FeatureTableSpec - (*FeatureTableMeta)(nil), // 2: feast.core.FeatureTableMeta - nil, // 3: feast.core.FeatureTableSpec.LabelsEntry - (*FeatureSpecV2)(nil), // 4: feast.core.FeatureSpecV2 - (*duration.Duration)(nil), // 5: google.protobuf.Duration - (*DataSource)(nil), // 6: feast.core.DataSource - (*timestamp.Timestamp)(nil), // 7: google.protobuf.Timestamp + (*FeatureTable)(nil), // 0: feast.core.FeatureTable + (*FeatureTableSpec)(nil), // 1: feast.core.FeatureTableSpec + (*FeatureTableMeta)(nil), // 2: feast.core.FeatureTableMeta + nil, // 3: feast.core.FeatureTableSpec.LabelsEntry + (*FeatureSpecV2)(nil), // 4: feast.core.FeatureSpecV2 + (*durationpb.Duration)(nil), // 5: google.protobuf.Duration + (*DataSource)(nil), // 6: feast.core.DataSource + (*timestamppb.Timestamp)(nil), // 7: google.protobuf.Timestamp } var file_feast_core_FeatureTable_proto_depIdxs = []int32{ 1, // 0: feast.core.FeatureTable.spec:type_name -> feast.core.FeatureTableSpec diff --git a/sdk/go/protos/feast/core/Store.pb.go b/sdk/go/protos/feast/core/Store.pb.go index 9498c6a40c7..6c46f10d24a 100644 --- a/sdk/go/protos/feast/core/Store.pb.go +++ b/sdk/go/protos/feast/core/Store.pb.go @@ -16,14 +16,13 @@ // Code generated by protoc-gen-go. DO NOT EDIT. // versions: -// protoc-gen-go v1.25.0 -// protoc v3.12.4 +// protoc-gen-go v1.27.1 +// protoc v3.11.2 // source: feast/core/Store.proto package core import ( - proto "github.com/golang/protobuf/proto" protoreflect "google.golang.org/protobuf/reflect/protoreflect" protoimpl "google.golang.org/protobuf/runtime/protoimpl" reflect "reflect" @@ -37,10 +36,6 @@ const ( _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) ) -// This is a compile-time assertion that a sufficiently up-to-date version -// of the legacy proto package is being used. -const _ = proto.ProtoPackageIsVersion4 - type Store_StoreType int32 const ( @@ -100,6 +95,59 @@ func (Store_StoreType) EnumDescriptor() ([]byte, []int) { return file_feast_core_Store_proto_rawDescGZIP(), []int{0, 0} } +// Optional. Priority of nodes when reading from cluster +type Store_RedisClusterConfig_ReadFrom int32 + +const ( + Store_RedisClusterConfig_MASTER Store_RedisClusterConfig_ReadFrom = 0 + Store_RedisClusterConfig_MASTER_PREFERRED Store_RedisClusterConfig_ReadFrom = 1 + Store_RedisClusterConfig_REPLICA Store_RedisClusterConfig_ReadFrom = 2 + Store_RedisClusterConfig_REPLICA_PREFERRED Store_RedisClusterConfig_ReadFrom = 3 +) + +// Enum value maps for Store_RedisClusterConfig_ReadFrom. +var ( + Store_RedisClusterConfig_ReadFrom_name = map[int32]string{ + 0: "MASTER", + 1: "MASTER_PREFERRED", + 2: "REPLICA", + 3: "REPLICA_PREFERRED", + } + Store_RedisClusterConfig_ReadFrom_value = map[string]int32{ + "MASTER": 0, + "MASTER_PREFERRED": 1, + "REPLICA": 2, + "REPLICA_PREFERRED": 3, + } +) + +func (x Store_RedisClusterConfig_ReadFrom) Enum() *Store_RedisClusterConfig_ReadFrom { + p := new(Store_RedisClusterConfig_ReadFrom) + *p = x + return p +} + +func (x Store_RedisClusterConfig_ReadFrom) String() string { + return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x)) +} + +func (Store_RedisClusterConfig_ReadFrom) Descriptor() protoreflect.EnumDescriptor { + return file_feast_core_Store_proto_enumTypes[1].Descriptor() +} + +func (Store_RedisClusterConfig_ReadFrom) Type() protoreflect.EnumType { + return &file_feast_core_Store_proto_enumTypes[1] +} + +func (x Store_RedisClusterConfig_ReadFrom) Number() protoreflect.EnumNumber { + return protoreflect.EnumNumber(x) +} + +// Deprecated: Use Store_RedisClusterConfig_ReadFrom.Descriptor instead. +func (Store_RedisClusterConfig_ReadFrom) EnumDescriptor() ([]byte, []int) { + return file_feast_core_Store_proto_rawDescGZIP(), []int{0, 1, 0} +} + // Store provides a location where Feast reads and writes feature values. // Feature values will be written to the Store in the form of FeatureRow elements. // The way FeatureRow is encoded and decoded when it is written to and read from @@ -323,7 +371,8 @@ type Store_RedisClusterConfig struct { // Useful for migrating key prefix without re-ingestion. Disabled by default. EnableFallback bool `protobuf:"varint,6,opt,name=enable_fallback,json=enableFallback,proto3" json:"enable_fallback,omitempty"` // Optional. This would be the fallback prefix to use if enable_fallback is true. - FallbackPrefix string `protobuf:"bytes,7,opt,name=fallback_prefix,json=fallbackPrefix,proto3" json:"fallback_prefix,omitempty"` + FallbackPrefix string `protobuf:"bytes,7,opt,name=fallback_prefix,json=fallbackPrefix,proto3" json:"fallback_prefix,omitempty"` + ReadFrom Store_RedisClusterConfig_ReadFrom `protobuf:"varint,8,opt,name=read_from,json=readFrom,proto3,enum=feast.core.Store_RedisClusterConfig_ReadFrom" json:"read_from,omitempty"` } func (x *Store_RedisClusterConfig) Reset() { @@ -407,6 +456,13 @@ func (x *Store_RedisClusterConfig) GetFallbackPrefix() string { return "" } +func (x *Store_RedisClusterConfig) GetReadFrom() Store_RedisClusterConfig_ReadFrom { + if x != nil { + return x.ReadFrom + } + return Store_RedisClusterConfig_MASTER +} + type Store_Subscription struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache @@ -489,7 +545,7 @@ var File_feast_core_Store_proto protoreflect.FileDescriptor var file_feast_core_Store_proto_rawDesc = []byte{ 0x0a, 0x16, 0x66, 0x65, 0x61, 0x73, 0x74, 0x2f, 0x63, 0x6f, 0x72, 0x65, 0x2f, 0x53, 0x74, 0x6f, 0x72, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x12, 0x0a, 0x66, 0x65, 0x61, 0x73, 0x74, 0x2e, - 0x63, 0x6f, 0x72, 0x65, 0x22, 0xf5, 0x07, 0x0a, 0x05, 0x53, 0x74, 0x6f, 0x72, 0x65, 0x12, 0x12, + 0x63, 0x6f, 0x72, 0x65, 0x22, 0x93, 0x09, 0x0a, 0x05, 0x53, 0x74, 0x6f, 0x72, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x2f, 0x0a, 0x04, 0x74, 0x79, 0x70, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x1b, 0x2e, 0x66, 0x65, 0x61, 0x73, 0x74, 0x2e, 0x63, 0x6f, 0x72, 0x65, 0x2e, 0x53, 0x74, @@ -521,7 +577,7 @@ var file_feast_core_Store_proto_rawDesc = []byte{ 0x79, 0x5f, 0x73, 0x65, 0x63, 0x6f, 0x6e, 0x64, 0x73, 0x18, 0x05, 0x20, 0x01, 0x28, 0x05, 0x52, 0x15, 0x66, 0x6c, 0x75, 0x73, 0x68, 0x46, 0x72, 0x65, 0x71, 0x75, 0x65, 0x6e, 0x63, 0x79, 0x53, 0x65, 0x63, 0x6f, 0x6e, 0x64, 0x73, 0x12, 0x10, 0x0a, 0x03, 0x73, 0x73, 0x6c, 0x18, 0x06, 0x20, - 0x01, 0x28, 0x08, 0x52, 0x03, 0x73, 0x73, 0x6c, 0x1a, 0xb9, 0x02, 0x0a, 0x12, 0x52, 0x65, 0x64, + 0x01, 0x28, 0x08, 0x52, 0x03, 0x73, 0x73, 0x6c, 0x1a, 0xd7, 0x03, 0x0a, 0x12, 0x52, 0x65, 0x64, 0x69, 0x73, 0x43, 0x6c, 0x75, 0x73, 0x74, 0x65, 0x72, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x2b, 0x0a, 0x11, 0x63, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x5f, 0x73, 0x74, 0x72, 0x69, 0x6e, 0x67, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x10, 0x63, 0x6f, 0x6e, 0x6e, @@ -541,24 +597,34 @@ var file_feast_core_Store_proto_rawDesc = []byte{ 0x62, 0x6c, 0x65, 0x46, 0x61, 0x6c, 0x6c, 0x62, 0x61, 0x63, 0x6b, 0x12, 0x27, 0x0a, 0x0f, 0x66, 0x61, 0x6c, 0x6c, 0x62, 0x61, 0x63, 0x6b, 0x5f, 0x70, 0x72, 0x65, 0x66, 0x69, 0x78, 0x18, 0x07, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0e, 0x66, 0x61, 0x6c, 0x6c, 0x62, 0x61, 0x63, 0x6b, 0x50, 0x72, - 0x65, 0x66, 0x69, 0x78, 0x1a, 0x5c, 0x0a, 0x0c, 0x53, 0x75, 0x62, 0x73, 0x63, 0x72, 0x69, 0x70, - 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x18, 0x0a, 0x07, 0x70, 0x72, 0x6f, 0x6a, 0x65, 0x63, 0x74, 0x18, - 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x70, 0x72, 0x6f, 0x6a, 0x65, 0x63, 0x74, 0x12, 0x12, - 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6e, 0x61, - 0x6d, 0x65, 0x12, 0x18, 0x0a, 0x07, 0x65, 0x78, 0x63, 0x6c, 0x75, 0x64, 0x65, 0x18, 0x04, 0x20, - 0x01, 0x28, 0x08, 0x52, 0x07, 0x65, 0x78, 0x63, 0x6c, 0x75, 0x64, 0x65, 0x4a, 0x04, 0x08, 0x02, - 0x10, 0x03, 0x22, 0x4e, 0x0a, 0x09, 0x53, 0x74, 0x6f, 0x72, 0x65, 0x54, 0x79, 0x70, 0x65, 0x12, - 0x0b, 0x0a, 0x07, 0x49, 0x4e, 0x56, 0x41, 0x4c, 0x49, 0x44, 0x10, 0x00, 0x12, 0x09, 0x0a, 0x05, - 0x52, 0x45, 0x44, 0x49, 0x53, 0x10, 0x01, 0x12, 0x11, 0x0a, 0x0d, 0x52, 0x45, 0x44, 0x49, 0x53, - 0x5f, 0x43, 0x4c, 0x55, 0x53, 0x54, 0x45, 0x52, 0x10, 0x04, 0x22, 0x04, 0x08, 0x02, 0x10, 0x02, - 0x22, 0x04, 0x08, 0x03, 0x10, 0x03, 0x22, 0x04, 0x08, 0x0c, 0x10, 0x0c, 0x22, 0x04, 0x08, 0x0d, - 0x10, 0x0d, 0x42, 0x08, 0x0a, 0x06, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x42, 0x53, 0x0a, 0x10, - 0x66, 0x65, 0x61, 0x73, 0x74, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x63, 0x6f, 0x72, 0x65, - 0x42, 0x0a, 0x53, 0x74, 0x6f, 0x72, 0x65, 0x50, 0x72, 0x6f, 0x74, 0x6f, 0x5a, 0x33, 0x67, 0x69, - 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x66, 0x65, 0x61, 0x73, 0x74, 0x2d, 0x64, - 0x65, 0x76, 0x2f, 0x66, 0x65, 0x61, 0x73, 0x74, 0x2f, 0x73, 0x64, 0x6b, 0x2f, 0x67, 0x6f, 0x2f, - 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x73, 0x2f, 0x66, 0x65, 0x61, 0x73, 0x74, 0x2f, 0x63, 0x6f, 0x72, - 0x65, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, + 0x65, 0x66, 0x69, 0x78, 0x12, 0x4a, 0x0a, 0x09, 0x72, 0x65, 0x61, 0x64, 0x5f, 0x66, 0x72, 0x6f, + 0x6d, 0x18, 0x08, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x2d, 0x2e, 0x66, 0x65, 0x61, 0x73, 0x74, 0x2e, + 0x63, 0x6f, 0x72, 0x65, 0x2e, 0x53, 0x74, 0x6f, 0x72, 0x65, 0x2e, 0x52, 0x65, 0x64, 0x69, 0x73, + 0x43, 0x6c, 0x75, 0x73, 0x74, 0x65, 0x72, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x2e, 0x52, 0x65, + 0x61, 0x64, 0x46, 0x72, 0x6f, 0x6d, 0x52, 0x08, 0x72, 0x65, 0x61, 0x64, 0x46, 0x72, 0x6f, 0x6d, + 0x22, 0x50, 0x0a, 0x08, 0x52, 0x65, 0x61, 0x64, 0x46, 0x72, 0x6f, 0x6d, 0x12, 0x0a, 0x0a, 0x06, + 0x4d, 0x41, 0x53, 0x54, 0x45, 0x52, 0x10, 0x00, 0x12, 0x14, 0x0a, 0x10, 0x4d, 0x41, 0x53, 0x54, + 0x45, 0x52, 0x5f, 0x50, 0x52, 0x45, 0x46, 0x45, 0x52, 0x52, 0x45, 0x44, 0x10, 0x01, 0x12, 0x0b, + 0x0a, 0x07, 0x52, 0x45, 0x50, 0x4c, 0x49, 0x43, 0x41, 0x10, 0x02, 0x12, 0x15, 0x0a, 0x11, 0x52, + 0x45, 0x50, 0x4c, 0x49, 0x43, 0x41, 0x5f, 0x50, 0x52, 0x45, 0x46, 0x45, 0x52, 0x52, 0x45, 0x44, + 0x10, 0x03, 0x1a, 0x5c, 0x0a, 0x0c, 0x53, 0x75, 0x62, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, + 0x6f, 0x6e, 0x12, 0x18, 0x0a, 0x07, 0x70, 0x72, 0x6f, 0x6a, 0x65, 0x63, 0x74, 0x18, 0x03, 0x20, + 0x01, 0x28, 0x09, 0x52, 0x07, 0x70, 0x72, 0x6f, 0x6a, 0x65, 0x63, 0x74, 0x12, 0x12, 0x0a, 0x04, + 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, + 0x12, 0x18, 0x0a, 0x07, 0x65, 0x78, 0x63, 0x6c, 0x75, 0x64, 0x65, 0x18, 0x04, 0x20, 0x01, 0x28, + 0x08, 0x52, 0x07, 0x65, 0x78, 0x63, 0x6c, 0x75, 0x64, 0x65, 0x4a, 0x04, 0x08, 0x02, 0x10, 0x03, + 0x22, 0x4e, 0x0a, 0x09, 0x53, 0x74, 0x6f, 0x72, 0x65, 0x54, 0x79, 0x70, 0x65, 0x12, 0x0b, 0x0a, + 0x07, 0x49, 0x4e, 0x56, 0x41, 0x4c, 0x49, 0x44, 0x10, 0x00, 0x12, 0x09, 0x0a, 0x05, 0x52, 0x45, + 0x44, 0x49, 0x53, 0x10, 0x01, 0x12, 0x11, 0x0a, 0x0d, 0x52, 0x45, 0x44, 0x49, 0x53, 0x5f, 0x43, + 0x4c, 0x55, 0x53, 0x54, 0x45, 0x52, 0x10, 0x04, 0x22, 0x04, 0x08, 0x02, 0x10, 0x02, 0x22, 0x04, + 0x08, 0x03, 0x10, 0x03, 0x22, 0x04, 0x08, 0x0c, 0x10, 0x0c, 0x22, 0x04, 0x08, 0x0d, 0x10, 0x0d, + 0x42, 0x08, 0x0a, 0x06, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x42, 0x53, 0x0a, 0x10, 0x66, 0x65, + 0x61, 0x73, 0x74, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x63, 0x6f, 0x72, 0x65, 0x42, 0x0a, + 0x53, 0x74, 0x6f, 0x72, 0x65, 0x50, 0x72, 0x6f, 0x74, 0x6f, 0x5a, 0x33, 0x67, 0x69, 0x74, 0x68, + 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x66, 0x65, 0x61, 0x73, 0x74, 0x2d, 0x64, 0x65, 0x76, + 0x2f, 0x66, 0x65, 0x61, 0x73, 0x74, 0x2f, 0x73, 0x64, 0x6b, 0x2f, 0x67, 0x6f, 0x2f, 0x70, 0x72, + 0x6f, 0x74, 0x6f, 0x73, 0x2f, 0x66, 0x65, 0x61, 0x73, 0x74, 0x2f, 0x63, 0x6f, 0x72, 0x65, 0x62, + 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, } var ( @@ -573,25 +639,27 @@ func file_feast_core_Store_proto_rawDescGZIP() []byte { return file_feast_core_Store_proto_rawDescData } -var file_feast_core_Store_proto_enumTypes = make([]protoimpl.EnumInfo, 1) +var file_feast_core_Store_proto_enumTypes = make([]protoimpl.EnumInfo, 2) var file_feast_core_Store_proto_msgTypes = make([]protoimpl.MessageInfo, 4) var file_feast_core_Store_proto_goTypes = []interface{}{ - (Store_StoreType)(0), // 0: feast.core.Store.StoreType - (*Store)(nil), // 1: feast.core.Store - (*Store_RedisConfig)(nil), // 2: feast.core.Store.RedisConfig - (*Store_RedisClusterConfig)(nil), // 3: feast.core.Store.RedisClusterConfig - (*Store_Subscription)(nil), // 4: feast.core.Store.Subscription + (Store_StoreType)(0), // 0: feast.core.Store.StoreType + (Store_RedisClusterConfig_ReadFrom)(0), // 1: feast.core.Store.RedisClusterConfig.ReadFrom + (*Store)(nil), // 2: feast.core.Store + (*Store_RedisConfig)(nil), // 3: feast.core.Store.RedisConfig + (*Store_RedisClusterConfig)(nil), // 4: feast.core.Store.RedisClusterConfig + (*Store_Subscription)(nil), // 5: feast.core.Store.Subscription } var file_feast_core_Store_proto_depIdxs = []int32{ 0, // 0: feast.core.Store.type:type_name -> feast.core.Store.StoreType - 4, // 1: feast.core.Store.subscriptions:type_name -> feast.core.Store.Subscription - 2, // 2: feast.core.Store.redis_config:type_name -> feast.core.Store.RedisConfig - 3, // 3: feast.core.Store.redis_cluster_config:type_name -> feast.core.Store.RedisClusterConfig - 4, // [4:4] is the sub-list for method output_type - 4, // [4:4] is the sub-list for method input_type - 4, // [4:4] is the sub-list for extension type_name - 4, // [4:4] is the sub-list for extension extendee - 0, // [0:4] is the sub-list for field type_name + 5, // 1: feast.core.Store.subscriptions:type_name -> feast.core.Store.Subscription + 3, // 2: feast.core.Store.redis_config:type_name -> feast.core.Store.RedisConfig + 4, // 3: feast.core.Store.redis_cluster_config:type_name -> feast.core.Store.RedisClusterConfig + 1, // 4: feast.core.Store.RedisClusterConfig.read_from:type_name -> feast.core.Store.RedisClusterConfig.ReadFrom + 5, // [5:5] is the sub-list for method output_type + 5, // [5:5] is the sub-list for method input_type + 5, // [5:5] is the sub-list for extension type_name + 5, // [5:5] is the sub-list for extension extendee + 0, // [0:5] is the sub-list for field type_name } func init() { file_feast_core_Store_proto_init() } @@ -658,7 +726,7 @@ func file_feast_core_Store_proto_init() { File: protoimpl.DescBuilder{ GoPackagePath: reflect.TypeOf(x{}).PkgPath(), RawDescriptor: file_feast_core_Store_proto_rawDesc, - NumEnums: 1, + NumEnums: 2, NumMessages: 4, NumExtensions: 0, NumServices: 0, diff --git a/sdk/go/protos/feast/serving/ServingService.pb.go b/sdk/go/protos/feast/serving/ServingService.pb.go index 7d40fa45d59..32e3461dfdf 100644 --- a/sdk/go/protos/feast/serving/ServingService.pb.go +++ b/sdk/go/protos/feast/serving/ServingService.pb.go @@ -15,8 +15,8 @@ // Code generated by protoc-gen-go. DO NOT EDIT. // versions: -// protoc-gen-go v1.25.0 -// protoc v3.12.4 +// protoc-gen-go v1.27.1 +// protoc v3.11.2 // source: feast/serving/ServingService.proto package serving @@ -25,13 +25,12 @@ import ( context "context" types "github.com/feast-dev/feast/sdk/go/protos/feast/types" _ "github.com/feast-dev/feast/sdk/go/protos/tensorflow_metadata/proto/v0" - proto "github.com/golang/protobuf/proto" - timestamp "github.com/golang/protobuf/ptypes/timestamp" grpc "google.golang.org/grpc" codes "google.golang.org/grpc/codes" status "google.golang.org/grpc/status" protoreflect "google.golang.org/protobuf/reflect/protoreflect" protoimpl "google.golang.org/protobuf/runtime/protoimpl" + timestamppb "google.golang.org/protobuf/types/known/timestamppb" reflect "reflect" sync "sync" ) @@ -43,10 +42,6 @@ const ( _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) ) -// This is a compile-time assertion that a sufficiently up-to-date version -// of the legacy proto package is being used. -const _ = proto.ProtoPackageIsVersion4 - type FeastServingType int32 const ( @@ -161,7 +156,7 @@ func (x GetOnlineFeaturesResponse_FieldStatus) Number() protoreflect.EnumNumber // Deprecated: Use GetOnlineFeaturesResponse_FieldStatus.Descriptor instead. func (GetOnlineFeaturesResponse_FieldStatus) EnumDescriptor() ([]byte, []int) { - return file_feast_serving_ServingService_proto_rawDescGZIP(), []int{4, 0} + return file_feast_serving_ServingService_proto_rawDescGZIP(), []int{6, 0} } type GetFeastServingInfoRequest struct { @@ -397,6 +392,152 @@ func (x *GetOnlineFeaturesRequestV2) GetProject() string { return "" } +// In JSON "val" field can be omitted +type FeatureList struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Val []string `protobuf:"bytes,1,rep,name=val,proto3" json:"val,omitempty"` +} + +func (x *FeatureList) Reset() { + *x = FeatureList{} + if protoimpl.UnsafeEnabled { + mi := &file_feast_serving_ServingService_proto_msgTypes[4] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *FeatureList) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*FeatureList) ProtoMessage() {} + +func (x *FeatureList) ProtoReflect() protoreflect.Message { + mi := &file_feast_serving_ServingService_proto_msgTypes[4] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use FeatureList.ProtoReflect.Descriptor instead. +func (*FeatureList) Descriptor() ([]byte, []int) { + return file_feast_serving_ServingService_proto_rawDescGZIP(), []int{4} +} + +func (x *FeatureList) GetVal() []string { + if x != nil { + return x.Val + } + return nil +} + +type GetOnlineFeaturesRequest struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + // Types that are assignable to Kind: + // *GetOnlineFeaturesRequest_FeatureService + // *GetOnlineFeaturesRequest_Features + Kind isGetOnlineFeaturesRequest_Kind `protobuf_oneof:"kind"` + // The entity data is specified in a columnar format + // A map of entity name -> list of values + Entities map[string]*types.RepeatedValue `protobuf:"bytes,3,rep,name=entities,proto3" json:"entities,omitempty" protobuf_key:"bytes,1,opt,name=key,proto3" protobuf_val:"bytes,2,opt,name=value,proto3"` + FullFeatureNames bool `protobuf:"varint,4,opt,name=full_feature_names,json=fullFeatureNames,proto3" json:"full_feature_names,omitempty"` +} + +func (x *GetOnlineFeaturesRequest) Reset() { + *x = GetOnlineFeaturesRequest{} + if protoimpl.UnsafeEnabled { + mi := &file_feast_serving_ServingService_proto_msgTypes[5] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *GetOnlineFeaturesRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*GetOnlineFeaturesRequest) ProtoMessage() {} + +func (x *GetOnlineFeaturesRequest) ProtoReflect() protoreflect.Message { + mi := &file_feast_serving_ServingService_proto_msgTypes[5] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use GetOnlineFeaturesRequest.ProtoReflect.Descriptor instead. +func (*GetOnlineFeaturesRequest) Descriptor() ([]byte, []int) { + return file_feast_serving_ServingService_proto_rawDescGZIP(), []int{5} +} + +func (m *GetOnlineFeaturesRequest) GetKind() isGetOnlineFeaturesRequest_Kind { + if m != nil { + return m.Kind + } + return nil +} + +func (x *GetOnlineFeaturesRequest) GetFeatureService() string { + if x, ok := x.GetKind().(*GetOnlineFeaturesRequest_FeatureService); ok { + return x.FeatureService + } + return "" +} + +func (x *GetOnlineFeaturesRequest) GetFeatures() *FeatureList { + if x, ok := x.GetKind().(*GetOnlineFeaturesRequest_Features); ok { + return x.Features + } + return nil +} + +func (x *GetOnlineFeaturesRequest) GetEntities() map[string]*types.RepeatedValue { + if x != nil { + return x.Entities + } + return nil +} + +func (x *GetOnlineFeaturesRequest) GetFullFeatureNames() bool { + if x != nil { + return x.FullFeatureNames + } + return false +} + +type isGetOnlineFeaturesRequest_Kind interface { + isGetOnlineFeaturesRequest_Kind() +} + +type GetOnlineFeaturesRequest_FeatureService struct { + FeatureService string `protobuf:"bytes,1,opt,name=feature_service,json=featureService,proto3,oneof"` +} + +type GetOnlineFeaturesRequest_Features struct { + Features *FeatureList `protobuf:"bytes,2,opt,name=features,proto3,oneof"` +} + +func (*GetOnlineFeaturesRequest_FeatureService) isGetOnlineFeaturesRequest_Kind() {} + +func (*GetOnlineFeaturesRequest_Features) isGetOnlineFeaturesRequest_Kind() {} + type GetOnlineFeaturesResponse struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache @@ -409,7 +550,7 @@ type GetOnlineFeaturesResponse struct { func (x *GetOnlineFeaturesResponse) Reset() { *x = GetOnlineFeaturesResponse{} if protoimpl.UnsafeEnabled { - mi := &file_feast_serving_ServingService_proto_msgTypes[4] + mi := &file_feast_serving_ServingService_proto_msgTypes[6] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -422,7 +563,7 @@ func (x *GetOnlineFeaturesResponse) String() string { func (*GetOnlineFeaturesResponse) ProtoMessage() {} func (x *GetOnlineFeaturesResponse) ProtoReflect() protoreflect.Message { - mi := &file_feast_serving_ServingService_proto_msgTypes[4] + mi := &file_feast_serving_ServingService_proto_msgTypes[6] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -435,7 +576,7 @@ func (x *GetOnlineFeaturesResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use GetOnlineFeaturesResponse.ProtoReflect.Descriptor instead. func (*GetOnlineFeaturesResponse) Descriptor() ([]byte, []int) { - return file_feast_serving_ServingService_proto_rawDescGZIP(), []int{4} + return file_feast_serving_ServingService_proto_rawDescGZIP(), []int{6} } func (x *GetOnlineFeaturesResponse) GetFieldValues() []*GetOnlineFeaturesResponse_FieldValues { @@ -452,7 +593,7 @@ type GetOnlineFeaturesRequestV2_EntityRow struct { // Request timestamp of this row. This value will be used, // together with maxAge, to determine feature staleness. - Timestamp *timestamp.Timestamp `protobuf:"bytes,1,opt,name=timestamp,proto3" json:"timestamp,omitempty"` + Timestamp *timestamppb.Timestamp `protobuf:"bytes,1,opt,name=timestamp,proto3" json:"timestamp,omitempty"` // Map containing mapping of entity name to entity value. Fields map[string]*types.Value `protobuf:"bytes,2,rep,name=fields,proto3" json:"fields,omitempty" protobuf_key:"bytes,1,opt,name=key,proto3" protobuf_val:"bytes,2,opt,name=value,proto3"` } @@ -460,7 +601,7 @@ type GetOnlineFeaturesRequestV2_EntityRow struct { func (x *GetOnlineFeaturesRequestV2_EntityRow) Reset() { *x = GetOnlineFeaturesRequestV2_EntityRow{} if protoimpl.UnsafeEnabled { - mi := &file_feast_serving_ServingService_proto_msgTypes[5] + mi := &file_feast_serving_ServingService_proto_msgTypes[7] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -473,7 +614,7 @@ func (x *GetOnlineFeaturesRequestV2_EntityRow) String() string { func (*GetOnlineFeaturesRequestV2_EntityRow) ProtoMessage() {} func (x *GetOnlineFeaturesRequestV2_EntityRow) ProtoReflect() protoreflect.Message { - mi := &file_feast_serving_ServingService_proto_msgTypes[5] + mi := &file_feast_serving_ServingService_proto_msgTypes[7] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -489,7 +630,7 @@ func (*GetOnlineFeaturesRequestV2_EntityRow) Descriptor() ([]byte, []int) { return file_feast_serving_ServingService_proto_rawDescGZIP(), []int{3, 0} } -func (x *GetOnlineFeaturesRequestV2_EntityRow) GetTimestamp() *timestamp.Timestamp { +func (x *GetOnlineFeaturesRequestV2_EntityRow) GetTimestamp() *timestamppb.Timestamp { if x != nil { return x.Timestamp } @@ -518,7 +659,7 @@ type GetOnlineFeaturesResponse_FieldValues struct { func (x *GetOnlineFeaturesResponse_FieldValues) Reset() { *x = GetOnlineFeaturesResponse_FieldValues{} if protoimpl.UnsafeEnabled { - mi := &file_feast_serving_ServingService_proto_msgTypes[7] + mi := &file_feast_serving_ServingService_proto_msgTypes[10] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -531,7 +672,7 @@ func (x *GetOnlineFeaturesResponse_FieldValues) String() string { func (*GetOnlineFeaturesResponse_FieldValues) ProtoMessage() {} func (x *GetOnlineFeaturesResponse_FieldValues) ProtoReflect() protoreflect.Message { - mi := &file_feast_serving_ServingService_proto_msgTypes[7] + mi := &file_feast_serving_ServingService_proto_msgTypes[10] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -544,7 +685,7 @@ func (x *GetOnlineFeaturesResponse_FieldValues) ProtoReflect() protoreflect.Mess // Deprecated: Use GetOnlineFeaturesResponse_FieldValues.ProtoReflect.Descriptor instead. func (*GetOnlineFeaturesResponse_FieldValues) Descriptor() ([]byte, []int) { - return file_feast_serving_ServingService_proto_rawDescGZIP(), []int{4, 0} + return file_feast_serving_ServingService_proto_rawDescGZIP(), []int{6, 0} } func (x *GetOnlineFeaturesResponse_FieldValues) GetFields() map[string]*types.Value { @@ -618,73 +759,98 @@ var file_feast_serving_ServingService_proto_rawDesc = []byte{ 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x28, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x12, 0x2e, 0x66, 0x65, 0x61, 0x73, 0x74, 0x2e, 0x74, 0x79, 0x70, 0x65, 0x73, 0x2e, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x52, 0x05, 0x76, - 0x61, 0x6c, 0x75, 0x65, 0x3a, 0x02, 0x38, 0x01, 0x22, 0xdd, 0x04, 0x0a, 0x19, 0x47, 0x65, 0x74, - 0x4f, 0x6e, 0x6c, 0x69, 0x6e, 0x65, 0x46, 0x65, 0x61, 0x74, 0x75, 0x72, 0x65, 0x73, 0x52, 0x65, - 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x57, 0x0a, 0x0c, 0x66, 0x69, 0x65, 0x6c, 0x64, 0x5f, - 0x76, 0x61, 0x6c, 0x75, 0x65, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x34, 0x2e, 0x66, - 0x65, 0x61, 0x73, 0x74, 0x2e, 0x73, 0x65, 0x72, 0x76, 0x69, 0x6e, 0x67, 0x2e, 0x47, 0x65, 0x74, - 0x4f, 0x6e, 0x6c, 0x69, 0x6e, 0x65, 0x46, 0x65, 0x61, 0x74, 0x75, 0x72, 0x65, 0x73, 0x52, 0x65, - 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x2e, 0x46, 0x69, 0x65, 0x6c, 0x64, 0x56, 0x61, 0x6c, 0x75, - 0x65, 0x73, 0x52, 0x0b, 0x66, 0x69, 0x65, 0x6c, 0x64, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x73, 0x1a, - 0x89, 0x03, 0x0a, 0x0b, 0x46, 0x69, 0x65, 0x6c, 0x64, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x73, 0x12, - 0x58, 0x0a, 0x06, 0x66, 0x69, 0x65, 0x6c, 0x64, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, - 0x40, 0x2e, 0x66, 0x65, 0x61, 0x73, 0x74, 0x2e, 0x73, 0x65, 0x72, 0x76, 0x69, 0x6e, 0x67, 0x2e, + 0x61, 0x6c, 0x75, 0x65, 0x3a, 0x02, 0x38, 0x01, 0x22, 0x1f, 0x0a, 0x0b, 0x46, 0x65, 0x61, 0x74, + 0x75, 0x72, 0x65, 0x4c, 0x69, 0x73, 0x74, 0x12, 0x10, 0x0a, 0x03, 0x76, 0x61, 0x6c, 0x18, 0x01, + 0x20, 0x03, 0x28, 0x09, 0x52, 0x03, 0x76, 0x61, 0x6c, 0x22, 0xe1, 0x02, 0x0a, 0x18, 0x47, 0x65, + 0x74, 0x4f, 0x6e, 0x6c, 0x69, 0x6e, 0x65, 0x46, 0x65, 0x61, 0x74, 0x75, 0x72, 0x65, 0x73, 0x52, + 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x29, 0x0a, 0x0f, 0x66, 0x65, 0x61, 0x74, 0x75, 0x72, + 0x65, 0x5f, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x48, + 0x00, 0x52, 0x0e, 0x66, 0x65, 0x61, 0x74, 0x75, 0x72, 0x65, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, + 0x65, 0x12, 0x38, 0x0a, 0x08, 0x66, 0x65, 0x61, 0x74, 0x75, 0x72, 0x65, 0x73, 0x18, 0x02, 0x20, + 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x66, 0x65, 0x61, 0x73, 0x74, 0x2e, 0x73, 0x65, 0x72, 0x76, + 0x69, 0x6e, 0x67, 0x2e, 0x46, 0x65, 0x61, 0x74, 0x75, 0x72, 0x65, 0x4c, 0x69, 0x73, 0x74, 0x48, + 0x00, 0x52, 0x08, 0x66, 0x65, 0x61, 0x74, 0x75, 0x72, 0x65, 0x73, 0x12, 0x51, 0x0a, 0x08, 0x65, + 0x6e, 0x74, 0x69, 0x74, 0x69, 0x65, 0x73, 0x18, 0x03, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x35, 0x2e, + 0x66, 0x65, 0x61, 0x73, 0x74, 0x2e, 0x73, 0x65, 0x72, 0x76, 0x69, 0x6e, 0x67, 0x2e, 0x47, 0x65, + 0x74, 0x4f, 0x6e, 0x6c, 0x69, 0x6e, 0x65, 0x46, 0x65, 0x61, 0x74, 0x75, 0x72, 0x65, 0x73, 0x52, + 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x2e, 0x45, 0x6e, 0x74, 0x69, 0x74, 0x69, 0x65, 0x73, 0x45, + 0x6e, 0x74, 0x72, 0x79, 0x52, 0x08, 0x65, 0x6e, 0x74, 0x69, 0x74, 0x69, 0x65, 0x73, 0x12, 0x2c, + 0x0a, 0x12, 0x66, 0x75, 0x6c, 0x6c, 0x5f, 0x66, 0x65, 0x61, 0x74, 0x75, 0x72, 0x65, 0x5f, 0x6e, + 0x61, 0x6d, 0x65, 0x73, 0x18, 0x04, 0x20, 0x01, 0x28, 0x08, 0x52, 0x10, 0x66, 0x75, 0x6c, 0x6c, + 0x46, 0x65, 0x61, 0x74, 0x75, 0x72, 0x65, 0x4e, 0x61, 0x6d, 0x65, 0x73, 0x1a, 0x57, 0x0a, 0x0d, + 0x45, 0x6e, 0x74, 0x69, 0x74, 0x69, 0x65, 0x73, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x12, 0x10, 0x0a, + 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, + 0x30, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, + 0x2e, 0x66, 0x65, 0x61, 0x73, 0x74, 0x2e, 0x74, 0x79, 0x70, 0x65, 0x73, 0x2e, 0x52, 0x65, 0x70, + 0x65, 0x61, 0x74, 0x65, 0x64, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, + 0x65, 0x3a, 0x02, 0x38, 0x01, 0x42, 0x06, 0x0a, 0x04, 0x6b, 0x69, 0x6e, 0x64, 0x22, 0xdd, 0x04, + 0x0a, 0x19, 0x47, 0x65, 0x74, 0x4f, 0x6e, 0x6c, 0x69, 0x6e, 0x65, 0x46, 0x65, 0x61, 0x74, 0x75, + 0x72, 0x65, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x57, 0x0a, 0x0c, 0x66, + 0x69, 0x65, 0x6c, 0x64, 0x5f, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, + 0x0b, 0x32, 0x34, 0x2e, 0x66, 0x65, 0x61, 0x73, 0x74, 0x2e, 0x73, 0x65, 0x72, 0x76, 0x69, 0x6e, + 0x67, 0x2e, 0x47, 0x65, 0x74, 0x4f, 0x6e, 0x6c, 0x69, 0x6e, 0x65, 0x46, 0x65, 0x61, 0x74, 0x75, + 0x72, 0x65, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x2e, 0x46, 0x69, 0x65, 0x6c, + 0x64, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x73, 0x52, 0x0b, 0x66, 0x69, 0x65, 0x6c, 0x64, 0x56, 0x61, + 0x6c, 0x75, 0x65, 0x73, 0x1a, 0x89, 0x03, 0x0a, 0x0b, 0x46, 0x69, 0x65, 0x6c, 0x64, 0x56, 0x61, + 0x6c, 0x75, 0x65, 0x73, 0x12, 0x58, 0x0a, 0x06, 0x66, 0x69, 0x65, 0x6c, 0x64, 0x73, 0x18, 0x01, + 0x20, 0x03, 0x28, 0x0b, 0x32, 0x40, 0x2e, 0x66, 0x65, 0x61, 0x73, 0x74, 0x2e, 0x73, 0x65, 0x72, + 0x76, 0x69, 0x6e, 0x67, 0x2e, 0x47, 0x65, 0x74, 0x4f, 0x6e, 0x6c, 0x69, 0x6e, 0x65, 0x46, 0x65, + 0x61, 0x74, 0x75, 0x72, 0x65, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x2e, 0x46, + 0x69, 0x65, 0x6c, 0x64, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x73, 0x2e, 0x46, 0x69, 0x65, 0x6c, 0x64, + 0x73, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x52, 0x06, 0x66, 0x69, 0x65, 0x6c, 0x64, 0x73, 0x12, 0x5e, + 0x0a, 0x08, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x65, 0x73, 0x18, 0x02, 0x20, 0x03, 0x28, 0x0b, + 0x32, 0x42, 0x2e, 0x66, 0x65, 0x61, 0x73, 0x74, 0x2e, 0x73, 0x65, 0x72, 0x76, 0x69, 0x6e, 0x67, + 0x2e, 0x47, 0x65, 0x74, 0x4f, 0x6e, 0x6c, 0x69, 0x6e, 0x65, 0x46, 0x65, 0x61, 0x74, 0x75, 0x72, + 0x65, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x2e, 0x46, 0x69, 0x65, 0x6c, 0x64, + 0x56, 0x61, 0x6c, 0x75, 0x65, 0x73, 0x2e, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x65, 0x73, 0x45, + 0x6e, 0x74, 0x72, 0x79, 0x52, 0x08, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x65, 0x73, 0x1a, 0x4d, + 0x0a, 0x0b, 0x46, 0x69, 0x65, 0x6c, 0x64, 0x73, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x12, 0x10, 0x0a, + 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, + 0x28, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x12, + 0x2e, 0x66, 0x65, 0x61, 0x73, 0x74, 0x2e, 0x74, 0x79, 0x70, 0x65, 0x73, 0x2e, 0x56, 0x61, 0x6c, + 0x75, 0x65, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x3a, 0x02, 0x38, 0x01, 0x1a, 0x71, 0x0a, + 0x0d, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x65, 0x73, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x12, 0x10, + 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x6b, 0x65, 0x79, + 0x12, 0x4a, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0e, 0x32, + 0x34, 0x2e, 0x66, 0x65, 0x61, 0x73, 0x74, 0x2e, 0x73, 0x65, 0x72, 0x76, 0x69, 0x6e, 0x67, 0x2e, 0x47, 0x65, 0x74, 0x4f, 0x6e, 0x6c, 0x69, 0x6e, 0x65, 0x46, 0x65, 0x61, 0x74, 0x75, 0x72, 0x65, - 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x2e, 0x46, 0x69, 0x65, 0x6c, 0x64, 0x56, - 0x61, 0x6c, 0x75, 0x65, 0x73, 0x2e, 0x46, 0x69, 0x65, 0x6c, 0x64, 0x73, 0x45, 0x6e, 0x74, 0x72, - 0x79, 0x52, 0x06, 0x66, 0x69, 0x65, 0x6c, 0x64, 0x73, 0x12, 0x5e, 0x0a, 0x08, 0x73, 0x74, 0x61, - 0x74, 0x75, 0x73, 0x65, 0x73, 0x18, 0x02, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x42, 0x2e, 0x66, 0x65, - 0x61, 0x73, 0x74, 0x2e, 0x73, 0x65, 0x72, 0x76, 0x69, 0x6e, 0x67, 0x2e, 0x47, 0x65, 0x74, 0x4f, - 0x6e, 0x6c, 0x69, 0x6e, 0x65, 0x46, 0x65, 0x61, 0x74, 0x75, 0x72, 0x65, 0x73, 0x52, 0x65, 0x73, - 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x2e, 0x46, 0x69, 0x65, 0x6c, 0x64, 0x56, 0x61, 0x6c, 0x75, 0x65, - 0x73, 0x2e, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x65, 0x73, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x52, - 0x08, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x65, 0x73, 0x1a, 0x4d, 0x0a, 0x0b, 0x46, 0x69, 0x65, - 0x6c, 0x64, 0x73, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, - 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x28, 0x0a, 0x05, 0x76, 0x61, - 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x12, 0x2e, 0x66, 0x65, 0x61, 0x73, - 0x74, 0x2e, 0x74, 0x79, 0x70, 0x65, 0x73, 0x2e, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x52, 0x05, 0x76, - 0x61, 0x6c, 0x75, 0x65, 0x3a, 0x02, 0x38, 0x01, 0x1a, 0x71, 0x0a, 0x0d, 0x53, 0x74, 0x61, 0x74, - 0x75, 0x73, 0x65, 0x73, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, - 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x4a, 0x0a, 0x05, 0x76, - 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x34, 0x2e, 0x66, 0x65, 0x61, - 0x73, 0x74, 0x2e, 0x73, 0x65, 0x72, 0x76, 0x69, 0x6e, 0x67, 0x2e, 0x47, 0x65, 0x74, 0x4f, 0x6e, - 0x6c, 0x69, 0x6e, 0x65, 0x46, 0x65, 0x61, 0x74, 0x75, 0x72, 0x65, 0x73, 0x52, 0x65, 0x73, 0x70, - 0x6f, 0x6e, 0x73, 0x65, 0x2e, 0x46, 0x69, 0x65, 0x6c, 0x64, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, - 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x3a, 0x02, 0x38, 0x01, 0x22, 0x5b, 0x0a, 0x0b, 0x46, - 0x69, 0x65, 0x6c, 0x64, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x12, 0x0b, 0x0a, 0x07, 0x49, 0x4e, - 0x56, 0x41, 0x4c, 0x49, 0x44, 0x10, 0x00, 0x12, 0x0b, 0x0a, 0x07, 0x50, 0x52, 0x45, 0x53, 0x45, - 0x4e, 0x54, 0x10, 0x01, 0x12, 0x0e, 0x0a, 0x0a, 0x4e, 0x55, 0x4c, 0x4c, 0x5f, 0x56, 0x41, 0x4c, - 0x55, 0x45, 0x10, 0x02, 0x12, 0x0d, 0x0a, 0x09, 0x4e, 0x4f, 0x54, 0x5f, 0x46, 0x4f, 0x55, 0x4e, - 0x44, 0x10, 0x03, 0x12, 0x13, 0x0a, 0x0f, 0x4f, 0x55, 0x54, 0x53, 0x49, 0x44, 0x45, 0x5f, 0x4d, - 0x41, 0x58, 0x5f, 0x41, 0x47, 0x45, 0x10, 0x04, 0x2a, 0x6f, 0x0a, 0x10, 0x46, 0x65, 0x61, 0x73, - 0x74, 0x53, 0x65, 0x72, 0x76, 0x69, 0x6e, 0x67, 0x54, 0x79, 0x70, 0x65, 0x12, 0x1e, 0x0a, 0x1a, - 0x46, 0x45, 0x41, 0x53, 0x54, 0x5f, 0x53, 0x45, 0x52, 0x56, 0x49, 0x4e, 0x47, 0x5f, 0x54, 0x59, - 0x50, 0x45, 0x5f, 0x49, 0x4e, 0x56, 0x41, 0x4c, 0x49, 0x44, 0x10, 0x00, 0x12, 0x1d, 0x0a, 0x19, - 0x46, 0x45, 0x41, 0x53, 0x54, 0x5f, 0x53, 0x45, 0x52, 0x56, 0x49, 0x4e, 0x47, 0x5f, 0x54, 0x59, - 0x50, 0x45, 0x5f, 0x4f, 0x4e, 0x4c, 0x49, 0x4e, 0x45, 0x10, 0x01, 0x12, 0x1c, 0x0a, 0x18, 0x46, - 0x45, 0x41, 0x53, 0x54, 0x5f, 0x53, 0x45, 0x52, 0x56, 0x49, 0x4e, 0x47, 0x5f, 0x54, 0x59, 0x50, - 0x45, 0x5f, 0x42, 0x41, 0x54, 0x43, 0x48, 0x10, 0x02, 0x32, 0xea, 0x01, 0x0a, 0x0e, 0x53, 0x65, - 0x72, 0x76, 0x69, 0x6e, 0x67, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x12, 0x6c, 0x0a, 0x13, - 0x47, 0x65, 0x74, 0x46, 0x65, 0x61, 0x73, 0x74, 0x53, 0x65, 0x72, 0x76, 0x69, 0x6e, 0x67, 0x49, - 0x6e, 0x66, 0x6f, 0x12, 0x29, 0x2e, 0x66, 0x65, 0x61, 0x73, 0x74, 0x2e, 0x73, 0x65, 0x72, 0x76, + 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x2e, 0x46, 0x69, 0x65, 0x6c, 0x64, 0x53, + 0x74, 0x61, 0x74, 0x75, 0x73, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x3a, 0x02, 0x38, 0x01, + 0x22, 0x5b, 0x0a, 0x0b, 0x46, 0x69, 0x65, 0x6c, 0x64, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x12, + 0x0b, 0x0a, 0x07, 0x49, 0x4e, 0x56, 0x41, 0x4c, 0x49, 0x44, 0x10, 0x00, 0x12, 0x0b, 0x0a, 0x07, + 0x50, 0x52, 0x45, 0x53, 0x45, 0x4e, 0x54, 0x10, 0x01, 0x12, 0x0e, 0x0a, 0x0a, 0x4e, 0x55, 0x4c, + 0x4c, 0x5f, 0x56, 0x41, 0x4c, 0x55, 0x45, 0x10, 0x02, 0x12, 0x0d, 0x0a, 0x09, 0x4e, 0x4f, 0x54, + 0x5f, 0x46, 0x4f, 0x55, 0x4e, 0x44, 0x10, 0x03, 0x12, 0x13, 0x0a, 0x0f, 0x4f, 0x55, 0x54, 0x53, + 0x49, 0x44, 0x45, 0x5f, 0x4d, 0x41, 0x58, 0x5f, 0x41, 0x47, 0x45, 0x10, 0x04, 0x2a, 0x6f, 0x0a, + 0x10, 0x46, 0x65, 0x61, 0x73, 0x74, 0x53, 0x65, 0x72, 0x76, 0x69, 0x6e, 0x67, 0x54, 0x79, 0x70, + 0x65, 0x12, 0x1e, 0x0a, 0x1a, 0x46, 0x45, 0x41, 0x53, 0x54, 0x5f, 0x53, 0x45, 0x52, 0x56, 0x49, + 0x4e, 0x47, 0x5f, 0x54, 0x59, 0x50, 0x45, 0x5f, 0x49, 0x4e, 0x56, 0x41, 0x4c, 0x49, 0x44, 0x10, + 0x00, 0x12, 0x1d, 0x0a, 0x19, 0x46, 0x45, 0x41, 0x53, 0x54, 0x5f, 0x53, 0x45, 0x52, 0x56, 0x49, + 0x4e, 0x47, 0x5f, 0x54, 0x59, 0x50, 0x45, 0x5f, 0x4f, 0x4e, 0x4c, 0x49, 0x4e, 0x45, 0x10, 0x01, + 0x12, 0x1c, 0x0a, 0x18, 0x46, 0x45, 0x41, 0x53, 0x54, 0x5f, 0x53, 0x45, 0x52, 0x56, 0x49, 0x4e, + 0x47, 0x5f, 0x54, 0x59, 0x50, 0x45, 0x5f, 0x42, 0x41, 0x54, 0x43, 0x48, 0x10, 0x02, 0x32, 0xea, + 0x01, 0x0a, 0x0e, 0x53, 0x65, 0x72, 0x76, 0x69, 0x6e, 0x67, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, + 0x65, 0x12, 0x6c, 0x0a, 0x13, 0x47, 0x65, 0x74, 0x46, 0x65, 0x61, 0x73, 0x74, 0x53, 0x65, 0x72, + 0x76, 0x69, 0x6e, 0x67, 0x49, 0x6e, 0x66, 0x6f, 0x12, 0x29, 0x2e, 0x66, 0x65, 0x61, 0x73, 0x74, + 0x2e, 0x73, 0x65, 0x72, 0x76, 0x69, 0x6e, 0x67, 0x2e, 0x47, 0x65, 0x74, 0x46, 0x65, 0x61, 0x73, + 0x74, 0x53, 0x65, 0x72, 0x76, 0x69, 0x6e, 0x67, 0x49, 0x6e, 0x66, 0x6f, 0x52, 0x65, 0x71, 0x75, + 0x65, 0x73, 0x74, 0x1a, 0x2a, 0x2e, 0x66, 0x65, 0x61, 0x73, 0x74, 0x2e, 0x73, 0x65, 0x72, 0x76, 0x69, 0x6e, 0x67, 0x2e, 0x47, 0x65, 0x74, 0x46, 0x65, 0x61, 0x73, 0x74, 0x53, 0x65, 0x72, 0x76, - 0x69, 0x6e, 0x67, 0x49, 0x6e, 0x66, 0x6f, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x2a, - 0x2e, 0x66, 0x65, 0x61, 0x73, 0x74, 0x2e, 0x73, 0x65, 0x72, 0x76, 0x69, 0x6e, 0x67, 0x2e, 0x47, - 0x65, 0x74, 0x46, 0x65, 0x61, 0x73, 0x74, 0x53, 0x65, 0x72, 0x76, 0x69, 0x6e, 0x67, 0x49, 0x6e, - 0x66, 0x6f, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x6a, 0x0a, 0x13, 0x47, 0x65, - 0x74, 0x4f, 0x6e, 0x6c, 0x69, 0x6e, 0x65, 0x46, 0x65, 0x61, 0x74, 0x75, 0x72, 0x65, 0x73, 0x56, - 0x32, 0x12, 0x29, 0x2e, 0x66, 0x65, 0x61, 0x73, 0x74, 0x2e, 0x73, 0x65, 0x72, 0x76, 0x69, 0x6e, + 0x69, 0x6e, 0x67, 0x49, 0x6e, 0x66, 0x6f, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, + 0x6a, 0x0a, 0x13, 0x47, 0x65, 0x74, 0x4f, 0x6e, 0x6c, 0x69, 0x6e, 0x65, 0x46, 0x65, 0x61, 0x74, + 0x75, 0x72, 0x65, 0x73, 0x56, 0x32, 0x12, 0x29, 0x2e, 0x66, 0x65, 0x61, 0x73, 0x74, 0x2e, 0x73, + 0x65, 0x72, 0x76, 0x69, 0x6e, 0x67, 0x2e, 0x47, 0x65, 0x74, 0x4f, 0x6e, 0x6c, 0x69, 0x6e, 0x65, + 0x46, 0x65, 0x61, 0x74, 0x75, 0x72, 0x65, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x56, + 0x32, 0x1a, 0x28, 0x2e, 0x66, 0x65, 0x61, 0x73, 0x74, 0x2e, 0x73, 0x65, 0x72, 0x76, 0x69, 0x6e, 0x67, 0x2e, 0x47, 0x65, 0x74, 0x4f, 0x6e, 0x6c, 0x69, 0x6e, 0x65, 0x46, 0x65, 0x61, 0x74, 0x75, - 0x72, 0x65, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x56, 0x32, 0x1a, 0x28, 0x2e, 0x66, - 0x65, 0x61, 0x73, 0x74, 0x2e, 0x73, 0x65, 0x72, 0x76, 0x69, 0x6e, 0x67, 0x2e, 0x47, 0x65, 0x74, - 0x4f, 0x6e, 0x6c, 0x69, 0x6e, 0x65, 0x46, 0x65, 0x61, 0x74, 0x75, 0x72, 0x65, 0x73, 0x52, 0x65, - 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x42, 0x5e, 0x0a, 0x13, 0x66, 0x65, 0x61, 0x73, 0x74, 0x2e, - 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x73, 0x65, 0x72, 0x76, 0x69, 0x6e, 0x67, 0x42, 0x0f, 0x53, - 0x65, 0x72, 0x76, 0x69, 0x6e, 0x67, 0x41, 0x50, 0x49, 0x50, 0x72, 0x6f, 0x74, 0x6f, 0x5a, 0x36, - 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x66, 0x65, 0x61, 0x73, 0x74, - 0x2d, 0x64, 0x65, 0x76, 0x2f, 0x66, 0x65, 0x61, 0x73, 0x74, 0x2f, 0x73, 0x64, 0x6b, 0x2f, 0x67, - 0x6f, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x73, 0x2f, 0x66, 0x65, 0x61, 0x73, 0x74, 0x2f, 0x73, - 0x65, 0x72, 0x76, 0x69, 0x6e, 0x67, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, + 0x72, 0x65, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x42, 0x5e, 0x0a, 0x13, 0x66, + 0x65, 0x61, 0x73, 0x74, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x73, 0x65, 0x72, 0x76, 0x69, + 0x6e, 0x67, 0x42, 0x0f, 0x53, 0x65, 0x72, 0x76, 0x69, 0x6e, 0x67, 0x41, 0x50, 0x49, 0x50, 0x72, + 0x6f, 0x74, 0x6f, 0x5a, 0x36, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, + 0x66, 0x65, 0x61, 0x73, 0x74, 0x2d, 0x64, 0x65, 0x76, 0x2f, 0x66, 0x65, 0x61, 0x73, 0x74, 0x2f, + 0x73, 0x64, 0x6b, 0x2f, 0x67, 0x6f, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x73, 0x2f, 0x66, 0x65, + 0x61, 0x73, 0x74, 0x2f, 0x73, 0x65, 0x72, 0x76, 0x69, 0x6e, 0x67, 0x62, 0x06, 0x70, 0x72, 0x6f, + 0x74, 0x6f, 0x33, } var ( @@ -700,7 +866,7 @@ func file_feast_serving_ServingService_proto_rawDescGZIP() []byte { } var file_feast_serving_ServingService_proto_enumTypes = make([]protoimpl.EnumInfo, 2) -var file_feast_serving_ServingService_proto_msgTypes = make([]protoimpl.MessageInfo, 10) +var file_feast_serving_ServingService_proto_msgTypes = make([]protoimpl.MessageInfo, 13) var file_feast_serving_ServingService_proto_goTypes = []interface{}{ (FeastServingType)(0), // 0: feast.serving.FeastServingType (GetOnlineFeaturesResponse_FieldStatus)(0), // 1: feast.serving.GetOnlineFeaturesResponse.FieldStatus @@ -708,36 +874,43 @@ var file_feast_serving_ServingService_proto_goTypes = []interface{}{ (*GetFeastServingInfoResponse)(nil), // 3: feast.serving.GetFeastServingInfoResponse (*FeatureReferenceV2)(nil), // 4: feast.serving.FeatureReferenceV2 (*GetOnlineFeaturesRequestV2)(nil), // 5: feast.serving.GetOnlineFeaturesRequestV2 - (*GetOnlineFeaturesResponse)(nil), // 6: feast.serving.GetOnlineFeaturesResponse - (*GetOnlineFeaturesRequestV2_EntityRow)(nil), // 7: feast.serving.GetOnlineFeaturesRequestV2.EntityRow - nil, // 8: feast.serving.GetOnlineFeaturesRequestV2.EntityRow.FieldsEntry - (*GetOnlineFeaturesResponse_FieldValues)(nil), // 9: feast.serving.GetOnlineFeaturesResponse.FieldValues - nil, // 10: feast.serving.GetOnlineFeaturesResponse.FieldValues.FieldsEntry - nil, // 11: feast.serving.GetOnlineFeaturesResponse.FieldValues.StatusesEntry - (*timestamp.Timestamp)(nil), // 12: google.protobuf.Timestamp - (*types.Value)(nil), // 13: feast.types.Value + (*FeatureList)(nil), // 6: feast.serving.FeatureList + (*GetOnlineFeaturesRequest)(nil), // 7: feast.serving.GetOnlineFeaturesRequest + (*GetOnlineFeaturesResponse)(nil), // 8: feast.serving.GetOnlineFeaturesResponse + (*GetOnlineFeaturesRequestV2_EntityRow)(nil), // 9: feast.serving.GetOnlineFeaturesRequestV2.EntityRow + nil, // 10: feast.serving.GetOnlineFeaturesRequestV2.EntityRow.FieldsEntry + nil, // 11: feast.serving.GetOnlineFeaturesRequest.EntitiesEntry + (*GetOnlineFeaturesResponse_FieldValues)(nil), // 12: feast.serving.GetOnlineFeaturesResponse.FieldValues + nil, // 13: feast.serving.GetOnlineFeaturesResponse.FieldValues.FieldsEntry + nil, // 14: feast.serving.GetOnlineFeaturesResponse.FieldValues.StatusesEntry + (*timestamppb.Timestamp)(nil), // 15: google.protobuf.Timestamp + (*types.Value)(nil), // 16: feast.types.Value + (*types.RepeatedValue)(nil), // 17: feast.types.RepeatedValue } var file_feast_serving_ServingService_proto_depIdxs = []int32{ 0, // 0: feast.serving.GetFeastServingInfoResponse.type:type_name -> feast.serving.FeastServingType 4, // 1: feast.serving.GetOnlineFeaturesRequestV2.features:type_name -> feast.serving.FeatureReferenceV2 - 7, // 2: feast.serving.GetOnlineFeaturesRequestV2.entity_rows:type_name -> feast.serving.GetOnlineFeaturesRequestV2.EntityRow - 9, // 3: feast.serving.GetOnlineFeaturesResponse.field_values:type_name -> feast.serving.GetOnlineFeaturesResponse.FieldValues - 12, // 4: feast.serving.GetOnlineFeaturesRequestV2.EntityRow.timestamp:type_name -> google.protobuf.Timestamp - 8, // 5: feast.serving.GetOnlineFeaturesRequestV2.EntityRow.fields:type_name -> feast.serving.GetOnlineFeaturesRequestV2.EntityRow.FieldsEntry - 13, // 6: feast.serving.GetOnlineFeaturesRequestV2.EntityRow.FieldsEntry.value:type_name -> feast.types.Value - 10, // 7: feast.serving.GetOnlineFeaturesResponse.FieldValues.fields:type_name -> feast.serving.GetOnlineFeaturesResponse.FieldValues.FieldsEntry - 11, // 8: feast.serving.GetOnlineFeaturesResponse.FieldValues.statuses:type_name -> feast.serving.GetOnlineFeaturesResponse.FieldValues.StatusesEntry - 13, // 9: feast.serving.GetOnlineFeaturesResponse.FieldValues.FieldsEntry.value:type_name -> feast.types.Value - 1, // 10: feast.serving.GetOnlineFeaturesResponse.FieldValues.StatusesEntry.value:type_name -> feast.serving.GetOnlineFeaturesResponse.FieldStatus - 2, // 11: feast.serving.ServingService.GetFeastServingInfo:input_type -> feast.serving.GetFeastServingInfoRequest - 5, // 12: feast.serving.ServingService.GetOnlineFeaturesV2:input_type -> feast.serving.GetOnlineFeaturesRequestV2 - 3, // 13: feast.serving.ServingService.GetFeastServingInfo:output_type -> feast.serving.GetFeastServingInfoResponse - 6, // 14: feast.serving.ServingService.GetOnlineFeaturesV2:output_type -> feast.serving.GetOnlineFeaturesResponse - 13, // [13:15] is the sub-list for method output_type - 11, // [11:13] is the sub-list for method input_type - 11, // [11:11] is the sub-list for extension type_name - 11, // [11:11] is the sub-list for extension extendee - 0, // [0:11] is the sub-list for field type_name + 9, // 2: feast.serving.GetOnlineFeaturesRequestV2.entity_rows:type_name -> feast.serving.GetOnlineFeaturesRequestV2.EntityRow + 6, // 3: feast.serving.GetOnlineFeaturesRequest.features:type_name -> feast.serving.FeatureList + 11, // 4: feast.serving.GetOnlineFeaturesRequest.entities:type_name -> feast.serving.GetOnlineFeaturesRequest.EntitiesEntry + 12, // 5: feast.serving.GetOnlineFeaturesResponse.field_values:type_name -> feast.serving.GetOnlineFeaturesResponse.FieldValues + 15, // 6: feast.serving.GetOnlineFeaturesRequestV2.EntityRow.timestamp:type_name -> google.protobuf.Timestamp + 10, // 7: feast.serving.GetOnlineFeaturesRequestV2.EntityRow.fields:type_name -> feast.serving.GetOnlineFeaturesRequestV2.EntityRow.FieldsEntry + 16, // 8: feast.serving.GetOnlineFeaturesRequestV2.EntityRow.FieldsEntry.value:type_name -> feast.types.Value + 17, // 9: feast.serving.GetOnlineFeaturesRequest.EntitiesEntry.value:type_name -> feast.types.RepeatedValue + 13, // 10: feast.serving.GetOnlineFeaturesResponse.FieldValues.fields:type_name -> feast.serving.GetOnlineFeaturesResponse.FieldValues.FieldsEntry + 14, // 11: feast.serving.GetOnlineFeaturesResponse.FieldValues.statuses:type_name -> feast.serving.GetOnlineFeaturesResponse.FieldValues.StatusesEntry + 16, // 12: feast.serving.GetOnlineFeaturesResponse.FieldValues.FieldsEntry.value:type_name -> feast.types.Value + 1, // 13: feast.serving.GetOnlineFeaturesResponse.FieldValues.StatusesEntry.value:type_name -> feast.serving.GetOnlineFeaturesResponse.FieldStatus + 2, // 14: feast.serving.ServingService.GetFeastServingInfo:input_type -> feast.serving.GetFeastServingInfoRequest + 5, // 15: feast.serving.ServingService.GetOnlineFeaturesV2:input_type -> feast.serving.GetOnlineFeaturesRequestV2 + 3, // 16: feast.serving.ServingService.GetFeastServingInfo:output_type -> feast.serving.GetFeastServingInfoResponse + 8, // 17: feast.serving.ServingService.GetOnlineFeaturesV2:output_type -> feast.serving.GetOnlineFeaturesResponse + 16, // [16:18] is the sub-list for method output_type + 14, // [14:16] is the sub-list for method input_type + 14, // [14:14] is the sub-list for extension type_name + 14, // [14:14] is the sub-list for extension extendee + 0, // [0:14] is the sub-list for field type_name } func init() { file_feast_serving_ServingService_proto_init() } @@ -795,7 +968,7 @@ func file_feast_serving_ServingService_proto_init() { } } file_feast_serving_ServingService_proto_msgTypes[4].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*GetOnlineFeaturesResponse); i { + switch v := v.(*FeatureList); i { case 0: return &v.state case 1: @@ -807,7 +980,19 @@ func file_feast_serving_ServingService_proto_init() { } } file_feast_serving_ServingService_proto_msgTypes[5].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*GetOnlineFeaturesRequestV2_EntityRow); i { + switch v := v.(*GetOnlineFeaturesRequest); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_feast_serving_ServingService_proto_msgTypes[6].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*GetOnlineFeaturesResponse); i { case 0: return &v.state case 1: @@ -819,6 +1004,18 @@ func file_feast_serving_ServingService_proto_init() { } } file_feast_serving_ServingService_proto_msgTypes[7].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*GetOnlineFeaturesRequestV2_EntityRow); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_feast_serving_ServingService_proto_msgTypes[10].Exporter = func(v interface{}, i int) interface{} { switch v := v.(*GetOnlineFeaturesResponse_FieldValues); i { case 0: return &v.state @@ -831,13 +1028,17 @@ func file_feast_serving_ServingService_proto_init() { } } } + file_feast_serving_ServingService_proto_msgTypes[5].OneofWrappers = []interface{}{ + (*GetOnlineFeaturesRequest_FeatureService)(nil), + (*GetOnlineFeaturesRequest_Features)(nil), + } type x struct{} out := protoimpl.TypeBuilder{ File: protoimpl.DescBuilder{ GoPackagePath: reflect.TypeOf(x{}).PkgPath(), RawDescriptor: file_feast_serving_ServingService_proto_rawDesc, NumEnums: 2, - NumMessages: 10, + NumMessages: 13, NumExtensions: 0, NumServices: 1, }, diff --git a/sdk/go/protos/feast/storage/Redis.pb.go b/sdk/go/protos/feast/storage/Redis.pb.go index 761bf2bb6ea..08ee629b7c0 100644 --- a/sdk/go/protos/feast/storage/Redis.pb.go +++ b/sdk/go/protos/feast/storage/Redis.pb.go @@ -15,15 +15,14 @@ // Code generated by protoc-gen-go. DO NOT EDIT. // versions: -// protoc-gen-go v1.25.0 -// protoc v3.12.4 +// protoc-gen-go v1.27.1 +// protoc v3.11.2 // source: feast/storage/Redis.proto package storage import ( types "github.com/feast-dev/feast/sdk/go/protos/feast/types" - proto "github.com/golang/protobuf/proto" protoreflect "google.golang.org/protobuf/reflect/protoreflect" protoimpl "google.golang.org/protobuf/runtime/protoimpl" reflect "reflect" @@ -37,10 +36,6 @@ const ( _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) ) -// This is a compile-time assertion that a sufficiently up-to-date version -// of the legacy proto package is being used. -const _ = proto.ProtoPackageIsVersion4 - type RedisKeyV2 struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache diff --git a/sdk/go/protos/feast/types/Field.pb.go b/sdk/go/protos/feast/types/Field.pb.go index 9dad77cdb91..73f46bb1ac3 100644 --- a/sdk/go/protos/feast/types/Field.pb.go +++ b/sdk/go/protos/feast/types/Field.pb.go @@ -15,14 +15,13 @@ // Code generated by protoc-gen-go. DO NOT EDIT. // versions: -// protoc-gen-go v1.25.0 -// protoc v3.12.4 +// protoc-gen-go v1.27.1 +// protoc v3.11.2 // source: feast/types/Field.proto package types import ( - proto "github.com/golang/protobuf/proto" protoreflect "google.golang.org/protobuf/reflect/protoreflect" protoimpl "google.golang.org/protobuf/runtime/protoimpl" reflect "reflect" @@ -36,10 +35,6 @@ const ( _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) ) -// This is a compile-time assertion that a sufficiently up-to-date version -// of the legacy proto package is being used. -const _ = proto.ProtoPackageIsVersion4 - type Field struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache diff --git a/sdk/go/protos/feast/types/Value.pb.go b/sdk/go/protos/feast/types/Value.pb.go index 3b194356331..9ae2806d515 100644 --- a/sdk/go/protos/feast/types/Value.pb.go +++ b/sdk/go/protos/feast/types/Value.pb.go @@ -15,14 +15,13 @@ // Code generated by protoc-gen-go. DO NOT EDIT. // versions: -// protoc-gen-go v1.25.0 -// protoc v3.12.4 +// protoc-gen-go v1.27.1 +// protoc v3.11.2 // source: feast/types/Value.proto package types import ( - proto "github.com/golang/protobuf/proto" protoreflect "google.golang.org/protobuf/reflect/protoreflect" protoimpl "google.golang.org/protobuf/runtime/protoimpl" reflect "reflect" @@ -36,28 +35,70 @@ const ( _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) ) -// This is a compile-time assertion that a sufficiently up-to-date version -// of the legacy proto package is being used. -const _ = proto.ProtoPackageIsVersion4 +type Null int32 + +const ( + Null_NULL Null = 0 +) + +// Enum value maps for Null. +var ( + Null_name = map[int32]string{ + 0: "NULL", + } + Null_value = map[string]int32{ + "NULL": 0, + } +) + +func (x Null) Enum() *Null { + p := new(Null) + *p = x + return p +} + +func (x Null) String() string { + return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x)) +} + +func (Null) Descriptor() protoreflect.EnumDescriptor { + return file_feast_types_Value_proto_enumTypes[0].Descriptor() +} + +func (Null) Type() protoreflect.EnumType { + return &file_feast_types_Value_proto_enumTypes[0] +} + +func (x Null) Number() protoreflect.EnumNumber { + return protoreflect.EnumNumber(x) +} + +// Deprecated: Use Null.Descriptor instead. +func (Null) EnumDescriptor() ([]byte, []int) { + return file_feast_types_Value_proto_rawDescGZIP(), []int{0} +} type ValueType_Enum int32 const ( - ValueType_INVALID ValueType_Enum = 0 - ValueType_BYTES ValueType_Enum = 1 - ValueType_STRING ValueType_Enum = 2 - ValueType_INT32 ValueType_Enum = 3 - ValueType_INT64 ValueType_Enum = 4 - ValueType_DOUBLE ValueType_Enum = 5 - ValueType_FLOAT ValueType_Enum = 6 - ValueType_BOOL ValueType_Enum = 7 - ValueType_BYTES_LIST ValueType_Enum = 11 - ValueType_STRING_LIST ValueType_Enum = 12 - ValueType_INT32_LIST ValueType_Enum = 13 - ValueType_INT64_LIST ValueType_Enum = 14 - ValueType_DOUBLE_LIST ValueType_Enum = 15 - ValueType_FLOAT_LIST ValueType_Enum = 16 - ValueType_BOOL_LIST ValueType_Enum = 17 + ValueType_INVALID ValueType_Enum = 0 + ValueType_BYTES ValueType_Enum = 1 + ValueType_STRING ValueType_Enum = 2 + ValueType_INT32 ValueType_Enum = 3 + ValueType_INT64 ValueType_Enum = 4 + ValueType_DOUBLE ValueType_Enum = 5 + ValueType_FLOAT ValueType_Enum = 6 + ValueType_BOOL ValueType_Enum = 7 + ValueType_UNIX_TIMESTAMP ValueType_Enum = 8 + ValueType_BYTES_LIST ValueType_Enum = 11 + ValueType_STRING_LIST ValueType_Enum = 12 + ValueType_INT32_LIST ValueType_Enum = 13 + ValueType_INT64_LIST ValueType_Enum = 14 + ValueType_DOUBLE_LIST ValueType_Enum = 15 + ValueType_FLOAT_LIST ValueType_Enum = 16 + ValueType_BOOL_LIST ValueType_Enum = 17 + ValueType_UNIX_TIMESTAMP_LIST ValueType_Enum = 18 + ValueType_NULL ValueType_Enum = 19 ) // Enum value maps for ValueType_Enum. @@ -71,6 +112,7 @@ var ( 5: "DOUBLE", 6: "FLOAT", 7: "BOOL", + 8: "UNIX_TIMESTAMP", 11: "BYTES_LIST", 12: "STRING_LIST", 13: "INT32_LIST", @@ -78,23 +120,28 @@ var ( 15: "DOUBLE_LIST", 16: "FLOAT_LIST", 17: "BOOL_LIST", + 18: "UNIX_TIMESTAMP_LIST", + 19: "NULL", } ValueType_Enum_value = map[string]int32{ - "INVALID": 0, - "BYTES": 1, - "STRING": 2, - "INT32": 3, - "INT64": 4, - "DOUBLE": 5, - "FLOAT": 6, - "BOOL": 7, - "BYTES_LIST": 11, - "STRING_LIST": 12, - "INT32_LIST": 13, - "INT64_LIST": 14, - "DOUBLE_LIST": 15, - "FLOAT_LIST": 16, - "BOOL_LIST": 17, + "INVALID": 0, + "BYTES": 1, + "STRING": 2, + "INT32": 3, + "INT64": 4, + "DOUBLE": 5, + "FLOAT": 6, + "BOOL": 7, + "UNIX_TIMESTAMP": 8, + "BYTES_LIST": 11, + "STRING_LIST": 12, + "INT32_LIST": 13, + "INT64_LIST": 14, + "DOUBLE_LIST": 15, + "FLOAT_LIST": 16, + "BOOL_LIST": 17, + "UNIX_TIMESTAMP_LIST": 18, + "NULL": 19, } ) @@ -109,11 +156,11 @@ func (x ValueType_Enum) String() string { } func (ValueType_Enum) Descriptor() protoreflect.EnumDescriptor { - return file_feast_types_Value_proto_enumTypes[0].Descriptor() + return file_feast_types_Value_proto_enumTypes[1].Descriptor() } func (ValueType_Enum) Type() protoreflect.EnumType { - return &file_feast_types_Value_proto_enumTypes[0] + return &file_feast_types_Value_proto_enumTypes[1] } func (x ValueType_Enum) Number() protoreflect.EnumNumber { @@ -170,6 +217,7 @@ type Value struct { // ValueType is referenced by the metadata types, FeatureInfo and EntityInfo. // The enum values do not have to match the oneof val field ids, but they should. + // In JSON "*_val" field can be omitted // // Types that are assignable to Val: // *Value_BytesVal @@ -179,6 +227,7 @@ type Value struct { // *Value_DoubleVal // *Value_FloatVal // *Value_BoolVal + // *Value_UnixTimestampVal // *Value_BytesListVal // *Value_StringListVal // *Value_Int32ListVal @@ -186,6 +235,8 @@ type Value struct { // *Value_DoubleListVal // *Value_FloatListVal // *Value_BoolListVal + // *Value_UnixTimestampListVal + // *Value_NullVal Val isValue_Val `protobuf_oneof:"val"` } @@ -277,6 +328,13 @@ func (x *Value) GetBoolVal() bool { return false } +func (x *Value) GetUnixTimestampVal() int64 { + if x, ok := x.GetVal().(*Value_UnixTimestampVal); ok { + return x.UnixTimestampVal + } + return 0 +} + func (x *Value) GetBytesListVal() *BytesList { if x, ok := x.GetVal().(*Value_BytesListVal); ok { return x.BytesListVal @@ -326,6 +384,20 @@ func (x *Value) GetBoolListVal() *BoolList { return nil } +func (x *Value) GetUnixTimestampListVal() *Int64List { + if x, ok := x.GetVal().(*Value_UnixTimestampListVal); ok { + return x.UnixTimestampListVal + } + return nil +} + +func (x *Value) GetNullVal() Null { + if x, ok := x.GetVal().(*Value_NullVal); ok { + return x.NullVal + } + return Null_NULL +} + type isValue_Val interface { isValue_Val() } @@ -358,6 +430,10 @@ type Value_BoolVal struct { BoolVal bool `protobuf:"varint,7,opt,name=bool_val,json=boolVal,proto3,oneof"` } +type Value_UnixTimestampVal struct { + UnixTimestampVal int64 `protobuf:"varint,8,opt,name=unix_timestamp_val,json=unixTimestampVal,proto3,oneof"` +} + type Value_BytesListVal struct { BytesListVal *BytesList `protobuf:"bytes,11,opt,name=bytes_list_val,json=bytesListVal,proto3,oneof"` } @@ -386,6 +462,14 @@ type Value_BoolListVal struct { BoolListVal *BoolList `protobuf:"bytes,17,opt,name=bool_list_val,json=boolListVal,proto3,oneof"` } +type Value_UnixTimestampListVal struct { + UnixTimestampListVal *Int64List `protobuf:"bytes,18,opt,name=unix_timestamp_list_val,json=unixTimestampListVal,proto3,oneof"` +} + +type Value_NullVal struct { + NullVal Null `protobuf:"varint,19,opt,name=null_val,json=nullVal,proto3,enum=feast.types.Null,oneof"` +} + func (*Value_BytesVal) isValue_Val() {} func (*Value_StringVal) isValue_Val() {} @@ -400,6 +484,8 @@ func (*Value_FloatVal) isValue_Val() {} func (*Value_BoolVal) isValue_Val() {} +func (*Value_UnixTimestampVal) isValue_Val() {} + func (*Value_BytesListVal) isValue_Val() {} func (*Value_StringListVal) isValue_Val() {} @@ -414,6 +500,10 @@ func (*Value_FloatListVal) isValue_Val() {} func (*Value_BoolListVal) isValue_Val() {} +func (*Value_UnixTimestampListVal) isValue_Val() {} + +func (*Value_NullVal) isValue_Val() {} + type BytesList struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache @@ -743,89 +833,157 @@ func (x *BoolList) GetVal() []bool { return nil } +// This is to avoid an issue of being unable to specify `repeated value` in oneofs or maps +// In JSON "val" field can be omitted +type RepeatedValue struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Val []*Value `protobuf:"bytes,1,rep,name=val,proto3" json:"val,omitempty"` +} + +func (x *RepeatedValue) Reset() { + *x = RepeatedValue{} + if protoimpl.UnsafeEnabled { + mi := &file_feast_types_Value_proto_msgTypes[9] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *RepeatedValue) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*RepeatedValue) ProtoMessage() {} + +func (x *RepeatedValue) ProtoReflect() protoreflect.Message { + mi := &file_feast_types_Value_proto_msgTypes[9] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use RepeatedValue.ProtoReflect.Descriptor instead. +func (*RepeatedValue) Descriptor() ([]byte, []int) { + return file_feast_types_Value_proto_rawDescGZIP(), []int{9} +} + +func (x *RepeatedValue) GetVal() []*Value { + if x != nil { + return x.Val + } + return nil +} + var File_feast_types_Value_proto protoreflect.FileDescriptor var file_feast_types_Value_proto_rawDesc = []byte{ 0x0a, 0x17, 0x66, 0x65, 0x61, 0x73, 0x74, 0x2f, 0x74, 0x79, 0x70, 0x65, 0x73, 0x2f, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x12, 0x0b, 0x66, 0x65, 0x61, 0x73, 0x74, - 0x2e, 0x74, 0x79, 0x70, 0x65, 0x73, 0x22, 0xe0, 0x01, 0x0a, 0x09, 0x56, 0x61, 0x6c, 0x75, 0x65, - 0x54, 0x79, 0x70, 0x65, 0x22, 0xd2, 0x01, 0x0a, 0x04, 0x45, 0x6e, 0x75, 0x6d, 0x12, 0x0b, 0x0a, + 0x2e, 0x74, 0x79, 0x70, 0x65, 0x73, 0x22, 0x97, 0x02, 0x0a, 0x09, 0x56, 0x61, 0x6c, 0x75, 0x65, + 0x54, 0x79, 0x70, 0x65, 0x22, 0x89, 0x02, 0x0a, 0x04, 0x45, 0x6e, 0x75, 0x6d, 0x12, 0x0b, 0x0a, 0x07, 0x49, 0x4e, 0x56, 0x41, 0x4c, 0x49, 0x44, 0x10, 0x00, 0x12, 0x09, 0x0a, 0x05, 0x42, 0x59, 0x54, 0x45, 0x53, 0x10, 0x01, 0x12, 0x0a, 0x0a, 0x06, 0x53, 0x54, 0x52, 0x49, 0x4e, 0x47, 0x10, 0x02, 0x12, 0x09, 0x0a, 0x05, 0x49, 0x4e, 0x54, 0x33, 0x32, 0x10, 0x03, 0x12, 0x09, 0x0a, 0x05, 0x49, 0x4e, 0x54, 0x36, 0x34, 0x10, 0x04, 0x12, 0x0a, 0x0a, 0x06, 0x44, 0x4f, 0x55, 0x42, 0x4c, 0x45, 0x10, 0x05, 0x12, 0x09, 0x0a, 0x05, 0x46, 0x4c, 0x4f, 0x41, 0x54, 0x10, 0x06, 0x12, 0x08, - 0x0a, 0x04, 0x42, 0x4f, 0x4f, 0x4c, 0x10, 0x07, 0x12, 0x0e, 0x0a, 0x0a, 0x42, 0x59, 0x54, 0x45, - 0x53, 0x5f, 0x4c, 0x49, 0x53, 0x54, 0x10, 0x0b, 0x12, 0x0f, 0x0a, 0x0b, 0x53, 0x54, 0x52, 0x49, - 0x4e, 0x47, 0x5f, 0x4c, 0x49, 0x53, 0x54, 0x10, 0x0c, 0x12, 0x0e, 0x0a, 0x0a, 0x49, 0x4e, 0x54, - 0x33, 0x32, 0x5f, 0x4c, 0x49, 0x53, 0x54, 0x10, 0x0d, 0x12, 0x0e, 0x0a, 0x0a, 0x49, 0x4e, 0x54, - 0x36, 0x34, 0x5f, 0x4c, 0x49, 0x53, 0x54, 0x10, 0x0e, 0x12, 0x0f, 0x0a, 0x0b, 0x44, 0x4f, 0x55, - 0x42, 0x4c, 0x45, 0x5f, 0x4c, 0x49, 0x53, 0x54, 0x10, 0x0f, 0x12, 0x0e, 0x0a, 0x0a, 0x46, 0x4c, - 0x4f, 0x41, 0x54, 0x5f, 0x4c, 0x49, 0x53, 0x54, 0x10, 0x10, 0x12, 0x0d, 0x0a, 0x09, 0x42, 0x4f, - 0x4f, 0x4c, 0x5f, 0x4c, 0x49, 0x53, 0x54, 0x10, 0x11, 0x22, 0xac, 0x05, 0x0a, 0x05, 0x56, 0x61, - 0x6c, 0x75, 0x65, 0x12, 0x1d, 0x0a, 0x09, 0x62, 0x79, 0x74, 0x65, 0x73, 0x5f, 0x76, 0x61, 0x6c, - 0x18, 0x01, 0x20, 0x01, 0x28, 0x0c, 0x48, 0x00, 0x52, 0x08, 0x62, 0x79, 0x74, 0x65, 0x73, 0x56, - 0x61, 0x6c, 0x12, 0x1f, 0x0a, 0x0a, 0x73, 0x74, 0x72, 0x69, 0x6e, 0x67, 0x5f, 0x76, 0x61, 0x6c, - 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x48, 0x00, 0x52, 0x09, 0x73, 0x74, 0x72, 0x69, 0x6e, 0x67, - 0x56, 0x61, 0x6c, 0x12, 0x1d, 0x0a, 0x09, 0x69, 0x6e, 0x74, 0x33, 0x32, 0x5f, 0x76, 0x61, 0x6c, - 0x18, 0x03, 0x20, 0x01, 0x28, 0x05, 0x48, 0x00, 0x52, 0x08, 0x69, 0x6e, 0x74, 0x33, 0x32, 0x56, - 0x61, 0x6c, 0x12, 0x1d, 0x0a, 0x09, 0x69, 0x6e, 0x74, 0x36, 0x34, 0x5f, 0x76, 0x61, 0x6c, 0x18, - 0x04, 0x20, 0x01, 0x28, 0x03, 0x48, 0x00, 0x52, 0x08, 0x69, 0x6e, 0x74, 0x36, 0x34, 0x56, 0x61, - 0x6c, 0x12, 0x1f, 0x0a, 0x0a, 0x64, 0x6f, 0x75, 0x62, 0x6c, 0x65, 0x5f, 0x76, 0x61, 0x6c, 0x18, - 0x05, 0x20, 0x01, 0x28, 0x01, 0x48, 0x00, 0x52, 0x09, 0x64, 0x6f, 0x75, 0x62, 0x6c, 0x65, 0x56, - 0x61, 0x6c, 0x12, 0x1d, 0x0a, 0x09, 0x66, 0x6c, 0x6f, 0x61, 0x74, 0x5f, 0x76, 0x61, 0x6c, 0x18, - 0x06, 0x20, 0x01, 0x28, 0x02, 0x48, 0x00, 0x52, 0x08, 0x66, 0x6c, 0x6f, 0x61, 0x74, 0x56, 0x61, - 0x6c, 0x12, 0x1b, 0x0a, 0x08, 0x62, 0x6f, 0x6f, 0x6c, 0x5f, 0x76, 0x61, 0x6c, 0x18, 0x07, 0x20, - 0x01, 0x28, 0x08, 0x48, 0x00, 0x52, 0x07, 0x62, 0x6f, 0x6f, 0x6c, 0x56, 0x61, 0x6c, 0x12, 0x3e, - 0x0a, 0x0e, 0x62, 0x79, 0x74, 0x65, 0x73, 0x5f, 0x6c, 0x69, 0x73, 0x74, 0x5f, 0x76, 0x61, 0x6c, - 0x18, 0x0b, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x16, 0x2e, 0x66, 0x65, 0x61, 0x73, 0x74, 0x2e, 0x74, - 0x79, 0x70, 0x65, 0x73, 0x2e, 0x42, 0x79, 0x74, 0x65, 0x73, 0x4c, 0x69, 0x73, 0x74, 0x48, 0x00, - 0x52, 0x0c, 0x62, 0x79, 0x74, 0x65, 0x73, 0x4c, 0x69, 0x73, 0x74, 0x56, 0x61, 0x6c, 0x12, 0x41, - 0x0a, 0x0f, 0x73, 0x74, 0x72, 0x69, 0x6e, 0x67, 0x5f, 0x6c, 0x69, 0x73, 0x74, 0x5f, 0x76, 0x61, - 0x6c, 0x18, 0x0c, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x17, 0x2e, 0x66, 0x65, 0x61, 0x73, 0x74, 0x2e, - 0x74, 0x79, 0x70, 0x65, 0x73, 0x2e, 0x53, 0x74, 0x72, 0x69, 0x6e, 0x67, 0x4c, 0x69, 0x73, 0x74, - 0x48, 0x00, 0x52, 0x0d, 0x73, 0x74, 0x72, 0x69, 0x6e, 0x67, 0x4c, 0x69, 0x73, 0x74, 0x56, 0x61, - 0x6c, 0x12, 0x3e, 0x0a, 0x0e, 0x69, 0x6e, 0x74, 0x33, 0x32, 0x5f, 0x6c, 0x69, 0x73, 0x74, 0x5f, - 0x76, 0x61, 0x6c, 0x18, 0x0d, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x16, 0x2e, 0x66, 0x65, 0x61, 0x73, - 0x74, 0x2e, 0x74, 0x79, 0x70, 0x65, 0x73, 0x2e, 0x49, 0x6e, 0x74, 0x33, 0x32, 0x4c, 0x69, 0x73, - 0x74, 0x48, 0x00, 0x52, 0x0c, 0x69, 0x6e, 0x74, 0x33, 0x32, 0x4c, 0x69, 0x73, 0x74, 0x56, 0x61, - 0x6c, 0x12, 0x3e, 0x0a, 0x0e, 0x69, 0x6e, 0x74, 0x36, 0x34, 0x5f, 0x6c, 0x69, 0x73, 0x74, 0x5f, - 0x76, 0x61, 0x6c, 0x18, 0x0e, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x16, 0x2e, 0x66, 0x65, 0x61, 0x73, + 0x0a, 0x04, 0x42, 0x4f, 0x4f, 0x4c, 0x10, 0x07, 0x12, 0x12, 0x0a, 0x0e, 0x55, 0x4e, 0x49, 0x58, + 0x5f, 0x54, 0x49, 0x4d, 0x45, 0x53, 0x54, 0x41, 0x4d, 0x50, 0x10, 0x08, 0x12, 0x0e, 0x0a, 0x0a, + 0x42, 0x59, 0x54, 0x45, 0x53, 0x5f, 0x4c, 0x49, 0x53, 0x54, 0x10, 0x0b, 0x12, 0x0f, 0x0a, 0x0b, + 0x53, 0x54, 0x52, 0x49, 0x4e, 0x47, 0x5f, 0x4c, 0x49, 0x53, 0x54, 0x10, 0x0c, 0x12, 0x0e, 0x0a, + 0x0a, 0x49, 0x4e, 0x54, 0x33, 0x32, 0x5f, 0x4c, 0x49, 0x53, 0x54, 0x10, 0x0d, 0x12, 0x0e, 0x0a, + 0x0a, 0x49, 0x4e, 0x54, 0x36, 0x34, 0x5f, 0x4c, 0x49, 0x53, 0x54, 0x10, 0x0e, 0x12, 0x0f, 0x0a, + 0x0b, 0x44, 0x4f, 0x55, 0x42, 0x4c, 0x45, 0x5f, 0x4c, 0x49, 0x53, 0x54, 0x10, 0x0f, 0x12, 0x0e, + 0x0a, 0x0a, 0x46, 0x4c, 0x4f, 0x41, 0x54, 0x5f, 0x4c, 0x49, 0x53, 0x54, 0x10, 0x10, 0x12, 0x0d, + 0x0a, 0x09, 0x42, 0x4f, 0x4f, 0x4c, 0x5f, 0x4c, 0x49, 0x53, 0x54, 0x10, 0x11, 0x12, 0x17, 0x0a, + 0x13, 0x55, 0x4e, 0x49, 0x58, 0x5f, 0x54, 0x49, 0x4d, 0x45, 0x53, 0x54, 0x41, 0x4d, 0x50, 0x5f, + 0x4c, 0x49, 0x53, 0x54, 0x10, 0x12, 0x12, 0x08, 0x0a, 0x04, 0x4e, 0x55, 0x4c, 0x4c, 0x10, 0x13, + 0x22, 0xdd, 0x06, 0x0a, 0x05, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x12, 0x1d, 0x0a, 0x09, 0x62, 0x79, + 0x74, 0x65, 0x73, 0x5f, 0x76, 0x61, 0x6c, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0c, 0x48, 0x00, 0x52, + 0x08, 0x62, 0x79, 0x74, 0x65, 0x73, 0x56, 0x61, 0x6c, 0x12, 0x1f, 0x0a, 0x0a, 0x73, 0x74, 0x72, + 0x69, 0x6e, 0x67, 0x5f, 0x76, 0x61, 0x6c, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x48, 0x00, 0x52, + 0x09, 0x73, 0x74, 0x72, 0x69, 0x6e, 0x67, 0x56, 0x61, 0x6c, 0x12, 0x1d, 0x0a, 0x09, 0x69, 0x6e, + 0x74, 0x33, 0x32, 0x5f, 0x76, 0x61, 0x6c, 0x18, 0x03, 0x20, 0x01, 0x28, 0x05, 0x48, 0x00, 0x52, + 0x08, 0x69, 0x6e, 0x74, 0x33, 0x32, 0x56, 0x61, 0x6c, 0x12, 0x1d, 0x0a, 0x09, 0x69, 0x6e, 0x74, + 0x36, 0x34, 0x5f, 0x76, 0x61, 0x6c, 0x18, 0x04, 0x20, 0x01, 0x28, 0x03, 0x48, 0x00, 0x52, 0x08, + 0x69, 0x6e, 0x74, 0x36, 0x34, 0x56, 0x61, 0x6c, 0x12, 0x1f, 0x0a, 0x0a, 0x64, 0x6f, 0x75, 0x62, + 0x6c, 0x65, 0x5f, 0x76, 0x61, 0x6c, 0x18, 0x05, 0x20, 0x01, 0x28, 0x01, 0x48, 0x00, 0x52, 0x09, + 0x64, 0x6f, 0x75, 0x62, 0x6c, 0x65, 0x56, 0x61, 0x6c, 0x12, 0x1d, 0x0a, 0x09, 0x66, 0x6c, 0x6f, + 0x61, 0x74, 0x5f, 0x76, 0x61, 0x6c, 0x18, 0x06, 0x20, 0x01, 0x28, 0x02, 0x48, 0x00, 0x52, 0x08, + 0x66, 0x6c, 0x6f, 0x61, 0x74, 0x56, 0x61, 0x6c, 0x12, 0x1b, 0x0a, 0x08, 0x62, 0x6f, 0x6f, 0x6c, + 0x5f, 0x76, 0x61, 0x6c, 0x18, 0x07, 0x20, 0x01, 0x28, 0x08, 0x48, 0x00, 0x52, 0x07, 0x62, 0x6f, + 0x6f, 0x6c, 0x56, 0x61, 0x6c, 0x12, 0x2e, 0x0a, 0x12, 0x75, 0x6e, 0x69, 0x78, 0x5f, 0x74, 0x69, + 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x5f, 0x76, 0x61, 0x6c, 0x18, 0x08, 0x20, 0x01, 0x28, + 0x03, 0x48, 0x00, 0x52, 0x10, 0x75, 0x6e, 0x69, 0x78, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, + 0x6d, 0x70, 0x56, 0x61, 0x6c, 0x12, 0x3e, 0x0a, 0x0e, 0x62, 0x79, 0x74, 0x65, 0x73, 0x5f, 0x6c, + 0x69, 0x73, 0x74, 0x5f, 0x76, 0x61, 0x6c, 0x18, 0x0b, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x16, 0x2e, + 0x66, 0x65, 0x61, 0x73, 0x74, 0x2e, 0x74, 0x79, 0x70, 0x65, 0x73, 0x2e, 0x42, 0x79, 0x74, 0x65, + 0x73, 0x4c, 0x69, 0x73, 0x74, 0x48, 0x00, 0x52, 0x0c, 0x62, 0x79, 0x74, 0x65, 0x73, 0x4c, 0x69, + 0x73, 0x74, 0x56, 0x61, 0x6c, 0x12, 0x41, 0x0a, 0x0f, 0x73, 0x74, 0x72, 0x69, 0x6e, 0x67, 0x5f, + 0x6c, 0x69, 0x73, 0x74, 0x5f, 0x76, 0x61, 0x6c, 0x18, 0x0c, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x17, + 0x2e, 0x66, 0x65, 0x61, 0x73, 0x74, 0x2e, 0x74, 0x79, 0x70, 0x65, 0x73, 0x2e, 0x53, 0x74, 0x72, + 0x69, 0x6e, 0x67, 0x4c, 0x69, 0x73, 0x74, 0x48, 0x00, 0x52, 0x0d, 0x73, 0x74, 0x72, 0x69, 0x6e, + 0x67, 0x4c, 0x69, 0x73, 0x74, 0x56, 0x61, 0x6c, 0x12, 0x3e, 0x0a, 0x0e, 0x69, 0x6e, 0x74, 0x33, + 0x32, 0x5f, 0x6c, 0x69, 0x73, 0x74, 0x5f, 0x76, 0x61, 0x6c, 0x18, 0x0d, 0x20, 0x01, 0x28, 0x0b, + 0x32, 0x16, 0x2e, 0x66, 0x65, 0x61, 0x73, 0x74, 0x2e, 0x74, 0x79, 0x70, 0x65, 0x73, 0x2e, 0x49, + 0x6e, 0x74, 0x33, 0x32, 0x4c, 0x69, 0x73, 0x74, 0x48, 0x00, 0x52, 0x0c, 0x69, 0x6e, 0x74, 0x33, + 0x32, 0x4c, 0x69, 0x73, 0x74, 0x56, 0x61, 0x6c, 0x12, 0x3e, 0x0a, 0x0e, 0x69, 0x6e, 0x74, 0x36, + 0x34, 0x5f, 0x6c, 0x69, 0x73, 0x74, 0x5f, 0x76, 0x61, 0x6c, 0x18, 0x0e, 0x20, 0x01, 0x28, 0x0b, + 0x32, 0x16, 0x2e, 0x66, 0x65, 0x61, 0x73, 0x74, 0x2e, 0x74, 0x79, 0x70, 0x65, 0x73, 0x2e, 0x49, + 0x6e, 0x74, 0x36, 0x34, 0x4c, 0x69, 0x73, 0x74, 0x48, 0x00, 0x52, 0x0c, 0x69, 0x6e, 0x74, 0x36, + 0x34, 0x4c, 0x69, 0x73, 0x74, 0x56, 0x61, 0x6c, 0x12, 0x41, 0x0a, 0x0f, 0x64, 0x6f, 0x75, 0x62, + 0x6c, 0x65, 0x5f, 0x6c, 0x69, 0x73, 0x74, 0x5f, 0x76, 0x61, 0x6c, 0x18, 0x0f, 0x20, 0x01, 0x28, + 0x0b, 0x32, 0x17, 0x2e, 0x66, 0x65, 0x61, 0x73, 0x74, 0x2e, 0x74, 0x79, 0x70, 0x65, 0x73, 0x2e, + 0x44, 0x6f, 0x75, 0x62, 0x6c, 0x65, 0x4c, 0x69, 0x73, 0x74, 0x48, 0x00, 0x52, 0x0d, 0x64, 0x6f, + 0x75, 0x62, 0x6c, 0x65, 0x4c, 0x69, 0x73, 0x74, 0x56, 0x61, 0x6c, 0x12, 0x3e, 0x0a, 0x0e, 0x66, + 0x6c, 0x6f, 0x61, 0x74, 0x5f, 0x6c, 0x69, 0x73, 0x74, 0x5f, 0x76, 0x61, 0x6c, 0x18, 0x10, 0x20, + 0x01, 0x28, 0x0b, 0x32, 0x16, 0x2e, 0x66, 0x65, 0x61, 0x73, 0x74, 0x2e, 0x74, 0x79, 0x70, 0x65, + 0x73, 0x2e, 0x46, 0x6c, 0x6f, 0x61, 0x74, 0x4c, 0x69, 0x73, 0x74, 0x48, 0x00, 0x52, 0x0c, 0x66, + 0x6c, 0x6f, 0x61, 0x74, 0x4c, 0x69, 0x73, 0x74, 0x56, 0x61, 0x6c, 0x12, 0x3b, 0x0a, 0x0d, 0x62, + 0x6f, 0x6f, 0x6c, 0x5f, 0x6c, 0x69, 0x73, 0x74, 0x5f, 0x76, 0x61, 0x6c, 0x18, 0x11, 0x20, 0x01, + 0x28, 0x0b, 0x32, 0x15, 0x2e, 0x66, 0x65, 0x61, 0x73, 0x74, 0x2e, 0x74, 0x79, 0x70, 0x65, 0x73, + 0x2e, 0x42, 0x6f, 0x6f, 0x6c, 0x4c, 0x69, 0x73, 0x74, 0x48, 0x00, 0x52, 0x0b, 0x62, 0x6f, 0x6f, + 0x6c, 0x4c, 0x69, 0x73, 0x74, 0x56, 0x61, 0x6c, 0x12, 0x4f, 0x0a, 0x17, 0x75, 0x6e, 0x69, 0x78, + 0x5f, 0x74, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x5f, 0x6c, 0x69, 0x73, 0x74, 0x5f, + 0x76, 0x61, 0x6c, 0x18, 0x12, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x16, 0x2e, 0x66, 0x65, 0x61, 0x73, 0x74, 0x2e, 0x74, 0x79, 0x70, 0x65, 0x73, 0x2e, 0x49, 0x6e, 0x74, 0x36, 0x34, 0x4c, 0x69, 0x73, - 0x74, 0x48, 0x00, 0x52, 0x0c, 0x69, 0x6e, 0x74, 0x36, 0x34, 0x4c, 0x69, 0x73, 0x74, 0x56, 0x61, - 0x6c, 0x12, 0x41, 0x0a, 0x0f, 0x64, 0x6f, 0x75, 0x62, 0x6c, 0x65, 0x5f, 0x6c, 0x69, 0x73, 0x74, - 0x5f, 0x76, 0x61, 0x6c, 0x18, 0x0f, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x17, 0x2e, 0x66, 0x65, 0x61, - 0x73, 0x74, 0x2e, 0x74, 0x79, 0x70, 0x65, 0x73, 0x2e, 0x44, 0x6f, 0x75, 0x62, 0x6c, 0x65, 0x4c, - 0x69, 0x73, 0x74, 0x48, 0x00, 0x52, 0x0d, 0x64, 0x6f, 0x75, 0x62, 0x6c, 0x65, 0x4c, 0x69, 0x73, - 0x74, 0x56, 0x61, 0x6c, 0x12, 0x3e, 0x0a, 0x0e, 0x66, 0x6c, 0x6f, 0x61, 0x74, 0x5f, 0x6c, 0x69, - 0x73, 0x74, 0x5f, 0x76, 0x61, 0x6c, 0x18, 0x10, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x16, 0x2e, 0x66, - 0x65, 0x61, 0x73, 0x74, 0x2e, 0x74, 0x79, 0x70, 0x65, 0x73, 0x2e, 0x46, 0x6c, 0x6f, 0x61, 0x74, - 0x4c, 0x69, 0x73, 0x74, 0x48, 0x00, 0x52, 0x0c, 0x66, 0x6c, 0x6f, 0x61, 0x74, 0x4c, 0x69, 0x73, - 0x74, 0x56, 0x61, 0x6c, 0x12, 0x3b, 0x0a, 0x0d, 0x62, 0x6f, 0x6f, 0x6c, 0x5f, 0x6c, 0x69, 0x73, - 0x74, 0x5f, 0x76, 0x61, 0x6c, 0x18, 0x11, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x15, 0x2e, 0x66, 0x65, - 0x61, 0x73, 0x74, 0x2e, 0x74, 0x79, 0x70, 0x65, 0x73, 0x2e, 0x42, 0x6f, 0x6f, 0x6c, 0x4c, 0x69, - 0x73, 0x74, 0x48, 0x00, 0x52, 0x0b, 0x62, 0x6f, 0x6f, 0x6c, 0x4c, 0x69, 0x73, 0x74, 0x56, 0x61, - 0x6c, 0x42, 0x05, 0x0a, 0x03, 0x76, 0x61, 0x6c, 0x22, 0x1d, 0x0a, 0x09, 0x42, 0x79, 0x74, 0x65, - 0x73, 0x4c, 0x69, 0x73, 0x74, 0x12, 0x10, 0x0a, 0x03, 0x76, 0x61, 0x6c, 0x18, 0x01, 0x20, 0x03, - 0x28, 0x0c, 0x52, 0x03, 0x76, 0x61, 0x6c, 0x22, 0x1e, 0x0a, 0x0a, 0x53, 0x74, 0x72, 0x69, 0x6e, - 0x67, 0x4c, 0x69, 0x73, 0x74, 0x12, 0x10, 0x0a, 0x03, 0x76, 0x61, 0x6c, 0x18, 0x01, 0x20, 0x03, - 0x28, 0x09, 0x52, 0x03, 0x76, 0x61, 0x6c, 0x22, 0x1d, 0x0a, 0x09, 0x49, 0x6e, 0x74, 0x33, 0x32, - 0x4c, 0x69, 0x73, 0x74, 0x12, 0x10, 0x0a, 0x03, 0x76, 0x61, 0x6c, 0x18, 0x01, 0x20, 0x03, 0x28, - 0x05, 0x52, 0x03, 0x76, 0x61, 0x6c, 0x22, 0x1d, 0x0a, 0x09, 0x49, 0x6e, 0x74, 0x36, 0x34, 0x4c, - 0x69, 0x73, 0x74, 0x12, 0x10, 0x0a, 0x03, 0x76, 0x61, 0x6c, 0x18, 0x01, 0x20, 0x03, 0x28, 0x03, - 0x52, 0x03, 0x76, 0x61, 0x6c, 0x22, 0x1e, 0x0a, 0x0a, 0x44, 0x6f, 0x75, 0x62, 0x6c, 0x65, 0x4c, - 0x69, 0x73, 0x74, 0x12, 0x10, 0x0a, 0x03, 0x76, 0x61, 0x6c, 0x18, 0x01, 0x20, 0x03, 0x28, 0x01, - 0x52, 0x03, 0x76, 0x61, 0x6c, 0x22, 0x1d, 0x0a, 0x09, 0x46, 0x6c, 0x6f, 0x61, 0x74, 0x4c, 0x69, - 0x73, 0x74, 0x12, 0x10, 0x0a, 0x03, 0x76, 0x61, 0x6c, 0x18, 0x01, 0x20, 0x03, 0x28, 0x02, 0x52, - 0x03, 0x76, 0x61, 0x6c, 0x22, 0x1c, 0x0a, 0x08, 0x42, 0x6f, 0x6f, 0x6c, 0x4c, 0x69, 0x73, 0x74, - 0x12, 0x10, 0x0a, 0x03, 0x76, 0x61, 0x6c, 0x18, 0x01, 0x20, 0x03, 0x28, 0x08, 0x52, 0x03, 0x76, - 0x61, 0x6c, 0x42, 0x55, 0x0a, 0x11, 0x66, 0x65, 0x61, 0x73, 0x74, 0x2e, 0x70, 0x72, 0x6f, 0x74, - 0x6f, 0x2e, 0x74, 0x79, 0x70, 0x65, 0x73, 0x42, 0x0a, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x50, 0x72, - 0x6f, 0x74, 0x6f, 0x5a, 0x34, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, - 0x66, 0x65, 0x61, 0x73, 0x74, 0x2d, 0x64, 0x65, 0x76, 0x2f, 0x66, 0x65, 0x61, 0x73, 0x74, 0x2f, - 0x73, 0x64, 0x6b, 0x2f, 0x67, 0x6f, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x73, 0x2f, 0x66, 0x65, - 0x61, 0x73, 0x74, 0x2f, 0x74, 0x79, 0x70, 0x65, 0x73, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, - 0x33, + 0x74, 0x48, 0x00, 0x52, 0x14, 0x75, 0x6e, 0x69, 0x78, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, + 0x6d, 0x70, 0x4c, 0x69, 0x73, 0x74, 0x56, 0x61, 0x6c, 0x12, 0x2e, 0x0a, 0x08, 0x6e, 0x75, 0x6c, + 0x6c, 0x5f, 0x76, 0x61, 0x6c, 0x18, 0x13, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x11, 0x2e, 0x66, 0x65, + 0x61, 0x73, 0x74, 0x2e, 0x74, 0x79, 0x70, 0x65, 0x73, 0x2e, 0x4e, 0x75, 0x6c, 0x6c, 0x48, 0x00, + 0x52, 0x07, 0x6e, 0x75, 0x6c, 0x6c, 0x56, 0x61, 0x6c, 0x42, 0x05, 0x0a, 0x03, 0x76, 0x61, 0x6c, + 0x22, 0x1d, 0x0a, 0x09, 0x42, 0x79, 0x74, 0x65, 0x73, 0x4c, 0x69, 0x73, 0x74, 0x12, 0x10, 0x0a, + 0x03, 0x76, 0x61, 0x6c, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0c, 0x52, 0x03, 0x76, 0x61, 0x6c, 0x22, + 0x1e, 0x0a, 0x0a, 0x53, 0x74, 0x72, 0x69, 0x6e, 0x67, 0x4c, 0x69, 0x73, 0x74, 0x12, 0x10, 0x0a, + 0x03, 0x76, 0x61, 0x6c, 0x18, 0x01, 0x20, 0x03, 0x28, 0x09, 0x52, 0x03, 0x76, 0x61, 0x6c, 0x22, + 0x1d, 0x0a, 0x09, 0x49, 0x6e, 0x74, 0x33, 0x32, 0x4c, 0x69, 0x73, 0x74, 0x12, 0x10, 0x0a, 0x03, + 0x76, 0x61, 0x6c, 0x18, 0x01, 0x20, 0x03, 0x28, 0x05, 0x52, 0x03, 0x76, 0x61, 0x6c, 0x22, 0x1d, + 0x0a, 0x09, 0x49, 0x6e, 0x74, 0x36, 0x34, 0x4c, 0x69, 0x73, 0x74, 0x12, 0x10, 0x0a, 0x03, 0x76, + 0x61, 0x6c, 0x18, 0x01, 0x20, 0x03, 0x28, 0x03, 0x52, 0x03, 0x76, 0x61, 0x6c, 0x22, 0x1e, 0x0a, + 0x0a, 0x44, 0x6f, 0x75, 0x62, 0x6c, 0x65, 0x4c, 0x69, 0x73, 0x74, 0x12, 0x10, 0x0a, 0x03, 0x76, + 0x61, 0x6c, 0x18, 0x01, 0x20, 0x03, 0x28, 0x01, 0x52, 0x03, 0x76, 0x61, 0x6c, 0x22, 0x1d, 0x0a, + 0x09, 0x46, 0x6c, 0x6f, 0x61, 0x74, 0x4c, 0x69, 0x73, 0x74, 0x12, 0x10, 0x0a, 0x03, 0x76, 0x61, + 0x6c, 0x18, 0x01, 0x20, 0x03, 0x28, 0x02, 0x52, 0x03, 0x76, 0x61, 0x6c, 0x22, 0x1c, 0x0a, 0x08, + 0x42, 0x6f, 0x6f, 0x6c, 0x4c, 0x69, 0x73, 0x74, 0x12, 0x10, 0x0a, 0x03, 0x76, 0x61, 0x6c, 0x18, + 0x01, 0x20, 0x03, 0x28, 0x08, 0x52, 0x03, 0x76, 0x61, 0x6c, 0x22, 0x35, 0x0a, 0x0d, 0x52, 0x65, + 0x70, 0x65, 0x61, 0x74, 0x65, 0x64, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x12, 0x24, 0x0a, 0x03, 0x76, + 0x61, 0x6c, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x12, 0x2e, 0x66, 0x65, 0x61, 0x73, 0x74, + 0x2e, 0x74, 0x79, 0x70, 0x65, 0x73, 0x2e, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x52, 0x03, 0x76, 0x61, + 0x6c, 0x2a, 0x10, 0x0a, 0x04, 0x4e, 0x75, 0x6c, 0x6c, 0x12, 0x08, 0x0a, 0x04, 0x4e, 0x55, 0x4c, + 0x4c, 0x10, 0x00, 0x42, 0x55, 0x0a, 0x11, 0x66, 0x65, 0x61, 0x73, 0x74, 0x2e, 0x70, 0x72, 0x6f, + 0x74, 0x6f, 0x2e, 0x74, 0x79, 0x70, 0x65, 0x73, 0x42, 0x0a, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x50, + 0x72, 0x6f, 0x74, 0x6f, 0x5a, 0x34, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, + 0x2f, 0x66, 0x65, 0x61, 0x73, 0x74, 0x2d, 0x64, 0x65, 0x76, 0x2f, 0x66, 0x65, 0x61, 0x73, 0x74, + 0x2f, 0x73, 0x64, 0x6b, 0x2f, 0x67, 0x6f, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x73, 0x2f, 0x66, + 0x65, 0x61, 0x73, 0x74, 0x2f, 0x74, 0x79, 0x70, 0x65, 0x73, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, + 0x6f, 0x33, } var ( @@ -840,33 +998,38 @@ func file_feast_types_Value_proto_rawDescGZIP() []byte { return file_feast_types_Value_proto_rawDescData } -var file_feast_types_Value_proto_enumTypes = make([]protoimpl.EnumInfo, 1) -var file_feast_types_Value_proto_msgTypes = make([]protoimpl.MessageInfo, 9) +var file_feast_types_Value_proto_enumTypes = make([]protoimpl.EnumInfo, 2) +var file_feast_types_Value_proto_msgTypes = make([]protoimpl.MessageInfo, 10) var file_feast_types_Value_proto_goTypes = []interface{}{ - (ValueType_Enum)(0), // 0: feast.types.ValueType.Enum - (*ValueType)(nil), // 1: feast.types.ValueType - (*Value)(nil), // 2: feast.types.Value - (*BytesList)(nil), // 3: feast.types.BytesList - (*StringList)(nil), // 4: feast.types.StringList - (*Int32List)(nil), // 5: feast.types.Int32List - (*Int64List)(nil), // 6: feast.types.Int64List - (*DoubleList)(nil), // 7: feast.types.DoubleList - (*FloatList)(nil), // 8: feast.types.FloatList - (*BoolList)(nil), // 9: feast.types.BoolList + (Null)(0), // 0: feast.types.Null + (ValueType_Enum)(0), // 1: feast.types.ValueType.Enum + (*ValueType)(nil), // 2: feast.types.ValueType + (*Value)(nil), // 3: feast.types.Value + (*BytesList)(nil), // 4: feast.types.BytesList + (*StringList)(nil), // 5: feast.types.StringList + (*Int32List)(nil), // 6: feast.types.Int32List + (*Int64List)(nil), // 7: feast.types.Int64List + (*DoubleList)(nil), // 8: feast.types.DoubleList + (*FloatList)(nil), // 9: feast.types.FloatList + (*BoolList)(nil), // 10: feast.types.BoolList + (*RepeatedValue)(nil), // 11: feast.types.RepeatedValue } var file_feast_types_Value_proto_depIdxs = []int32{ - 3, // 0: feast.types.Value.bytes_list_val:type_name -> feast.types.BytesList - 4, // 1: feast.types.Value.string_list_val:type_name -> feast.types.StringList - 5, // 2: feast.types.Value.int32_list_val:type_name -> feast.types.Int32List - 6, // 3: feast.types.Value.int64_list_val:type_name -> feast.types.Int64List - 7, // 4: feast.types.Value.double_list_val:type_name -> feast.types.DoubleList - 8, // 5: feast.types.Value.float_list_val:type_name -> feast.types.FloatList - 9, // 6: feast.types.Value.bool_list_val:type_name -> feast.types.BoolList - 7, // [7:7] is the sub-list for method output_type - 7, // [7:7] is the sub-list for method input_type - 7, // [7:7] is the sub-list for extension type_name - 7, // [7:7] is the sub-list for extension extendee - 0, // [0:7] is the sub-list for field type_name + 4, // 0: feast.types.Value.bytes_list_val:type_name -> feast.types.BytesList + 5, // 1: feast.types.Value.string_list_val:type_name -> feast.types.StringList + 6, // 2: feast.types.Value.int32_list_val:type_name -> feast.types.Int32List + 7, // 3: feast.types.Value.int64_list_val:type_name -> feast.types.Int64List + 8, // 4: feast.types.Value.double_list_val:type_name -> feast.types.DoubleList + 9, // 5: feast.types.Value.float_list_val:type_name -> feast.types.FloatList + 10, // 6: feast.types.Value.bool_list_val:type_name -> feast.types.BoolList + 7, // 7: feast.types.Value.unix_timestamp_list_val:type_name -> feast.types.Int64List + 0, // 8: feast.types.Value.null_val:type_name -> feast.types.Null + 3, // 9: feast.types.RepeatedValue.val:type_name -> feast.types.Value + 10, // [10:10] is the sub-list for method output_type + 10, // [10:10] is the sub-list for method input_type + 10, // [10:10] is the sub-list for extension type_name + 10, // [10:10] is the sub-list for extension extendee + 0, // [0:10] is the sub-list for field type_name } func init() { file_feast_types_Value_proto_init() } @@ -983,6 +1146,18 @@ func file_feast_types_Value_proto_init() { return nil } } + file_feast_types_Value_proto_msgTypes[9].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*RepeatedValue); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } } file_feast_types_Value_proto_msgTypes[1].OneofWrappers = []interface{}{ (*Value_BytesVal)(nil), @@ -992,6 +1167,7 @@ func file_feast_types_Value_proto_init() { (*Value_DoubleVal)(nil), (*Value_FloatVal)(nil), (*Value_BoolVal)(nil), + (*Value_UnixTimestampVal)(nil), (*Value_BytesListVal)(nil), (*Value_StringListVal)(nil), (*Value_Int32ListVal)(nil), @@ -999,14 +1175,16 @@ func file_feast_types_Value_proto_init() { (*Value_DoubleListVal)(nil), (*Value_FloatListVal)(nil), (*Value_BoolListVal)(nil), + (*Value_UnixTimestampListVal)(nil), + (*Value_NullVal)(nil), } type x struct{} out := protoimpl.TypeBuilder{ File: protoimpl.DescBuilder{ GoPackagePath: reflect.TypeOf(x{}).PkgPath(), RawDescriptor: file_feast_types_Value_proto_rawDesc, - NumEnums: 1, - NumMessages: 9, + NumEnums: 2, + NumMessages: 10, NumExtensions: 0, NumServices: 0, }, diff --git a/sdk/go/protos/tensorflow_metadata/proto/v0/path.pb.go b/sdk/go/protos/tensorflow_metadata/proto/v0/path.pb.go index 1daa7687f94..5abc145a622 100644 --- a/sdk/go/protos/tensorflow_metadata/proto/v0/path.pb.go +++ b/sdk/go/protos/tensorflow_metadata/proto/v0/path.pb.go @@ -15,14 +15,13 @@ // Code generated by protoc-gen-go. DO NOT EDIT. // versions: -// protoc-gen-go v1.25.0 -// protoc v3.12.4 +// protoc-gen-go v1.27.1 +// protoc v3.11.2 // source: tensorflow_metadata/proto/v0/path.proto package v0 import ( - proto "github.com/golang/protobuf/proto" protoreflect "google.golang.org/protobuf/reflect/protoreflect" protoimpl "google.golang.org/protobuf/runtime/protoimpl" reflect "reflect" @@ -36,10 +35,6 @@ const ( _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) ) -// This is a compile-time assertion that a sufficiently up-to-date version -// of the legacy proto package is being used. -const _ = proto.ProtoPackageIsVersion4 - // A path is a more general substitute for the name of a field or feature that // can be used for flat examples as well as structured data. For example, if // we had data in a protocol buffer: diff --git a/sdk/go/protos/tensorflow_metadata/proto/v0/schema.pb.go b/sdk/go/protos/tensorflow_metadata/proto/v0/schema.pb.go index 940779a1917..0200d3afb61 100644 --- a/sdk/go/protos/tensorflow_metadata/proto/v0/schema.pb.go +++ b/sdk/go/protos/tensorflow_metadata/proto/v0/schema.pb.go @@ -15,17 +15,16 @@ // Code generated by protoc-gen-go. DO NOT EDIT. // versions: -// protoc-gen-go v1.25.0 -// protoc v3.12.4 +// protoc-gen-go v1.27.1 +// protoc v3.11.2 // source: tensorflow_metadata/proto/v0/schema.proto package v0 import ( - proto "github.com/golang/protobuf/proto" - any "github.com/golang/protobuf/ptypes/any" protoreflect "google.golang.org/protobuf/reflect/protoreflect" protoimpl "google.golang.org/protobuf/runtime/protoimpl" + anypb "google.golang.org/protobuf/types/known/anypb" reflect "reflect" sync "sync" ) @@ -37,10 +36,6 @@ const ( _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) ) -// This is a compile-time assertion that a sufficiently up-to-date version -// of the legacy proto package is being used. -const _ = proto.ProtoPackageIsVersion4 - // LifecycleStage. Only UNKNOWN_STAGE, BETA, and PRODUCTION features are // actually validated. // PLANNED, ALPHA, and DEBUG are treated as DEPRECATED. @@ -908,7 +903,7 @@ type Annotation struct { // developer notes etc. Comment []string `protobuf:"bytes,2,rep,name=comment" json:"comment,omitempty"` // Application-specific metadata may be attached here. - ExtraMetadata []*any.Any `protobuf:"bytes,3,rep,name=extra_metadata,json=extraMetadata" json:"extra_metadata,omitempty"` + ExtraMetadata []*anypb.Any `protobuf:"bytes,3,rep,name=extra_metadata,json=extraMetadata" json:"extra_metadata,omitempty"` } func (x *Annotation) Reset() { @@ -957,7 +952,7 @@ func (x *Annotation) GetComment() []string { return nil } -func (x *Annotation) GetExtraMetadata() []*any.Any { +func (x *Annotation) GetExtraMetadata() []*anypb.Any { if x != nil { return x.ExtraMetadata } @@ -3539,9 +3534,9 @@ var file_tensorflow_metadata_proto_v0_schema_proto_goTypes = []interface{}{ (*TensorRepresentation_DenseTensor)(nil), // 36: tensorflow.metadata.v0.TensorRepresentation.DenseTensor (*TensorRepresentation_VarLenSparseTensor)(nil), // 37: tensorflow.metadata.v0.TensorRepresentation.VarLenSparseTensor (*TensorRepresentation_SparseTensor)(nil), // 38: tensorflow.metadata.v0.TensorRepresentation.SparseTensor - nil, // 39: tensorflow.metadata.v0.TensorRepresentationGroup.TensorRepresentationEntry - (*any.Any)(nil), // 40: google.protobuf.Any - (*Path)(nil), // 41: tensorflow.metadata.v0.Path + nil, // 39: tensorflow.metadata.v0.TensorRepresentationGroup.TensorRepresentationEntry + (*anypb.Any)(nil), // 40: google.protobuf.Any + (*Path)(nil), // 41: tensorflow.metadata.v0.Path } var file_tensorflow_metadata_proto_v0_schema_proto_depIdxs = []int32{ 5, // 0: tensorflow.metadata.v0.Schema.feature:type_name -> tensorflow.metadata.v0.Feature diff --git a/sdk/go/protos/tensorflow_metadata/proto/v0/statistics.pb.go b/sdk/go/protos/tensorflow_metadata/proto/v0/statistics.pb.go index fbf6247a1d3..cd8bdf5d894 100644 --- a/sdk/go/protos/tensorflow_metadata/proto/v0/statistics.pb.go +++ b/sdk/go/protos/tensorflow_metadata/proto/v0/statistics.pb.go @@ -19,14 +19,13 @@ // Code generated by protoc-gen-go. DO NOT EDIT. // versions: -// protoc-gen-go v1.25.0 -// protoc v3.12.4 +// protoc-gen-go v1.27.1 +// protoc v3.11.2 // source: tensorflow_metadata/proto/v0/statistics.proto package v0 import ( - proto "github.com/golang/protobuf/proto" protoreflect "google.golang.org/protobuf/reflect/protoreflect" protoimpl "google.golang.org/protobuf/runtime/protoimpl" reflect "reflect" @@ -40,10 +39,6 @@ const ( _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) ) -// This is a compile-time assertion that a sufficiently up-to-date version -// of the legacy proto package is being used. -const _ = proto.ProtoPackageIsVersion4 - // The types supported by the feature statistics. When aggregating // tf.Examples, if the bytelist contains a string, it is recommended to encode // it here as STRING instead of BYTES in order to calculate string-specific diff --git a/sdk/python/feast/cli.py b/sdk/python/feast/cli.py index cf26a80d3cb..a01d387a225 100644 --- a/sdk/python/feast/cli.py +++ b/sdk/python/feast/cli.py @@ -456,6 +456,20 @@ def serve_command(ctx: click.Context, port: int): store.serve(port) +@cli.command("serve_transformations") +@click.option( + "--port", "-p", type=click.INT, default=6569, help="Specify a port for the server" +) +@click.pass_context +def serve_transformations_command(ctx: click.Context, port: int): + """[Experimental] Start a the feature consumption server locally on a given port.""" + repo = ctx.obj["CHDIR"] + cli_check_repo(repo) + store = FeatureStore(repo_path=str(repo)) + + store.serve_transformations(port) + + @cli.group(name="alpha") def alpha_cmd(): """ diff --git a/sdk/python/feast/feature_store.py b/sdk/python/feast/feature_store.py index d6d0953d4cb..3fc510eb38e 100644 --- a/sdk/python/feast/feature_store.py +++ b/sdk/python/feast/feature_store.py @@ -1102,6 +1102,18 @@ def get_feature_server_endpoint(self) -> Optional[str]: """Returns endpoint for the feature server, if it exists.""" return self._provider.get_feature_server_endpoint() + @log_exceptions_and_usage + def serve_transformations(self, port: int) -> None: + """Start the feature transformation server locally on a given port.""" + if not flags_helper.enable_python_feature_server(self.config): + raise ExperimentalFeatureNotEnabled(flags.FLAG_PYTHON_FEATURE_SERVER_NAME) + if not flags_helper.enable_on_demand_feature_views(self.config): + raise ExperimentalFeatureNotEnabled(flags.FLAG_ON_DEMAND_TRANSFORM_NAME) + + from feast import transformation_server + + transformation_server.start_server(self, port) + def _entity_row_to_key(row: GetOnlineFeaturesRequestV2.EntityRow) -> EntityKeyProto: names, values = zip(*row.fields.items()) diff --git a/sdk/python/feast/repo_config.py b/sdk/python/feast/repo_config.py index 9a04723201a..1aa6ec2f7a4 100644 --- a/sdk/python/feast/repo_config.py +++ b/sdk/python/feast/repo_config.py @@ -174,7 +174,6 @@ def _validate_online_store_config(cls, values): raise ValidationError( [ErrorWrapper(e, loc="online_store")], model=RepoConfig, ) - return values @root_validator(pre=True) diff --git a/sdk/python/feast/transformation_server.py b/sdk/python/feast/transformation_server.py new file mode 100644 index 00000000000..6793db5ccb2 --- /dev/null +++ b/sdk/python/feast/transformation_server.py @@ -0,0 +1,74 @@ +import logging +import sys +from concurrent import futures + +import grpc +import pyarrow as pa +from grpc_reflection.v1alpha import reflection + +from feast.errors import OnDemandFeatureViewNotFoundException +from feast.feature_store import FeatureStore +from feast.protos.feast.serving.TransformationService_pb2 import ( + DESCRIPTOR, + TRANSFORMATION_SERVICE_TYPE_PYTHON, + GetTransformationServiceInfoResponse, + TransformFeaturesResponse, + ValueType, +) +from feast.protos.feast.serving.TransformationService_pb2_grpc import ( + TransformationServiceServicer, + add_TransformationServiceServicer_to_server, +) +from feast.version import get_version + +log = logging.getLogger(__name__) + + +class TransformationServer(TransformationServiceServicer): + def __init__(self, fs: FeatureStore) -> None: + super().__init__() + self.fs = fs + + def GetTransformationServiceInfo(self, request, context): + response = GetTransformationServiceInfoResponse( + type=TRANSFORMATION_SERVICE_TYPE_PYTHON, + transformation_service_type_details=f"Python: {sys.version}, Feast: {get_version()}", + ) + return response + + def TransformFeatures(self, request, context): + try: + odfv = self.fs.get_on_demand_feature_view( + request.on_demand_feature_view_name + ) + except OnDemandFeatureViewNotFoundException: + context.set_code(grpc.StatusCode.INVALID_ARGUMENT) + raise + + df = pa.ipc.open_file(request.transformation_input.arrow_value).read_pandas() + + result_df = odfv.get_transformed_features_df(True, df) + result_arrow = pa.Table.from_pandas(result_df) + sink = pa.BufferOutputStream() + writer = pa.ipc.new_file(sink, result_arrow.schema) + writer.write_table(result_arrow) + writer.close() + + buf = sink.getvalue().to_pybytes() + + return TransformFeaturesResponse( + transformation_output=ValueType(arrow_value=buf) + ) + + +def start_server(store: FeatureStore, port: int): + server = grpc.server(futures.ThreadPoolExecutor(max_workers=10)) + add_TransformationServiceServicer_to_server(TransformationServer(store), server) + service_names_available_for_reflection = ( + DESCRIPTOR.services_by_name["TransformationService"].full_name, + reflection.SERVICE_NAME, + ) + reflection.enable_server_reflection(service_names_available_for_reflection, server) + server.add_insecure_port(f"[::]:{port}") + server.start() + server.wait_for_termination() diff --git a/sdk/python/setup.py b/sdk/python/setup.py index ea4bb670b89..7337df7cbaf 100644 --- a/sdk/python/setup.py +++ b/sdk/python/setup.py @@ -47,6 +47,7 @@ "google-api-core>=1.23.0", "googleapis-common-protos==1.52.*", "grpcio>=1.34.0", + "grpcio-reflection>=1.34.0" "Jinja2>=2.0.0", "jsonschema", "mmh3", From 6b10a82438112568faaae885c8bdafde0afa7a79 Mon Sep 17 00:00:00 2001 From: Judah Rand <17158624+judahrand@users.noreply.github.com> Date: Fri, 15 Oct 2021 18:03:09 +0100 Subject: [PATCH 11/50] Simplify BigQuery load jobs (#1935) * Simplify BigQuery load jobs Signed-off-by: Judah Rand <17158624+judahrand@users.noreply.github.com> * Ensure `pyarrow` supports `use_compliant_nested_type` Signed-off-by: Judah Rand <17158624+judahrand@users.noreply.github.com> --- .../feast/infra/offline_stores/bigquery.py | 44 ++----------------- sdk/python/setup.py | 6 +-- .../universal/data_sources/bigquery.py | 7 +-- sdk/python/tests/utils/data_source_utils.py | 3 +- 4 files changed, 9 insertions(+), 51 deletions(-) diff --git a/sdk/python/feast/infra/offline_stores/bigquery.py b/sdk/python/feast/infra/offline_stores/bigquery.py index bb750a8c97c..6753223f2ed 100644 --- a/sdk/python/feast/infra/offline_stores/bigquery.py +++ b/sdk/python/feast/infra/offline_stores/bigquery.py @@ -256,8 +256,8 @@ def to_bigquery( job_config = bigquery.QueryJobConfig(destination=path) if not job_config.dry_run and self.on_demand_feature_views: - job = _write_pyarrow_table_to_bq( - self.client, self.to_arrow(), job_config.destination + job = self.client.load_table_from_dataframe( + self.to_df(), job_config.destination ) job.result() print(f"Done writing to '{job_config.destination}'.") @@ -366,7 +366,7 @@ def _upload_entity_df_and_get_entity_schema( elif isinstance(entity_df, pd.DataFrame): # Drop the index so that we dont have unnecessary columns entity_df.reset_index(drop=True, inplace=True) - job = _write_df_to_bq(client, entity_df, table_name) + job = client.load_table_from_dataframe(entity_df, table_name) block_until_done(client, job) entity_schema = dict(zip(entity_df.columns, entity_df.dtypes)) else: @@ -400,44 +400,6 @@ def _get_bigquery_client(project: Optional[str] = None, location: Optional[str] return client -def _write_df_to_bq( - client: bigquery.Client, df: pd.DataFrame, table_name: str -) -> bigquery.LoadJob: - # It is complicated to get BQ to understand that we want an ARRAY - # https://cloud.google.com/bigquery/docs/reference/rest/v2/tables#parquetoptions - # https://github.com/googleapis/python-bigquery/issues/19 - writer = pyarrow.BufferOutputStream() - pyarrow.parquet.write_table( - pyarrow.Table.from_pandas(df), writer, use_compliant_nested_type=True - ) - return _write_pyarrow_buffer_to_bq(client, writer.getvalue(), table_name,) - - -def _write_pyarrow_table_to_bq( - client: bigquery.Client, table: pyarrow.Table, table_name: str -) -> bigquery.LoadJob: - # It is complicated to get BQ to understand that we want an ARRAY - # https://cloud.google.com/bigquery/docs/reference/rest/v2/tables#parquetoptions - # https://github.com/googleapis/python-bigquery/issues/19 - writer = pyarrow.BufferOutputStream() - pyarrow.parquet.write_table(table, writer, use_compliant_nested_type=True) - return _write_pyarrow_buffer_to_bq(client, writer.getvalue(), table_name,) - - -def _write_pyarrow_buffer_to_bq( - client: bigquery.Client, buf: pyarrow.Buffer, table_name: str -) -> bigquery.LoadJob: - reader = pyarrow.BufferReader(buf) - - parquet_options = bigquery.format_options.ParquetOptions() - parquet_options.enable_list_inference = True - job_config = bigquery.LoadJobConfig() - job_config.source_format = bigquery.SourceFormat.PARQUET - job_config.parquet_options = parquet_options - - return client.load_table_from_file(reader, table_name, job_config=job_config,) - - # TODO: Optimizations # * Use GENERATE_UUID() instead of ROW_NUMBER(), or join on entity columns directly # * Precompute ROW_NUMBER() so that it doesn't have to be recomputed for every query on entity_dataframe diff --git a/sdk/python/setup.py b/sdk/python/setup.py index 7337df7cbaf..40eb6e30f09 100644 --- a/sdk/python/setup.py +++ b/sdk/python/setup.py @@ -54,7 +54,7 @@ "pandas>=1.0.0", "pandavro==1.5.*", "protobuf>=3.10", - "pyarrow>=2.0.0", + "pyarrow>=4.0.0", "pydantic>=1.0.0", "PyYAML>=5.4.*", "tabulate==0.8.*", @@ -66,7 +66,7 @@ ] GCP_REQUIRED = [ - "google-cloud-bigquery>=2.14.0", + "google-cloud-bigquery>=2.28.1", "google-cloud-bigquery-storage >= 2.0.0", "google-cloud-datastore>=2.1.*", "google-cloud-storage>=1.34.*", @@ -112,7 +112,7 @@ "firebase-admin==4.5.2", "pre-commit", "assertpy==1.1", - "google-cloud-bigquery>=2.14.0", + "google-cloud-bigquery>=2.28.1", "google-cloud-bigquery-storage >= 2.0.0", "google-cloud-datastore>=2.1.*", "google-cloud-storage>=1.20.*", diff --git a/sdk/python/tests/integration/feature_repos/universal/data_sources/bigquery.py b/sdk/python/tests/integration/feature_repos/universal/data_sources/bigquery.py index 46e0535d736..766c31150e1 100644 --- a/sdk/python/tests/integration/feature_repos/universal/data_sources/bigquery.py +++ b/sdk/python/tests/integration/feature_repos/universal/data_sources/bigquery.py @@ -6,10 +6,7 @@ from feast import BigQuerySource from feast.data_source import DataSource -from feast.infra.offline_stores.bigquery import ( - BigQueryOfflineStoreConfig, - _write_df_to_bq, -) +from feast.infra.offline_stores.bigquery import BigQueryOfflineStoreConfig from tests.integration.feature_repos.universal.data_source_creator import ( DataSourceCreator, ) @@ -69,7 +66,7 @@ def create_data_source( f"{self.gcp_project}.{self.project_name}.{destination_name}" ) - job = _write_df_to_bq(self.client, df, destination_name) + job = self.client.load_table_from_dataframe(df, destination_name) job.result() self.tables.append(destination_name) diff --git a/sdk/python/tests/utils/data_source_utils.py b/sdk/python/tests/utils/data_source_utils.py index af9663203a2..6e3d77ead0b 100644 --- a/sdk/python/tests/utils/data_source_utils.py +++ b/sdk/python/tests/utils/data_source_utils.py @@ -7,7 +7,6 @@ from feast import BigQuerySource, FileSource from feast.data_format import ParquetFormat -from feast.infra.offline_stores.bigquery import _write_df_to_bq @contextlib.contextmanager @@ -39,7 +38,7 @@ def simple_bq_source_using_table_ref_arg( client.update_dataset(dataset, ["default_table_expiration_ms"]) table_ref = f"{gcp_project}.{bigquery_dataset}.table_{random.randrange(100, 999)}" - job = _write_df_to_bq(client, df, table_ref) + job = client.load_table_from_dataframe(df, table_ref) job.result() return BigQuerySource( From cbfc72a5a4bedf79e66782d65cc5535e3a806c14 Mon Sep 17 00:00:00 2001 From: Matt Delacour Date: Fri, 15 Oct 2021 14:53:10 -0400 Subject: [PATCH 12/50] Add final_output_feature_names in Query context to avoid SELECT * EXCEPT (#1911) * Add final_output_feature_names in Query context to avoid SELECT * EXCEPT at the end Signed-off-by: Matt Delacour * Remove the drop_columns concept for AWS Redshift Signed-off-by: Matt Delacour * Format files Signed-off-by: Matt Delacour * Add again integration tests about backfill rows Signed-off-by: Matt Delacour * Add teardown to datasource creator Signed-off-by: Matt Delacour * Remove teardown logic in tests as it s part of conftest Signed-off-by: Matt Delacour * Fix linter Signed-off-by: Matt Delacour * Add pytest.mark.universal to new test Signed-off-by: Matt Delacour --- .../feast/infra/offline_stores/bigquery.py | 10 +- .../infra/offline_stores/offline_utils.py | 13 ++- .../feast/infra/offline_stores/redshift.py | 44 +++---- sdk/python/feast/infra/utils/aws_utils.py | 20 +--- .../test_universal_historical_retrieval.py | 109 +++++++++++++++++- 5 files changed, 146 insertions(+), 50 deletions(-) diff --git a/sdk/python/feast/infra/offline_stores/bigquery.py b/sdk/python/feast/infra/offline_stores/bigquery.py index 6753223f2ed..9e4d3efc2e1 100644 --- a/sdk/python/feast/infra/offline_stores/bigquery.py +++ b/sdk/python/feast/infra/offline_stores/bigquery.py @@ -164,6 +164,7 @@ def query_generator() -> Iterator[str]: query_context, left_table_query_string=table_reference, entity_df_event_timestamp_col=entity_df_event_timestamp_col, + entity_df_columns=entity_schema.keys(), query_template=MULTIPLE_FEATURE_VIEW_POINT_IN_TIME_JOIN, full_feature_names=full_feature_names, ) @@ -517,14 +518,17 @@ def _get_bigquery_client(project: Optional[str] = None, location: Optional[str] Thus we only need to compute the latest timestamp of each feature. */ {{ featureview.name }}__latest AS ( - SELECT * EXCEPT(row_number) + SELECT + event_timestamp, + {% if featureview.created_timestamp_column %}created_timestamp,{% endif %} + {{featureview.name}}__entity_row_unique_id FROM ( SELECT *, ROW_NUMBER() OVER( PARTITION BY {{featureview.name}}__entity_row_unique_id ORDER BY event_timestamp DESC{% if featureview.created_timestamp_column %},created_timestamp DESC{% endif %} - ) AS row_number, + ) AS row_number FROM {{ featureview.name }}__base {% if featureview.created_timestamp_column %} INNER JOIN {{ featureview.name }}__dedup @@ -558,7 +562,7 @@ def _get_bigquery_client(project: Optional[str] = None, location: Optional[str] The entity_dataframe dataset being our source of truth here. */ -SELECT * EXCEPT(entity_timestamp, {% for featureview in featureviews %} {{featureview.name}}__entity_row_unique_id{% if loop.last %}{% else %},{% endif %}{% endfor %}) +SELECT {{ final_output_feature_names | join(', ')}} FROM entity_dataframe {% for featureview in featureviews %} LEFT JOIN ( diff --git a/sdk/python/feast/infra/offline_stores/offline_utils.py b/sdk/python/feast/infra/offline_stores/offline_utils.py index 1e23ca54fb2..10c0de2f9bd 100644 --- a/sdk/python/feast/infra/offline_stores/offline_utils.py +++ b/sdk/python/feast/infra/offline_stores/offline_utils.py @@ -2,7 +2,7 @@ import uuid from dataclasses import asdict, dataclass from datetime import timedelta -from typing import Any, Dict, List, Optional, Set, Tuple +from typing import Any, Dict, KeysView, List, Optional, Set, Tuple import numpy as np import pandas as pd @@ -153,12 +153,22 @@ def build_point_in_time_query( feature_view_query_contexts: List[FeatureViewQueryContext], left_table_query_string: str, entity_df_event_timestamp_col: str, + entity_df_columns: KeysView[str], query_template: str, full_feature_names: bool = False, ) -> str: """Build point-in-time query between each feature view table and the entity dataframe for Bigquery and Redshift""" template = Environment(loader=BaseLoader()).from_string(source=query_template) + final_output_feature_names = list(entity_df_columns) + final_output_feature_names.extend( + [ + (f"{fv.name}__{feature}" if full_feature_names else feature) + for fv in feature_view_query_contexts + for feature in fv.features + ] + ) + # Add additional fields to dict template_context = { "left_table_query_string": left_table_query_string, @@ -168,6 +178,7 @@ def build_point_in_time_query( ), "featureviews": [asdict(context) for context in feature_view_query_contexts], "full_feature_names": full_feature_names, + "final_output_feature_names": final_output_feature_names, } query = template.render(template_context) diff --git a/sdk/python/feast/infra/offline_stores/redshift.py b/sdk/python/feast/infra/offline_stores/redshift.py index c3cd461ce15..8c14a7d237b 100644 --- a/sdk/python/feast/infra/offline_stores/redshift.py +++ b/sdk/python/feast/infra/offline_stores/redshift.py @@ -149,6 +149,7 @@ def query_generator() -> Iterator[str]: query_context, left_table_query_string=table_name, entity_df_event_timestamp_col=entity_df_event_timestamp_col, + entity_df_columns=entity_schema.keys(), query_template=MULTIPLE_FEATURE_VIEW_POINT_IN_TIME_JOIN, full_feature_names=full_feature_names, ) @@ -174,11 +175,6 @@ def query_generator() -> Iterator[str]: on_demand_feature_views=OnDemandFeatureView.get_requested_odfvs( feature_refs, project, registry ), - drop_columns=["entity_timestamp"] - + [ - f"{feature_view.projection.name_to_use()}__entity_row_unique_id" - for feature_view in feature_views - ], ) @@ -191,7 +187,6 @@ def __init__( config: RepoConfig, full_feature_names: bool, on_demand_feature_views: Optional[List[OnDemandFeatureView]], - drop_columns: Optional[List[str]] = None, ): """Initialize RedshiftRetrievalJob object. @@ -202,8 +197,6 @@ def __init__( config: Feast repo config full_feature_names: Whether to add the feature view prefixes to the feature names on_demand_feature_views: A list of on demand transforms to apply at retrieval time - drop_columns: Optionally a list of columns to drop before unloading to S3. - This is a convenient field, since "SELECT ... EXCEPT col" isn't supported in Redshift. """ if not isinstance(query, str): self._query_generator = query @@ -225,7 +218,6 @@ def query_generator() -> Iterator[str]: ) self._full_feature_names = full_feature_names self._on_demand_feature_views = on_demand_feature_views - self._drop_columns = drop_columns @property def full_feature_names(self) -> bool: @@ -246,7 +238,6 @@ def _to_df_internal(self) -> pd.DataFrame: self._s3_path, self._config.offline_store.iam_role, query, - self._drop_columns, ) def _to_arrow_internal(self) -> pa.Table: @@ -260,7 +251,6 @@ def _to_arrow_internal(self) -> pa.Table: self._s3_path, self._config.offline_store.iam_role, query, - self._drop_columns, ) def to_s3(self) -> str: @@ -279,7 +269,6 @@ def to_s3(self) -> str: self._s3_path, self._config.offline_store.iam_role, query, - self._drop_columns, ) return self._s3_path @@ -302,9 +291,6 @@ def to_redshift(self, table_name: str) -> None: with self._query_generator() as query: query = f'CREATE TABLE "{table_name}" AS ({query});\n' - if self._drop_columns is not None: - for column in self._drop_columns: - query += f"ALTER TABLE {table_name} DROP COLUMN {column};\n" aws_utils.execute_redshift_statement( self._redshift_client, @@ -479,19 +465,23 @@ def _upload_entity_df_and_get_entity_schema( */ {{ featureview.name }}__latest AS ( SELECT - {{featureview.name}}__entity_row_unique_id, - MAX(event_timestamp) AS event_timestamp + event_timestamp, + {% if featureview.created_timestamp_column %}created_timestamp,{% endif %} + {{featureview.name}}__entity_row_unique_id + FROM + ( + SELECT *, + ROW_NUMBER() OVER( + PARTITION BY {{featureview.name}}__entity_row_unique_id + ORDER BY event_timestamp DESC{% if featureview.created_timestamp_column %},created_timestamp DESC{% endif %} + ) AS row_number + FROM {{ featureview.name }}__base {% if featureview.created_timestamp_column %} - ,MAX(created_timestamp) AS created_timestamp + INNER JOIN {{ featureview.name }}__dedup + USING ({{featureview.name}}__entity_row_unique_id, event_timestamp, created_timestamp) {% endif %} - - FROM {{ featureview.name }}__base - {% if featureview.created_timestamp_column %} - INNER JOIN {{ featureview.name }}__dedup - USING ({{featureview.name}}__entity_row_unique_id, event_timestamp, created_timestamp) - {% endif %} - - GROUP BY {{featureview.name}}__entity_row_unique_id + ) + WHERE row_number = 1 ), /* @@ -518,7 +508,7 @@ def _upload_entity_df_and_get_entity_schema( The entity_dataframe dataset being our source of truth here. */ -SELECT * +SELECT {{ final_output_feature_names | join(', ')}} FROM entity_dataframe {% for featureview in featureviews %} LEFT JOIN ( diff --git a/sdk/python/feast/infra/utils/aws_utils.py b/sdk/python/feast/infra/utils/aws_utils.py index 68e5be27397..28d48a489e6 100644 --- a/sdk/python/feast/infra/utils/aws_utils.py +++ b/sdk/python/feast/infra/utils/aws_utils.py @@ -2,7 +2,7 @@ import os import tempfile import uuid -from typing import Dict, Iterator, List, Optional, Tuple +from typing import Dict, Iterator, Optional, Tuple import pandas as pd import pyarrow as pa @@ -324,7 +324,6 @@ def execute_redshift_query_and_unload_to_s3( s3_path: str, iam_role: str, query: str, - drop_columns: Optional[List[str]] = None, ) -> None: """Unload Redshift Query results to S3 @@ -337,16 +336,11 @@ def execute_redshift_query_and_unload_to_s3( iam_role: IAM Role for Redshift to assume during the UNLOAD command. The role must grant permission to write to the S3 location. query: The SQL query to execute - drop_columns: Optionally a list of columns to drop before unloading to S3. - This is a convenient field, since "SELECT ... EXCEPT col" isn't supported in Redshift. """ # Run the query, unload the results to S3 unique_table_name = "_" + str(uuid.uuid4()).replace("-", "") query = f"CREATE TEMPORARY TABLE {unique_table_name} AS ({query});\n" - if drop_columns is not None: - for column in drop_columns: - query += f"ALTER TABLE {unique_table_name} DROP COLUMN {column};\n" query += f"UNLOAD ('SELECT * FROM {unique_table_name}') TO '{s3_path}/' IAM_ROLE '{iam_role}' PARQUET" execute_redshift_statement(redshift_data_client, cluster_id, database, user, query) @@ -360,20 +354,12 @@ def unload_redshift_query_to_pa( s3_path: str, iam_role: str, query: str, - drop_columns: Optional[List[str]] = None, ) -> pa.Table: """ Unload Redshift Query results to S3 and get the results in PyArrow Table format """ bucket, key = get_bucket_and_key(s3_path) execute_redshift_query_and_unload_to_s3( - redshift_data_client, - cluster_id, - database, - user, - s3_path, - iam_role, - query, - drop_columns, + redshift_data_client, cluster_id, database, user, s3_path, iam_role, query, ) with tempfile.TemporaryDirectory() as temp_dir: @@ -391,7 +377,6 @@ def unload_redshift_query_to_df( s3_path: str, iam_role: str, query: str, - drop_columns: Optional[List[str]] = None, ) -> pd.DataFrame: """ Unload Redshift Query results to S3 and get the results in Pandas DataFrame format """ table = unload_redshift_query_to_pa( @@ -403,7 +388,6 @@ def unload_redshift_query_to_df( s3_path, iam_role, query, - drop_columns, ) return table.to_pandas() diff --git a/sdk/python/tests/integration/offline_store/test_universal_historical_retrieval.py b/sdk/python/tests/integration/offline_store/test_universal_historical_retrieval.py index 2f88f2194c1..e6194cb0128 100644 --- a/sdk/python/tests/integration/offline_store/test_universal_historical_retrieval.py +++ b/sdk/python/tests/integration/offline_store/test_universal_historical_retrieval.py @@ -1,4 +1,6 @@ -from datetime import datetime +import random +import time +from datetime import datetime, timedelta from typing import Any, Dict, List, Optional import numpy as np @@ -8,15 +10,18 @@ from pytz import utc from feast import utils +from feast.entity import Entity from feast.errors import ( FeatureNameCollisionError, RequestDataNotFoundInEntityDfException, ) +from feast.feature import Feature from feast.feature_service import FeatureService from feast.feature_view import FeatureView from feast.infra.offline_stores.offline_utils import ( DEFAULT_ENTITY_DF_EVENT_TIMESTAMP_COL, ) +from feast.value_type import ValueType from tests.integration.feature_repos.repo_configuration import ( construct_universal_feature_views, table_name_from_data_source, @@ -480,6 +485,108 @@ def test_historical_features(environment, universal_data_sources, full_feature_n ) +@pytest.mark.integration +@pytest.mark.universal +def test_historical_features_from_bigquery_sources_containing_backfills(environment): + store = environment.feature_store + + now = datetime.now().replace(microsecond=0, second=0, minute=0) + tomorrow = now + timedelta(days=1) + + entity_df = pd.DataFrame( + data=[ + {"driver_id": 1001, "event_timestamp": now + timedelta(days=2)}, + {"driver_id": 1002, "event_timestamp": now + timedelta(days=2)}, + ] + ) + + driver_stats_df = pd.DataFrame( + data=[ + # Duplicated rows simple case + { + "driver_id": 1001, + "avg_daily_trips": 10, + "event_timestamp": now, + "created": now, + }, + { + "driver_id": 1001, + "avg_daily_trips": 20, + "event_timestamp": now, + "created": tomorrow, + }, + # Duplicated rows after a backfill + { + "driver_id": 1002, + "avg_daily_trips": 30, + "event_timestamp": now, + "created": tomorrow, + }, + { + "driver_id": 1002, + "avg_daily_trips": 40, + "event_timestamp": tomorrow, + "created": now, + }, + ] + ) + + expected_df = pd.DataFrame( + data=[ + { + "driver_id": 1001, + "event_timestamp": now + timedelta(days=2), + "avg_daily_trips": 20, + }, + { + "driver_id": 1002, + "event_timestamp": now + timedelta(days=2), + "avg_daily_trips": 40, + }, + ] + ) + + driver_stats_data_source = environment.data_source_creator.create_data_source( + df=driver_stats_df, + destination_name=f"test_driver_stats_{int(time.time_ns())}_{random.randint(1000, 9999)}", + event_timestamp_column="event_timestamp", + created_timestamp_column="created", + ) + + driver = Entity(name="driver", join_key="driver_id", value_type=ValueType.INT64) + driver_fv = FeatureView( + name="driver_stats", + entities=["driver"], + features=[Feature(name="avg_daily_trips", dtype=ValueType.INT32)], + batch_source=driver_stats_data_source, + ttl=None, + ) + + store.apply([driver, driver_fv]) + + offline_job = store.get_historical_features( + entity_df=entity_df, + features=["driver_stats:avg_daily_trips"], + full_feature_names=False, + ) + + start_time = datetime.utcnow() + actual_df = offline_job.to_df() + + print(f"actual_df shape: {actual_df.shape}") + end_time = datetime.utcnow() + print(str(f"Time to execute job_from_df.to_df() = '{(end_time - start_time)}'\n")) + + assert sorted(expected_df.columns) == sorted(actual_df.columns) + assert_frame_equal( + expected_df.sort_values(by=["driver_id"]).reset_index(drop=True), + actual_df[expected_df.columns] + .sort_values(by=["driver_id"]) + .reset_index(drop=True), + check_dtype=False, + ) + + def response_feature_name(feature: str, full_feature_names: bool) -> str: if feature in {"conv_rate", "avg_daily_trips"} and full_feature_names: return f"driver_stats__{feature}" From f5f550042ac0e8b5703c9b07ecdaf58f64eb802b Mon Sep 17 00:00:00 2001 From: Danny Chiao Date: Fri, 15 Oct 2021 16:54:09 -0400 Subject: [PATCH 13/50] Allows registering of features in request data as RequestFeatureView. Refactors common logic into a BaseFeatureView class (#1931) * Initial implementation of Request Feature View without validation, with some refactoring of feature views Signed-off-by: Danny Chiao * Refactor constructor in base class Signed-off-by: Danny Chiao * Refactor to have constructor init in base class Signed-off-by: Danny Chiao * Moving copy methods into base class Signed-off-by: Danny Chiao * lint Signed-off-by: Danny Chiao * Implement request feature view validation in historical and online retrieval Signed-off-by: Danny Chiao * Add tests for request_fv Signed-off-by: Danny Chiao * Update CLI to understand request feature views Signed-off-by: Danny Chiao * Fix error to be more generic and apply to all feature views Signed-off-by: Danny Chiao * Add type of feature view to feature view list command Signed-off-by: Danny Chiao * Add type of feature view to feature view list command Signed-off-by: Danny Chiao * Lint imports Signed-off-by: Danny Chiao * Fix new lines and nits Signed-off-by: Danny Chiao * Fix repo apply bug Signed-off-by: Danny Chiao * Fix comments Signed-off-by: Danny Chiao * format Signed-off-by: Danny Chiao * fix test Signed-off-by: Danny Chiao * reverse naming Signed-off-by: Danny Chiao * reverse naming Signed-off-by: Danny Chiao * reverse naming Signed-off-by: Danny Chiao * Add back to cli Signed-off-by: Danny Chiao * Comments Signed-off-by: Danny Chiao * Lint Signed-off-by: Danny Chiao * Remove extra data in response Signed-off-by: Danny Chiao * revert change Signed-off-by: Danny Chiao * revert change Signed-off-by: Danny Chiao --- protos/feast/core/Registry.proto | 2 + protos/feast/core/RequestFeatureView.proto | 43 ++++ sdk/python/feast/base_feature_view.py | 204 +++++++++++++++ sdk/python/feast/cli.py | 25 +- sdk/python/feast/errors.py | 7 +- sdk/python/feast/feature_service.py | 5 +- sdk/python/feast/feature_store.py | 236 ++++++++++++++---- sdk/python/feast/feature_view.py | 54 ++-- .../infra/offline_stores/offline_store.py | 1 + sdk/python/feast/on_demand_feature_view.py | 143 +++-------- sdk/python/feast/registry.py | 146 ++++++----- sdk/python/feast/repo_operations.py | 40 ++- sdk/python/feast/request_feature_view.py | 91 +++++++ sdk/python/feast/usage.py | 2 + .../feature_repos/repo_configuration.py | 2 + .../feature_repos/universal/feature_views.py | 10 + .../test_universal_historical_retrieval.py | 46 +++- .../online_store/test_universal_online.py | 26 +- 18 files changed, 799 insertions(+), 284 deletions(-) create mode 100644 protos/feast/core/RequestFeatureView.proto create mode 100644 sdk/python/feast/base_feature_view.py create mode 100644 sdk/python/feast/request_feature_view.py diff --git a/protos/feast/core/Registry.proto b/protos/feast/core/Registry.proto index b8570301e96..035e87a49f8 100644 --- a/protos/feast/core/Registry.proto +++ b/protos/feast/core/Registry.proto @@ -26,6 +26,7 @@ import "feast/core/FeatureService.proto"; import "feast/core/FeatureTable.proto"; import "feast/core/FeatureView.proto"; import "feast/core/OnDemandFeatureView.proto"; +import "feast/core/RequestFeatureView.proto"; import "google/protobuf/timestamp.proto"; message Registry { @@ -33,6 +34,7 @@ message Registry { repeated FeatureTable feature_tables = 2; repeated FeatureView feature_views = 6; repeated OnDemandFeatureView on_demand_feature_views = 8; + repeated RequestFeatureView request_feature_views = 9; repeated FeatureService feature_services = 7; string registry_schema_version = 3; // to support migrations; incremented when schema is changed diff --git a/protos/feast/core/RequestFeatureView.proto b/protos/feast/core/RequestFeatureView.proto new file mode 100644 index 00000000000..c9ee540e6f9 --- /dev/null +++ b/protos/feast/core/RequestFeatureView.proto @@ -0,0 +1,43 @@ +// +// Copyright 2021 The Feast Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + + +syntax = "proto3"; +package feast.core; + +option go_package = "github.com/feast-dev/feast/sdk/go/protos/feast/core"; +option java_outer_classname = "RequestFeatureViewProto"; +option java_package = "feast.proto.core"; + +import "feast/core/FeatureView.proto"; +import "feast/core/Feature.proto"; +import "feast/core/DataSource.proto"; + +message RequestFeatureView { + // User-specified specifications of this feature view. + RequestFeatureViewSpec spec = 1; +} + +message RequestFeatureViewSpec { + // Name of the feature view. Must be unique. Not updated. + string name = 1; + + // Name of Feast project that this feature view belongs to. + string project = 2; + + // Request data which contains the underlying data schema and list of associated features + DataSource request_data_source = 3; +} diff --git a/sdk/python/feast/base_feature_view.py b/sdk/python/feast/base_feature_view.py new file mode 100644 index 00000000000..10f949d9a1c --- /dev/null +++ b/sdk/python/feast/base_feature_view.py @@ -0,0 +1,204 @@ +# Copyright 2021 The Feast Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +import warnings +from abc import ABC, abstractmethod +from typing import List, Type + +from google.protobuf.json_format import MessageToJson +from proto import Message + +from feast.feature import Feature +from feast.feature_view_projection import FeatureViewProjection + +warnings.simplefilter("once", DeprecationWarning) + + +class BaseFeatureView(ABC): + """A FeatureView defines a logical grouping of features to be served.""" + + @abstractmethod + def __init__(self, name: str, features: List[Feature]): + self._name = name + self._features = features + self._projection = FeatureViewProjection.from_definition(self) + + @property + def name(self) -> str: + return self._name + + @property + def features(self) -> List[Feature]: + return self._features + + @features.setter + def features(self, value): + self._features = value + + @property + def projection(self) -> FeatureViewProjection: + return self._projection + + @projection.setter + def projection(self, value): + self._projection = value + + @property + @abstractmethod + def proto_class(self) -> Type[Message]: + pass + + @abstractmethod + def to_proto(self) -> Message: + pass + + @classmethod + @abstractmethod + def from_proto(cls, feature_view_proto): + pass + + @abstractmethod + def __copy__(self): + """ + Generates a deep copy of this feature view + + Returns: + A copy of this FeatureView + """ + pass + + def __repr__(self): + items = (f"{k} = {v}" for k, v in self.__dict__.items()) + return f"<{self.__class__.__name__}({', '.join(items)})>" + + def __str__(self): + return str(MessageToJson(self.to_proto())) + + def __hash__(self): + return hash((id(self), self.name)) + + def __getitem__(self, item): + assert isinstance(item, list) + + referenced_features = [] + for feature in self.features: + if feature.name in item: + referenced_features.append(feature) + + cp = self.__copy__() + cp.projection.features = referenced_features + + return cp + + def __eq__(self, other): + if not isinstance(other, BaseFeatureView): + raise TypeError( + "Comparisons should only involve BaseFeatureView class objects." + ) + + if self.name != other.name: + return False + + if sorted(self.features) != sorted(other.features): + return False + + return True + + def ensure_valid(self): + """ + Validates the state of this feature view locally. + + Raises: + ValueError: The feature view is invalid. + """ + if not self.name: + raise ValueError("Feature view needs a name.") + + def with_name(self, name: str): + """ + Renames this feature view by returning a copy of this feature view with an alias + set for the feature view name. This rename operation is only used as part of query + operations and will not modify the underlying FeatureView. + + Args: + name: Name to assign to the FeatureView copy. + + Returns: + A copy of this FeatureView with the name replaced with the 'name' input. + """ + cp = self.__copy__() + cp.projection.name_alias = name + + return cp + + def set_projection(self, feature_view_projection: FeatureViewProjection) -> None: + """ + Setter for the projection object held by this FeatureView. A projection is an + object that stores the modifications to a FeatureView that is applied to the FeatureView + when the FeatureView is used such as during feature_store.get_historical_features. + This method also performs checks to ensure the projection is consistent with this + FeatureView before doing the set. + + Args: + feature_view_projection: The FeatureViewProjection object to set this FeatureView's + 'projection' field to. + """ + if feature_view_projection.name != self.name: + raise ValueError( + f"The projection for the {self.name} FeatureView cannot be applied because it differs in name. " + f"The projection is named {feature_view_projection.name} and the name indicates which " + "FeatureView the projection is for." + ) + + for feature in feature_view_projection.features: + if feature not in self.features: + raise ValueError( + f"The projection for {self.name} cannot be applied because it contains {feature.name} which the " + "FeatureView doesn't have." + ) + + self.projection = feature_view_projection + + def with_projection(self, feature_view_projection: FeatureViewProjection): + """ + Sets the feature view projection by returning a copy of this on-demand feature view + with its projection set to the given projection. A projection is an + object that stores the modifications to a feature view that is used during + query operations. + + Args: + feature_view_projection: The FeatureViewProjection object to link to this + OnDemandFeatureView. + + Returns: + A copy of this OnDemandFeatureView with its projection replaced with the + 'feature_view_projection' argument. + """ + if feature_view_projection.name != self.name: + raise ValueError( + f"The projection for the {self.name} FeatureView cannot be applied because it differs in name. " + f"The projection is named {feature_view_projection.name} and the name indicates which " + "FeatureView the projection is for." + ) + + for feature in feature_view_projection.features: + if feature not in self.features: + raise ValueError( + f"The projection for {self.name} cannot be applied because it contains {feature.name} which the " + "FeatureView doesn't have." + ) + + cp = self.__copy__() + cp.projection = feature_view_projection + + return cp diff --git a/sdk/python/feast/cli.py b/sdk/python/feast/cli.py index a01d387a225..71d7ceab647 100644 --- a/sdk/python/feast/cli.py +++ b/sdk/python/feast/cli.py @@ -25,6 +25,8 @@ from feast import flags, flags_helper, utils from feast.errors import FeastObjectNotFoundException, FeastProviderLoginError from feast.feature_store import FeatureStore +from feast.feature_view import FeatureView +from feast.on_demand_feature_view import OnDemandFeatureView from feast.repo_config import load_repo_config from feast.repo_operations import ( apply_total, @@ -261,12 +263,29 @@ def feature_view_list(ctx: click.Context): cli_check_repo(repo) store = FeatureStore(repo_path=str(repo)) table = [] - for feature_view in store.list_feature_views(): - table.append([feature_view.name, feature_view.entities]) + for feature_view in [ + *store.list_feature_views(), + *store.list_request_feature_views(), + *store.list_on_demand_feature_views(), + ]: + entities = set() + if isinstance(feature_view, FeatureView): + entities.update(feature_view.entities) + elif isinstance(feature_view, OnDemandFeatureView): + for backing_fv in feature_view.inputs.values(): + if isinstance(backing_fv, FeatureView): + entities.update(backing_fv.entities) + table.append( + [ + feature_view.name, + entities if len(entities) > 0 else "n/a", + type(feature_view).__name__, + ] + ) from tabulate import tabulate - print(tabulate(table, headers=["NAME", "ENTITIES"], tablefmt="plain")) + print(tabulate(table, headers=["NAME", "ENTITIES", "TYPE"], tablefmt="plain")) @cli.group(name="on-demand-feature-views") diff --git a/sdk/python/feast/errors.py b/sdk/python/feast/errors.py index f95e1991057..03dd2409a7f 100644 --- a/sdk/python/feast/errors.py +++ b/sdk/python/feast/errors.py @@ -53,14 +53,14 @@ def __init__(self, name, project=None): class RequestDataNotFoundInEntityDfException(FeastObjectNotFoundException): def __init__(self, feature_name, feature_view_name): super().__init__( - f"Feature {feature_name} not found in the entity dataframe, but required by on demand feature view {feature_view_name}" + f"Feature {feature_name} not found in the entity dataframe, but required by feature view {feature_view_name}" ) class RequestDataNotFoundInEntityRowsException(FeastObjectNotFoundException): def __init__(self, feature_names): super().__init__( - f"Required request data source features {feature_names} not found in the entity rows, but required by on demand feature views" + f"Required request data source features {feature_names} not found in the entity rows, but required by feature views" ) @@ -263,9 +263,10 @@ def __init__(self, entity_type: type): class ConflictingFeatureViewNames(Exception): + # TODO: print file location of conflicting feature views def __init__(self, feature_view_name: str): super().__init__( - f"The feature view name: {feature_view_name} refers to both an on-demand feature view and a feature view" + f"The feature view name: {feature_view_name} refers to feature views of different types." ) diff --git a/sdk/python/feast/feature_service.py b/sdk/python/feast/feature_service.py index 9f8e9af1fb3..46afdfff1e4 100644 --- a/sdk/python/feast/feature_service.py +++ b/sdk/python/feast/feature_service.py @@ -3,6 +3,7 @@ from google.protobuf.json_format import MessageToJson +from feast.base_feature_view import BaseFeatureView from feast.feature_table import FeatureTable from feast.feature_view import FeatureView from feast.feature_view_projection import FeatureViewProjection @@ -59,9 +60,7 @@ def __init__( self.feature_view_projections.append( FeatureViewProjection.from_definition(feature_grouping) ) - elif isinstance(feature_grouping, FeatureView) or isinstance( - feature_grouping, OnDemandFeatureView - ): + elif isinstance(feature_grouping, BaseFeatureView): self.feature_view_projections.append(feature_grouping.projection) else: raise ValueError( diff --git a/sdk/python/feast/feature_store.py b/sdk/python/feast/feature_store.py index 3fc510eb38e..2af349cd3e2 100644 --- a/sdk/python/feast/feature_store.py +++ b/sdk/python/feast/feature_store.py @@ -12,6 +12,7 @@ # See the License for the specific language governing permissions and # limitations under the License. import copy +import itertools import os import warnings from collections import Counter, OrderedDict, defaultdict @@ -24,7 +25,7 @@ from tqdm import tqdm from feast import feature_server, flags, flags_helper, utils -from feast.data_source import RequestDataSource +from feast.base_feature_view import BaseFeatureView from feast.entity import Entity from feast.errors import ( EntityNotFoundException, @@ -56,6 +57,7 @@ from feast.protos.feast.types.EntityKey_pb2 import EntityKey as EntityKeyProto from feast.registry import Registry from feast.repo_config import RepoConfig, load_repo_config +from feast.request_feature_view import RequestFeatureView from feast.type_map import python_value_to_proto_value from feast.usage import UsageEvent, log_event, log_exceptions, log_exceptions_and_usage from feast.value_type import ValueType @@ -190,8 +192,25 @@ def list_feature_views(self, allow_cache: bool = False) -> List[FeatureView]: """ return self._list_feature_views(allow_cache) + @log_exceptions_and_usage + def list_request_feature_views( + self, allow_cache: bool = False + ) -> List[RequestFeatureView]: + """ + Retrieves the list of feature views from the registry. + + Args: + allow_cache: Whether to allow returning entities from a cached registry. + + Returns: + A list of feature views. + """ + return self._registry.list_request_feature_views( + self.project, allow_cache=allow_cache + ) + def _list_feature_views( - self, allow_cache: bool = False, hide_dummy_entity: bool = True + self, allow_cache: bool = False, hide_dummy_entity: bool = True, ) -> List[FeatureView]: feature_views = [] for fv in self._registry.list_feature_views( @@ -347,8 +366,17 @@ def apply( Entity, FeatureView, OnDemandFeatureView, + RequestFeatureView, FeatureService, - List[Union[FeatureView, OnDemandFeatureView, Entity, FeatureService]], + List[ + Union[ + FeatureView, + OnDemandFeatureView, + RequestFeatureView, + Entity, + FeatureService, + ] + ], ], commit: bool = True, ): @@ -394,6 +422,9 @@ def apply( assert isinstance(objects, list) views_to_update = [ob for ob in objects if isinstance(ob, FeatureView)] + request_views_to_update = [ + ob for ob in objects if isinstance(ob, RequestFeatureView) + ] odfvs_to_update = [ob for ob in objects if isinstance(ob, OnDemandFeatureView)] if ( not flags_helper.enable_on_demand_feature_views(self.config) @@ -404,7 +435,9 @@ def apply( if len(odfvs_to_update) > 0: log_event(UsageEvent.APPLY_WITH_ODFV) - _validate_feature_views(views_to_update) + _validate_feature_views( + [*views_to_update, *odfvs_to_update, *request_views_to_update] + ) entities_to_update = [ob for ob in objects if isinstance(ob, Entity)] services_to_update = [ob for ob in objects if isinstance(ob, FeatureService)] @@ -425,7 +458,7 @@ def apply( if len(views_to_update) + len(entities_to_update) + len( services_to_update - ) + len(odfvs_to_update) != len(objects): + ) + len(odfvs_to_update) + len(request_views_to_update) != len(objects): raise ValueError("Unknown object type provided as part of apply() call") # DUMMY_ENTITY is a placeholder entity used in entityless FeatureViews @@ -436,12 +469,10 @@ def apply( ) entities_to_update.append(DUMMY_ENTITY) - for view in views_to_update: + for view in itertools.chain( + views_to_update, odfvs_to_update, request_views_to_update + ): self._registry.apply_feature_view(view, project=self.project, commit=False) - for odfv in odfvs_to_update: - self._registry.apply_on_demand_feature_view( - odfv, project=self.project, commit=False - ) for ent in entities_to_update: self._registry.apply_entity(ent, project=self.project, commit=False) for feature_service in services_to_update: @@ -555,38 +586,50 @@ def get_historical_features( ) _feature_refs = self._get_features(features, feature_refs) - all_feature_views, all_on_demand_feature_views = self._get_feature_views_to_use( - features - ) + ( + all_feature_views, + all_request_feature_views, + all_on_demand_feature_views, + ) = self._get_feature_views_to_use(features) # TODO(achal): _group_feature_refs returns the on demand feature views, but it's no passed into the provider. # This is a weird interface quirk - we should revisit the `get_historical_features` to # pass in the on demand feature views as well. - fvs, odfvs = _group_feature_refs( - _feature_refs, all_feature_views, all_on_demand_feature_views + fvs, odfvs, request_fvs, request_fv_refs = _group_feature_refs( + _feature_refs, + all_feature_views, + all_request_feature_views, + all_on_demand_feature_views, ) feature_views = list(view for view, _ in fvs) on_demand_feature_views = list(view for view, _ in odfvs) + request_feature_views = list(view for view, _ in request_fvs) if len(on_demand_feature_views) > 0: log_event(UsageEvent.GET_HISTORICAL_FEATURES_WITH_ODFV) + if len(request_feature_views) > 0: + log_event(UsageEvent.GET_HISTORICAL_FEATURES_WITH_REQUEST_FV) # Check that the right request data is present in the entity_df if type(entity_df) == pd.DataFrame: entity_pd_df = cast(pd.DataFrame, entity_df) + for fv in request_feature_views: + for feature in fv.features: + if feature.name not in entity_pd_df.columns: + raise RequestDataNotFoundInEntityDfException( + feature_name=feature.name, feature_view_name=fv.name + ) for odfv in on_demand_feature_views: - odfv_inputs = odfv.inputs.values() - for odfv_input in odfv_inputs: - if type(odfv_input) == RequestDataSource: - request_data_source = cast(RequestDataSource, odfv_input) - for feature_name in request_data_source.schema.keys(): - if feature_name not in entity_pd_df.columns: - raise RequestDataNotFoundInEntityDfException( - feature_name=feature_name, - feature_view_name=odfv.name, - ) + odfv_request_data_schema = odfv.get_request_data_schema() + for feature_name in odfv_request_data_schema.keys(): + if feature_name not in entity_pd_df.columns: + raise RequestDataNotFoundInEntityDfException( + feature_name=feature_name, feature_view_name=odfv.name, + ) _validate_feature_refs(_feature_refs, full_feature_names) - + # Drop refs that refer to RequestFeatureViews since they don't need to be fetched and + # already exist in the entity_df + _feature_refs = [ref for ref in _feature_refs if ref not in request_fv_refs] provider = self._get_provider() job = provider.get_historical_features( @@ -633,7 +676,7 @@ def materialize_incremental( ... """ - feature_views_to_materialize = [] + feature_views_to_materialize: List[FeatureView] = [] if feature_views is None: feature_views_to_materialize = self._list_feature_views( hide_dummy_entity=False @@ -725,7 +768,7 @@ def materialize( f"The given start_date {start_date} is greater than the given end_date {end_date}." ) - feature_views_to_materialize = [] + feature_views_to_materialize: List[FeatureView] = [] if feature_views is None: feature_views_to_materialize = self._list_feature_views( hide_dummy_entity=False @@ -816,16 +859,30 @@ def get_online_features( >>> online_response_dict = online_response.to_dict() """ _feature_refs = self._get_features(features, feature_refs) - all_feature_views, all_on_demand_feature_views = self._get_feature_views_to_use( + ( + all_feature_views, + all_request_feature_views, + all_on_demand_feature_views, + ) = self._get_feature_views_to_use( features=features, allow_cache=True, hide_dummy_entity=False ) _validate_feature_refs(_feature_refs, full_feature_names) - grouped_refs, grouped_odfv_refs = _group_feature_refs( - _feature_refs, all_feature_views, all_on_demand_feature_views + ( + grouped_refs, + grouped_odfv_refs, + grouped_request_fv_refs, + _, + ) = _group_feature_refs( + _feature_refs, + all_feature_views, + all_request_feature_views, + all_on_demand_feature_views, ) if len(grouped_odfv_refs) > 0: log_event(UsageEvent.GET_ONLINE_FEATURES_WITH_ODFV) + if len(grouped_request_fv_refs) > 0: + log_event(UsageEvent.GET_ONLINE_FEATURES_WITH_REQUEST_FV) feature_views = list(view for view, _ in grouped_refs) entityless_case = DUMMY_ENTITY_NAME in [ @@ -854,8 +911,8 @@ def get_online_features( ) entity_name_to_join_key_map[entity_name] = join_key - needed_request_data_features = self._get_needed_request_data_features( - grouped_odfv_refs + needed_request_data, needed_request_fv_features = self.get_needed_request_data( + grouped_odfv_refs, grouped_request_fv_refs ) join_key_rows = [] @@ -865,7 +922,10 @@ def get_online_features( join_key_row = {} for entity_name, entity_value in row.items(): # Found request data - if entity_name in needed_request_data_features: + if ( + entity_name in needed_request_data + or entity_name in needed_request_fv_features + ): if entity_name not in request_data_features: request_data_features[entity_name] = [] request_data_features[entity_name].append(entity_value) @@ -881,10 +941,9 @@ def get_online_features( # May be empty if this entity row was request data join_key_rows.append(join_key_row) - if len(needed_request_data_features) != len(request_data_features.keys()): - raise RequestDataNotFoundInEntityRowsException( - feature_names=needed_request_data_features - ) + self.ensure_request_data_values_exist( + needed_request_data, needed_request_fv_features, request_data_features + ) entity_row_proto_list = _infer_online_entity_rows(join_key_rows) @@ -934,6 +993,41 @@ def get_online_features( result_rows, ) + def get_needed_request_data( + self, + grouped_odfv_refs: List[Tuple[OnDemandFeatureView, List[str]]], + grouped_request_fv_refs: List[Tuple[RequestFeatureView, List[str]]], + ) -> Tuple[Set[str], Set[str]]: + needed_request_data: Set[str] = set() + needed_request_fv_features: Set[str] = set() + for odfv, _ in grouped_odfv_refs: + odfv_request_data_schema = odfv.get_request_data_schema() + needed_request_data.update(odfv_request_data_schema.keys()) + for request_fv, _ in grouped_request_fv_refs: + for feature in request_fv.features: + needed_request_fv_features.add(feature.name) + return needed_request_data, needed_request_fv_features + + def ensure_request_data_values_exist( + self, + needed_request_data: Set[str], + needed_request_fv_features: Set[str], + request_data_features: Dict[str, List[Any]], + ): + if len(needed_request_data) + len(needed_request_fv_features) != len( + request_data_features.keys() + ): + missing_features = [ + x + for x in itertools.chain( + needed_request_data, needed_request_fv_features + ) + if x not in request_data_features + ] + raise RequestDataNotFoundInEntityRowsException( + feature_names=missing_features + ) + def _populate_result_rows_from_feature_view( self, table_join_keys: List[str], @@ -983,18 +1077,24 @@ def _populate_result_rows_from_feature_view( feature_ref ] = GetOnlineFeaturesResponse.FieldStatus.PRESENT - def _get_needed_request_data_features(self, grouped_odfv_refs) -> Set[str]: + def _get_needed_request_data_features( + self, + grouped_odfv_refs: List[Tuple[OnDemandFeatureView, List[str]]], + grouped_request_fv_refs: List[Tuple[RequestFeatureView, List[str]]], + ) -> Set[str]: needed_request_data_features = set() for odfv_to_feature_names in grouped_odfv_refs: odfv, requested_feature_names = odfv_to_feature_names - odfv_inputs = odfv.inputs.values() - for odfv_input in odfv_inputs: - if type(odfv_input) == RequestDataSource: - request_data_source = cast(RequestDataSource, odfv_input) - for feature_name in request_data_source.schema.keys(): - needed_request_data_features.add(feature_name) + odfv_request_data_schema = odfv.get_request_data_schema() + for feature_name in odfv_request_data_schema.keys(): + needed_request_data_features.add(feature_name) + for request_fv_to_feature_names in grouped_request_fv_refs: + request_fv, requested_feature_names = request_fv_to_feature_names + for fv in request_fv.features: + needed_request_data_features.add(fv.name) return needed_request_data_features + # TODO(adchia): remove request data, which isn't part of the feature_refs def _augment_response_with_on_demand_transforms( self, feature_refs: List[str], @@ -1049,13 +1149,20 @@ def _get_feature_views_to_use( features: Optional[Union[List[str], FeatureService]], allow_cache=False, hide_dummy_entity: bool = True, - ) -> Tuple[List[FeatureView], List[OnDemandFeatureView]]: + ) -> Tuple[List[FeatureView], List[RequestFeatureView], List[OnDemandFeatureView]]: fvs = { fv.name: fv for fv in self._list_feature_views(allow_cache, hide_dummy_entity) } + request_fvs = { + fv.name: fv + for fv in self._registry.list_request_feature_views( + project=self.project, allow_cache=allow_cache + ) + } + od_fvs = { fv.name: fv for fv in self._registry.list_on_demand_feature_views( @@ -1064,7 +1171,7 @@ def _get_feature_views_to_use( } if isinstance(features, FeatureService): - fvs_to_use, od_fvs_to_use = [], [] + fvs_to_use, request_fvs_to_use, od_fvs_to_use = [], [], [] for fv_name, projection in [ (projection.name, projection) for projection in features.feature_view_projections @@ -1073,6 +1180,10 @@ def _get_feature_views_to_use( fvs_to_use.append( fvs[fv_name].with_projection(copy.copy(projection)) ) + elif fv_name in request_fvs: + request_fvs_to_use.append( + request_fvs[fv_name].with_projection(copy.copy(projection)) + ) elif fv_name in od_fvs: od_fvs_to_use.append( od_fvs[fv_name].with_projection(copy.copy(projection)) @@ -1083,9 +1194,13 @@ def _get_feature_views_to_use( f"{fv_name} which doesn't exist. Please make sure that you have created the feature view" f'{fv_name} and that you have registered it by running "apply".' ) - views_to_use = (fvs_to_use, od_fvs_to_use) + views_to_use = (fvs_to_use, request_fvs_to_use, od_fvs_to_use) else: - views_to_use = ([*fvs.values()], [*od_fvs.values()]) + views_to_use = ( + [*fvs.values()], + [*request_fvs.values()], + [*od_fvs.values()], + ) return views_to_use @@ -1160,15 +1275,24 @@ def _validate_feature_refs(feature_refs: List[str], full_feature_names: bool = F def _group_feature_refs( features: List[str], all_feature_views: List[FeatureView], + all_request_feature_views: List[RequestFeatureView], all_on_demand_feature_views: List[OnDemandFeatureView], ) -> Tuple[ - List[Tuple[FeatureView, List[str]]], List[Tuple[OnDemandFeatureView, List[str]]] + List[Tuple[FeatureView, List[str]]], + List[Tuple[OnDemandFeatureView, List[str]]], + List[Tuple[RequestFeatureView, List[str]]], + Set[str], ]: """ Get list of feature views and corresponding feature names based on feature references""" # view name to view proto view_index = {view.projection.name_to_use(): view for view in all_feature_views} + # request view name to proto + request_view_index = { + view.projection.name_to_use(): view for view in all_request_feature_views + } + # on demand view to on demand view proto on_demand_view_index = { view.projection.name_to_use(): view for view in all_on_demand_feature_views @@ -1176,6 +1300,8 @@ def _group_feature_refs( # view name to feature names views_features = defaultdict(list) + request_views_features = defaultdict(list) + request_view_refs = set() # on demand view name to feature names on_demand_view_features = defaultdict(list) @@ -1186,17 +1312,23 @@ def _group_feature_refs( views_features[view_name].append(feat_name) elif view_name in on_demand_view_index: on_demand_view_features[view_name].append(feat_name) + elif view_name in request_view_index: + request_views_features[view_name].append(feat_name) + request_view_refs.add(ref) else: raise FeatureViewNotFoundException(view_name) fvs_result: List[Tuple[FeatureView, List[str]]] = [] odfvs_result: List[Tuple[OnDemandFeatureView, List[str]]] = [] + request_fvs_result: List[Tuple[RequestFeatureView, List[str]]] = [] for view_name, feature_names in views_features.items(): fvs_result.append((view_index[view_name], feature_names)) + for view_name, feature_names in request_views_features.items(): + request_fvs_result.append((request_view_index[view_name], feature_names)) for view_name, feature_names in on_demand_view_features.items(): odfvs_result.append((on_demand_view_index[view_name], feature_names)) - return fvs_result, odfvs_result + return fvs_result, odfvs_result, request_fvs_result, request_view_refs def _get_table_entity_keys( @@ -1257,7 +1389,7 @@ def _print_materialization_log( ) -def _validate_feature_views(feature_views: List[FeatureView]): +def _validate_feature_views(feature_views: List[BaseFeatureView]): """ Verify feature views have case-insensitively unique names""" fv_names = set() for fv in feature_views: diff --git a/sdk/python/feast/feature_view.py b/sdk/python/feast/feature_view.py index 90a80a92335..a8186991cd5 100644 --- a/sdk/python/feast/feature_view.py +++ b/sdk/python/feast/feature_view.py @@ -15,12 +15,12 @@ import re import warnings from datetime import datetime, timedelta -from typing import Dict, List, Optional, Tuple, Union +from typing import Dict, List, Optional, Tuple, Type, Union from google.protobuf.duration_pb2 import Duration -from google.protobuf.json_format import MessageToJson from feast import utils +from feast.base_feature_view import BaseFeatureView from feast.data_source import DataSource from feast.errors import RegistryInferenceFailure from feast.feature import Feature @@ -47,7 +47,7 @@ DUMMY_ENTITY_VAL = "" -class FeatureView: +class FeatureView(BaseFeatureView): """ A FeatureView defines a logical grouping of serveable features. @@ -67,9 +67,7 @@ class FeatureView: FeatureViews. """ - name: str entities: List[str] - features: List[Feature] tags: Optional[Dict[str, str]] ttl: timedelta online: bool @@ -79,7 +77,6 @@ class FeatureView: created_timestamp: Optional[datetime] = None last_updated_timestamp: Optional[datetime] = None materialization_intervals: List[Tuple[datetime, datetime]] - projection: FeatureViewProjection @log_exceptions def __init__( @@ -123,10 +120,9 @@ def __init__( f"Entity or Feature name." ) - self.name = name + super().__init__(name, _features) self.entities = entities if entities else [DUMMY_ENTITY_NAME] - self.features = _features - self.tags = tags if tags else {} + self.tags = tags if tags is not None else {} if isinstance(ttl, Duration): self.ttl = timedelta(seconds=int(ttl.seconds)) @@ -143,17 +139,9 @@ def __init__( self.created_timestamp: Optional[datetime] = None self.last_updated_timestamp: Optional[datetime] = None - self.projection = FeatureViewProjection.from_definition(self) - - def __repr__(self): - items = (f"{k} = {v}" for k, v in self.__dict__.items()) - return f"<{self.__class__.__name__}({', '.join(items)})>" - - def __str__(self): - return str(MessageToJson(self.to_proto())) - + # Note: Python requires redefining hash in child classes that override __eq__ def __hash__(self): - return hash((id(self), self.name)) + return super().__hash__() def __copy__(self): fv = FeatureView( @@ -170,28 +158,17 @@ def __copy__(self): fv.projection = copy.copy(self.projection) return fv - def __getitem__(self, item): - assert isinstance(item, list) - - referenced_features = [] - for feature in self.features: - if feature.name in item: - referenced_features.append(feature) - - cp = self.__copy__() - cp.projection.features = referenced_features - - return cp - def __eq__(self, other): if not isinstance(other, FeatureView): raise TypeError( "Comparisons should only involve FeatureView class objects." ) + if not super().__eq__(other): + return False + if ( self.tags != other.tags - or self.name != other.name or self.ttl != other.ttl or self.online != other.online ): @@ -199,8 +176,6 @@ def __eq__(self, other): if sorted(self.entities) != sorted(other.entities): return False - if sorted(self.features) != sorted(other.features): - return False if self.batch_source != other.batch_source: return False if self.stream_source != other.stream_source: @@ -208,19 +183,22 @@ def __eq__(self, other): return True - def is_valid(self): + def ensure_valid(self): """ Validates the state of this feature view locally. Raises: ValueError: The feature view does not have a name or does not have entities. """ - if not self.name: - raise ValueError("Feature view needs a name.") + super().ensure_valid() if not self.entities: raise ValueError("Feature view has no entities.") + @property + def proto_class(self) -> Type[FeatureViewProto]: + return FeatureViewProto + def with_name(self, name: str): """ Renames this feature view by returning a copy of this feature view with an alias diff --git a/sdk/python/feast/infra/offline_stores/offline_store.py b/sdk/python/feast/infra/offline_stores/offline_store.py index 4fa108d86b0..48c70f9d6c2 100644 --- a/sdk/python/feast/infra/offline_stores/offline_store.py +++ b/sdk/python/feast/infra/offline_stores/offline_store.py @@ -44,6 +44,7 @@ def to_df(self) -> pd.DataFrame: if self.on_demand_feature_views is None: return features_df + # TODO(adchia): Fix requirement to specify dependent feature views in feature_refs for odfv in self.on_demand_feature_views: features_df = features_df.join( odfv.get_transformed_features_df(self.full_feature_names, features_df) diff --git a/sdk/python/feast/on_demand_feature_view.py b/sdk/python/feast/on_demand_feature_view.py index 0516346986f..95832fa143c 100644 --- a/sdk/python/feast/on_demand_feature_view.py +++ b/sdk/python/feast/on_demand_feature_view.py @@ -1,12 +1,13 @@ import copy import functools from types import MethodType -from typing import Dict, List, Union, cast +from typing import Dict, List, Type, Union import dill import pandas as pd from feast import errors +from feast.base_feature_view import BaseFeatureView from feast.data_source import RequestDataSource from feast.errors import RegistryInferenceFailure from feast.feature import Feature @@ -30,7 +31,7 @@ from feast.value_type import ValueType -class OnDemandFeatureView: +class OnDemandFeatureView(BaseFeatureView): """ [Experimental] An OnDemandFeatureView defines on demand transformations on existing feature view values and request data. @@ -42,11 +43,11 @@ class OnDemandFeatureView: udf: User defined transformation function that takes as input pandas dataframes """ - name: str - features: List[Feature] + # TODO(adchia): remove inputs from proto and declaration inputs: Dict[str, Union[FeatureView, RequestDataSource]] + input_feature_views: Dict[str, FeatureView] + input_request_data_sources: Dict[str, RequestDataSource] udf: MethodType - projection: FeatureViewProjection @log_exceptions def __init__( @@ -59,15 +60,21 @@ def __init__( """ Creates an OnDemandFeatureView object. """ - - self.name = name - self.features = features + super().__init__(name, features) self.inputs = inputs + self.input_feature_views = {} + self.input_request_data_sources = {} + for input_ref, odfv_input in inputs.items(): + if isinstance(odfv_input, RequestDataSource): + self.input_request_data_sources[input_ref] = odfv_input + else: + self.input_feature_views[input_ref] = odfv_input + self.udf = udf - self.projection = FeatureViewProjection.from_definition(self) - def __hash__(self) -> int: - return hash((id(self), self.name)) + @property + def proto_class(self) -> Type[OnDemandFeatureViewProto]: + return OnDemandFeatureViewProto def __copy__(self): fv = OnDemandFeatureView( @@ -76,70 +83,6 @@ def __copy__(self): fv.projection = copy.copy(self.projection) return fv - def __getitem__(self, item): - assert isinstance(item, list) - - referenced_features = [] - for feature in self.features: - if feature.name in item: - referenced_features.append(feature) - - cp = self.__copy__() - cp.projection.features = referenced_features - - return cp - - def with_name(self, name: str): - """ - Renames this on-demand feature view by returning a copy of this feature view with an alias - set for the feature view name. This rename operation is only used as part of query - operations and will not modify the underlying OnDemandFeatureView. - - Args: - name: Name to assign to the OnDemandFeatureView copy. - - Returns: - A copy of this OnDemandFeatureView with the name replaced with the 'name' input. - """ - cp = self.__copy__() - cp.projection.name_alias = name - - return cp - - def with_projection(self, feature_view_projection: FeatureViewProjection): - """ - Sets the feature view projection by returning a copy of this on-demand feature view - with its projection set to the given projection. A projection is an - object that stores the modifications to a feature view that is used during - query operations. - - Args: - feature_view_projection: The FeatureViewProjection object to link to this - OnDemandFeatureView. - - Returns: - A copy of this OnDemandFeatureView with its projection replaced with the - 'feature_view_projection' argument. - """ - if feature_view_projection.name != self.name: - raise ValueError( - f"The projection for the {self.name} FeatureView cannot be applied because it differs in name. " - f"The projection is named {feature_view_projection.name} and the name indicates which " - "FeatureView the projection is for." - ) - - for feature in feature_view_projection.features: - if feature not in self.features: - raise ValueError( - f"The projection for {self.name} cannot be applied because it contains {feature.name} which the " - "FeatureView doesn't have." - ) - - cp = self.__copy__() - cp.projection = feature_view_projection - - return cp - def to_proto(self) -> OnDemandFeatureViewProto: """ Converts an on demand feature view object to its protobuf representation. @@ -148,15 +91,12 @@ def to_proto(self) -> OnDemandFeatureViewProto: A OnDemandFeatureViewProto protobuf. """ inputs = {} - for feature_ref, input in self.inputs.items(): - if type(input) == FeatureView: - fv = cast(FeatureView, input) - inputs[feature_ref] = OnDemandInput(feature_view=fv.to_proto()) - else: - request_data_source = cast(RequestDataSource, input) - inputs[feature_ref] = OnDemandInput( - request_data_source=request_data_source.to_proto() - ) + for input_ref, fv in self.input_feature_views.items(): + inputs[input_ref] = OnDemandInput(feature_view=fv.to_proto()) + for input_ref, request_data_source in self.input_request_data_sources.items(): + inputs[input_ref] = OnDemandInput( + request_data_source=request_data_source.to_proto() + ) spec = OnDemandFeatureViewSpec( name=self.name, @@ -217,6 +157,12 @@ def from_proto(cls, on_demand_feature_view_proto: OnDemandFeatureViewProto): return on_demand_feature_view_obj + def get_request_data_schema(self) -> Dict[str, ValueType]: + schema: Dict[str, ValueType] = {} + for request_data_source in self.input_request_data_sources.values(): + schema.update(request_data_source.schema) + return schema + def get_transformed_features_df( self, full_feature_names: bool, df_with_features: pd.DataFrame ) -> pd.DataFrame: @@ -225,10 +171,7 @@ def get_transformed_features_df( # Copy over un-prefixed features even if not requested since transform may need it columns_to_cleanup = [] if full_feature_names: - for input in self.inputs.values(): - if type(input) != FeatureView: - continue - input_fv = cast(FeatureView, input) + for input_fv in self.input_feature_views.values(): for feature in input_fv.features: full_feature_ref = f"{input_fv.name}__{feature.name}" if full_feature_ref in df_with_features.keys(): @@ -248,25 +191,19 @@ def infer_features(self): """ Infers the set of features associated to this feature view from the input source. - Args: - config: Configuration object used to configure the feature store. - Raises: RegistryInferenceFailure: The set of features could not be inferred. """ df = pd.DataFrame() - for input in self.inputs.values(): - if type(input) == FeatureView: - feature_view = cast(FeatureView, input) - for feature in feature_view.features: - dtype = feast_value_type_to_pandas_type(feature.dtype) - df[f"{feature_view.name}__{feature.name}"] = pd.Series(dtype=dtype) - df[f"{feature.name}"] = pd.Series(dtype=dtype) - else: - request_data = cast(RequestDataSource, input) - for feature_name, feature_type in request_data.schema.items(): - dtype = feast_value_type_to_pandas_type(feature_type) - df[f"{feature_name}"] = pd.Series(dtype=dtype) + for feature_view in self.input_feature_views.values(): + for feature in feature_view.features: + dtype = feast_value_type_to_pandas_type(feature.dtype) + df[f"{feature_view.name}__{feature.name}"] = pd.Series(dtype=dtype) + df[f"{feature.name}"] = pd.Series(dtype=dtype) + for request_data in self.input_request_data_sources.values(): + for feature_name, feature_type in request_data.schema.items(): + dtype = feast_value_type_to_pandas_type(feature_type) + df[f"{feature_name}"] = pd.Series(dtype=dtype) output_df: pd.DataFrame = self.udf.__call__(df) inferred_features = [] for f, dt in zip(output_df.columns, output_df.dtypes): diff --git a/sdk/python/feast/registry.py b/sdk/python/feast/registry.py index 0681ecb6798..ba1397a44ee 100644 --- a/sdk/python/feast/registry.py +++ b/sdk/python/feast/registry.py @@ -14,10 +14,14 @@ from datetime import datetime, timedelta from pathlib import Path -from typing import List, Optional, Set +from typing import Dict, List, Optional from urllib.parse import urlparse +from google.protobuf.internal.containers import RepeatedCompositeFieldContainer +from proto import Message + from feast import importer +from feast.base_feature_view import BaseFeatureView from feast.entity import Entity from feast.errors import ( ConflictingFeatureViewNames, @@ -33,6 +37,7 @@ from feast.on_demand_feature_view import OnDemandFeatureView from feast.protos.feast.core.Registry_pb2 import Registry as RegistryProto from feast.repo_config import RegistryConfig +from feast.request_feature_view import RequestFeatureView REGISTRY_SCHEMA_VERSION = "1" @@ -290,7 +295,7 @@ def apply_feature_table( self.commit() def apply_feature_view( - self, feature_view: FeatureView, project: str, commit: bool = True + self, feature_view: BaseFeatureView, project: str, commit: bool = True ): """ Registers a single feature view with Feast @@ -300,74 +305,46 @@ def apply_feature_view( project: Feast project that this feature view belongs to commit: Whether the change should be persisted immediately """ - feature_view.is_valid() + feature_view.ensure_valid() feature_view_proto = feature_view.to_proto() feature_view_proto.spec.project = project self._prepare_registry_for_changes() assert self.cached_registry_proto - if feature_view.name in self._get_existing_on_demand_feature_view_names(): - raise ConflictingFeatureViewNames(feature_view.name) + self._check_conflicting_feature_view_names(feature_view) + existing_feature_views_of_same_type: RepeatedCompositeFieldContainer + if isinstance(feature_view, FeatureView): + existing_feature_views_of_same_type = ( + self.cached_registry_proto.feature_views + ) + elif isinstance(feature_view, OnDemandFeatureView): + existing_feature_views_of_same_type = ( + self.cached_registry_proto.on_demand_feature_views + ) + elif isinstance(feature_view, RequestFeatureView): + existing_feature_views_of_same_type = ( + self.cached_registry_proto.request_feature_views + ) + else: + raise ValueError(f"Unexpected feature view type: {type(feature_view)}") for idx, existing_feature_view_proto in enumerate( - self.cached_registry_proto.feature_views + existing_feature_views_of_same_type ): if ( existing_feature_view_proto.spec.name == feature_view_proto.spec.name and existing_feature_view_proto.spec.project == project - ): - if FeatureView.from_proto(existing_feature_view_proto) == feature_view: - return - else: - del self.cached_registry_proto.feature_views[idx] - break - - self.cached_registry_proto.feature_views.append(feature_view_proto) - if commit: - self.commit() - - def apply_on_demand_feature_view( - self, - on_demand_feature_view: OnDemandFeatureView, - project: str, - commit: bool = True, - ): - """ - Registers a single on demand feature view with Feast - - Args: - on_demand_feature_view: Feature view that will be registered - project: Feast project that this feature view belongs to - commit: Whether the change should be persisted immediately - """ - on_demand_feature_view_proto = on_demand_feature_view.to_proto() - on_demand_feature_view_proto.spec.project = project - self._prepare_registry_for_changes() - assert self.cached_registry_proto - - if on_demand_feature_view.name in self._get_existing_feature_view_names(): - raise ConflictingFeatureViewNames(on_demand_feature_view.name) - - for idx, existing_feature_view_proto in enumerate( - self.cached_registry_proto.on_demand_feature_views - ): - if ( - existing_feature_view_proto.spec.name - == on_demand_feature_view_proto.spec.name - and existing_feature_view_proto.spec.project == project ): if ( - OnDemandFeatureView.from_proto(existing_feature_view_proto) - == on_demand_feature_view + feature_view.__class__.from_proto(existing_feature_view_proto) + == feature_view ): return else: - del self.cached_registry_proto.on_demand_feature_views[idx] + del self.cached_registry_proto.feature_views[idx] break - self.cached_registry_proto.on_demand_feature_views.append( - on_demand_feature_view_proto - ) + existing_feature_views_of_same_type.append(feature_view_proto) if commit: self.commit() @@ -487,18 +464,40 @@ def list_feature_views( Args: allow_cache: Allow returning feature views from the cached registry - project: Filter feature tables based on project name + project: Filter feature views based on project name Returns: List of feature views """ registry_proto = self._get_registry_proto(allow_cache=allow_cache) - feature_views = [] + feature_views: List[FeatureView] = [] for feature_view_proto in registry_proto.feature_views: if feature_view_proto.spec.project == project: feature_views.append(FeatureView.from_proto(feature_view_proto)) return feature_views + def list_request_feature_views( + self, project: str, allow_cache: bool = False + ) -> List[RequestFeatureView]: + """ + Retrieve a list of request feature views from the registry + + Args: + allow_cache: Allow returning feature views from the cached registry + project: Filter feature views based on project name + + Returns: + List of feature views + """ + registry_proto = self._get_registry_proto(allow_cache=allow_cache) + feature_views: List[RequestFeatureView] = [] + for request_feature_view_proto in registry_proto.request_feature_views: + if request_feature_view_proto.spec.project == project: + feature_views.append( + RequestFeatureView.from_proto(request_feature_view_proto) + ) + return feature_views + def get_feature_table(self, name: str, project: str) -> FeatureTable: """ Retrieves a feature table. @@ -616,6 +615,18 @@ def delete_feature_view(self, name: str, project: str, commit: bool = True): self.commit() return + for idx, existing_request_feature_view_proto in enumerate( + self.cached_registry_proto.request_feature_views + ): + if ( + existing_request_feature_view_proto.spec.name == name + and existing_request_feature_view_proto.spec.project == project + ): + del self.cached_registry_proto.request_feature_views[idx] + if commit: + self.commit() + return + raise FeatureViewNotFoundException(name, project) def commit(self): @@ -675,15 +686,22 @@ def _get_registry_proto(self, allow_cache: bool = False) -> RegistryProto: self.cache_being_updated = False return registry_proto - def _get_existing_feature_view_names(self) -> Set[str]: - assert self.cached_registry_proto - return set([fv.spec.name for fv in self.cached_registry_proto.feature_views]) + def _check_conflicting_feature_view_names(self, feature_view: BaseFeatureView): + name_to_fv_protos = self._existing_feature_view_names_to_fvs() + if feature_view.name in name_to_fv_protos: + if not isinstance( + name_to_fv_protos.get(feature_view.name), feature_view.proto_class + ): + raise ConflictingFeatureViewNames(feature_view.name) - def _get_existing_on_demand_feature_view_names(self) -> Set[str]: + def _existing_feature_view_names_to_fvs(self) -> Dict[str, Message]: assert self.cached_registry_proto - return set( - [ - odfv.spec.name - for odfv in self.cached_registry_proto.on_demand_feature_views - ] - ) + odfvs = { + fv.spec.name: fv + for fv in self.cached_registry_proto.on_demand_feature_views + } + fvs = {fv.spec.name: fv for fv in self.cached_registry_proto.feature_views} + request_fvs = { + fv.spec.name: fv for fv in self.cached_registry_proto.request_feature_views + } + return {**odfvs, **fvs, **request_fvs} diff --git a/sdk/python/feast/repo_operations.py b/sdk/python/feast/repo_operations.py index 9378cbdf318..39ab8f21f49 100644 --- a/sdk/python/feast/repo_operations.py +++ b/sdk/python/feast/repo_operations.py @@ -5,12 +5,13 @@ import sys from importlib.abc import Loader from pathlib import Path -from typing import List, NamedTuple, Set, Tuple, Union +from typing import List, NamedTuple, Set, Tuple, Union, cast import click from click.exceptions import BadParameter from feast import Entity, FeatureTable +from feast.base_feature_view import BaseFeatureView from feast.feature_service import FeatureService from feast.feature_store import FeatureStore, _validate_feature_views from feast.feature_view import FeatureView @@ -19,6 +20,7 @@ from feast.on_demand_feature_view import OnDemandFeatureView from feast.registry import Registry from feast.repo_config import RepoConfig +from feast.request_feature_view import RequestFeatureView from feast.usage import log_exceptions_and_usage @@ -34,6 +36,7 @@ class ParsedRepo(NamedTuple): feature_tables: Set[FeatureTable] feature_views: Set[FeatureView] on_demand_feature_views: Set[OnDemandFeatureView] + request_feature_views: Set[RequestFeatureView] entities: Set[Entity] feature_services: Set[FeatureService] @@ -99,6 +102,7 @@ def parse_repo(repo_root: Path) -> ParsedRepo: feature_views=set(), feature_services=set(), on_demand_feature_views=set(), + request_feature_views=set(), ) for repo_file in get_repo_files(repo_root): @@ -116,6 +120,8 @@ def parse_repo(repo_root: Path) -> ParsedRepo: res.feature_services.add(obj) elif isinstance(obj, OnDemandFeatureView): res.on_demand_feature_views.add(obj) + elif isinstance(obj, RequestFeatureView): + res.request_feature_views.add(obj) return res @@ -136,7 +142,13 @@ def apply_total(repo_config: RepoConfig, repo_path: Path, skip_source_validation registry._initialize_registry() sys.dont_write_bytecode = True repo = parse_repo(repo_path) - _validate_feature_views(list(repo.feature_views)) + _validate_feature_views( + [ + *list(repo.feature_views), + *list(repo.on_demand_feature_views), + *list(repo.request_feature_views), + ] + ) if not skip_source_validation: data_sources = [t.batch_source for t in repo.feature_views] @@ -193,7 +205,7 @@ def apply_total(repo_config: RepoConfig, repo_path: Path, skip_source_validation # Add / update views + entities + services all_to_apply: List[ - Union[Entity, FeatureView, FeatureService, OnDemandFeatureView] + Union[Entity, BaseFeatureView, FeatureService, OnDemandFeatureView] ] = [] all_to_apply.extend(entities_to_keep) all_to_apply.extend(views_to_keep) @@ -225,13 +237,19 @@ def apply_total(repo_config: RepoConfig, repo_path: Path, skip_source_validation ) infra_provider = get_provider(repo_config, repo_path) + views_to_keep_in_infra = [ + view for view in views_to_keep if isinstance(view, FeatureView) + ] for name in [view.name for view in repo.feature_tables] + [ - table.name for table in repo.feature_views + table.name for table in views_to_keep_in_infra ]: click.echo( f"Deploying infrastructure for {Style.BRIGHT + Fore.GREEN}{name}{Style.RESET_ALL}" ) - for name in [view.name for view in views_to_delete] + [ + views_to_delete_from_infra = [ + view for view in views_to_delete if isinstance(view, FeatureView) + ] + for name in [view.name for view in views_to_delete_from_infra] + [ table.name for table in tables_to_delete ]: click.echo( @@ -241,10 +259,10 @@ def apply_total(repo_config: RepoConfig, repo_path: Path, skip_source_validation all_to_delete: List[Union[FeatureTable, FeatureView]] = [] all_to_delete.extend(tables_to_delete) - all_to_delete.extend(views_to_delete) + all_to_delete.extend(views_to_delete_from_infra) all_to_keep: List[Union[FeatureTable, FeatureView]] = [] all_to_keep.extend(tables_to_keep) - all_to_keep.extend(views_to_delete) + all_to_keep.extend(views_to_keep_in_infra) infra_provider.update_infra( project, tables_to_delete=all_to_delete, @@ -272,9 +290,11 @@ def _tag_registry_entities_for_keep_delete( def _tag_registry_views_for_keep_delete( project: str, registry: Registry, repo: ParsedRepo -) -> Tuple[Set[FeatureView], Set[FeatureView]]: - views_to_keep: Set[FeatureView] = repo.feature_views - views_to_delete: Set[FeatureView] = set() +) -> Tuple[Set[BaseFeatureView], Set[BaseFeatureView]]: + views_to_keep: Set[BaseFeatureView] = cast(Set[BaseFeatureView], repo.feature_views) + for request_fv in repo.request_feature_views: + views_to_keep.add(request_fv) + views_to_delete: Set[BaseFeatureView] = set() repo_feature_view_names = set(t.name for t in repo.feature_views) for registry_view in registry.list_feature_views(project=project): if registry_view.name not in repo_feature_view_names: diff --git a/sdk/python/feast/request_feature_view.py b/sdk/python/feast/request_feature_view.py new file mode 100644 index 00000000000..5d716f2f8d2 --- /dev/null +++ b/sdk/python/feast/request_feature_view.py @@ -0,0 +1,91 @@ +import copy +from typing import Type + +from feast.base_feature_view import BaseFeatureView +from feast.data_source import RequestDataSource +from feast.feature import Feature +from feast.feature_view_projection import FeatureViewProjection +from feast.protos.feast.core.RequestFeatureView_pb2 import ( + RequestFeatureView as RequestFeatureViewProto, +) +from feast.protos.feast.core.RequestFeatureView_pb2 import RequestFeatureViewSpec +from feast.usage import log_exceptions + + +class RequestFeatureView(BaseFeatureView): + """ + [Experimental] An RequestFeatureView defines a feature that is available from the inference request. + + Args: + name: Name of the group of features. + request_data_source: Request data source that specifies the schema and features + """ + + request_data_source: RequestDataSource + + @log_exceptions + def __init__( + self, name: str, request_data_source: RequestDataSource, + ): + """ + Creates an RequestFeatureView object. + """ + super().__init__( + name=name, + features=[ + Feature(name=name, dtype=dtype) + for name, dtype in request_data_source.schema.items() + ], + ) + self.request_data_source = request_data_source + + @property + def proto_class(self) -> Type[RequestFeatureViewProto]: + return RequestFeatureViewProto + + def to_proto(self) -> RequestFeatureViewProto: + """ + Converts an request feature view object to its protobuf representation. + + Returns: + A RequestFeatureViewProto protobuf. + """ + spec = RequestFeatureViewSpec( + name=self.name, request_data_source=self.request_data_source.to_proto() + ) + + return RequestFeatureViewProto(spec=spec) + + @classmethod + def from_proto(cls, request_feature_view_proto: RequestFeatureViewProto): + """ + Creates a request feature view from a protobuf representation. + + Args: + request_feature_view_proto: A protobuf representation of an request feature view. + + Returns: + A RequestFeatureView object based on the request feature view protobuf. + """ + + request_feature_view_obj = cls( + name=request_feature_view_proto.spec.name, + request_data_source=RequestDataSource.from_proto( + request_feature_view_proto.spec.request_data_source + ), + ) + + # FeatureViewProjections are not saved in the RequestFeatureView proto. + # Create the default projection. + request_feature_view_obj.projection = FeatureViewProjection.from_definition( + request_feature_view_obj + ) + + return request_feature_view_obj + + def __copy__(self): + fv = RequestFeatureView( + name=self.name, request_data_source=self.request_data_source + ) + fv.projection = copy.copy(self.projection) + return fv diff --git a/sdk/python/feast/usage.py b/sdk/python/feast/usage.py index 855f622fa70..1a0a89471d5 100644 --- a/sdk/python/feast/usage.py +++ b/sdk/python/feast/usage.py @@ -47,6 +47,8 @@ class UsageEvent(enum.Enum): APPLY_WITH_ODFV = 1 GET_HISTORICAL_FEATURES_WITH_ODFV = 2 GET_ONLINE_FEATURES_WITH_ODFV = 3 + GET_HISTORICAL_FEATURES_WITH_REQUEST_FV = 4 + GET_ONLINE_FEATURES_WITH_REQUEST_FV = 5 def __str__(self): return self.name.lower() diff --git a/sdk/python/tests/integration/feature_repos/repo_configuration.py b/sdk/python/tests/integration/feature_repos/repo_configuration.py index b98de8a1422..c51585823f1 100644 --- a/sdk/python/tests/integration/feature_repos/repo_configuration.py +++ b/sdk/python/tests/integration/feature_repos/repo_configuration.py @@ -29,6 +29,7 @@ conv_rate_plus_100_feature_view, create_conv_rate_request_data_source, create_customer_daily_profile_feature_view, + create_driver_age_request_feature_view, create_driver_hourly_stats_feature_view, create_global_stats_feature_view, create_location_stats_feature_view, @@ -222,6 +223,7 @@ def construct_universal_feature_views( "input_request": create_conv_rate_request_data_source(), } ), + "driver_age_request_fv": create_driver_age_request_feature_view(), "order": create_order_feature_view(data_sources["orders"]), "location": create_location_stats_feature_view(data_sources["location"]), } diff --git a/sdk/python/tests/integration/feature_repos/universal/feature_views.py b/sdk/python/tests/integration/feature_repos/universal/feature_views.py index f68d1a6555b..b566fa46099 100644 --- a/sdk/python/tests/integration/feature_repos/universal/feature_views.py +++ b/sdk/python/tests/integration/feature_repos/universal/feature_views.py @@ -5,6 +5,7 @@ from feast import Feature, FeatureView, OnDemandFeatureView, ValueType from feast.data_source import DataSource, RequestDataSource +from feast.request_feature_view import RequestFeatureView def driver_feature_view( @@ -63,6 +64,15 @@ def conv_rate_plus_100_feature_view( ) +def create_driver_age_request_feature_view(): + return RequestFeatureView( + name="driver_age", + request_data_source=RequestDataSource( + name="driver_age_source", schema={"driver_age": ValueType.INT32} + ), + ) + + def create_conv_rate_request_data_source(): return RequestDataSource( name="conv_rate_input", schema={"val_to_add": ValueType.INT32} diff --git a/sdk/python/tests/integration/offline_store/test_universal_historical_retrieval.py b/sdk/python/tests/integration/offline_store/test_universal_historical_retrieval.py index e6194cb0128..b558bbe3e89 100644 --- a/sdk/python/tests/integration/offline_store/test_universal_historical_retrieval.py +++ b/sdk/python/tests/integration/offline_store/test_universal_historical_retrieval.py @@ -256,19 +256,35 @@ def test_historical_features(environment, universal_data_sources, full_feature_n entity_df_with_request_data["val_to_add"] = [ i for i in range(len(entity_df_with_request_data)) ] + entity_df_with_request_data["driver_age"] = [ + i + 100 for i in range(len(entity_df_with_request_data)) + ] - customer_fv, driver_fv, driver_odfv, location_fv, order_fv, global_fv = ( + ( + customer_fv, + driver_fv, + driver_odfv, + location_fv, + order_fv, + global_fv, + driver_age_request_fv, + ) = ( feature_views["customer"], feature_views["driver"], feature_views["driver_odfv"], feature_views["location"], feature_views["order"], feature_views["global"], + feature_views["driver_age_request_fv"], ) feature_service = FeatureService( name="convrate_plus100", - features=[feature_views["driver"][["conv_rate"]], feature_views["driver_odfv"]], + features=[ + feature_views["driver"][["conv_rate"]], + driver_odfv, + driver_age_request_fv, + ], ) feature_service_entity_mapping = FeatureService( name="entity_mapping", @@ -291,6 +307,7 @@ def test_historical_features(environment, universal_data_sources, full_feature_n location_fv, order_fv, global_fv, + driver_age_request_fv, driver(), customer(), location(), @@ -356,7 +373,12 @@ def test_historical_features(environment, universal_data_sources, full_feature_n # Not requesting the on demand transform with an entity_df query (can't add request data in them) expected_df_query = expected_df.drop( - columns=["conv_rate_plus_100", "val_to_add", "conv_rate_plus_val_to_add"] + columns=[ + "conv_rate_plus_100", + "val_to_add", + "conv_rate_plus_val_to_add", + "driver_age", + ] ) assert sorted(expected_df_query.columns) == sorted( actual_df_from_sql_entities.columns @@ -408,6 +430,7 @@ def test_historical_features(environment, universal_data_sources, full_feature_n "order:order_is_success", "global_stats:num_rides", "global_stats:avg_ride_length", + "driver_age:driver_age", ], full_feature_names=full_feature_names, ) @@ -483,6 +506,22 @@ def test_historical_features(environment, universal_data_sources, full_feature_n ], full_feature_names=full_feature_names, ) + # If request data is missing that's needed for a request feature view, throw an error + with pytest.raises(RequestDataNotFoundInEntityDfException): + store.get_historical_features( + entity_df=entity_df, + features=[ + "driver_stats:conv_rate", + "driver_stats:avg_daily_trips", + "customer_profile:current_balance", + "customer_profile:avg_passenger_count", + "customer_profile:lifetime_trip_count", + "driver_age:driver_age", + "global_stats:num_rides", + "global_stats:avg_ride_length", + ], + full_feature_names=full_feature_names, + ) @pytest.mark.integration @@ -624,6 +663,7 @@ def assert_feature_service_correctness( "customer_id", response_feature_name("conv_rate", full_feature_names), "conv_rate_plus_100", + "driver_age", ] ] actual_df_from_df_entities = ( diff --git a/sdk/python/tests/integration/online_store/test_universal_online.py b/sdk/python/tests/integration/online_store/test_universal_online.py index 381635061e7..b9824ae1636 100644 --- a/sdk/python/tests/integration/online_store/test_universal_online.py +++ b/sdk/python/tests/integration/online_store/test_universal_online.py @@ -32,7 +32,11 @@ def test_online_retrieval(environment, universal_data_sources, full_feature_name feature_service = FeatureService( "convrate_plus100", - features=[feature_views["driver"][["conv_rate"]], feature_views["driver_odfv"]], + features=[ + feature_views["driver"][["conv_rate"]], + feature_views["driver_odfv"], + feature_views["driver_age_request_fv"], + ], ) feature_service_entity_mapping = FeatureService( name="entity_mapping", @@ -95,7 +99,7 @@ def test_online_retrieval(environment, universal_data_sources, full_feature_name global_df = datasets["global"] entity_rows = [ - {"driver": d, "customer_id": c, "val_to_add": 50} + {"driver": d, "customer_id": c, "val_to_add": 50, "driver_age": 25} for (d, c) in zip(sample_drivers, sample_customers) ] @@ -110,6 +114,7 @@ def test_online_retrieval(environment, universal_data_sources, full_feature_name "order:order_is_success", "global_stats:num_rides", "global_stats:avg_ride_length", + "driver_age:driver_age", ] unprefixed_feature_refs = [f.rsplit(":", 1)[-1] for f in feature_refs if ":" in f] # Remove the on demand feature view output features, since they're not present in the source dataframe @@ -129,7 +134,8 @@ def test_online_retrieval(environment, universal_data_sources, full_feature_name len(keys) == len(feature_refs) + 3 ) # Add three for the driver id and the customer id entity keys + val_to_add request data. for feature in feature_refs: - if full_feature_names: + # full_feature_names does not apply to request feature views + if full_feature_names and feature != "driver_age:driver_age": assert feature.replace(":", "__") in keys else: assert feature.rsplit(":", 1)[-1] in keys @@ -178,12 +184,14 @@ def test_online_retrieval(environment, universal_data_sources, full_feature_name # Check what happens for missing values missing_responses_dict = fs.get_online_features( features=feature_refs, - entity_rows=[{"driver": 0, "customer_id": 0, "val_to_add": 100}], + entity_rows=[ + {"driver": 0, "customer_id": 0, "val_to_add": 100, "driver_age": 125} + ], full_feature_names=full_feature_names, ).to_dict() assert missing_responses_dict is not None for unprefixed_feature_ref in unprefixed_feature_refs: - if unprefixed_feature_ref not in {"num_rides", "avg_ride_length"}: + if unprefixed_feature_ref not in {"num_rides", "avg_ride_length", "driver_age"}: tc.assertIsNone( missing_responses_dict[ response_feature_name(unprefixed_feature_ref, full_feature_names) @@ -198,6 +206,14 @@ def test_online_retrieval(environment, universal_data_sources, full_feature_name full_feature_names=full_feature_names, ).to_dict() + # Also with request data + with pytest.raises(RequestDataNotFoundInEntityRowsException): + fs.get_online_features( + features=feature_refs, + entity_rows=[{"driver": 0, "customer_id": 0, "val_to_add": 20}], + full_feature_names=full_feature_names, + ).to_dict() + assert_feature_service_correctness( fs, feature_service, From cf82ed6c2dc3478887718a67dd186ab7694b245d Mon Sep 17 00:00:00 2001 From: Danny Chiao Date: Fri, 15 Oct 2021 21:01:59 +0000 Subject: [PATCH 14/50] GitBook: [#307] Update hero image in master --- .../assets/feast-marchitecture-211014.png | Bin 0 -> 78580 bytes docs/README.md | 11 +- docs/assets/statistics-sources (1).png | Bin 0 -> 165507 bytes docs/assets/statistics-sources (2).png | Bin 0 -> 165507 bytes docs/assets/statistics-sources (3).png | Bin 0 -> 165507 bytes docs/assets/statistics-sources (4).png | Bin 0 -> 165507 bytes docs/community.md | 15 +- .../concepts/feature-service.md | 3 +- docs/getting-started/concepts/feature-view.md | 12 +- docs/getting-started/quickstart.md | 29 ++- .../adding-a-new-offline-store.md | 27 ++- .../adding-support-for-a-new-online-store.md | 29 ++- docs/project/feast-0.9-vs-feast-0.10+.md | 196 +++--------------- docs/project/release-process.md | 19 +- docs/reference/feature-repository/README.md | 15 +- .../feature-repository/feast-ignore.md | 15 +- .../feature-repository/feature-store-yaml.md | 9 +- docs/reference/offline-stores/bigquery.md | 3 +- docs/reference/offline-stores/file.md | 3 +- docs/reference/offline-stores/redshift.md | 95 +-------- docs/reference/online-stores/datastore.md | 5 +- docs/reference/online-stores/dynamodb.md | 42 +--- docs/reference/online-stores/redis.md | 9 +- docs/reference/online-stores/sqlite.md | 3 +- .../providers/amazon-web-services.md | 3 +- .../providers/google-cloud-platform.md | 93 +-------- docs/reference/providers/local.md | 3 +- docs/roadmap.md | 51 +++-- docs/specs/datastore_online_example (1).png | Bin 0 -> 108753 bytes docs/specs/redis_online_example (1).png | Bin 0 -> 125654 bytes 30 files changed, 180 insertions(+), 510 deletions(-) create mode 100644 docs/.gitbook/assets/feast-marchitecture-211014.png create mode 100644 docs/assets/statistics-sources (1).png create mode 100644 docs/assets/statistics-sources (2).png create mode 100644 docs/assets/statistics-sources (3).png create mode 100644 docs/assets/statistics-sources (4).png create mode 100644 docs/specs/datastore_online_example (1).png create mode 100644 docs/specs/redis_online_example (1).png diff --git a/docs/.gitbook/assets/feast-marchitecture-211014.png b/docs/.gitbook/assets/feast-marchitecture-211014.png new file mode 100644 index 0000000000000000000000000000000000000000..7ae82da02118529d2b2bffc729a994b00de8b93e GIT binary patch literal 78580 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%0A}Fvc8=_6==$#sZy5^~mko_BMk9!Gy0N>4 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