From a752211e1d0d6b44901d88f435328fc355d16c20 Mon Sep 17 00:00:00 2001 From: Derek Chang <13246451+derek1032@users.noreply.github.com> Date: Fri, 26 Aug 2022 02:17:47 +0800 Subject: [PATCH 01/46] feat: Add workgroup to athena offline store config (#3139) * fix: FeastModuleImportError Signed-off-by: derek1032 * chore: Add aws region for athena universal tests Signed-off-by: derek1032 * feat: Add workgroup attribute for Athena offline store config Signed-off-by: derek1032 * fix: Add fail_if_exists condition to query string Signed-off-by: derek1032 * chore: Format python codes Signed-off-by: derek1032 * chore: use os.getenv and default value Signed-off-by: derek1032 Signed-off-by: derek1032 --- Makefile | 9 ++- .../contrib/athena_offline_store/athena.py | 11 +++ .../athena_offline_store/athena_source.py | 1 + .../athena_offline_store/tests/data_source.py | 35 ++++----- .../contrib/athena_repo_configuration.py | 6 +- sdk/python/feast/infra/utils/aws_utils.py | 73 +++++++++++++------ 6 files changed, 85 insertions(+), 50 deletions(-) diff --git a/Makefile b/Makefile index 8e03ed5349f..990604da575 100644 --- a/Makefile +++ b/Makefile @@ -159,16 +159,19 @@ test-python-universal-mssql: sdk/python/tests -#To use Athena as an offline store, you need to create an Athena database and an S3 bucket on AWS. https://docs.aws.amazon.com/athena/latest/ug/getting-started.html -#Modify environment variables ATHENA_DATA_SOURCE, ATHENA_DATABASE, ATHENA_S3_BUCKET_NAME if you want to change the data source, database, and bucket name of S3 to use. -#If tests fail with the pytest -n 8 option, change the number to 1. +# To use Athena as an offline store, you need to create an Athena database and an S3 bucket on AWS. +# https://docs.aws.amazon.com/athena/latest/ug/getting-started.html +# Modify environment variables ATHENA_REGION, ATHENA_DATA_SOURCE, ATHENA_DATABASE, ATHENA_WORKGROUP or +# ATHENA_S3_BUCKET_NAME according to your needs. If tests fail with the pytest -n 8 option, change the number to 1. test-python-universal-athena: PYTHONPATH='.' \ FULL_REPO_CONFIGS_MODULE=sdk.python.feast.infra.offline_stores.contrib.athena_repo_configuration \ PYTEST_PLUGINS=feast.infra.offline_stores.contrib.athena_offline_store.tests \ FEAST_USAGE=False IS_TEST=True \ + ATHENA_REGION=ap-northeast-2 \ ATHENA_DATA_SOURCE=AwsDataCatalog \ ATHENA_DATABASE=default \ + ATHENA_WORKGROUP=primary \ ATHENA_S3_BUCKET_NAME=feast-integration-tests \ python -m pytest -n 8 --integration \ -k "not test_go_feature_server and \ diff --git a/sdk/python/feast/infra/offline_stores/contrib/athena_offline_store/athena.py b/sdk/python/feast/infra/offline_stores/contrib/athena_offline_store/athena.py index 5095a43d57c..e3bb4e8ccaa 100644 --- a/sdk/python/feast/infra/offline_stores/contrib/athena_offline_store/athena.py +++ b/sdk/python/feast/infra/offline_stores/contrib/athena_offline_store/athena.py @@ -60,6 +60,9 @@ class AthenaOfflineStoreConfig(FeastConfigBaseModel): database: StrictStr """ Athena database name """ + workgroup: StrictStr + """ Athena workgroup name """ + s3_staging_location: StrictStr """ S3 path for importing & exporting data to Athena """ @@ -243,6 +246,7 @@ def query_generator() -> Iterator[str]: athena_client, config.offline_store.data_source, config.offline_store.database, + config.offline_store.workgroup, f"DROP TABLE IF EXISTS {config.offline_store.database}.{table_name}", ) @@ -293,6 +297,7 @@ def write_logged_features( athena_client=athena_client, data_source=config.offline_store.data_source, database=config.offline_store.database, + workgroup=config.offline_store.workgroup, s3_resource=s3_resource, s3_path=s3_path, table_name=destination.table_name, @@ -378,6 +383,7 @@ def _to_df_internal(self) -> pd.DataFrame: self._athena_client, self._config.offline_store.data_source, self._config.offline_store.database, + self._config.offline_store.workgroup, self._s3_resource, temp_external_location, self.get_temp_table_dml_header(temp_table_name, temp_external_location) @@ -394,6 +400,7 @@ def _to_arrow_internal(self) -> pa.Table: self._athena_client, self._config.offline_store.data_source, self._config.offline_store.database, + self._config.offline_store.workgroup, self._s3_resource, temp_external_location, self.get_temp_table_dml_header(temp_table_name, temp_external_location) @@ -432,6 +439,7 @@ def to_athena(self, table_name: str) -> None: self._athena_client, self._config.offline_store.data_source, self._config.offline_store.database, + self._config.offline_store.workgroup, query, ) @@ -449,6 +457,7 @@ def _upload_entity_df( athena_client, config.offline_store.data_source, config.offline_store.database, + config.offline_store.workgroup, s3_resource, f"{config.offline_store.s3_staging_location}/entity_df/{table_name}/{table_name}.parquet", table_name, @@ -460,6 +469,7 @@ def _upload_entity_df( athena_client, config.offline_store.data_source, config.offline_store.database, + config.offline_store.workgroup, f"CREATE TABLE {table_name} AS ({entity_df})", ) else: @@ -514,6 +524,7 @@ def _get_entity_df_event_timestamp_range( athena_client, config.offline_store.data_source, config.offline_store.database, + config.offline_store.workgroup, f"SELECT MIN({entity_df_event_timestamp_col}) AS min, MAX({entity_df_event_timestamp_col}) AS max " f"FROM ({entity_df})", ) diff --git a/sdk/python/feast/infra/offline_stores/contrib/athena_offline_store/athena_source.py b/sdk/python/feast/infra/offline_stores/contrib/athena_offline_store/athena_source.py index bac027ff3eb..8e9e3893f3a 100644 --- a/sdk/python/feast/infra/offline_stores/contrib/athena_offline_store/athena_source.py +++ b/sdk/python/feast/infra/offline_stores/contrib/athena_offline_store/athena_source.py @@ -225,6 +225,7 @@ def get_table_column_names_and_types( client, config.offline_store.data_source, config.offline_store.database, + config.offline_store.workgroup, f"SELECT * FROM ({self.query}) LIMIT 1", ) columns = aws_utils.get_athena_query_result(client, statement_id)[ diff --git a/sdk/python/feast/infra/offline_stores/contrib/athena_offline_store/tests/data_source.py b/sdk/python/feast/infra/offline_stores/contrib/athena_offline_store/tests/data_source.py index 92e0d6e5f60..384ab69e81f 100644 --- a/sdk/python/feast/infra/offline_stores/contrib/athena_offline_store/tests/data_source.py +++ b/sdk/python/feast/infra/offline_stores/contrib/athena_offline_store/tests/data_source.py @@ -27,27 +27,20 @@ class AthenaDataSourceCreator(DataSourceCreator): def __init__(self, project_name: str, *args, **kwargs): super().__init__(project_name) - self.client = aws_utils.get_athena_data_client("ap-northeast-2") - self.s3 = aws_utils.get_s3_resource("ap-northeast-2") - data_source = ( - os.environ.get("ATHENA_DATA_SOURCE") - if os.environ.get("ATHENA_DATA_SOURCE") - else "AwsDataCatalog" - ) - database = ( - os.environ.get("ATHENA_DATABASE") - if os.environ.get("ATHENA_DATABASE") - else "default" - ) - bucket_name = ( - os.environ.get("ATHENA_S3_BUCKET_NAME") - if os.environ.get("ATHENA_S3_BUCKET_NAME") - else "feast-integration-tests" - ) + + region = os.getenv("ATHENA_REGION", "ap-northeast-2") + data_source = os.getenv("ATHENA_DATA_SOURCE", "AwsDataCatalog") + database = os.getenv("ATHENA_DATABASE", "default") + workgroup = os.getenv("ATHENA_WORKGROUP", "primary") + bucket_name = os.getenv("ATHENA_S3_BUCKET_NAME", "feast-integration-tests") + + self.client = aws_utils.get_athena_data_client(region) + self.s3 = aws_utils.get_s3_resource(region) self.offline_store_config = AthenaOfflineStoreConfig( - data_source=f"{data_source}", - region="ap-northeast-2", - database=f"{database}", + data_source=data_source, + region=region, + database=database, + workgroup=workgroup, s3_staging_location=f"s3://{bucket_name}/test_dir", ) @@ -77,6 +70,7 @@ def create_data_source( self.client, self.offline_store_config.data_source, self.offline_store_config.database, + self.offline_store_config.workgroup, self.s3, s3_target, table_name, @@ -126,5 +120,6 @@ def teardown(self): self.client, self.offline_store_config.data_source, self.offline_store_config.database, + self.offline_store_config.workgroup, f"DROP TABLE IF EXISTS {table}", ) diff --git a/sdk/python/feast/infra/offline_stores/contrib/athena_repo_configuration.py b/sdk/python/feast/infra/offline_stores/contrib/athena_repo_configuration.py index 32376eb6527..09bc6ce961c 100644 --- a/sdk/python/feast/infra/offline_stores/contrib/athena_repo_configuration.py +++ b/sdk/python/feast/infra/offline_stores/contrib/athena_repo_configuration.py @@ -1,9 +1,9 @@ +from feast.infra.offline_stores.contrib.athena_offline_store.tests.data_source import ( + AthenaDataSourceCreator, +) from tests.integration.feature_repos.integration_test_repo_config import ( IntegrationTestRepoConfig, ) -from tests.integration.feature_repos.universal.data_sources.athena import ( - AthenaDataSourceCreator, -) FULL_REPO_CONFIGS = [ IntegrationTestRepoConfig( diff --git a/sdk/python/feast/infra/utils/aws_utils.py b/sdk/python/feast/infra/utils/aws_utils.py index 07ce3ab17d4..7e8335ac92e 100644 --- a/sdk/python/feast/infra/utils/aws_utils.py +++ b/sdk/python/feast/infra/utils/aws_utils.py @@ -677,7 +677,7 @@ def list_s3_files(aws_region: str, path: str) -> List[str]: return files -# Athena +# Athena utils def get_athena_data_client(aws_region: str): @@ -696,16 +696,17 @@ def get_athena_data_client(aws_region: str): reraise=True, ) def execute_athena_query_async( - athena_data_client, data_source: str, database: str, query: str + athena_data_client, data_source: str, database: str, workgroup: str, query: str ) -> dict: """Execute Athena statement asynchronously. Does not wait for the query to finish. Raises AthenaCredentialsError if the statement couldn't be executed due to the validation error. Args: - athena_data_client: athena Data API Service client - data_source: athena Cluster Identifier - database: athena Database Name + athena_data_client: Athena Data API Service client + data_source: Athena Data Source + database: Athena Database Name + workgroup: Athena Workgroup Name query: The SQL query to execute Returns: JSON response @@ -716,7 +717,7 @@ def execute_athena_query_async( return athena_data_client.start_query_execution( QueryString=query, QueryExecutionContext={"Database": database}, - WorkGroup="primary", + WorkGroup=workgroup, ) except ClientError as e: @@ -755,16 +756,19 @@ def wait_for_athena_execution(athena_data_client, execution: dict) -> None: def drop_temp_table( - athena_data_client, data_source: str, database: str, temp_table: str + athena_data_client, data_source: str, database: str, workgroup: str, temp_table: str ): query = f"DROP TABLE `{database}.{temp_table}`" - execute_athena_query_async(athena_data_client, data_source, database, query) + execute_athena_query_async( + athena_data_client, data_source, database, workgroup, query + ) def execute_athena_query( athena_data_client, data_source: str, database: str, + workgroup: str, query: str, temp_table: str = None, ) -> str: @@ -775,22 +779,25 @@ def execute_athena_query( Args: - athena_data_client: athena Data API Service client - data_source: athena data source Name - database: athena Database Name + athena_data_client: Athena Data API Service client + data_source: Athena Data Source Name + database: Athena Database Name + workgroup: Athena Workgroup Name query: The SQL query to execute - temp_table: temp table name to be deleted after query execution. + temp_table: Temp table name to be deleted after query execution. Returns: Statement ID """ execution = execute_athena_query_async( - athena_data_client, data_source, database, query + athena_data_client, data_source, database, workgroup, query ) wait_for_athena_execution(athena_data_client, execution) if temp_table is not None: - drop_temp_table(athena_data_client, data_source, database, temp_table) + drop_temp_table( + athena_data_client, data_source, database, workgroup, temp_table + ) return execution["QueryExecutionId"] @@ -822,6 +829,7 @@ def unload_athena_query_to_pa( athena_data_client, data_source: str, database: str, + workgroup: str, s3_resource, s3_path: str, query: str, @@ -831,7 +839,7 @@ def unload_athena_query_to_pa( bucket, key = get_bucket_and_key(s3_path) execute_athena_query_and_unload_to_s3( - athena_data_client, data_source, database, query, temp_table + athena_data_client, data_source, database, workgroup, query, temp_table ) with tempfile.TemporaryDirectory() as temp_dir: @@ -844,6 +852,7 @@ def unload_athena_query_to_df( athena_data_client, data_source: str, database: str, + workgroup: str, s3_resource, s3_path: str, query: str, @@ -854,6 +863,7 @@ def unload_athena_query_to_df( athena_data_client, data_source, database, + workgroup, s3_resource, s3_path, query, @@ -866,6 +876,7 @@ def execute_athena_query_and_unload_to_s3( athena_data_client, data_source: str, database: str, + workgroup: str, query: str, temp_table: str, ) -> None: @@ -873,20 +884,29 @@ def execute_athena_query_and_unload_to_s3( Args: athena_data_client: Athena Data API Service client - data_source: Athena data source - database: Redshift Database Name + data_source: Athena Data Source + database: Athena Database Name + workgroup: Athena Workgroup Name query: The SQL query to execute temp_table: temp table name to be deleted after query execution. """ - execute_athena_query(athena_data_client, data_source, database, query, temp_table) + execute_athena_query( + athena_data_client=athena_data_client, + data_source=data_source, + database=database, + workgroup=workgroup, + query=query, + temp_table=temp_table, + ) def upload_df_to_athena( athena_client, data_source: str, database: str, + workgroup: str, s3_resource, s3_path: str, table_name: str, @@ -900,6 +920,7 @@ def upload_df_to_athena( athena_client: Athena API Service client data_source: Athena Data Source database: Athena Database Name + workgroup: Athena Workgroup Name s3_resource: S3 Resource object s3_path: S3 path where the Parquet file is temporarily uploaded table_name: The name of the new Data Catalog table where we copy the dataframe @@ -924,6 +945,7 @@ def upload_df_to_athena( athena_client, data_source=data_source, database=database, + workgroup=workgroup, s3_resource=s3_resource, s3_path=s3_path, table_name=table_name, @@ -935,6 +957,7 @@ def upload_arrow_table_to_athena( athena_client, data_source: str, database: str, + workgroup: str, s3_resource, s3_path: str, table_name: str, @@ -952,8 +975,9 @@ def upload_arrow_table_to_athena( Args: table: The Arrow Table or Path to parquet dataset to upload athena_client: Athena API Service client - data_source: Athena data source + data_source: Athena Data Source database: Athena Database Name + workgroup: Athena Workgroup Name s3_resource: S3 Resource object s3_path: S3 path where the Parquet file is temporarily uploaded table_name: The name of the new Athena table where we copy the dataframe @@ -986,7 +1010,7 @@ def upload_arrow_table_to_athena( s3_resource.Object(bucket, key).put(Body=parquet_temp_file) create_query = ( - f"CREATE EXTERNAL TABLE {database}.{table_name} " + f"CREATE EXTERNAL TABLE {database}.{table_name} {'IF NOT EXISTS' if not fail_if_exists else ''}" f"({column_query_list}) " f"STORED AS PARQUET " f"LOCATION '{s3_path[:s3_path.rfind('/')]}' " @@ -995,10 +1019,11 @@ def upload_arrow_table_to_athena( try: execute_athena_query( - athena_client, - data_source, - database, - f"{create_query}", + athena_data_client=athena_client, + data_source=data_source, + database=database, + workgroup=workgroup, + query=f"{create_query}", ) finally: pass From 3ae7650ffd9b02d20dce0d82606b27ca1e339fdc Mon Sep 17 00:00:00 2001 From: Felix Wang Date: Thu, 25 Aug 2022 11:52:45 -0700 Subject: [PATCH 02/46] chore: Revert "chore: Delete generated RTD docs (#3126)" (#3141) Revert "chore: Delete generated RTD docs (#3126)" This reverts commit 9af5bc214a141c3013f19d8e8f45bd8f94c5b199. Signed-off-by: Felix Wang Signed-off-by: Felix Wang --- .gitignore | 3 +- sdk/python/docs/source/conf.py | 201 ++++++++++ sdk/python/docs/source/feast.diff.rst | 37 ++ .../docs/source/feast.dqm.profilers.rst | 29 ++ sdk/python/docs/source/feast.dqm.rst | 29 ++ ....infra.materialization.contrib.bytewax.rst | 29 ++ .../feast.infra.materialization.contrib.rst | 10 + .../feast.infra.materialization.lambda.rst | 29 ++ .../source/feast.infra.materialization.rst | 45 +++ ...ne_stores.contrib.athena_offline_store.rst | 37 ++ ...res.contrib.athena_offline_store.tests.rst | 21 + ...ine_stores.contrib.mssql_offline_store.rst | 37 ++ ...ores.contrib.mssql_offline_store.tests.rst | 21 + ..._stores.contrib.postgres_offline_store.rst | 37 ++ ...s.contrib.postgres_offline_store.tests.rst | 21 + .../feast.infra.offline_stores.contrib.rst | 65 +++ ...ine_stores.contrib.spark_offline_store.rst | 37 ++ ...ores.contrib.spark_offline_store.tests.rst | 21 + ...contrib.trino_offline_store.connectors.rst | 21 + ...ine_stores.contrib.trino_offline_store.rst | 55 +++ ...ontrib.trino_offline_store.test_config.rst | 21 + ...ores.contrib.trino_offline_store.tests.rst | 21 + .../source/feast.infra.offline_stores.rst | 101 +++++ ..._stores.contrib.cassandra_online_store.rst | 21 + ...line_stores.contrib.hbase_online_store.rst | 21 + .../feast.infra.online_stores.contrib.rst | 54 +++ .../docs/source/feast.infra.online_stores.rst | 77 ++++ .../docs/source/feast.infra.registry.rst | 69 ++++ .../source/feast.infra.registry_stores.rst | 21 + sdk/python/docs/source/feast.infra.rst | 81 ++++ .../source/feast.infra.utils.postgres.rst | 29 ++ sdk/python/docs/source/feast.infra.utils.rst | 37 ++ sdk/python/docs/source/feast.loaders.rst | 21 + .../docs/source/feast.protos.feast.core.rst | 333 +++++++++++++++ sdk/python/docs/source/feast.protos.feast.rst | 21 + .../source/feast.protos.feast.serving.rst | 61 +++ .../source/feast.protos.feast.storage.rst | 29 ++ .../docs/source/feast.protos.feast.types.rst | 61 +++ sdk/python/docs/source/feast.protos.rst | 18 + sdk/python/docs/source/feast.rst | 378 ++++++++++++++++++ sdk/python/docs/source/feast.ui.rst | 10 + sdk/python/docs/source/index.rst | 353 ++++++++++++++++ sdk/python/docs/source/modules.rst | 7 + 43 files changed, 2628 insertions(+), 2 deletions(-) create mode 100644 sdk/python/docs/source/conf.py create mode 100644 sdk/python/docs/source/feast.diff.rst create mode 100644 sdk/python/docs/source/feast.dqm.profilers.rst create mode 100644 sdk/python/docs/source/feast.dqm.rst create mode 100644 sdk/python/docs/source/feast.infra.materialization.contrib.bytewax.rst create mode 100644 sdk/python/docs/source/feast.infra.materialization.contrib.rst create mode 100644 sdk/python/docs/source/feast.infra.materialization.lambda.rst create mode 100644 sdk/python/docs/source/feast.infra.materialization.rst create mode 100644 sdk/python/docs/source/feast.infra.offline_stores.contrib.athena_offline_store.rst create mode 100644 sdk/python/docs/source/feast.infra.offline_stores.contrib.athena_offline_store.tests.rst create mode 100644 sdk/python/docs/source/feast.infra.offline_stores.contrib.mssql_offline_store.rst create mode 100644 sdk/python/docs/source/feast.infra.offline_stores.contrib.mssql_offline_store.tests.rst create mode 100644 sdk/python/docs/source/feast.infra.offline_stores.contrib.postgres_offline_store.rst create mode 100644 sdk/python/docs/source/feast.infra.offline_stores.contrib.postgres_offline_store.tests.rst create mode 100644 sdk/python/docs/source/feast.infra.offline_stores.contrib.rst create mode 100644 sdk/python/docs/source/feast.infra.offline_stores.contrib.spark_offline_store.rst create mode 100644 sdk/python/docs/source/feast.infra.offline_stores.contrib.spark_offline_store.tests.rst create mode 100644 sdk/python/docs/source/feast.infra.offline_stores.contrib.trino_offline_store.connectors.rst create mode 100644 sdk/python/docs/source/feast.infra.offline_stores.contrib.trino_offline_store.rst create mode 100644 sdk/python/docs/source/feast.infra.offline_stores.contrib.trino_offline_store.test_config.rst create mode 100644 sdk/python/docs/source/feast.infra.offline_stores.contrib.trino_offline_store.tests.rst create mode 100644 sdk/python/docs/source/feast.infra.offline_stores.rst create mode 100644 sdk/python/docs/source/feast.infra.online_stores.contrib.cassandra_online_store.rst create mode 100644 sdk/python/docs/source/feast.infra.online_stores.contrib.hbase_online_store.rst create mode 100644 sdk/python/docs/source/feast.infra.online_stores.contrib.rst create mode 100644 sdk/python/docs/source/feast.infra.online_stores.rst create mode 100644 sdk/python/docs/source/feast.infra.registry.rst create mode 100644 sdk/python/docs/source/feast.infra.registry_stores.rst create mode 100644 sdk/python/docs/source/feast.infra.rst create mode 100644 sdk/python/docs/source/feast.infra.utils.postgres.rst create mode 100644 sdk/python/docs/source/feast.infra.utils.rst create mode 100644 sdk/python/docs/source/feast.loaders.rst create mode 100644 sdk/python/docs/source/feast.protos.feast.core.rst create mode 100644 sdk/python/docs/source/feast.protos.feast.rst create mode 100644 sdk/python/docs/source/feast.protos.feast.serving.rst create mode 100644 sdk/python/docs/source/feast.protos.feast.storage.rst create mode 100644 sdk/python/docs/source/feast.protos.feast.types.rst create mode 100644 sdk/python/docs/source/feast.protos.rst create mode 100644 sdk/python/docs/source/feast.rst create mode 100644 sdk/python/docs/source/feast.ui.rst create mode 100644 sdk/python/docs/source/index.rst create mode 100644 sdk/python/docs/source/modules.rst diff --git a/.gitignore b/.gitignore index 1edde846ff2..6a86eb2682b 100644 --- a/.gitignore +++ b/.gitignore @@ -125,8 +125,6 @@ instance/ # Sphinx documentation docs/_build/ -sdk/python/docs/source -sdk/python/docs/html # PyBuilder target/ @@ -186,6 +184,7 @@ dmypy.json *.code-workspace # Protos +sdk/python/docs/html sdk/python/feast/protos/ sdk/go/protos/ go/protos/ diff --git a/sdk/python/docs/source/conf.py b/sdk/python/docs/source/conf.py new file mode 100644 index 00000000000..5e8fd11d161 --- /dev/null +++ b/sdk/python/docs/source/conf.py @@ -0,0 +1,201 @@ +# -*- coding: utf-8 -*- +# +# Feast documentation build configuration file, created by +# sphinx-quickstart on Sat Nov 30 15:06:53 2019. +# +# This file is execfile()d with the current directory set to its +# containing dir. +# +# Note that not all possible configuration values are present in this +# autogenerated file. +# +# All configuration values have a default; values that are commented out +# serve to show the default. + +# If extensions (or modules to document with autodoc) are in another directory, +# add these directories to sys.path here. If the directory is relative to the +# documentation root, use os.path.abspath to make it absolute, like shown here. +# +import os +import sys + +import sphinx_rtd_theme + +sys.path.insert(0, os.path.abspath("../../feast")) +sys.path.insert(0, os.path.abspath("../..")) + + +# -- Build protos --------------------------------------------------------- + +# For an unknown reason, the Python protos stopped being built correctly. +# See https://readthedocs.org/projects/feast/builds/17686555/ for an +# example where the Python protos did not build, which subsequently broke +# the RTD build. In order to fix this, we manually compile the protos. +import subprocess + +from pathlib import Path + +# cwd will be feast/sdk/python/docs/source +cwd = Path(os.getcwd()) + +# Change to feast/ +os.chdir(cwd.parent.parent.parent.parent) + +# Compile Python protos +result = subprocess.run(["python", "setup.py", "build_python_protos", "--inplace"], capture_output=True) +stdout = result.stdout.decode("utf-8") +stderr = result.stderr.decode("utf-8") +print(f"Apply stdout:\n{stdout}") +print(f"Apply stderr:\n{stderr}") + +# -- General configuration ------------------------------------------------ + +# If your documentation needs a minimal Sphinx version, state it here. +# +# needs_sphinx = '1.0' + +# Add any Sphinx extension module names here, as strings. They can be +# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom +# ones. +extensions = [ + "sphinx.ext.doctest", + "sphinx.ext.intersphinx", + "sphinx.ext.todo", + "sphinx.ext.coverage", + "sphinx.ext.mathjax", + "sphinx.ext.ifconfig", + "sphinx.ext.viewcode", + "sphinx.ext.githubpages", + "sphinx.ext.napoleon", + "sphinx.ext.autodoc", + "sphinx_rtd_theme", +] + +# Add any paths that contain templates here, relative to this directory. +templates_path = ["_templates"] + +# The suffix(es) of source filenames. +# You can specify multiple suffix as a list of string: +# +# source_suffix = ['.rst', '.md'] +source_suffix = ".rst" + +# The master toctree document. +master_doc = "index" + +# General information about the project. +project = "Feast" +copyright = "2021, Feast Authors" +author = "Feast Authors" + +# The version info for the project you're documenting, acts as replacement for +# |version| and |release|, also used in various other places throughout the +# built documents. +# +# The short X.Y version. + +# TODO: Add the below versions back to documentation building. +# version = ( +# os.popen("git describe --tags $(git rev-list --tags --max-count=1)").read().strip() +# ) +# The full version, including alpha/beta/rc tags. +# release = ( +# os.popen("git describe --tags $(git rev-list --tags --max-count=1)").read().strip() +# ) + +# The language for content autogenerated by Sphinx. Refer to documentation +# for a list of supported languages. +# +# This is also used if you do content translation via gettext catalogs. +# Usually you set "language" from the command line for these cases. +language = None + +# List of patterns, relative to source directory, that match files and +# directories to ignore when looking for source files. +# This patterns also effect to html_static_path and html_extra_path +exclude_patterns = [] + +# The name of the Pygments (syntax highlighting) style to use. +pygments_style = "sphinx" + +# If true, `todo` and `todoList` produce output, else they produce nothing. +todo_include_todos = True + + +# -- Options for HTML output ---------------------------------------------- + +# The theme to use for HTML and HTML Help pages. See the documentation for +# a list of builtin themes. +# +html_theme = "sphinx_rtd_theme" + +# Theme options are theme-specific and customize the look and feel of a theme +# further. For a list of options available for each theme, see the +# documentation. +# +html_theme_options = {} + +# Add any paths that contain custom static files (such as style sheets) here, +# relative to this directory. They are copied after the builtin static files, +# so a file named "default.css" will overwrite the builtin "default.css". +html_static_path = ["_static"] + + +# -- Options for HTMLHelp output ------------------------------------------ + +# Output file base name for HTML help builder. +htmlhelp_basename = "Feastdoc" + + +# -- Options for LaTeX output --------------------------------------------- + +latex_elements = { + # The paper size ('letterpaper' or 'a4paper'). + # + # 'papersize': 'letterpaper', + # The font size ('10pt', '11pt' or '12pt'). + # + # 'pointsize': '10pt', + # Additional stuff for the LaTeX preamble. + # + # 'preamble': '', + # Latex figure (float) alignment + # + # 'figure_align': 'htbp', +} + +# Grouping the document tree into LaTeX files. List of tuples +# (source start file, target name, title, +# author, documentclass [howto, manual, or own class]). +latex_documents = [ + (master_doc, "Feast.tex", "Feast Documentation", "Feast Authors", "manual") +] + + +# -- Options for manual page output --------------------------------------- + +# One entry per manual page. List of tuples +# (source start file, name, description, authors, manual section). +man_pages = [(master_doc, "feast", "Feast Documentation", [author], 1)] + + +# -- Options for Texinfo output ------------------------------------------- + +# Grouping the document tree into Texinfo files. List of tuples +# (source start file, target name, title, author, +# dir menu entry, description, category) +texinfo_documents = [ + ( + master_doc, + "Feast", + "Feast Documentation", + author, + "Feast", + "One line description of project.", + "Miscellaneous", + ) +] + + +# Example configuration for intersphinx: refer to the Python standard library. +intersphinx_mapping = {"https://docs.python.org/": None} diff --git a/sdk/python/docs/source/feast.diff.rst b/sdk/python/docs/source/feast.diff.rst new file mode 100644 index 00000000000..e4142171711 --- /dev/null +++ b/sdk/python/docs/source/feast.diff.rst @@ -0,0 +1,37 @@ +feast.diff package +================== + +Submodules +---------- + +feast.diff.infra\_diff module +----------------------------- + +.. automodule:: feast.diff.infra_diff + :members: + :undoc-members: + :show-inheritance: + +feast.diff.property\_diff module +-------------------------------- + +.. automodule:: feast.diff.property_diff + :members: + :undoc-members: + :show-inheritance: + +feast.diff.registry\_diff module +-------------------------------- + +.. automodule:: feast.diff.registry_diff + :members: + :undoc-members: + :show-inheritance: + +Module contents +--------------- + +.. automodule:: feast.diff + :members: + :undoc-members: + :show-inheritance: diff --git a/sdk/python/docs/source/feast.dqm.profilers.rst b/sdk/python/docs/source/feast.dqm.profilers.rst new file mode 100644 index 00000000000..24f452ada8f --- /dev/null +++ b/sdk/python/docs/source/feast.dqm.profilers.rst @@ -0,0 +1,29 @@ +feast.dqm.profilers package +=========================== + +Submodules +---------- + +feast.dqm.profilers.ge\_profiler module +--------------------------------------- + +.. automodule:: feast.dqm.profilers.ge_profiler + :members: + :undoc-members: + :show-inheritance: + +feast.dqm.profilers.profiler module +----------------------------------- + +.. automodule:: feast.dqm.profilers.profiler + :members: + :undoc-members: + :show-inheritance: + +Module contents +--------------- + +.. automodule:: feast.dqm.profilers + :members: + :undoc-members: + :show-inheritance: diff --git a/sdk/python/docs/source/feast.dqm.rst b/sdk/python/docs/source/feast.dqm.rst new file mode 100644 index 00000000000..0c1b82f0fa2 --- /dev/null +++ b/sdk/python/docs/source/feast.dqm.rst @@ -0,0 +1,29 @@ +feast.dqm package +================= + +Subpackages +----------- + +.. toctree:: + :maxdepth: 4 + + feast.dqm.profilers + +Submodules +---------- + +feast.dqm.errors module +----------------------- + +.. automodule:: feast.dqm.errors + :members: + :undoc-members: + :show-inheritance: + +Module contents +--------------- + +.. automodule:: feast.dqm + :members: + :undoc-members: + :show-inheritance: diff --git a/sdk/python/docs/source/feast.infra.materialization.contrib.bytewax.rst b/sdk/python/docs/source/feast.infra.materialization.contrib.bytewax.rst new file mode 100644 index 00000000000..86fbaa61515 --- /dev/null +++ b/sdk/python/docs/source/feast.infra.materialization.contrib.bytewax.rst @@ -0,0 +1,29 @@ +feast.infra.materialization.contrib.bytewax package +================================================================= + +Submodules +---------- + +feast.infra.materialization.contrib.bytewax.bytewax\_materialization\_engine +---------------------------------------------------------------------- + +.. automodule:: feast.infra.materialization.contrib.bytewax.bytewax_materialization_engine + :members: + :undoc-members: + :show-inheritance: + +feast.infra.materialization.contrib.bytewax.bytewax\_materialization\_job +---------------------------------------------------------------------- + +.. automodule:: feast.infra.materialization.contrib.bytewax.bytewax_materialization_job + :members: + :undoc-members: + :show-inheritance: + +Module contents +--------------- + +.. automodule:: feast.infra.materialization.contrib.bytewax + :members: + :undoc-members: + :show-inheritance: diff --git a/sdk/python/docs/source/feast.infra.materialization.contrib.rst b/sdk/python/docs/source/feast.infra.materialization.contrib.rst new file mode 100644 index 00000000000..f9d77006610 --- /dev/null +++ b/sdk/python/docs/source/feast.infra.materialization.contrib.rst @@ -0,0 +1,10 @@ +feast.infra.materialization.contrib package +========================================== + +Subpackages +----------- + +.. toctree:: + :maxdepth: 4 + + feast.infra.materialization.contrib.bytewax diff --git a/sdk/python/docs/source/feast.infra.materialization.lambda.rst b/sdk/python/docs/source/feast.infra.materialization.lambda.rst new file mode 100644 index 00000000000..7ca1d44314a --- /dev/null +++ b/sdk/python/docs/source/feast.infra.materialization.lambda.rst @@ -0,0 +1,29 @@ +feast.infra.materialization.lambda package +========================================== + +Submodules +---------- + +feast.infra.materialization.lambda.app module +--------------------------------------------- + +.. automodule:: feast.infra.materialization.lambda.app + :members: + :undoc-members: + :show-inheritance: + +feast.infra.materialization.lambda.lambda\_engine module +-------------------------------------------------------- + +.. automodule:: feast.infra.materialization.lambda.lambda_engine + :members: + :undoc-members: + :show-inheritance: + +Module contents +--------------- + +.. automodule:: feast.infra.materialization.lambda + :members: + :undoc-members: + :show-inheritance: diff --git a/sdk/python/docs/source/feast.infra.materialization.rst b/sdk/python/docs/source/feast.infra.materialization.rst new file mode 100644 index 00000000000..6e526c367cd --- /dev/null +++ b/sdk/python/docs/source/feast.infra.materialization.rst @@ -0,0 +1,45 @@ +feast.infra.materialization package +=================================== + +Subpackages +----------- + +.. toctree:: + :maxdepth: 4 + + feast.infra.materialization.lambda + +Submodules +---------- + +feast.infra.materialization.batch\_materialization\_engine module +----------------------------------------------------------------- + +.. automodule:: feast.infra.materialization.batch_materialization_engine + :members: + :undoc-members: + :show-inheritance: + +feast.infra.materialization.local\_engine module +------------------------------------------------ + +.. automodule:: feast.infra.materialization.local_engine + :members: + :undoc-members: + :show-inheritance: + +feast.infra.materialization.snowflake\_engine module +---------------------------------------------------- + +.. automodule:: feast.infra.materialization.snowflake_engine + :members: + :undoc-members: + :show-inheritance: + +Module contents +--------------- + +.. automodule:: feast.infra.materialization + :members: + :undoc-members: + :show-inheritance: diff --git a/sdk/python/docs/source/feast.infra.offline_stores.contrib.athena_offline_store.rst b/sdk/python/docs/source/feast.infra.offline_stores.contrib.athena_offline_store.rst new file mode 100644 index 00000000000..d2275b2b393 --- /dev/null +++ b/sdk/python/docs/source/feast.infra.offline_stores.contrib.athena_offline_store.rst @@ -0,0 +1,37 @@ +feast.infra.offline\_stores.contrib.athena\_offline\_store package +================================================================== + +Subpackages +----------- + +.. toctree:: + :maxdepth: 4 + + feast.infra.offline_stores.contrib.athena_offline_store.tests + +Submodules +---------- + +feast.infra.offline\_stores.contrib.athena\_offline\_store.athena module +------------------------------------------------------------------------ + +.. automodule:: feast.infra.offline_stores.contrib.athena_offline_store.athena + :members: + :undoc-members: + :show-inheritance: + +feast.infra.offline\_stores.contrib.athena\_offline\_store.athena\_source module +-------------------------------------------------------------------------------- + +.. automodule:: feast.infra.offline_stores.contrib.athena_offline_store.athena_source + :members: + :undoc-members: + :show-inheritance: + +Module contents +--------------- + +.. automodule:: feast.infra.offline_stores.contrib.athena_offline_store + :members: + :undoc-members: + :show-inheritance: diff --git a/sdk/python/docs/source/feast.infra.offline_stores.contrib.athena_offline_store.tests.rst b/sdk/python/docs/source/feast.infra.offline_stores.contrib.athena_offline_store.tests.rst new file mode 100644 index 00000000000..47a8f83e2b7 --- /dev/null +++ b/sdk/python/docs/source/feast.infra.offline_stores.contrib.athena_offline_store.tests.rst @@ -0,0 +1,21 @@ +feast.infra.offline\_stores.contrib.athena\_offline\_store.tests package +======================================================================== + +Submodules +---------- + +feast.infra.offline\_stores.contrib.athena\_offline\_store.tests.data\_source module +------------------------------------------------------------------------------------ + +.. automodule:: feast.infra.offline_stores.contrib.athena_offline_store.tests.data_source + :members: + :undoc-members: + :show-inheritance: + +Module contents +--------------- + +.. automodule:: feast.infra.offline_stores.contrib.athena_offline_store.tests + :members: + :undoc-members: + :show-inheritance: diff --git a/sdk/python/docs/source/feast.infra.offline_stores.contrib.mssql_offline_store.rst b/sdk/python/docs/source/feast.infra.offline_stores.contrib.mssql_offline_store.rst new file mode 100644 index 00000000000..8fb0b966bf8 --- /dev/null +++ b/sdk/python/docs/source/feast.infra.offline_stores.contrib.mssql_offline_store.rst @@ -0,0 +1,37 @@ +feast.infra.offline\_stores.contrib.mssql\_offline\_store package +================================================================= + +Subpackages +----------- + +.. toctree:: + :maxdepth: 4 + + feast.infra.offline_stores.contrib.mssql_offline_store.tests + +Submodules +---------- + +feast.infra.offline\_stores.contrib.mssql\_offline\_store.mssql module +---------------------------------------------------------------------- + +.. automodule:: feast.infra.offline_stores.contrib.mssql_offline_store.mssql + :members: + :undoc-members: + :show-inheritance: + +feast.infra.offline\_stores.contrib.mssql\_offline\_store.mssqlserver\_source module +------------------------------------------------------------------------------------ + +.. automodule:: feast.infra.offline_stores.contrib.mssql_offline_store.mssqlserver_source + :members: + :undoc-members: + :show-inheritance: + +Module contents +--------------- + +.. automodule:: feast.infra.offline_stores.contrib.mssql_offline_store + :members: + :undoc-members: + :show-inheritance: diff --git a/sdk/python/docs/source/feast.infra.offline_stores.contrib.mssql_offline_store.tests.rst b/sdk/python/docs/source/feast.infra.offline_stores.contrib.mssql_offline_store.tests.rst new file mode 100644 index 00000000000..2f01ddd091a --- /dev/null +++ b/sdk/python/docs/source/feast.infra.offline_stores.contrib.mssql_offline_store.tests.rst @@ -0,0 +1,21 @@ +feast.infra.offline\_stores.contrib.mssql\_offline\_store.tests package +======================================================================= + +Submodules +---------- + +feast.infra.offline\_stores.contrib.mssql\_offline\_store.tests.data\_source module +----------------------------------------------------------------------------------- + +.. automodule:: feast.infra.offline_stores.contrib.mssql_offline_store.tests.data_source + :members: + :undoc-members: + :show-inheritance: + +Module contents +--------------- + +.. automodule:: feast.infra.offline_stores.contrib.mssql_offline_store.tests + :members: + :undoc-members: + :show-inheritance: diff --git a/sdk/python/docs/source/feast.infra.offline_stores.contrib.postgres_offline_store.rst b/sdk/python/docs/source/feast.infra.offline_stores.contrib.postgres_offline_store.rst new file mode 100644 index 00000000000..a80690fe859 --- /dev/null +++ b/sdk/python/docs/source/feast.infra.offline_stores.contrib.postgres_offline_store.rst @@ -0,0 +1,37 @@ +feast.infra.offline\_stores.contrib.postgres\_offline\_store package +==================================================================== + +Subpackages +----------- + +.. toctree:: + :maxdepth: 4 + + feast.infra.offline_stores.contrib.postgres_offline_store.tests + +Submodules +---------- + +feast.infra.offline\_stores.contrib.postgres\_offline\_store.postgres module +---------------------------------------------------------------------------- + +.. automodule:: feast.infra.offline_stores.contrib.postgres_offline_store.postgres + :members: + :undoc-members: + :show-inheritance: + +feast.infra.offline\_stores.contrib.postgres\_offline\_store.postgres\_source module +------------------------------------------------------------------------------------ + +.. automodule:: feast.infra.offline_stores.contrib.postgres_offline_store.postgres_source + :members: + :undoc-members: + :show-inheritance: + +Module contents +--------------- + +.. automodule:: feast.infra.offline_stores.contrib.postgres_offline_store + :members: + :undoc-members: + :show-inheritance: diff --git a/sdk/python/docs/source/feast.infra.offline_stores.contrib.postgres_offline_store.tests.rst b/sdk/python/docs/source/feast.infra.offline_stores.contrib.postgres_offline_store.tests.rst new file mode 100644 index 00000000000..35e60d2998b --- /dev/null +++ b/sdk/python/docs/source/feast.infra.offline_stores.contrib.postgres_offline_store.tests.rst @@ -0,0 +1,21 @@ +feast.infra.offline\_stores.contrib.postgres\_offline\_store.tests package +========================================================================== + +Submodules +---------- + +feast.infra.offline\_stores.contrib.postgres\_offline\_store.tests.data\_source module +-------------------------------------------------------------------------------------- + +.. automodule:: feast.infra.offline_stores.contrib.postgres_offline_store.tests.data_source + :members: + :undoc-members: + :show-inheritance: + +Module contents +--------------- + +.. automodule:: feast.infra.offline_stores.contrib.postgres_offline_store.tests + :members: + :undoc-members: + :show-inheritance: diff --git a/sdk/python/docs/source/feast.infra.offline_stores.contrib.rst b/sdk/python/docs/source/feast.infra.offline_stores.contrib.rst new file mode 100644 index 00000000000..ec74ddab05c --- /dev/null +++ b/sdk/python/docs/source/feast.infra.offline_stores.contrib.rst @@ -0,0 +1,65 @@ +feast.infra.offline\_stores.contrib package +=========================================== + +Subpackages +----------- + +.. toctree:: + :maxdepth: 4 + + feast.infra.offline_stores.contrib.athena_offline_store + feast.infra.offline_stores.contrib.mssql_offline_store + feast.infra.offline_stores.contrib.postgres_offline_store + feast.infra.offline_stores.contrib.spark_offline_store + feast.infra.offline_stores.contrib.trino_offline_store + +Submodules +---------- + +feast.infra.offline\_stores.contrib.athena\_repo\_configuration module +---------------------------------------------------------------------- + +.. automodule:: feast.infra.offline_stores.contrib.athena_repo_configuration + :members: + :undoc-members: + :show-inheritance: + +feast.infra.offline\_stores.contrib.mssql\_repo\_configuration module +--------------------------------------------------------------------- + +.. automodule:: feast.infra.offline_stores.contrib.mssql_repo_configuration + :members: + :undoc-members: + :show-inheritance: + +feast.infra.offline\_stores.contrib.postgres\_repo\_configuration module +------------------------------------------------------------------------ + +.. automodule:: feast.infra.offline_stores.contrib.postgres_repo_configuration + :members: + :undoc-members: + :show-inheritance: + +feast.infra.offline\_stores.contrib.spark\_repo\_configuration module +--------------------------------------------------------------------- + +.. automodule:: feast.infra.offline_stores.contrib.spark_repo_configuration + :members: + :undoc-members: + :show-inheritance: + +feast.infra.offline\_stores.contrib.trino\_repo\_configuration module +--------------------------------------------------------------------- + +.. automodule:: feast.infra.offline_stores.contrib.trino_repo_configuration + :members: + :undoc-members: + :show-inheritance: + +Module contents +--------------- + +.. automodule:: feast.infra.offline_stores.contrib + :members: + :undoc-members: + :show-inheritance: diff --git a/sdk/python/docs/source/feast.infra.offline_stores.contrib.spark_offline_store.rst b/sdk/python/docs/source/feast.infra.offline_stores.contrib.spark_offline_store.rst new file mode 100644 index 00000000000..b8b79bb48e8 --- /dev/null +++ b/sdk/python/docs/source/feast.infra.offline_stores.contrib.spark_offline_store.rst @@ -0,0 +1,37 @@ +feast.infra.offline\_stores.contrib.spark\_offline\_store package +================================================================= + +Subpackages +----------- + +.. toctree:: + :maxdepth: 4 + + feast.infra.offline_stores.contrib.spark_offline_store.tests + +Submodules +---------- + +feast.infra.offline\_stores.contrib.spark\_offline\_store.spark module +---------------------------------------------------------------------- + +.. automodule:: feast.infra.offline_stores.contrib.spark_offline_store.spark + :members: + :undoc-members: + :show-inheritance: + +feast.infra.offline\_stores.contrib.spark\_offline\_store.spark\_source module +------------------------------------------------------------------------------ + +.. automodule:: feast.infra.offline_stores.contrib.spark_offline_store.spark_source + :members: + :undoc-members: + :show-inheritance: + +Module contents +--------------- + +.. automodule:: feast.infra.offline_stores.contrib.spark_offline_store + :members: + :undoc-members: + :show-inheritance: diff --git a/sdk/python/docs/source/feast.infra.offline_stores.contrib.spark_offline_store.tests.rst b/sdk/python/docs/source/feast.infra.offline_stores.contrib.spark_offline_store.tests.rst new file mode 100644 index 00000000000..8b0f9bd88b3 --- /dev/null +++ b/sdk/python/docs/source/feast.infra.offline_stores.contrib.spark_offline_store.tests.rst @@ -0,0 +1,21 @@ +feast.infra.offline\_stores.contrib.spark\_offline\_store.tests package +======================================================================= + +Submodules +---------- + +feast.infra.offline\_stores.contrib.spark\_offline\_store.tests.data\_source module +----------------------------------------------------------------------------------- + +.. automodule:: feast.infra.offline_stores.contrib.spark_offline_store.tests.data_source + :members: + :undoc-members: + :show-inheritance: + +Module contents +--------------- + +.. automodule:: feast.infra.offline_stores.contrib.spark_offline_store.tests + :members: + :undoc-members: + :show-inheritance: diff --git a/sdk/python/docs/source/feast.infra.offline_stores.contrib.trino_offline_store.connectors.rst b/sdk/python/docs/source/feast.infra.offline_stores.contrib.trino_offline_store.connectors.rst new file mode 100644 index 00000000000..a0ee8dceabf --- /dev/null +++ b/sdk/python/docs/source/feast.infra.offline_stores.contrib.trino_offline_store.connectors.rst @@ -0,0 +1,21 @@ +feast.infra.offline\_stores.contrib.trino\_offline\_store.connectors package +============================================================================ + +Submodules +---------- + +feast.infra.offline\_stores.contrib.trino\_offline\_store.connectors.upload module +---------------------------------------------------------------------------------- + +.. automodule:: feast.infra.offline_stores.contrib.trino_offline_store.connectors.upload + :members: + :undoc-members: + :show-inheritance: + +Module contents +--------------- + +.. automodule:: feast.infra.offline_stores.contrib.trino_offline_store.connectors + :members: + :undoc-members: + :show-inheritance: diff --git a/sdk/python/docs/source/feast.infra.offline_stores.contrib.trino_offline_store.rst b/sdk/python/docs/source/feast.infra.offline_stores.contrib.trino_offline_store.rst new file mode 100644 index 00000000000..857326003f3 --- /dev/null +++ b/sdk/python/docs/source/feast.infra.offline_stores.contrib.trino_offline_store.rst @@ -0,0 +1,55 @@ +feast.infra.offline\_stores.contrib.trino\_offline\_store package +================================================================= + +Subpackages +----------- + +.. toctree:: + :maxdepth: 4 + + feast.infra.offline_stores.contrib.trino_offline_store.connectors + feast.infra.offline_stores.contrib.trino_offline_store.test_config + feast.infra.offline_stores.contrib.trino_offline_store.tests + +Submodules +---------- + +feast.infra.offline\_stores.contrib.trino\_offline\_store.trino module +---------------------------------------------------------------------- + +.. automodule:: feast.infra.offline_stores.contrib.trino_offline_store.trino + :members: + :undoc-members: + :show-inheritance: + +feast.infra.offline\_stores.contrib.trino\_offline\_store.trino\_queries module +------------------------------------------------------------------------------- + +.. automodule:: feast.infra.offline_stores.contrib.trino_offline_store.trino_queries + :members: + :undoc-members: + :show-inheritance: + +feast.infra.offline\_stores.contrib.trino\_offline\_store.trino\_source module +------------------------------------------------------------------------------ + +.. automodule:: feast.infra.offline_stores.contrib.trino_offline_store.trino_source + :members: + :undoc-members: + :show-inheritance: + +feast.infra.offline\_stores.contrib.trino\_offline\_store.trino\_type\_map module +--------------------------------------------------------------------------------- + +.. automodule:: feast.infra.offline_stores.contrib.trino_offline_store.trino_type_map + :members: + :undoc-members: + :show-inheritance: + +Module contents +--------------- + +.. automodule:: feast.infra.offline_stores.contrib.trino_offline_store + :members: + :undoc-members: + :show-inheritance: diff --git a/sdk/python/docs/source/feast.infra.offline_stores.contrib.trino_offline_store.test_config.rst b/sdk/python/docs/source/feast.infra.offline_stores.contrib.trino_offline_store.test_config.rst new file mode 100644 index 00000000000..ef43a191d0a --- /dev/null +++ b/sdk/python/docs/source/feast.infra.offline_stores.contrib.trino_offline_store.test_config.rst @@ -0,0 +1,21 @@ +feast.infra.offline\_stores.contrib.trino\_offline\_store.test\_config package +============================================================================== + +Submodules +---------- + +feast.infra.offline\_stores.contrib.trino\_offline\_store.test\_config.manual\_tests module +------------------------------------------------------------------------------------------- + +.. automodule:: feast.infra.offline_stores.contrib.trino_offline_store.test_config.manual_tests + :members: + :undoc-members: + :show-inheritance: + +Module contents +--------------- + +.. automodule:: feast.infra.offline_stores.contrib.trino_offline_store.test_config + :members: + :undoc-members: + :show-inheritance: diff --git a/sdk/python/docs/source/feast.infra.offline_stores.contrib.trino_offline_store.tests.rst b/sdk/python/docs/source/feast.infra.offline_stores.contrib.trino_offline_store.tests.rst new file mode 100644 index 00000000000..9102f1f8d64 --- /dev/null +++ b/sdk/python/docs/source/feast.infra.offline_stores.contrib.trino_offline_store.tests.rst @@ -0,0 +1,21 @@ +feast.infra.offline\_stores.contrib.trino\_offline\_store.tests package +======================================================================= + +Submodules +---------- + +feast.infra.offline\_stores.contrib.trino\_offline\_store.tests.data\_source module +----------------------------------------------------------------------------------- + +.. automodule:: feast.infra.offline_stores.contrib.trino_offline_store.tests.data_source + :members: + :undoc-members: + :show-inheritance: + +Module contents +--------------- + +.. automodule:: feast.infra.offline_stores.contrib.trino_offline_store.tests + :members: + :undoc-members: + :show-inheritance: diff --git a/sdk/python/docs/source/feast.infra.offline_stores.rst b/sdk/python/docs/source/feast.infra.offline_stores.rst new file mode 100644 index 00000000000..7949c9efb32 --- /dev/null +++ b/sdk/python/docs/source/feast.infra.offline_stores.rst @@ -0,0 +1,101 @@ +feast.infra.offline\_stores package +=================================== + +Subpackages +----------- + +.. toctree:: + :maxdepth: 4 + + feast.infra.offline_stores.contrib + +Submodules +---------- + +feast.infra.offline\_stores.bigquery module +------------------------------------------- + +.. automodule:: feast.infra.offline_stores.bigquery + :members: + :undoc-members: + :show-inheritance: + +feast.infra.offline\_stores.bigquery\_source module +--------------------------------------------------- + +.. automodule:: feast.infra.offline_stores.bigquery_source + :members: + :undoc-members: + :show-inheritance: + +feast.infra.offline\_stores.file module +--------------------------------------- + +.. automodule:: feast.infra.offline_stores.file + :members: + :undoc-members: + :show-inheritance: + +feast.infra.offline\_stores.file\_source module +----------------------------------------------- + +.. automodule:: feast.infra.offline_stores.file_source + :members: + :undoc-members: + :show-inheritance: + +feast.infra.offline\_stores.offline\_store module +------------------------------------------------- + +.. automodule:: feast.infra.offline_stores.offline_store + :members: + :undoc-members: + :show-inheritance: + +feast.infra.offline\_stores.offline\_utils module +------------------------------------------------- + +.. automodule:: feast.infra.offline_stores.offline_utils + :members: + :undoc-members: + :show-inheritance: + +feast.infra.offline\_stores.redshift module +------------------------------------------- + +.. automodule:: feast.infra.offline_stores.redshift + :members: + :undoc-members: + :show-inheritance: + +feast.infra.offline\_stores.redshift\_source module +--------------------------------------------------- + +.. automodule:: feast.infra.offline_stores.redshift_source + :members: + :undoc-members: + :show-inheritance: + +feast.infra.offline\_stores.snowflake module +-------------------------------------------- + +.. automodule:: feast.infra.offline_stores.snowflake + :members: + :undoc-members: + :show-inheritance: + +feast.infra.offline\_stores.snowflake\_source module +---------------------------------------------------- + +.. automodule:: feast.infra.offline_stores.snowflake_source + :members: + :undoc-members: + :show-inheritance: + +Module contents +--------------- + +.. automodule:: feast.infra.offline_stores + :members: + :undoc-members: + :show-inheritance: diff --git a/sdk/python/docs/source/feast.infra.online_stores.contrib.cassandra_online_store.rst b/sdk/python/docs/source/feast.infra.online_stores.contrib.cassandra_online_store.rst new file mode 100644 index 00000000000..3770cc8af70 --- /dev/null +++ b/sdk/python/docs/source/feast.infra.online_stores.contrib.cassandra_online_store.rst @@ -0,0 +1,21 @@ +feast.infra.online\_stores.contrib.cassandra\_online\_store package +=================================================================== + +Submodules +---------- + +feast.infra.online\_stores.contrib.cassandra\_online\_store.cassandra\_online\_store module +------------------------------------------------------------------------------------------- + +.. automodule:: feast.infra.online_stores.contrib.cassandra_online_store.cassandra_online_store + :members: + :undoc-members: + :show-inheritance: + +Module contents +--------------- + +.. automodule:: feast.infra.online_stores.contrib.cassandra_online_store + :members: + :undoc-members: + :show-inheritance: diff --git a/sdk/python/docs/source/feast.infra.online_stores.contrib.hbase_online_store.rst b/sdk/python/docs/source/feast.infra.online_stores.contrib.hbase_online_store.rst new file mode 100644 index 00000000000..ce249023049 --- /dev/null +++ b/sdk/python/docs/source/feast.infra.online_stores.contrib.hbase_online_store.rst @@ -0,0 +1,21 @@ +feast.infra.online\_stores.contrib.hbase\_online\_store package +=============================================================== + +Submodules +---------- + +feast.infra.online\_stores.contrib.hbase\_online\_store.hbase module +-------------------------------------------------------------------- + +.. automodule:: feast.infra.online_stores.contrib.hbase_online_store.hbase + :members: + :undoc-members: + :show-inheritance: + +Module contents +--------------- + +.. automodule:: feast.infra.online_stores.contrib.hbase_online_store + :members: + :undoc-members: + :show-inheritance: diff --git a/sdk/python/docs/source/feast.infra.online_stores.contrib.rst b/sdk/python/docs/source/feast.infra.online_stores.contrib.rst new file mode 100644 index 00000000000..6afe9071ace --- /dev/null +++ b/sdk/python/docs/source/feast.infra.online_stores.contrib.rst @@ -0,0 +1,54 @@ +feast.infra.online\_stores.contrib package +========================================== + +Subpackages +----------- + +.. toctree:: + :maxdepth: 4 + + feast.infra.online_stores.contrib.cassandra_online_store + feast.infra.online_stores.contrib.hbase_online_store + +Submodules +---------- + +feast.infra.online\_stores.contrib.cassandra\_repo\_configuration module +------------------------------------------------------------------------ + +.. automodule:: feast.infra.online_stores.contrib.cassandra_repo_configuration + :members: + :undoc-members: + :show-inheritance: + +feast.infra.online\_stores.contrib.hbase\_repo\_configuration module +-------------------------------------------------------------------- + +.. automodule:: feast.infra.online_stores.contrib.hbase_repo_configuration + :members: + :undoc-members: + :show-inheritance: + +feast.infra.online\_stores.contrib.postgres module +-------------------------------------------------- + +.. automodule:: feast.infra.online_stores.contrib.postgres + :members: + :undoc-members: + :show-inheritance: + +feast.infra.online\_stores.contrib.postgres\_repo\_configuration module +----------------------------------------------------------------------- + +.. automodule:: feast.infra.online_stores.contrib.postgres_repo_configuration + :members: + :undoc-members: + :show-inheritance: + +Module contents +--------------- + +.. automodule:: feast.infra.online_stores.contrib + :members: + :undoc-members: + :show-inheritance: diff --git a/sdk/python/docs/source/feast.infra.online_stores.rst b/sdk/python/docs/source/feast.infra.online_stores.rst new file mode 100644 index 00000000000..65758c409c0 --- /dev/null +++ b/sdk/python/docs/source/feast.infra.online_stores.rst @@ -0,0 +1,77 @@ +feast.infra.online\_stores package +================================== + +Subpackages +----------- + +.. toctree:: + :maxdepth: 4 + + feast.infra.online_stores.contrib + +Submodules +---------- + +feast.infra.online\_stores.datastore module +------------------------------------------- + +.. automodule:: feast.infra.online_stores.datastore + :members: + :undoc-members: + :show-inheritance: + +feast.infra.online\_stores.dynamodb module +------------------------------------------ + +.. automodule:: feast.infra.online_stores.dynamodb + :members: + :undoc-members: + :show-inheritance: + +feast.infra.online\_stores.helpers module +----------------------------------------- + +.. automodule:: feast.infra.online_stores.helpers + :members: + :undoc-members: + :show-inheritance: + +feast.infra.online\_stores.online\_store module +----------------------------------------------- + +.. automodule:: feast.infra.online_stores.online_store + :members: + :undoc-members: + :show-inheritance: + +feast.infra.online\_stores.redis module +--------------------------------------- + +.. automodule:: feast.infra.online_stores.redis + :members: + :undoc-members: + :show-inheritance: + +feast.infra.online\_stores.snowflake module +------------------------------------------- + +.. automodule:: feast.infra.online_stores.snowflake + :members: + :undoc-members: + :show-inheritance: + +feast.infra.online\_stores.sqlite module +---------------------------------------- + +.. automodule:: feast.infra.online_stores.sqlite + :members: + :undoc-members: + :show-inheritance: + +Module contents +--------------- + +.. automodule:: feast.infra.online_stores + :members: + :undoc-members: + :show-inheritance: diff --git a/sdk/python/docs/source/feast.infra.registry.rst b/sdk/python/docs/source/feast.infra.registry.rst new file mode 100644 index 00000000000..7a2d9689975 --- /dev/null +++ b/sdk/python/docs/source/feast.infra.registry.rst @@ -0,0 +1,69 @@ +feast.infra.registry package +============================ + +Submodules +---------- + +feast.infra.registry.base\_registry module +------------------------------------------ + +.. automodule:: feast.infra.registry.base_registry + :members: + :undoc-members: + :show-inheritance: + +feast.infra.registry.file module +-------------------------------- + +.. automodule:: feast.infra.registry.file + :members: + :undoc-members: + :show-inheritance: + +feast.infra.registry.gcs module +------------------------------- + +.. automodule:: feast.infra.registry.gcs + :members: + :undoc-members: + :show-inheritance: + +feast.infra.registry.registry module +------------------------------------ + +.. automodule:: feast.infra.registry.registry + :members: + :undoc-members: + :show-inheritance: + +feast.infra.registry.registry\_store module +------------------------------------------- + +.. automodule:: feast.infra.registry.registry_store + :members: + :undoc-members: + :show-inheritance: + +feast.infra.registry.s3 module +------------------------------ + +.. automodule:: feast.infra.registry.s3 + :members: + :undoc-members: + :show-inheritance: + +feast.infra.registry.sql module +------------------------------- + +.. automodule:: feast.infra.registry.sql + :members: + :undoc-members: + :show-inheritance: + +Module contents +--------------- + +.. automodule:: feast.infra.registry + :members: + :undoc-members: + :show-inheritance: diff --git a/sdk/python/docs/source/feast.infra.registry_stores.rst b/sdk/python/docs/source/feast.infra.registry_stores.rst new file mode 100644 index 00000000000..cff02fa3380 --- /dev/null +++ b/sdk/python/docs/source/feast.infra.registry_stores.rst @@ -0,0 +1,21 @@ +feast.infra.registry\_stores package +==================================== + +Submodules +---------- + +feast.infra.registry\_stores.sql module +--------------------------------------- + +.. automodule:: feast.infra.registry_stores.sql + :members: + :undoc-members: + :show-inheritance: + +Module contents +--------------- + +.. automodule:: feast.infra.registry_stores + :members: + :undoc-members: + :show-inheritance: diff --git a/sdk/python/docs/source/feast.infra.rst b/sdk/python/docs/source/feast.infra.rst new file mode 100644 index 00000000000..96c0828451a --- /dev/null +++ b/sdk/python/docs/source/feast.infra.rst @@ -0,0 +1,81 @@ +feast.infra package +=================== + +Subpackages +----------- + +.. toctree:: + :maxdepth: 4 + + feast.infra.materialization + feast.infra.offline_stores + feast.infra.online_stores + feast.infra.registry + feast.infra.utils + +Submodules +---------- + +feast.infra.aws module +---------------------- + +.. automodule:: feast.infra.aws + :members: + :undoc-members: + :show-inheritance: + +feast.infra.gcp module +---------------------- + +.. automodule:: feast.infra.gcp + :members: + :undoc-members: + :show-inheritance: + +feast.infra.infra\_object module +-------------------------------- + +.. automodule:: feast.infra.infra_object + :members: + :undoc-members: + :show-inheritance: + +feast.infra.key\_encoding\_utils module +--------------------------------------- + +.. automodule:: feast.infra.key_encoding_utils + :members: + :undoc-members: + :show-inheritance: + +feast.infra.local module +------------------------ + +.. automodule:: feast.infra.local + :members: + :undoc-members: + :show-inheritance: + +feast.infra.passthrough\_provider module +---------------------------------------- + +.. automodule:: feast.infra.passthrough_provider + :members: + :undoc-members: + :show-inheritance: + +feast.infra.provider module +--------------------------- + +.. automodule:: feast.infra.provider + :members: + :undoc-members: + :show-inheritance: + +Module contents +--------------- + +.. automodule:: feast.infra + :members: + :undoc-members: + :show-inheritance: diff --git a/sdk/python/docs/source/feast.infra.utils.postgres.rst b/sdk/python/docs/source/feast.infra.utils.postgres.rst new file mode 100644 index 00000000000..119c8c1dee9 --- /dev/null +++ b/sdk/python/docs/source/feast.infra.utils.postgres.rst @@ -0,0 +1,29 @@ +feast.infra.utils.postgres package +================================== + +Submodules +---------- + +feast.infra.utils.postgres.connection\_utils module +--------------------------------------------------- + +.. automodule:: feast.infra.utils.postgres.connection_utils + :members: + :undoc-members: + :show-inheritance: + +feast.infra.utils.postgres.postgres\_config module +-------------------------------------------------- + +.. automodule:: feast.infra.utils.postgres.postgres_config + :members: + :undoc-members: + :show-inheritance: + +Module contents +--------------- + +.. automodule:: feast.infra.utils.postgres + :members: + :undoc-members: + :show-inheritance: diff --git a/sdk/python/docs/source/feast.infra.utils.rst b/sdk/python/docs/source/feast.infra.utils.rst new file mode 100644 index 00000000000..e4116e7a172 --- /dev/null +++ b/sdk/python/docs/source/feast.infra.utils.rst @@ -0,0 +1,37 @@ +feast.infra.utils package +========================= + +Subpackages +----------- + +.. toctree:: + :maxdepth: 4 + + feast.infra.utils.postgres + +Submodules +---------- + +feast.infra.utils.aws\_utils module +----------------------------------- + +.. automodule:: feast.infra.utils.aws_utils + :members: + :undoc-members: + :show-inheritance: + +feast.infra.utils.hbase\_utils module +------------------------------------- + +.. automodule:: feast.infra.utils.hbase_utils + :members: + :undoc-members: + :show-inheritance: + +Module contents +--------------- + +.. automodule:: feast.infra.utils + :members: + :undoc-members: + :show-inheritance: diff --git a/sdk/python/docs/source/feast.loaders.rst b/sdk/python/docs/source/feast.loaders.rst new file mode 100644 index 00000000000..d4968a29999 --- /dev/null +++ b/sdk/python/docs/source/feast.loaders.rst @@ -0,0 +1,21 @@ +feast.loaders package +===================== + +Submodules +---------- + +feast.loaders.yaml module +------------------------- + +.. automodule:: feast.loaders.yaml + :members: + :undoc-members: + :show-inheritance: + +Module contents +--------------- + +.. automodule:: feast.loaders + :members: + :undoc-members: + :show-inheritance: diff --git a/sdk/python/docs/source/feast.protos.feast.core.rst b/sdk/python/docs/source/feast.protos.feast.core.rst new file mode 100644 index 00000000000..aaed49cd731 --- /dev/null +++ b/sdk/python/docs/source/feast.protos.feast.core.rst @@ -0,0 +1,333 @@ +feast.protos.feast.core package +=============================== + +Submodules +---------- + +feast.protos.feast.core.Aggregation\_pb2 module +----------------------------------------------- + +.. automodule:: feast.protos.feast.core.Aggregation_pb2 + :members: + :undoc-members: + :show-inheritance: + +feast.protos.feast.core.Aggregation\_pb2\_grpc module +----------------------------------------------------- + +.. automodule:: feast.protos.feast.core.Aggregation_pb2_grpc + :members: + :undoc-members: + :show-inheritance: + +feast.protos.feast.core.DataFormat\_pb2 module +---------------------------------------------- + +.. automodule:: feast.protos.feast.core.DataFormat_pb2 + :members: + :undoc-members: + :show-inheritance: + +feast.protos.feast.core.DataFormat\_pb2\_grpc module +---------------------------------------------------- + +.. automodule:: feast.protos.feast.core.DataFormat_pb2_grpc + :members: + :undoc-members: + :show-inheritance: + +feast.protos.feast.core.DataSource\_pb2 module +---------------------------------------------- + +.. automodule:: feast.protos.feast.core.DataSource_pb2 + :members: + :undoc-members: + :show-inheritance: + +feast.protos.feast.core.DataSource\_pb2\_grpc module +---------------------------------------------------- + +.. automodule:: feast.protos.feast.core.DataSource_pb2_grpc + :members: + :undoc-members: + :show-inheritance: + +feast.protos.feast.core.DatastoreTable\_pb2 module +-------------------------------------------------- + +.. automodule:: feast.protos.feast.core.DatastoreTable_pb2 + :members: + :undoc-members: + :show-inheritance: + +feast.protos.feast.core.DatastoreTable\_pb2\_grpc module +-------------------------------------------------------- + +.. automodule:: feast.protos.feast.core.DatastoreTable_pb2_grpc + :members: + :undoc-members: + :show-inheritance: + +feast.protos.feast.core.DynamoDBTable\_pb2 module +------------------------------------------------- + +.. automodule:: feast.protos.feast.core.DynamoDBTable_pb2 + :members: + :undoc-members: + :show-inheritance: + +feast.protos.feast.core.DynamoDBTable\_pb2\_grpc module +------------------------------------------------------- + +.. automodule:: feast.protos.feast.core.DynamoDBTable_pb2_grpc + :members: + :undoc-members: + :show-inheritance: + +feast.protos.feast.core.Entity\_pb2 module +------------------------------------------ + +.. automodule:: feast.protos.feast.core.Entity_pb2 + :members: + :undoc-members: + :show-inheritance: + +feast.protos.feast.core.Entity\_pb2\_grpc module +------------------------------------------------ + +.. automodule:: feast.protos.feast.core.Entity_pb2_grpc + :members: + :undoc-members: + :show-inheritance: + +feast.protos.feast.core.FeatureService\_pb2 module +-------------------------------------------------- + +.. automodule:: feast.protos.feast.core.FeatureService_pb2 + :members: + :undoc-members: + :show-inheritance: + +feast.protos.feast.core.FeatureService\_pb2\_grpc module +-------------------------------------------------------- + +.. automodule:: feast.protos.feast.core.FeatureService_pb2_grpc + :members: + :undoc-members: + :show-inheritance: + +feast.protos.feast.core.FeatureTable\_pb2 module +------------------------------------------------ + +.. automodule:: feast.protos.feast.core.FeatureTable_pb2 + :members: + :undoc-members: + :show-inheritance: + +feast.protos.feast.core.FeatureTable\_pb2\_grpc module +------------------------------------------------------ + +.. automodule:: feast.protos.feast.core.FeatureTable_pb2_grpc + :members: + :undoc-members: + :show-inheritance: + +feast.protos.feast.core.FeatureViewProjection\_pb2 module +--------------------------------------------------------- + +.. automodule:: feast.protos.feast.core.FeatureViewProjection_pb2 + :members: + :undoc-members: + :show-inheritance: + +feast.protos.feast.core.FeatureViewProjection\_pb2\_grpc module +--------------------------------------------------------------- + +.. automodule:: feast.protos.feast.core.FeatureViewProjection_pb2_grpc + :members: + :undoc-members: + :show-inheritance: + +feast.protos.feast.core.FeatureView\_pb2 module +----------------------------------------------- + +.. automodule:: feast.protos.feast.core.FeatureView_pb2 + :members: + :undoc-members: + :show-inheritance: + +feast.protos.feast.core.FeatureView\_pb2\_grpc module +----------------------------------------------------- + +.. automodule:: feast.protos.feast.core.FeatureView_pb2_grpc + :members: + :undoc-members: + :show-inheritance: + +feast.protos.feast.core.Feature\_pb2 module +------------------------------------------- + +.. automodule:: feast.protos.feast.core.Feature_pb2 + :members: + :undoc-members: + :show-inheritance: + +feast.protos.feast.core.Feature\_pb2\_grpc module +------------------------------------------------- + +.. automodule:: feast.protos.feast.core.Feature_pb2_grpc + :members: + :undoc-members: + :show-inheritance: + +feast.protos.feast.core.InfraObject\_pb2 module +----------------------------------------------- + +.. automodule:: feast.protos.feast.core.InfraObject_pb2 + :members: + :undoc-members: + :show-inheritance: + +feast.protos.feast.core.InfraObject\_pb2\_grpc module +----------------------------------------------------- + +.. automodule:: feast.protos.feast.core.InfraObject_pb2_grpc + :members: + :undoc-members: + :show-inheritance: + +feast.protos.feast.core.OnDemandFeatureView\_pb2 module +------------------------------------------------------- + +.. automodule:: feast.protos.feast.core.OnDemandFeatureView_pb2 + :members: + :undoc-members: + :show-inheritance: + +feast.protos.feast.core.OnDemandFeatureView\_pb2\_grpc module +------------------------------------------------------------- + +.. automodule:: feast.protos.feast.core.OnDemandFeatureView_pb2_grpc + :members: + :undoc-members: + :show-inheritance: + +feast.protos.feast.core.Registry\_pb2 module +-------------------------------------------- + +.. automodule:: feast.protos.feast.core.Registry_pb2 + :members: + :undoc-members: + :show-inheritance: + +feast.protos.feast.core.Registry\_pb2\_grpc module +-------------------------------------------------- + +.. automodule:: feast.protos.feast.core.Registry_pb2_grpc + :members: + :undoc-members: + :show-inheritance: + +feast.protos.feast.core.RequestFeatureView\_pb2 module +------------------------------------------------------ + +.. automodule:: feast.protos.feast.core.RequestFeatureView_pb2 + :members: + :undoc-members: + :show-inheritance: + +feast.protos.feast.core.RequestFeatureView\_pb2\_grpc module +------------------------------------------------------------ + +.. automodule:: feast.protos.feast.core.RequestFeatureView_pb2_grpc + :members: + :undoc-members: + :show-inheritance: + +feast.protos.feast.core.SavedDataset\_pb2 module +------------------------------------------------ + +.. automodule:: feast.protos.feast.core.SavedDataset_pb2 + :members: + :undoc-members: + :show-inheritance: + +feast.protos.feast.core.SavedDataset\_pb2\_grpc module +------------------------------------------------------ + +.. automodule:: feast.protos.feast.core.SavedDataset_pb2_grpc + :members: + :undoc-members: + :show-inheritance: + +feast.protos.feast.core.SqliteTable\_pb2 module +----------------------------------------------- + +.. automodule:: feast.protos.feast.core.SqliteTable_pb2 + :members: + :undoc-members: + :show-inheritance: + +feast.protos.feast.core.SqliteTable\_pb2\_grpc module +----------------------------------------------------- + +.. automodule:: feast.protos.feast.core.SqliteTable_pb2_grpc + :members: + :undoc-members: + :show-inheritance: + +feast.protos.feast.core.Store\_pb2 module +----------------------------------------- + +.. automodule:: feast.protos.feast.core.Store_pb2 + :members: + :undoc-members: + :show-inheritance: + +feast.protos.feast.core.Store\_pb2\_grpc module +----------------------------------------------- + +.. automodule:: feast.protos.feast.core.Store_pb2_grpc + :members: + :undoc-members: + :show-inheritance: + +feast.protos.feast.core.StreamFeatureView\_pb2 module +----------------------------------------------------- + +.. automodule:: feast.protos.feast.core.StreamFeatureView_pb2 + :members: + :undoc-members: + :show-inheritance: + +feast.protos.feast.core.StreamFeatureView\_pb2\_grpc module +----------------------------------------------------------- + +.. automodule:: feast.protos.feast.core.StreamFeatureView_pb2_grpc + :members: + :undoc-members: + :show-inheritance: + +feast.protos.feast.core.ValidationProfile\_pb2 module +----------------------------------------------------- + +.. automodule:: feast.protos.feast.core.ValidationProfile_pb2 + :members: + :undoc-members: + :show-inheritance: + +feast.protos.feast.core.ValidationProfile\_pb2\_grpc module +----------------------------------------------------------- + +.. automodule:: feast.protos.feast.core.ValidationProfile_pb2_grpc + :members: + :undoc-members: + :show-inheritance: + +Module contents +--------------- + +.. automodule:: feast.protos.feast.core + :members: + :undoc-members: + :show-inheritance: diff --git a/sdk/python/docs/source/feast.protos.feast.rst b/sdk/python/docs/source/feast.protos.feast.rst new file mode 100644 index 00000000000..f519165db8e --- /dev/null +++ b/sdk/python/docs/source/feast.protos.feast.rst @@ -0,0 +1,21 @@ +feast.protos.feast package +========================== + +Subpackages +----------- + +.. toctree:: + :maxdepth: 4 + + feast.protos.feast.core + feast.protos.feast.serving + feast.protos.feast.storage + feast.protos.feast.types + +Module contents +--------------- + +.. automodule:: feast.protos.feast + :members: + :undoc-members: + :show-inheritance: diff --git a/sdk/python/docs/source/feast.protos.feast.serving.rst b/sdk/python/docs/source/feast.protos.feast.serving.rst new file mode 100644 index 00000000000..792335b189d --- /dev/null +++ b/sdk/python/docs/source/feast.protos.feast.serving.rst @@ -0,0 +1,61 @@ +feast.protos.feast.serving package +================================== + +Submodules +---------- + +feast.protos.feast.serving.Connector\_pb2 module +------------------------------------------------ + +.. automodule:: feast.protos.feast.serving.Connector_pb2 + :members: + :undoc-members: + :show-inheritance: + +feast.protos.feast.serving.Connector\_pb2\_grpc module +------------------------------------------------------ + +.. automodule:: feast.protos.feast.serving.Connector_pb2_grpc + :members: + :undoc-members: + :show-inheritance: + +feast.protos.feast.serving.ServingService\_pb2 module +----------------------------------------------------- + +.. automodule:: feast.protos.feast.serving.ServingService_pb2 + :members: + :undoc-members: + :show-inheritance: + +feast.protos.feast.serving.ServingService\_pb2\_grpc module +----------------------------------------------------------- + +.. automodule:: feast.protos.feast.serving.ServingService_pb2_grpc + :members: + :undoc-members: + :show-inheritance: + +feast.protos.feast.serving.TransformationService\_pb2 module +------------------------------------------------------------ + +.. automodule:: feast.protos.feast.serving.TransformationService_pb2 + :members: + :undoc-members: + :show-inheritance: + +feast.protos.feast.serving.TransformationService\_pb2\_grpc module +------------------------------------------------------------------ + +.. automodule:: feast.protos.feast.serving.TransformationService_pb2_grpc + :members: + :undoc-members: + :show-inheritance: + +Module contents +--------------- + +.. automodule:: feast.protos.feast.serving + :members: + :undoc-members: + :show-inheritance: diff --git a/sdk/python/docs/source/feast.protos.feast.storage.rst b/sdk/python/docs/source/feast.protos.feast.storage.rst new file mode 100644 index 00000000000..90bc1adc9b5 --- /dev/null +++ b/sdk/python/docs/source/feast.protos.feast.storage.rst @@ -0,0 +1,29 @@ +feast.protos.feast.storage package +================================== + +Submodules +---------- + +feast.protos.feast.storage.Redis\_pb2 module +-------------------------------------------- + +.. automodule:: feast.protos.feast.storage.Redis_pb2 + :members: + :undoc-members: + :show-inheritance: + +feast.protos.feast.storage.Redis\_pb2\_grpc module +-------------------------------------------------- + +.. automodule:: feast.protos.feast.storage.Redis_pb2_grpc + :members: + :undoc-members: + :show-inheritance: + +Module contents +--------------- + +.. automodule:: feast.protos.feast.storage + :members: + :undoc-members: + :show-inheritance: diff --git a/sdk/python/docs/source/feast.protos.feast.types.rst b/sdk/python/docs/source/feast.protos.feast.types.rst new file mode 100644 index 00000000000..aeb31bc9ad3 --- /dev/null +++ b/sdk/python/docs/source/feast.protos.feast.types.rst @@ -0,0 +1,61 @@ +feast.protos.feast.types package +================================ + +Submodules +---------- + +feast.protos.feast.types.EntityKey\_pb2 module +---------------------------------------------- + +.. automodule:: feast.protos.feast.types.EntityKey_pb2 + :members: + :undoc-members: + :show-inheritance: + +feast.protos.feast.types.EntityKey\_pb2\_grpc module +---------------------------------------------------- + +.. automodule:: feast.protos.feast.types.EntityKey_pb2_grpc + :members: + :undoc-members: + :show-inheritance: + +feast.protos.feast.types.Field\_pb2 module +------------------------------------------ + +.. automodule:: feast.protos.feast.types.Field_pb2 + :members: + :undoc-members: + :show-inheritance: + +feast.protos.feast.types.Field\_pb2\_grpc module +------------------------------------------------ + +.. automodule:: feast.protos.feast.types.Field_pb2_grpc + :members: + :undoc-members: + :show-inheritance: + +feast.protos.feast.types.Value\_pb2 module +------------------------------------------ + +.. automodule:: feast.protos.feast.types.Value_pb2 + :members: + :undoc-members: + :show-inheritance: + +feast.protos.feast.types.Value\_pb2\_grpc module +------------------------------------------------ + +.. automodule:: feast.protos.feast.types.Value_pb2_grpc + :members: + :undoc-members: + :show-inheritance: + +Module contents +--------------- + +.. automodule:: feast.protos.feast.types + :members: + :undoc-members: + :show-inheritance: diff --git a/sdk/python/docs/source/feast.protos.rst b/sdk/python/docs/source/feast.protos.rst new file mode 100644 index 00000000000..7bec91eb030 --- /dev/null +++ b/sdk/python/docs/source/feast.protos.rst @@ -0,0 +1,18 @@ +feast.protos package +==================== + +Subpackages +----------- + +.. toctree:: + :maxdepth: 4 + + feast.protos.feast + +Module contents +--------------- + +.. automodule:: feast.protos + :members: + :undoc-members: + :show-inheritance: diff --git a/sdk/python/docs/source/feast.rst b/sdk/python/docs/source/feast.rst new file mode 100644 index 00000000000..b0ed92c4cce --- /dev/null +++ b/sdk/python/docs/source/feast.rst @@ -0,0 +1,378 @@ +feast package +============= + +Subpackages +----------- + +.. toctree:: + :maxdepth: 4 + + feast.diff + feast.dqm + feast.infra + feast.loaders + feast.protos + feast.ui + +Submodules +---------- + +feast.aggregation module +------------------------ + +.. automodule:: feast.aggregation + :members: + :undoc-members: + :show-inheritance: + +feast.base\_feature\_view module +-------------------------------- + +.. automodule:: feast.base_feature_view + :members: + :undoc-members: + :show-inheritance: + +feast.batch\_feature\_view module +--------------------------------- + +.. automodule:: feast.batch_feature_view + :members: + :undoc-members: + :show-inheritance: + +feast.cli module +---------------- + +.. automodule:: feast.cli + :members: + :undoc-members: + :show-inheritance: + +feast.constants module +---------------------- + +.. automodule:: feast.constants + :members: + :undoc-members: + :show-inheritance: + +feast.data\_format module +------------------------- + +.. automodule:: feast.data_format + :members: + :undoc-members: + :show-inheritance: + +feast.data\_source module +------------------------- + +.. automodule:: feast.data_source + :members: + :undoc-members: + :show-inheritance: + +feast.driver\_test\_data module +------------------------------- + +.. automodule:: feast.driver_test_data + :members: + :undoc-members: + :show-inheritance: + +feast.entity module +------------------- + +.. automodule:: feast.entity + :members: + :undoc-members: + :show-inheritance: + +feast.errors module +------------------- + +.. automodule:: feast.errors + :members: + :undoc-members: + :show-inheritance: + +feast.feast\_object module +-------------------------- + +.. automodule:: feast.feast_object + :members: + :undoc-members: + :show-inheritance: + +feast.feature module +-------------------- + +.. automodule:: feast.feature + :members: + :undoc-members: + :show-inheritance: + +feast.feature\_logging module +----------------------------- + +.. automodule:: feast.feature_logging + :members: + :undoc-members: + :show-inheritance: + +feast.feature\_server module +---------------------------- + +.. automodule:: feast.feature_server + :members: + :undoc-members: + :show-inheritance: + +feast.feature\_service module +----------------------------- + +.. automodule:: feast.feature_service + :members: + :undoc-members: + :show-inheritance: + +feast.feature\_store module +--------------------------- + +.. automodule:: feast.feature_store + :members: + :undoc-members: + :show-inheritance: + +feast.feature\_view module +-------------------------- + +.. automodule:: feast.feature_view + :members: + :undoc-members: + :show-inheritance: + +feast.feature\_view\_projection module +-------------------------------------- + +.. automodule:: feast.feature_view_projection + :members: + :undoc-members: + :show-inheritance: + +feast.field module +------------------ + +.. automodule:: feast.field + :members: + :undoc-members: + :show-inheritance: + +feast.file\_utils module +------------------------ + +.. automodule:: feast.file_utils + :members: + :undoc-members: + :show-inheritance: + +feast.flags\_helper module +-------------------------- + +.. automodule:: feast.flags_helper + :members: + :undoc-members: + :show-inheritance: + +feast.importer module +--------------------- + +.. automodule:: feast.importer + :members: + :undoc-members: + :show-inheritance: + +feast.inference module +---------------------- + +.. automodule:: feast.inference + :members: + :undoc-members: + :show-inheritance: + +feast.names module +------------------ + +.. automodule:: feast.names + :members: + :undoc-members: + :show-inheritance: + +feast.on\_demand\_feature\_view module +-------------------------------------- + +.. automodule:: feast.on_demand_feature_view + :members: + :undoc-members: + :show-inheritance: + +feast.online\_response module +----------------------------- + +.. automodule:: feast.online_response + :members: + :undoc-members: + :show-inheritance: + +feast.project\_metadata module +------------------------------ + +.. automodule:: feast.project_metadata + :members: + :undoc-members: + :show-inheritance: + +feast.proto\_json module +------------------------ + +.. automodule:: feast.proto_json + :members: + :undoc-members: + :show-inheritance: + +feast.repo\_config module +------------------------- + +.. automodule:: feast.repo_config + :members: + :undoc-members: + :show-inheritance: + +feast.repo\_contents module +--------------------------- + +.. automodule:: feast.repo_contents + :members: + :undoc-members: + :show-inheritance: + +feast.repo\_operations module +----------------------------- + +.. automodule:: feast.repo_operations + :members: + :undoc-members: + :show-inheritance: + +feast.repo\_upgrade module +-------------------------- + +.. automodule:: feast.repo_upgrade + :members: + :undoc-members: + :show-inheritance: + +feast.request\_feature\_view module +----------------------------------- + +.. automodule:: feast.request_feature_view + :members: + :undoc-members: + :show-inheritance: + +feast.saved\_dataset module +--------------------------- + +.. automodule:: feast.saved_dataset + :members: + :undoc-members: + :show-inheritance: + +feast.stream\_feature\_view module +---------------------------------- + +.. automodule:: feast.stream_feature_view + :members: + :undoc-members: + :show-inheritance: + +feast.transformation\_server module +----------------------------------- + +.. automodule:: feast.transformation_server + :members: + :undoc-members: + :show-inheritance: + +feast.type\_map module +---------------------- + +.. automodule:: feast.type_map + :members: + :undoc-members: + :show-inheritance: + +feast.types module +------------------ + +.. automodule:: feast.types + :members: + :undoc-members: + :show-inheritance: + +feast.ui\_server module +----------------------- + +.. automodule:: feast.ui_server + :members: + :undoc-members: + :show-inheritance: + +feast.usage module +------------------ + +.. automodule:: feast.usage + :members: + :undoc-members: + :show-inheritance: + +feast.utils module +------------------ + +.. automodule:: feast.utils + :members: + :undoc-members: + :show-inheritance: + +feast.value\_type module +------------------------ + +.. automodule:: feast.value_type + :members: + :undoc-members: + :show-inheritance: + +feast.version module +-------------------- + +.. automodule:: feast.version + :members: + :undoc-members: + :show-inheritance: + +feast.wait module +----------------- + +.. automodule:: feast.wait + :members: + :undoc-members: + :show-inheritance: + +Module contents +--------------- + +.. automodule:: feast + :members: + :undoc-members: + :show-inheritance: diff --git a/sdk/python/docs/source/feast.ui.rst b/sdk/python/docs/source/feast.ui.rst new file mode 100644 index 00000000000..01b16cb0a6a --- /dev/null +++ b/sdk/python/docs/source/feast.ui.rst @@ -0,0 +1,10 @@ +feast.ui package +================ + +Module contents +--------------- + +.. automodule:: feast.ui + :members: + :undoc-members: + :show-inheritance: diff --git a/sdk/python/docs/source/index.rst b/sdk/python/docs/source/index.rst new file mode 100644 index 00000000000..58823d2fe1e --- /dev/null +++ b/sdk/python/docs/source/index.rst @@ -0,0 +1,353 @@ +Feast Python API Documentation +============================== + + +Feature Store +================== + +.. automodule:: feast.feature_store + :members: + :undoc-members: + :show-inheritance: + +Config +================== + +.. automodule:: feast.repo_config + :members: + :exclude-members: load_repo_config, FeastBaseModel + +Data Source +================== + +.. automodule:: feast.data_source + :inherited-members: + :members: + :exclude-members: KafkaOptions, KafkaSource, KinesisOptions, KinesisSource, PushSource, RequestSource, RequestDataSource + +Request Source +------------------ + +.. automodule:: feast.data_source + :members: RequestSource + +Push Source +------------------ + +.. automodule:: feast.data_source + :members: PushSource + +BigQuery Source +------------------ + +.. automodule:: feast.infra.offline_stores.bigquery_source + :members: + :exclude-members: BigQueryOptions + +Redshift Source +------------------ + +.. automodule:: feast.infra.offline_stores.redshift_source + :members: + :exclude-members: RedshiftOptions + +Snowflake Source +------------------ + +.. automodule:: feast.infra.offline_stores.snowflake_source + :members: + :exclude-members: SnowflakeOptions + +Spark Source +------------------ + +.. automodule:: feast.infra.offline_stores.contrib.spark_offline_store.spark_source + :members: + :exclude-members: SparkOptions + +Trino Source +------------------ + +.. automodule:: feast.infra.offline_stores.contrib.trino_offline_store.trino_source + :members: + :exclude-members: TrinoOptions + +PostgreSQL Source +------------------ + +.. automodule:: feast.infra.offline_stores.contrib.postgres_offline_store.postgres_source + :members: + :exclude-members: PostgreSQLOptions + +File Source +------------------ + +.. automodule:: feast.infra.offline_stores.file_source + :members: + :exclude-members: FileOptions + +Entity +================== + +.. automodule:: feast.entity + :inherited-members: + :members: + +Feature View +================== + +.. automodule:: feast.base_feature_view + :members: + +Feature View +---------------------- + +.. automodule:: feast.feature_view + :members: + +On Demand Feature View +---------------------- + +.. automodule:: feast.on_demand_feature_view + :members: + +Stream Feature View +---------------------- + +.. automodule:: feast.stream_feature_view + :members: + +Feature +================== + +.. automodule:: feast.feature + :inherited-members: + :members: + +Feature Service +================== + +.. automodule:: feast.feature_service + :inherited-members: + :members: + +Registry +================== + +.. automodule:: feast.infra.registry.base_registry + :inherited-members: + :members: + +Registry +---------------------- + +.. automodule:: feast.infra.registry.registry + :inherited-members: + :members: + +SQL Registry +---------------------- + +.. automodule:: feast.infra.registry.sql + :inherited-members: + :members: + +Registry Store +================== + +.. automodule:: feast.infra.registry.registry_store + :inherited-members: + :members: + :exclude-members: NoopRegistryStore + +File Registry Store +----------------------- + +.. automodule:: feast.infra.registry.file + :members: + :noindex: + +GCS Registry Store +----------------------- + +.. automodule:: feast.infra.registry.gcs + :members: + :noindex: + +S3 Registry Store +----------------------- + +.. automodule:: feast.infra.registry.s3 + :members: + :noindex: + +PostgreSQL Registry Store +----------------------- + +.. automodule:: feast.infra.registry.contrib.postgres.postgres_registry_store + :members: + :noindex: + +Provider +================== + +.. automodule:: feast.infra.provider + :inherited-members: + :members: + +Passthrough Provider +-------------------- + +.. automodule:: feast.infra.passthrough_provider + :members: + +Local Provider +------------------ + +.. automodule:: feast.infra.local + :members: + +GCP Provider +------------------ + +.. automodule:: feast.infra.gcp + :members: + +AWS Provider +------------------ + +.. automodule:: feast.infra.aws + :members: + +Offline Store +================== + +.. automodule:: feast.infra.offline_stores.offline_store + :members: + +File Offline Store +------------------ + +.. automodule:: feast.infra.offline_stores.file + :members: + +BigQuery Offline Store +---------------------- + +.. automodule:: feast.infra.offline_stores.bigquery + :members: + +Redshift Offline Store +---------------------- + +.. automodule:: feast.infra.offline_stores.redshift + :members: + +Snowflake Offline Store +----------------------- + +.. automodule:: feast.infra.offline_stores.snowflake + :members: + +Spark Offline Store +------------------- + +.. automodule:: feast.infra.offline_stores.contrib.spark_offline_store.spark + :members: + +Trino Offline Store +------------------- + +.. automodule:: feast.infra.offline_stores.contrib.trino_offline_store.trino + :members: + +PostgreSQL Offline Store +------------------------ + +.. automodule:: feast.infra.offline_stores.contrib.postgres_offline_store.postgres + :members: + + +Online Store +================== + +.. automodule:: feast.infra.online_stores.online_store + :inherited-members: + :members: + +Sqlite Online Store +------------------- + +.. automodule:: feast.infra.online_stores.sqlite + :members: + :noindex: + +Datastore Online Store +---------------------- + +.. automodule:: feast.infra.online_stores.datastore + :members: + :noindex: + +DynamoDB Online Store +--------------------- + +.. automodule:: feast.infra.online_stores.dynamodb + :members: + :noindex: + +Redis Online Store +------------------ + +.. automodule:: feast.infra.online_stores.redis + :members: + :noindex: + +PostgreSQL Online Store +----------------------- + +.. automodule:: feast.infra.online_stores.contrib.postgres + :members: + :noindex: + +HBase Online Store +----------------------- + +.. automodule:: feast.infra.online_stores.contrib.hbase_online_store.hbase + :members: + :noindex: + +Cassandra Online Store +----------------------- + +.. automodule:: feast.infra.online_stores.contrib.cassandra_online_store.cassandra_online_store + :members: + :noindex: + + +Batch Materialization Engine +============================ + +.. automodule:: feast.infra.materialization + :members: BatchMaterializationEngine, MaterializationJob, MaterializationTask + +Local Engine +------------ +.. autoclass:: feast.infra.materialization.LocalMaterializationEngine + :members: + :noindex: + +(Alpha) Lambda Based Engine +--------------------------- + +.. automodule:: feast.infra.materialization.lambda.lambda_engine + :members: + :noindex: + + +Bytewax Engine +--------------------------- + +.. automodule:: feast.infra.materialization.contrib.bytewax + :members: + :noindex: diff --git a/sdk/python/docs/source/modules.rst b/sdk/python/docs/source/modules.rst new file mode 100644 index 00000000000..3a6f8333abd --- /dev/null +++ b/sdk/python/docs/source/modules.rst @@ -0,0 +1,7 @@ +feast +===== + +.. toctree:: + :maxdepth: 4 + + feast From 31774e202a5eb3b2d195b23da945a1143dea75c8 Mon Sep 17 00:00:00 2001 From: Felix Wang Date: Thu, 25 Aug 2022 12:22:18 -0700 Subject: [PATCH 03/46] chore: Add missing docs (#3143) Add missing docs Signed-off-by: Felix Wang Signed-off-by: Felix Wang --- sdk/python/docs/source/feast.infra.rst | 2 +- .../feast.infra.transformation_servers.rst | 21 ++ sdk/python/docs/source/index.rst | 296 ++++++++++++------ 3 files changed, 214 insertions(+), 105 deletions(-) create mode 100644 sdk/python/docs/source/feast.infra.transformation_servers.rst diff --git a/sdk/python/docs/source/feast.infra.rst b/sdk/python/docs/source/feast.infra.rst index 96c0828451a..50e1f37f1c6 100644 --- a/sdk/python/docs/source/feast.infra.rst +++ b/sdk/python/docs/source/feast.infra.rst @@ -7,10 +7,10 @@ Subpackages .. toctree:: :maxdepth: 4 - feast.infra.materialization feast.infra.offline_stores feast.infra.online_stores feast.infra.registry + feast.infra.transformation_servers feast.infra.utils Submodules diff --git a/sdk/python/docs/source/feast.infra.transformation_servers.rst b/sdk/python/docs/source/feast.infra.transformation_servers.rst new file mode 100644 index 00000000000..7de2dc79f29 --- /dev/null +++ b/sdk/python/docs/source/feast.infra.transformation_servers.rst @@ -0,0 +1,21 @@ +feast.infra.transformation\_servers package +=========================================== + +Submodules +---------- + +feast.infra.transformation\_servers.app module +---------------------------------------------- + +.. automodule:: feast.infra.transformation_servers.app + :members: + :undoc-members: + :show-inheritance: + +Module contents +--------------- + +.. automodule:: feast.infra.transformation_servers + :members: + :undoc-members: + :show-inheritance: diff --git a/sdk/python/docs/source/index.rst b/sdk/python/docs/source/index.rst index 58823d2fe1e..beca384137d 100644 --- a/sdk/python/docs/source/index.rst +++ b/sdk/python/docs/source/index.rst @@ -1,353 +1,441 @@ Feast Python API Documentation ============================== +.. We prefer 'autoclass' instead of 'autoclass' as 'autoclass' can specify a class, whereas + 'autoclass' will pull in all public classes and methods from that module, which we typically + do not want. Feature Store ================== -.. automodule:: feast.feature_store - :members: - :undoc-members: - :show-inheritance: +.. autoclass:: feast.feature_store.FeatureStore + :members: Config ================== -.. automodule:: feast.repo_config +.. autoclass:: feast.repo_config.RepoConfig + :members: + +.. autoclass:: feast.repo_config.RegistryConfig :members: - :exclude-members: load_repo_config, FeastBaseModel Data Source ================== -.. automodule:: feast.data_source - :inherited-members: +.. autoclass:: feast.data_source.DataSource :members: - :exclude-members: KafkaOptions, KafkaSource, KinesisOptions, KinesisSource, PushSource, RequestSource, RequestDataSource -Request Source +File Source ------------------ -.. automodule:: feast.data_source - :members: RequestSource +.. autoclass:: feast.infra.offline_stores.file_source.FileSource + :members: -Push Source +Snowflake Source ------------------ -.. automodule:: feast.data_source - :members: PushSource +.. autoclass:: feast.infra.offline_stores.snowflake_source.SnowflakeSource + :members: BigQuery Source ------------------ -.. automodule:: feast.infra.offline_stores.bigquery_source +.. autoclass:: feast.infra.offline_stores.bigquery_source.BigQuerySource :members: - :exclude-members: BigQueryOptions Redshift Source ------------------ -.. automodule:: feast.infra.offline_stores.redshift_source +.. autoclass:: feast.infra.offline_stores.redshift_source.RedshiftSource :members: - :exclude-members: RedshiftOptions -Snowflake Source +Spark Source ------------------ -.. automodule:: feast.infra.offline_stores.snowflake_source +.. autoclass:: feast.infra.offline_stores.contrib.spark_offline_store.spark_source.SparkSource :members: - :exclude-members: SnowflakeOptions -Spark Source +Trino Source ------------------ -.. automodule:: feast.infra.offline_stores.contrib.spark_offline_store.spark_source +.. autoclass:: feast.infra.offline_stores.contrib.trino_offline_store.trino_source.TrinoSource :members: - :exclude-members: SparkOptions -Trino Source +PostgreSQL Source ------------------ -.. automodule:: feast.infra.offline_stores.contrib.trino_offline_store.trino_source +.. autoclass:: feast.infra.offline_stores.contrib.postgres_offline_store.postgres_source.PostgreSQLSource :members: - :exclude-members: TrinoOptions -PostgreSQL Source +Request Source ------------------ -.. automodule:: feast.infra.offline_stores.contrib.postgres_offline_store.postgres_source +.. autoclass:: feast.data_source.RequestSource :members: - :exclude-members: PostgreSQLOptions -File Source +Push Source +------------------ + +.. autoclass:: feast.data_source.PushSource + :members: + +Kafka Source +------------------ + +.. autoclass:: feast.data_source.KafkaSource + :members: + +Kinesis Source ------------------ -.. automodule:: feast.infra.offline_stores.file_source +.. autoclass:: feast.data_source.KinesisSource :members: - :exclude-members: FileOptions Entity ================== -.. automodule:: feast.entity - :inherited-members: +.. autoclass:: feast.entity.Entity :members: Feature View ================== -.. automodule:: feast.base_feature_view +.. autoclass:: feast.base_feature_view.BaseFeatureView :members: Feature View ---------------------- -.. automodule:: feast.feature_view +.. autoclass:: feast.feature_view.FeatureView :members: On Demand Feature View ---------------------- -.. automodule:: feast.on_demand_feature_view +.. autoclass:: feast.on_demand_feature_view.OnDemandFeatureView + :members: + +Batch Feature View +---------------------- + +.. autoclass:: feast.batch_feature_view.BatchFeatureView :members: Stream Feature View ---------------------- -.. automodule:: feast.stream_feature_view +.. autoclass:: feast.stream_feature_view.StreamFeatureView :members: -Feature +Field ================== -.. automodule:: feast.feature - :inherited-members: +.. autoclass:: feast.field.Field :members: Feature Service ================== -.. automodule:: feast.feature_service - :inherited-members: +.. autoclass:: feast.feature_service.FeatureService :members: Registry ================== -.. automodule:: feast.infra.registry.base_registry - :inherited-members: +.. autoclass:: feast.infra.registry.base_registry.BaseRegistry :members: Registry ---------------------- -.. automodule:: feast.infra.registry.registry - :inherited-members: +.. autoclass:: feast.infra.registry.registry.Registry :members: SQL Registry ---------------------- -.. automodule:: feast.infra.registry.sql - :inherited-members: +.. autoclass:: feast.infra.registry.sql.SqlRegistry :members: Registry Store ================== -.. automodule:: feast.infra.registry.registry_store - :inherited-members: +.. autoclass:: feast.infra.registry.registry_store.RegistryStore :members: - :exclude-members: NoopRegistryStore File Registry Store ----------------------- -.. automodule:: feast.infra.registry.file +.. autoclass:: feast.infra.registry.file.FileRegistryStore :members: - :noindex: GCS Registry Store ----------------------- -.. automodule:: feast.infra.registry.gcs +.. autoclass:: feast.infra.registry.gcs.GCSRegistryStore :members: - :noindex: S3 Registry Store ----------------------- -.. automodule:: feast.infra.registry.s3 +.. autoclass:: feast.infra.registry.s3.S3RegistryStore :members: - :noindex: PostgreSQL Registry Store ----------------------- -.. automodule:: feast.infra.registry.contrib.postgres.postgres_registry_store +.. autoclass:: feast.infra.registry.contrib.postgres.postgres_registry_store.PostgreSQLRegistryStore :members: - :noindex: Provider ================== -.. automodule:: feast.infra.provider - :inherited-members: +.. autoclass:: feast.infra.provider.Provider :members: Passthrough Provider -------------------- -.. automodule:: feast.infra.passthrough_provider +.. autoclass:: feast.infra.passthrough_provider.PassthroughProvider :members: Local Provider ------------------ -.. automodule:: feast.infra.local +.. autoclass:: feast.infra.local.LocalProvider :members: GCP Provider ------------------ -.. automodule:: feast.infra.gcp +.. autoclass:: feast.infra.gcp.GcpProvider :members: AWS Provider ------------------ -.. automodule:: feast.infra.aws +.. autoclass:: feast.infra.aws.AwsProvider :members: Offline Store ================== -.. automodule:: feast.infra.offline_stores.offline_store +.. autoclass:: feast.infra.offline_stores.offline_store.OfflineStore + :members: + +.. autoclass:: feast.infra.offline_stores.offline_store.RetrievalJob :members: File Offline Store ------------------ -.. automodule:: feast.infra.offline_stores.file +.. autoclass:: feast.infra.offline_stores.file.FileOfflineStore + :members: + +.. autoclass:: feast.infra.offline_stores.file.FileOfflineStoreConfig + :members: + +.. autoclass:: feast.infra.offline_stores.file.FileRetrievalJob + :members: + +Snowflake Offline Store +----------------------- + +.. autoclass:: feast.infra.offline_stores.snowflake.SnowflakeOfflineStore + :members: + +.. autoclass:: feast.infra.offline_stores.snowflake.SnowflakeOfflineStoreConfig + :members: + +.. autoclass:: feast.infra.offline_stores.snowflake.SnowflakeRetrievalJob :members: BigQuery Offline Store ---------------------- -.. automodule:: feast.infra.offline_stores.bigquery +.. autoclass:: feast.infra.offline_stores.bigquery.BigQueryOfflineStore + :members: + +.. autoclass:: feast.infra.offline_stores.bigquery.BigQueryOfflineStoreConfig + :members: + +.. autoclass:: feast.infra.offline_stores.bigquery.BigQueryRetrievalJob :members: Redshift Offline Store ---------------------- -.. automodule:: feast.infra.offline_stores.redshift +.. autoclass:: feast.infra.offline_stores.redshift.RedshiftOfflineStore :members: -Snowflake Offline Store ------------------------ +.. autoclass:: feast.infra.offline_stores.redshift.RedshiftOfflineStoreConfig + :members: -.. automodule:: feast.infra.offline_stores.snowflake +.. autoclass:: feast.infra.offline_stores.redshift.RedshiftRetrievalJob :members: Spark Offline Store ------------------- -.. automodule:: feast.infra.offline_stores.contrib.spark_offline_store.spark +.. autoclass:: feast.infra.offline_stores.contrib.spark_offline_store.spark.SparkOfflineStore + :members: + +.. autoclass:: feast.infra.offline_stores.contrib.spark_offline_store.spark.SparkOfflineStoreConfig + :members: + +.. autoclass:: feast.infra.offline_stores.contrib.spark_offline_store.spark.SparkRetrievalJob :members: Trino Offline Store ------------------- -.. automodule:: feast.infra.offline_stores.contrib.trino_offline_store.trino +.. autoclass:: feast.infra.offline_stores.contrib.trino_offline_store.trino.TrinoOfflineStore + :members: + +.. autoclass:: feast.infra.offline_stores.contrib.trino_offline_store.trino.TrinoOfflineStoreConfig + :members: + +.. autoclass:: feast.infra.offline_stores.contrib.trino_offline_store.trino.TrinoRetrievalJob :members: PostgreSQL Offline Store ------------------------ -.. automodule:: feast.infra.offline_stores.contrib.postgres_offline_store.postgres +.. autoclass:: feast.infra.offline_stores.contrib.postgres_offline_store.postgres.PostgreSQLOfflineStore + :members: + +.. autoclass:: feast.infra.offline_stores.contrib.postgres_offline_store.postgres.PostgreSQLOfflineStoreConfig :members: +.. autoclass:: feast.infra.offline_stores.contrib.postgres_offline_store.postgres.PostgreSQLRetrievalJob + :members: Online Store ================== -.. automodule:: feast.infra.online_stores.online_store - :inherited-members: +.. autoclass:: feast.infra.online_stores.online_store.OnlineStore :members: Sqlite Online Store ------------------- -.. automodule:: feast.infra.online_stores.sqlite +.. autoclass:: feast.infra.online_stores.sqlite.SqliteOnlineStore + :members: + +.. autoclass:: feast.infra.online_stores.sqlite.SqliteOnlineStoreConfig :members: - :noindex: Datastore Online Store ---------------------- -.. automodule:: feast.infra.online_stores.datastore +.. autoclass:: feast.infra.online_stores.datastore.DatastoreOnlineStore + :members: + +.. autoclass:: feast.infra.online_stores.datastore.DatastoreOnlineStoreConfig :members: - :noindex: DynamoDB Online Store --------------------- -.. automodule:: feast.infra.online_stores.dynamodb +.. autoclass:: feast.infra.online_stores.dynamodb.DynamoDBOnlineStore + :members: + +.. autoclass:: feast.infra.online_stores.dynamodb.DynamoDBOnlineStoreConfig :members: - :noindex: Redis Online Store ------------------ -.. automodule:: feast.infra.online_stores.redis +.. autoclass:: feast.infra.online_stores.redis.RedisOnlineStore + :members: + +.. autoclass:: feast.infra.online_stores.redis.RedisOnlineStoreConfig :members: - :noindex: PostgreSQL Online Store ----------------------- -.. automodule:: feast.infra.online_stores.contrib.postgres +.. autoclass:: feast.infra.online_stores.contrib.postgres.PostgreSQLOnlineStore + :members: + +.. autoclass:: feast.infra.online_stores.contrib.postgres.PostgreSQLOnlineStoreConfig :members: - :noindex: HBase Online Store ----------------------- -.. automodule:: feast.infra.online_stores.contrib.hbase_online_store.hbase +.. autoclass:: feast.infra.online_stores.contrib.hbase_online_store.hbase.HbaseOnlineStore + :members: + +.. autoclass:: feast.infra.online_stores.contrib.hbase_online_store.hbase.HbaseOnlineStoreConfig :members: - :noindex: Cassandra Online Store ----------------------- -.. automodule:: feast.infra.online_stores.contrib.cassandra_online_store.cassandra_online_store +.. autoclass:: feast.infra.online_stores.contrib.cassandra_online_store.cassandra_online_store.CassandraOnlineStore :members: - :noindex: +.. autoclass:: feast.infra.online_stores.contrib.cassandra_online_store.cassandra_online_store.CassandraOnlineStoreConfig + :members: Batch Materialization Engine ============================ -.. automodule:: feast.infra.materialization - :members: BatchMaterializationEngine, MaterializationJob, MaterializationTask +.. autoclass:: feast.infra.materialization.batch_materialization_engine.BatchMaterializationEngine + :members: + +.. autoclass:: feast.infra.materialization.batch_materialization_engine.MaterializationJob + :members: + +.. autoclass:: feast.infra.materialization.batch_materialization_engine.MaterializationTask + :members: Local Engine ------------ -.. autoclass:: feast.infra.materialization.LocalMaterializationEngine + +.. autoclass:: feast.infra.materialization.local_engine.LocalMaterializationEngine + :members: + +.. autoclass:: feast.infra.materialization.local_engine.LocalMaterializationEngineConfig :members: - :noindex: -(Alpha) Lambda Based Engine +.. autoclass:: feast.infra.materialization.local_engine.LocalMaterializationJob + :members: + +Bytewax Engine --------------------------- -.. automodule:: feast.infra.materialization.lambda.lambda_engine +.. autoclass:: feast.infra.materialization.contrib.bytewax.bytewax_materialization_engine.BytewaxMaterializationEngine :members: - :noindex: +.. autoclass:: feast.infra.materialization.contrib.bytewax.bytewax_materialization_engine.BytewaxMaterializationEngineConfig + :members: -Bytewax Engine +.. autoclass:: feast.infra.materialization.contrib.bytewax.bytewax_materialization_job.BytewaxMaterializationJob + :members: + +Snowflake Engine --------------------------- -.. automodule:: feast.infra.materialization.contrib.bytewax +.. autoclass:: feast.infra.materialization.snowflake_engine.SnowflakeMaterializationEngine + :members: + +.. autoclass:: feast.infra.materialization.snowflake_engine.SnowflakeMaterializationEngineConfig + :members: + +.. autoclass:: feast.infra.materialization.snowflake_engine.SnowflakeMaterializationJob + :members: + +(Alpha) AWS Lambda Engine +--------------------------- + +.. autoclass:: feast.infra.materialization.aws_lambda.lambda_engine.LambdaMaterializationEngine + :members: + +.. autoclass:: feast.infra.materialization.aws_lambda.lambda_engine.LambdaMaterializationEngineConfig + :members: + +.. autoclass:: feast.infra.materialization.aws_lambda.lambda_engine.LambdaMaterializationJob :members: - :noindex: From 20a9dd98550ad8daf291381a771b3da798e4c1a4 Mon Sep 17 00:00:00 2001 From: Kevin Zhang Date: Thu, 25 Aug 2022 13:56:08 -0700 Subject: [PATCH 04/46] fix: Fix release workflow (#3144) * Fix Signed-off-by: Kevin Zhang * Fix Signed-off-by: Kevin Zhang * Fix Signed-off-by: Kevin Zhang * Fix helm charts Signed-off-by: Kevin Zhang Signed-off-by: Kevin Zhang --- .github/workflows/build_wheels.yml | 6 +- .github/workflows/release.yml | 61 ++++++++++--------- .../helm/validate-helm-chart-versions.sh | 2 +- 3 files changed, 37 insertions(+), 32 deletions(-) diff --git a/.github/workflows/build_wheels.yml b/.github/workflows/build_wheels.yml index 278be10b890..841f5da87b3 100644 --- a/.github/workflows/build_wheels.yml +++ b/.github/workflows/build_wheels.yml @@ -17,7 +17,10 @@ jobs: version_without_prefix: ${{ steps.get_release_version_without_prefix.outputs.version_without_prefix }} highest_semver_tag: ${{ steps.get_highest_semver.outputs.highest_semver_tag }} steps: - - uses: actions/checkout@v2 + - name: Checkout + uses: actions/checkout@v2 + with: + persist-credentials: false - name: Get release version id: get_release_version run: echo ::set-output name=release_version::${GITHUB_REF#refs/*/} @@ -38,6 +41,7 @@ jobs: echo ::set-output name=highest_semver_tag::$(get_tag_release -m) fi - name: Check output + id: check_output env: RELEASE_VERSION: ${{ steps.get_release_version.outputs.release_version }} VERSION_WITHOUT_PREFIX: ${{ steps.get_release_version_without_prefix.outputs.version_without_prefix }} diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index feab7b0eef9..83eb5bcdd5f 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -42,40 +42,41 @@ jobs: echo "Current version is ${CURRENT_VERSION}" echo "Next version is ${NEXT_VERSION}" - # publish-web-ui-npm: - # if: github.repository == 'feast-dev/feast' - # needs: get_dry_release_versions - # runs-on: ubuntu-latest - # env: - # # This publish is working using an NPM automation token to bypass 2FA - # NPM_TOKEN: ${{ secrets.NPM_TOKEN }} - # CURRENT_VERSION: ${{ needs.get_dry_release_versions.outputs.current_version }} - # NEXT_VERSION: ${{ needs.get_dry_release_versions.outputs.next_version }} - # steps: - # - uses: actions/checkout@v2 - # - uses: actions/setup-node@v2 - # with: - # node-version: '17.x' - # registry-url: 'https://registry.npmjs.org' - # - name: Bump file versions (temporarily for Web UI publish) - # run: python ./infra/scripts/release/bump_file_versions.py ${CURRENT_VERSION} ${NEXT_VERSION} - # - name: Install yarn dependencies - # working-directory: ./ui - # run: yarn install - # - name: Build yarn rollup - # working-directory: ./ui - # run: yarn build:lib - # - name: Publish UI package - # working-directory: ./ui - # run: npm publish - # env: - # # This publish is working using an NPM automation token to bypass 2FA - # NPM_TOKEN: ${{ secrets.NPM_TOKEN }} + publish-web-ui-npm: + if: github.repository == 'feast-dev/feast' + needs: get_dry_release_versions + runs-on: ubuntu-latest + env: + # This publish is working using an NPM automation token to bypass 2FA + NPM_TOKEN: ${{ secrets.NPM_TOKEN }} + CURRENT_VERSION: ${{ needs.get_dry_release_versions.outputs.current_version }} + NEXT_VERSION: ${{ needs.get_dry_release_versions.outputs.next_version }} + steps: + - uses: actions/checkout@v2 + - uses: actions/setup-node@v2 + with: + node-version: '17.x' + registry-url: 'https://registry.npmjs.org' + - name: Bump file versions (temporarily for Web UI publish) + run: python ./infra/scripts/release/bump_file_versions.py ${CURRENT_VERSION} ${NEXT_VERSION} + - name: Install yarn dependencies + working-directory: ./ui + run: yarn install + - name: Build yarn rollup + working-directory: ./ui + run: yarn build:lib + - name: Publish UI package + if: github.event.inputs.dry_run == 'false' + working-directory: ./ui + run: npm publish + env: + # This publish is working using an NPM automation token to bypass 2FA + NPM_TOKEN: ${{ secrets.NPM_TOKEN }} release: name: release runs-on: ubuntu-latest - #needs: publish-web-ui-npm + needs: publish-web-ui-npm env: GITHUB_TOKEN: ${{ github.event.inputs.token }} GIT_AUTHOR_NAME: feast-ci-bot diff --git a/infra/scripts/helm/validate-helm-chart-versions.sh b/infra/scripts/helm/validate-helm-chart-versions.sh index aac79d93154..17a87e163aa 100755 --- a/infra/scripts/helm/validate-helm-chart-versions.sh +++ b/infra/scripts/helm/validate-helm-chart-versions.sh @@ -3,7 +3,7 @@ set -e # Amount of file locations that need to be bumped in unison when versions increment -UNIQUE_VERSIONS_COUNT=20 +UNIQUE_VERSIONS_COUNT=21 # Change in release 0.24.0 if [ $# -ne 1 ]; then echo "Please provide a single semver version (without a \"v\" prefix) to test the repository against, e.g 0.99.0" From 8fbf13ccce723012afa671794d2673a78854b12b Mon Sep 17 00:00:00 2001 From: sfc-gh-madkins <82121043+sfc-gh-madkins@users.noreply.github.com> Date: Mon, 29 Aug 2022 12:20:36 -0500 Subject: [PATCH 05/46] chore: Add tagging for snowflake engine (#3147) Signed-off-by: Miles Adkins Signed-off-by: Miles Adkins --- .../utils/snowflake/snowpark/snowflake_udfs.py | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/sdk/python/feast/infra/utils/snowflake/snowpark/snowflake_udfs.py b/sdk/python/feast/infra/utils/snowflake/snowpark/snowflake_udfs.py index 7fde4dd3a1c..02311ca55d6 100644 --- a/sdk/python/feast/infra/utils/snowflake/snowpark/snowflake_udfs.py +++ b/sdk/python/feast/infra/utils/snowflake/snowpark/snowflake_udfs.py @@ -1,3 +1,4 @@ +import sys from binascii import unhexlify import pandas @@ -24,6 +25,8 @@ # ValueType.BYTES = 1 @vectorized(input=pandas.DataFrame) def feast_snowflake_binary_to_bytes_proto(df): + sys._xoptions["snowflake_partner_attribution"].append("feast") + df = list( map( ValueProto.SerializeToString, @@ -45,6 +48,8 @@ def feast_snowflake_binary_to_bytes_proto(df): # ValueType.STRING = 2 @vectorized(input=pandas.DataFrame) def feast_snowflake_varchar_to_string_proto(df): + sys._xoptions["snowflake_partner_attribution"].append("feast") + df = list( map( ValueProto.SerializeToString, @@ -66,6 +71,8 @@ def feast_snowflake_varchar_to_string_proto(df): # ValueType.INT32 = 3 @vectorized(input=pandas.DataFrame) def feast_snowflake_number_to_int32_proto(df): + sys._xoptions["snowflake_partner_attribution"].append("feast") + df = list( map( ValueProto.SerializeToString, @@ -87,6 +94,8 @@ def feast_snowflake_number_to_int32_proto(df): # ValueType.INT64 = 4 @vectorized(input=pandas.DataFrame) def feast_snowflake_number_to_int64_proto(df): + sys._xoptions["snowflake_partner_attribution"].append("feast") + df = list( map( ValueProto.SerializeToString, @@ -110,6 +119,8 @@ def feast_snowflake_number_to_int64_proto(df): # ValueType.FLOAT = 5 & ValueType.DOUBLE = 6 @vectorized(input=pandas.DataFrame) def feast_snowflake_float_to_double_proto(df): + sys._xoptions["snowflake_partner_attribution"].append("feast") + df = list( map( ValueProto.SerializeToString, @@ -131,6 +142,8 @@ def feast_snowflake_float_to_double_proto(df): # ValueType.BOOL = 7 @vectorized(input=pandas.DataFrame) def feast_snowflake_boolean_to_bool_boolean_proto(df): + sys._xoptions["snowflake_partner_attribution"].append("feast") + df = list( map( ValueProto.SerializeToString, @@ -152,6 +165,7 @@ def feast_snowflake_boolean_to_bool_boolean_proto(df): # ValueType.UNIX_TIMESTAMP = 8 @vectorized(input=pandas.DataFrame) def feast_snowflake_timestamp_to_unix_timestamp_proto(df): + sys._xoptions["snowflake_partner_attribution"].append("feast") df = list( map( @@ -177,6 +191,8 @@ def feast_snowflake_timestamp_to_unix_timestamp_proto(df): # converts 1 to n many entity keys to a single binary for lookups @vectorized(input=pandas.DataFrame) def feast_serialize_entity_keys(df): + sys._xoptions["snowflake_partner_attribution"].append("feast") + join_keys = create_entity_dict(df[0].values[0], df[2].values[0]) df = pandas.DataFrame.from_dict( @@ -222,6 +238,8 @@ def feast_serialize_entity_keys(df): # converts 1 to n many entity keys to a single binary for lookups @vectorized(input=pandas.DataFrame) def feast_entity_key_proto_to_string(df): + sys._xoptions["snowflake_partner_attribution"].append("feast") + join_keys = create_entity_dict(df[0].values[0], df[2].values[0]) df = pandas.DataFrame.from_dict( From 50e8755d41ca2eacd41e31fc0be1202c69b61fdd Mon Sep 17 00:00:00 2001 From: sfc-gh-madkins <82121043+sfc-gh-madkins@users.noreply.github.com> Date: Mon, 29 Aug 2022 17:03:38 -0500 Subject: [PATCH 06/46] feat: Add snowflake support for date & number with scale (#3148) Signed-off-by: Miles Adkins Signed-off-by: Miles Adkins --- .../feast/infra/materialization/snowflake_engine.py | 6 +++++- .../feast/infra/offline_stores/snowflake_source.py | 13 ++++++------- sdk/python/feast/type_map.py | 2 ++ 3 files changed, 13 insertions(+), 8 deletions(-) diff --git a/sdk/python/feast/infra/materialization/snowflake_engine.py b/sdk/python/feast/infra/materialization/snowflake_engine.py index 1663cbcbc0a..80deb13f96e 100644 --- a/sdk/python/feast/infra/materialization/snowflake_engine.py +++ b/sdk/python/feast/infra/materialization/snowflake_engine.py @@ -334,7 +334,11 @@ def generate_snowflake_materialization_query( ) if feature_value_type_name == "UNIX_TIMESTAMP": - feature_sql = f'{feature_sql}(DATE_PART(EPOCH_NANOSECOND, "{feature.name}")) AS "{feature.name}"' + feature_sql = f'{feature_sql}(DATE_PART(EPOCH_NANOSECOND, "{feature.name}"::TIMESTAMP_LTZ)) AS "{feature.name}"' + elif feature_value_type_name == "DOUBLE": + feature_sql = ( + f'{feature_sql}("{feature.name}"::DOUBLE) AS "{feature.name}"' + ) else: feature_sql = f'{feature_sql}("{feature.name}") AS "{feature.name}"' diff --git a/sdk/python/feast/infra/offline_stores/snowflake_source.py b/sdk/python/feast/infra/offline_stores/snowflake_source.py index a25e8fd9034..40e50b3cab9 100644 --- a/sdk/python/feast/infra/offline_stores/snowflake_source.py +++ b/sdk/python/feast/infra/offline_stores/snowflake_source.py @@ -264,18 +264,17 @@ def get_table_column_names_and_types( ] else: raise NotImplementedError( - "Numbers larger than INT64 are not supported" + "NaNs or Numbers larger than INT64 are not supported" ) else: - raise NotImplementedError( - "The following Snowflake Data Type is not supported: DECIMAL -- Convert to DOUBLE" - ) - elif row["type_code"] in [3, 5, 9, 10, 12]: + row["snowflake_type"] = "NUMBERwSCALE" + + elif row["type_code"] in [5, 9, 10, 12]: error = snowflake_unsupported_map[row["type_code"]] raise NotImplementedError( f"The following Snowflake Data Type is not supported: {error}" ) - elif row["type_code"] in [1, 2, 4, 6, 7, 8, 11, 13]: + elif row["type_code"] in [1, 2, 3, 4, 6, 7, 8, 11, 13]: row["snowflake_type"] = snowflake_type_code_map[row["type_code"]] else: raise NotImplementedError( @@ -291,6 +290,7 @@ def get_table_column_names_and_types( 0: "NUMBER", 1: "DOUBLE", 2: "VARCHAR", + 3: "DATE", 4: "TIMESTAMP", 6: "TIMESTAMP_LTZ", 7: "TIMESTAMP_TZ", @@ -300,7 +300,6 @@ def get_table_column_names_and_types( } snowflake_unsupported_map = { - 3: "DATE -- Convert to TIMESTAMP", 5: "VARIANT -- Try converting to VARCHAR", 9: "OBJECT -- Try converting to VARCHAR", 10: "ARRAY -- Try converting to VARCHAR", diff --git a/sdk/python/feast/type_map.py b/sdk/python/feast/type_map.py index 2cb1c4fefb0..3bec457dcbb 100644 --- a/sdk/python/feast/type_map.py +++ b/sdk/python/feast/type_map.py @@ -625,8 +625,10 @@ def snowflake_type_to_feast_value_type(snowflake_type: str) -> ValueType: "VARCHAR": ValueType.STRING, "NUMBER32": ValueType.INT32, "NUMBER64": ValueType.INT64, + "NUMBERwSCALE": ValueType.DOUBLE, "DOUBLE": ValueType.DOUBLE, "BOOLEAN": ValueType.BOOL, + "DATE": ValueType.UNIX_TIMESTAMP, "TIMESTAMP": ValueType.UNIX_TIMESTAMP, "TIMESTAMP_TZ": ValueType.UNIX_TIMESTAMP, "TIMESTAMP_LTZ": ValueType.UNIX_TIMESTAMP, From a14234da699aa18e3f8d12cfd7874d766139be00 Mon Sep 17 00:00:00 2001 From: Achal Shah Date: Mon, 29 Aug 2022 15:17:36 -0700 Subject: [PATCH 07/46] ci: Fix java feature server Dockerfile to use updated builder (#3152) Signed-off-by: Achal Shah Signed-off-by: Achal Shah --- go/embedded/online_features.go | 12 +++--- go/internal/feast/onlineserving/serving.go | 39 ++++++++++--------- .../feast/transformation/transformation.go | 8 ++-- java/infra/docker/feature-server/Dockerfile | 2 +- 4 files changed, 32 insertions(+), 29 deletions(-) diff --git a/go/embedded/online_features.go b/go/embedded/online_features.go index 7fd34d16e40..3c470e4b244 100644 --- a/go/embedded/online_features.go +++ b/go/embedded/online_features.go @@ -378,12 +378,12 @@ func (s *OnlineFeatureService) StopGrpcServer() { } /* - Read Record Batch from memory managed by Python caller. - Python part uses C ABI interface to export this record into C Data Interface, - and then it provides pointers (dataPtr & schemaPtr) to the Go part. - Here we import this data from given pointers and wrap the underlying values - into Go Arrow Interface (array.Record). - See export code here https://github.com/feast-dev/feast/blob/master/sdk/python/feast/embedded_go/online_features_service.py +Read Record Batch from memory managed by Python caller. +Python part uses C ABI interface to export this record into C Data Interface, +and then it provides pointers (dataPtr & schemaPtr) to the Go part. +Here we import this data from given pointers and wrap the underlying values +into Go Arrow Interface (array.Record). +See export code here https://github.com/feast-dev/feast/blob/master/sdk/python/feast/embedded_go/online_features_service.py */ func readArrowRecord(data DataTable) (arrow.Record, error) { return cdata.ImportCRecordBatch( diff --git a/go/internal/feast/onlineserving/serving.go b/go/internal/feast/onlineserving/serving.go index 3c6f5451537..131cc6d1f14 100644 --- a/go/internal/feast/onlineserving/serving.go +++ b/go/internal/feast/onlineserving/serving.go @@ -21,11 +21,11 @@ import ( ) /* - FeatureVector type represent result of retrieving single feature for multiple rows. - It can be imagined as a column in output dataframe / table. - It contains of feature name, list of values (across all rows), - list of statuses and list of timestamp. All these lists have equal length. - And this length is also equal to number of entity rows received in request. +FeatureVector type represent result of retrieving single feature for multiple rows. +It can be imagined as a column in output dataframe / table. +It contains of feature name, list of values (across all rows), +list of statuses and list of timestamp. All these lists have equal length. +And this length is also equal to number of entity rows received in request. */ type FeatureVector struct { Name string @@ -40,11 +40,11 @@ type FeatureViewAndRefs struct { } /* - We group all features from a single request by entities they attached to. - Thus, we will be able to call online retrieval per entity and not per each feature View. - In this struct we collect all features and views that belongs to a group. - We also store here projected entity keys (only ones that needed to retrieve these features) - and indexes to map result of retrieval into output response. +We group all features from a single request by entities they attached to. +Thus, we will be able to call online retrieval per entity and not per each feature View. +In this struct we collect all features and views that belongs to a group. +We also store here projected entity keys (only ones that needed to retrieve these features) +and indexes to map result of retrieval into output response. */ type GroupedFeaturesPerEntitySet struct { // A list of requested feature references of the form featureViewName:featureName that share this entity set @@ -59,11 +59,12 @@ type GroupedFeaturesPerEntitySet struct { } /* - Return - (1) requested feature views and features grouped per View - (2) requested on demand feature views - existed in the registry +Return + (1) requested feature views and features grouped per View + (2) requested on demand feature views + +existed in the registry */ func GetFeatureViewsToUseByService( featureService *model.FeatureService, @@ -124,10 +125,12 @@ func GetFeatureViewsToUseByService( } /* - Return - (1) requested feature views and features grouped per View - (2) requested on demand feature views - existed in the registry +Return + + (1) requested feature views and features grouped per View + (2) requested on demand feature views + +existed in the registry */ func GetFeatureViewsToUseByFeatureRefs( features []string, diff --git a/go/internal/feast/transformation/transformation.go b/go/internal/feast/transformation/transformation.go index 810b4e9bcbd..1cf1dd3311b 100644 --- a/go/internal/feast/transformation/transformation.go +++ b/go/internal/feast/transformation/transformation.go @@ -20,10 +20,10 @@ import ( ) /* - TransformationCallback is a Python callback function's expected signature. - The function should accept name of the on demand feature view and pointers to input & output record batches. - Each record batch is being passed as two pointers: pointer to array (data) and pointer to schema. - Python function is expected to return number of rows added to the output record batch. +TransformationCallback is a Python callback function's expected signature. +The function should accept name of the on demand feature view and pointers to input & output record batches. +Each record batch is being passed as two pointers: pointer to array (data) and pointer to schema. +Python function is expected to return number of rows added to the output record batch. */ type TransformationCallback func(ODFVName string, inputArrPtr, inputSchemaPtr, outArrPtr, outSchemaPtr uintptr, fullFeatureNames bool) int diff --git a/java/infra/docker/feature-server/Dockerfile b/java/infra/docker/feature-server/Dockerfile index a728340d6b4..bf4e172f763 100644 --- a/java/infra/docker/feature-server/Dockerfile +++ b/java/infra/docker/feature-server/Dockerfile @@ -2,7 +2,7 @@ # Build stage 1: Builder # ============================================================ -FROM maven:3.6-jdk-11 as builder +FROM maven:3-jdk-11 as builder WORKDIR /build From 5d3bdd06c4ea5c5bfdb05e79def98fc15302a4d4 Mon Sep 17 00:00:00 2001 From: Achal Shah Date: Tue, 30 Aug 2022 10:38:25 -0700 Subject: [PATCH 08/46] =?UTF-8?q?docs:=20Move=20CONTRIBUTING.md=20to=20dev?= =?UTF-8?q?elopment=20guide=20and=20document=20backward=E2=80=A6=20(#3150)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * docs: Move CONTRIBUTING.md to development guide and document backwards compatibility policy Signed-off-by: Achal Shah * fix Signed-off-by: Achal Shah Signed-off-by: Achal Shah --- CONTRIBUTING.md | 419 +----------------------- docs/SUMMARY.md | 1 + docs/project/compatibility.md | 13 + docs/project/development-guide.md | 454 ++++++++++++++++++++++++--- docs/reference/codebase-structure.md | 5 + 5 files changed, 430 insertions(+), 462 deletions(-) create mode 100644 docs/project/compatibility.md diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index ae259a72fa8..2bc09150028 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,420 +1,3 @@

Development Guide: Main Feast Repository

-> Please see [Development Guide](https://docs.feast.dev/project/development-guide) for project level development instructions. - -

Maintainer's Guide

- -> Please see [Maintainer's Guide](https://docs.feast.dev/project/maintainers) for instructions for maintainers. Normal developers can also use this guide to setup their forks for localized integration tests. - -

Table of Contents

- -- [Overview](#overview) -- [Community](#community) -- [Making a pull request](#making-a-pull-request) - - [Pull request checklist](#pull-request-checklist) - - [Forking the repo](#forking-the-repo) - - [Pre-commit Hooks](#pre-commit-hooks) - - [Signing off commits](#signing-off-commits) - - [Incorporating upstream changes from master](#incorporating-upstream-changes-from-master) -- [Feast Python SDK / CLI](#feast-python-sdk--cli) - - [Environment Setup](#environment-setup) - - [Code Style & Linting](#code-style--linting) - - [Unit Tests](#unit-tests) - - [Integration Tests](#integration-tests) - - [Local integration tests](#local-integration-tests) - - [(Advanced) Full integration tests](#advanced-full-integration-tests) - - [(Advanced) Running specific provider tests or running your test against specific online or offline stores](#advanced-running-specific-provider-tests-or-running-your-test-against-specific-online-or-offline-stores) - - [(Experimental) Run full integration tests against containerized services](#experimental-run-full-integration-tests-against-containerized-services) - - [Contrib integration tests](#contrib-integration-tests) - - [(Contrib) Running tests for Spark offline store](#contrib-running-tests-for-spark-offline-store) - - [(Contrib) Running tests for Trino offline store](#contrib-running-tests-for-trino-offline-store) - - [(Contrib) Running tests for Postgres offline store](#contrib-running-tests-for-postgres-offline-store) - - [(Contrib) Running tests for Postgres online store](#contrib-running-tests-for-postgres-online-store) - - [(Contrib) Running tests for HBase online store](#contrib-running-tests-for-hbase-online-store) -- [(Experimental) Feast UI](#experimental-feast-ui) -- [Feast Java Serving](#feast-java-serving) -- [Developing the Feast Helm charts](#developing-the-feast-helm-charts) - - [Feast Java Feature Server Helm Chart](#feast-java-feature-server-helm-chart) - - [Feast Python / Go Feature Server Helm Chart](#feast-python--go-feature-server-helm-chart) -- [Feast Go Client](#feast-go-client) - - [Environment Setup](#environment-setup-1) - - [Building](#building) - - [Code Style & Linting](#code-style--linting-1) - - [Unit Tests](#unit-tests-1) - - [Testing with Github Actions workflows](#testing-with-github-actions-workflows) -- [Issues](#issues) - -## Overview -This guide is targeted at developers looking to contribute to Feast components in -the main Feast repository: -- [Feast Python SDK / CLI](#feast-python-sdk--cli) -- [Feast Java Serving](#feast-java-serving) -- [Feast Go Client](#feast-go-client) - -Please see [this page](https://docs.feast.dev/reference/codebase-structure) for more details on the structure of the entire codebase. - -## Community -See [Contribution process](https://docs.feast.dev/project/contributing) and [Community](https://docs.feast.dev/community) for details on how to get more involved in the community. - -A quick few highlights: -- [RFCs](https://drive.google.com/drive/u/0/folders/0AAe8j7ZK3sxSUk9PVA) -- [Community Slack](https://slack.feast.dev/) -- [Feast Dev Mailing List](https://groups.google.com/g/feast-dev) -- [Community Calendar](https://calendar.google.com/calendar/u/0?cid=ZTFsZHVhdGM3MDU3YTJucTBwMzNqNW5rajBAZ3JvdXAuY2FsZW5kYXIuZ29vZ2xlLmNvbQ) - - Includes biweekly community calls at 10AM PST - -## Making a pull request -We use the convention that the assignee of a PR is the person with the next action. - -This means that often, the assignee may be empty (if no reviewer has been found yet), the reviewer, or the PR writer if there are comments to be addressed. - -### Pull request checklist -A quick list of things to keep in mind as you're making changes: -- As you make changes - - Make your changes in a [forked repo](#forking-the-repo) (instead of making a branch on the main Feast repo) - - [Sign your commits](#signing-off-commits) as you go (to avoid DCO checks failing) - - [Rebase from master](#incorporating-upstream-changes-from-master) instead of using `git pull` on your PR branch - - Install [pre-commit hooks](#pre-commit-hooks) to ensure all the default linters / formatters are run when you push. -- When you make the PR - - Make a pull request from the forked repo you made - - Ensure you add a GitHub **label** (i.e. a kind tag to the PR (e.g. `kind/bug` or `kind/housekeeping`)) or else checks will fail. - - Ensure you leave a release note for any user facing changes in the PR. There is a field automatically generated in the PR request. You can write `NONE` in that field if there are no user facing changes. - - Please run tests locally before submitting a PR (e.g. for Python, the [local integration tests](#local-integration-tests)) - - Try to keep PRs smaller. This makes them easier to review. - -### Forking the repo -Fork the Feast Github repo and clone your fork locally. Then make changes to a local branch to the fork. - -See [Creating a pull request from a fork](https://docs.github.com/en/github/collaborating-with-pull-requests/proposing-changes-to-your-work-with-pull-requests/creating-a-pull-request-from-a-fork) - -### Pre-commit Hooks -Setup [`pre-commit`](https://pre-commit.com/) to automatically lint and format the codebase on commit: -1. Ensure that you have Python (3.7 and above) with `pip`, installed. -2. Install `pre-commit` with `pip` & install pre-push hooks -```sh -pip install pre-commit -pre-commit install --hook-type pre-commit --hook-type pre-push -``` -3. On push, the pre-commit hook will run. This runs `make format` and `make lint`. - -### Signing off commits -> :warning: Warning: using the default integrations with IDEs like VSCode or IntelliJ will not sign commits. -> When you submit a PR, you'll have to re-sign commits to pass the DCO check. - -Use git signoffs to sign your commits. See -https://docs.github.com/en/github/authenticating-to-github/managing-commit-signature-verification for details - -Then, you can sign off commits with the `-s` flag: -``` -git commit -s -m "My first commit" -``` - -GPG-signing commits with `-S` is optional. - -### Incorporating upstream changes from master -Our preference is the use of `git rebase [master]` instead of `git merge` : `git pull -r`. - -Note that this means if you are midway through working through a PR and rebase, you'll have to force push: -`git push --force-with-lease origin [branch name]` - -## Feast Python SDK / CLI -### Environment Setup -Setting up your development environment for Feast Python SDK / CLI: -1. Ensure that you have Docker installed in your environment. Docker is used to provision service dependencies during testing, and build images for feature servers and other components. - 1. Please note that we use [Docker with BuiltKit](https://docs.docker.com/develop/develop-images/build_enhancements/). -2. Ensure that you have `make`, Python (3.8 and above) with `pip`, installed. -3. _Recommended:_ Create a virtual environment to isolate development dependencies to be installed -```sh -# create & activate a virtual environment -python -m venv venv/ -source venv/bin/activate -``` - -3. Upgrade `pip` if outdated -```sh -pip install --upgrade pip -``` - -4. (Optional): Install Node & Yarn. Then run the following to build Feast UI artifacts for use in `feast ui` -``` -make build-ui -``` - -5. Install development dependencies for Feast Python SDK / CLI -```sh -pip install -e ".[dev]" -``` - -This will allow the installed feast version to automatically reflect changes to your local development version of Feast without needing to reinstall everytime you make code changes. - -### Code Style & Linting -Feast Python SDK / CLI codebase: -- Conforms to [Black code style](https://black.readthedocs.io/en/stable/the_black_code_style.html) -- Has type annotations as enforced by `mypy` -- Has imports sorted by `isort` -- Is lintable by `flake8` - -To ensure your Python code conforms to Feast Python code standards: -- Autoformat your code to conform to the code style: -```sh -make format-python -``` - -- Lint your Python code before submitting it for review: -```sh -make lint-python -``` - -> Setup [pre-commit hooks](#pre-commit-hooks) to automatically format and lint on commit. - -### Unit Tests -Unit tests (`pytest`) for the Feast Python SDK / CLI can run as follows: -```sh -make test-python -``` - -> :warning: Local configuration can interfere with Unit tests and cause them to fail: -> - Ensure [no AWS configuration is present](https://boto3.amazonaws.com/v1/documentation/api/latest/guide/configuration.html) -> and [no AWS credentials can be accessed](https://boto3.amazonaws.com/v1/documentation/api/latest/guide/credentials.html#configuring-credentials) by `boto3` -> - Ensure Feast Python SDK / CLI is not configured with configuration overrides (ie `~/.feast/config` should be empty). - -### Integration Tests -There are two sets of tests you can run: -1. Local integration tests (for faster development, tests file offline store & key online stores) -2. Full integration tests (requires cloud environment setups) - -#### Local integration tests -For this approach of running tests, you'll need to have docker set up locally: [Get Docker](https://docs.docker.com/get-docker/) - -It leverages a file based offline store to test against emulated versions of Datastore, DynamoDB, and Redis, using ephemeral containers. - -These tests create new temporary tables / datasets locally only, and they are cleaned up. when the containers are torn down. - -```sh -make test-python-integration-local -``` - -#### (Advanced) Full integration tests -To test across clouds, on top of setting up Redis, you also need GCP / AWS / Snowflake setup. - -> Note: you can manually control what tests are run today by inspecting -> [RepoConfiguration](https://github.com/feast-dev/feast/blob/master/sdk/python/tests/integration/feature_repos/repo_configuration.py) -> and commenting out tests that are added to `DEFAULT_FULL_REPO_CONFIGS` - -**GCP** -1. You can get free credits [here](https://cloud.google.com/free/docs/free-cloud-features#free-trial). -2. You will need to setup a service account, enable the BigQuery API, and create a staging location for a bucket. - * Setup your service account and project using steps 1-5 [here](https://codelabs.developers.google.com/codelabs/cloud-bigquery-python#0). - * Remember to save your `PROJECT_ID` and your `key.json`. These will be your secrets that you will need to configure in Github actions. Namely, `secrets.GCP_PROJECT_ID` and `secrets.GCP_SA_KEY`. The `GCP_SA_KEY` value is the contents of your `key.json` file. - * Follow these [instructions](https://cloud.google.com/storage/docs/creating-buckets) in your project to create a bucket for running GCP tests and remember to save the bucket name. - * Make sure to add the service account email that you created in the previous step to the users that can access your bucket. Then, make sure to give the account the correct access roles, namely `objectCreator`, `objectViewer`, `objectAdmin`, and `admin`, so that your tests can use the bucket. -3. Install the [Cloud SDK](https://cloud.google.com/sdk/docs/install). -4. Login to gcloud if you haven't already: - ``` - gcloud auth login - gcloud auth application-default login - ``` - - When you run `gcloud auth application-default login`, you should see some output of the form: - ``` - Credentials saved to file: [$HOME/.config/gcloud/application_default_credentials.json] - ``` - - You should run `export GOOGLE_APPLICATION_CREDENTIALS="$HOME/.config/gcloud/application_default_credentials.json”` to add the application credentials to your .zshrc or .bashrc. -5. Run `export GCLOUD_PROJECT=[your project id from step 2]` to your .zshrc or .bashrc. -6. Running `gcloud config list` should give you something like this: - ```sh - $ gcloud config list - [core] - account = [your email] - disable_usage_reporting = True - project = [your project id] - - Your active configuration is: [default] - ``` -7. Export GCP specific environment variables in your workflow. Namely, - ```sh - export GCS_REGION='[your gcs region e.g US]' - export GCS_STAGING_LOCATION='[your gcs staging location]' - ``` - **NOTE**: Your `GCS_STAGING_LOCATION` should be in the form `gs://` where the bucket name is from step 2. - -8. Once authenticated, you should be able to run the integration tests for BigQuery without any failures. - -**AWS** -1. Setup AWS by creating an account, database, and cluster. You will need to enable Redshift and Dynamo. - * You can get free credits [here](https://aws.amazon.com/free/?all-free-tier.sort-by=item.additionalFields.SortRank&al[…]f.Free%20Tier%20Types=*all&awsf.Free%20Tier%20Categories=*all). -2. To run the AWS Redshift and Dynamo integration tests you will have to export your own AWS credentials. Namely, - -```sh -export AWS_REGION='[your aws region]' -export AWS_CLUSTER_ID='[your aws cluster id]' -export AWS_USER='[your aws user]' -export AWS_DB='[your aws database]' -export AWS_STAGING_LOCATION='[your s3 staging location uri]' -export AWS_IAM_ROLE='[redshift and s3 access role]' -export AWS_LAMBDA_ROLE='[your aws lambda execution role]' -export AWS_REGISTRY_PATH='[your aws registry path]' -``` - -**Snowflake** -1. See https://signup.snowflake.com/ to setup a trial. -2. Setup your account and if you are not an `ACCOUNTADMIN` (if you created your own account, you should be), give yourself the `SYSADMIN` role. - ```sql - grant role accountadmin, sysadmin to user user2; - ``` - * Also remember to save your [account name](https://docs.snowflake.com/en/user-guide/admin-account-identifier.html#:~:text=organization_name%20is%20the%20name%20of,your%20account%20within%20your%20organization), username, and role. - * Your account name can be found under -3. Create Dashboard and add a Tile. -4. Create a warehouse and database named `FEAST` with the schemas `OFFLINE` and `ONLINE`. - ```sql - create or replace warehouse feast_tests_wh with - warehouse_size='MEDIUM' --set your warehouse size to whatever your budget allows-- - auto_suspend = 180 - auto_resume = true - initially_suspended=true; - - create or replace database FEAST; - use database FEAST; - create schema OFFLINE; - create schema ONLINE; - ``` -5. You will need to create a data unloading location(either on S3, GCP, or Azure). Detailed instructions [here](https://docs.snowflake.com/en/user-guide/data-unload-overview.html). You will need to save the storage export location and the storage export name. You will need to create a [storage integration ](https://docs.snowflake.com/en/sql-reference/sql/create-storage-integration.html) in your warehouse to make this work. Name this storage integration `FEAST_S3`. -6. Then to run successfully, you'll need some environment variables setup: - ```sh - export SNOWFLAKE_CI_DEPLOYMENT='[your snowflake account name]' - export SNOWFLAKE_CI_USER='[your snowflake username]' - export SNOWFLAKE_CI_PASSWORD='[your snowflake pw]' - export SNOWFLAKE_CI_ROLE='[your CI role e.g. SYSADMIN]' - export SNOWFLAKE_CI_WAREHOUSE='[your warehouse]' - export BLOB_EXPORT_STORAGE_NAME='[your data unloading storage name]' - export BLOB_EXPORT_URI='[your data unloading blob uri]` - ``` -7. Once everything is setup, running snowflake integration tests should pass without failures. - -Note that for Snowflake / GCP / AWS, running `make test-python-integration` will create new temporary tables / datasets in your cloud storage tables. - -#### (Advanced) Running specific provider tests or running your test against specific online or offline stores - -1. If you don't need to have your test run against all of the providers(`gcp`, `aws`, and `snowflake`) or don't need to run against all of the online stores, you can tag your test with specific providers or stores that you need(`@pytest.mark.universal_online_stores` or `@pytest.mark.universal_online_stores` with the `only` parameter). The `only` parameter selects specific offline providers and online stores that your test will test against. Example: - -```python -# Only parametrizes this test with the sqlite online store -@pytest.mark.universal_online_stores(only=["sqlite"]) -def test_feature_get_online_features_types_match(): -``` - -2. You can also filter tests to run by using pytest's cli filtering. Instead of using the make commands to test Feast, you can filter tests by name with the `-k` parameter. The parametrized integration tests are all uniquely identified by their provider and online store so the `-k` option can select only the tests that you need to run. For example, to run only Redshift related tests, you can use the following command: - -```sh -python -m pytest -n 8 --integration -k Redshift sdk/python/tests -``` - -#### (Experimental) Run full integration tests against containerized services -Test across clouds requires existing accounts on GCP / AWS / Snowflake, and may incur costs when using these services. - -For this approach of running tests, you'll need to have docker set up locally: [Get Docker](https://docs.docker.com/get-docker/) - -It's possible to run some integration tests against emulated local versions of these services, using ephemeral containers. -These tests create new temporary tables / datasets locally only, and they are cleaned up. when the containers are torn down. - -The services with containerized replacements currently implemented are: -- Datastore -- DynamoDB -- Redis -- Trino -- HBase -- Postgres -- Cassandra - -You can run `make test-python-integration-container` to run tests against the containerized versions of dependencies. - -### Contrib integration tests -#### (Contrib) Running tests for Spark offline store -You can run `make test-python-universal-spark` to run all tests against the Spark offline store. (Note: you'll have to run `pip install -e ".[dev]"` first). - -Not all tests are passing yet - -#### (Contrib) Running tests for Trino offline store -You can run `make test-python-universal-trino` to run all tests against the Trino offline store. (Note: you'll have to run `pip install -e ".[dev]"` first) - -#### (Contrib) Running tests for Postgres offline store -You can run `test-python-universal-postgres-offline` to run all tests against the Postgres offline store. (Note: you'll have to run `pip install -e ".[dev]"` first) - -#### (Contrib) Running tests for Postgres online store -You can run `test-python-universal-postgres-online` to run all tests against the Postgres offline store. (Note: you'll have to run `pip install -e ".[dev]"` first) - -#### (Contrib) Running tests for HBase online store -TODO - -## (Experimental) Feast UI -See [Feast contributing guide](ui/CONTRIBUTING.md) - -## Feast Java Serving -See [Java contributing guide](java/CONTRIBUTING.md) - -See also development instructions related to the helm chart below at [Developing the Feast Helm charts](#developing-the-feast-helm-charts) - -## Developing the Feast Helm charts -There are 3 helm charts: -- Feast Java feature server -- Feast Python / Go feature server -- (deprecated) Feast Python feature server - -Generally, you can override the images in the helm charts with locally built Docker images, and install the local helm -chart. - -All README's for helm charts are generated using [helm-docs](https://github.com/norwoodj/helm-docs). You can install it -(e.g. with `brew install norwoodj/tap/helm-docs`) and then run `make build-helm-docs`. - -### Feast Java Feature Server Helm Chart -See the Java demo example (it has development instructions too using minikube) [here](examples/java-demo/README.md) - -It will: -- run `make build-java-docker-dev` to build local Java feature server binaries -- configure the included `application-override.yaml` to override the image tag to use the locally built dev images. -- install the local chart with `helm install feast-release ../../../infra/charts/feast --values application-override.yaml` - -### Feast Python / Go Feature Server Helm Chart -See the Python demo example (it has development instructions too using minikube) [here](examples/python-helm-demo/README.md) - -It will: -- run `make build-feature-server-dev` to build a local python feature server binary -- install the local chart with `helm install feast-release ../../../infra/charts/feast-feature-server --set image.tag=dev --set feature_store_yaml_base64=$(base64 feature_store.yaml)` - -## Feast Go Client -### Environment Setup -Setting up your development environment for Feast Go SDK: - -- Install Golang, [`protoc` with the Golang & grpc plugins](https://developers.google.com/protocol-buffers/docs/gotutorial#compiling-your-protocol-buffers) - -### Building -Build the Feast Go Client with the `go` toolchain: -```sh -make compile-go-lib -``` - -### Code Style & Linting -Feast Go Client codebase: -- Conforms to the code style enforced by `go fmt`. -- Is lintable by `go vet`. - -Autoformat your Go code to satisfy the Code Style standard: -```sh -go fmt -``` - -Lint your Go code: -```sh -go vet -``` - -> Setup [pre-commit hooks](#pre-commit-hooks) to automatically format and lint on commit. - -### Unit Tests -Unit tests for the Feast Go Client can be run as follows: -```sh -make test-go -``` - -### Testing with Github Actions workflows - -Please refer to the maintainers [doc](./docs/project/maintainers.md) if you would like to locally test out the github actions workflow changes. This document will help you setup your fork to test the ci integration tests and other workflows without needing to make a pull request against feast-dev master. +> Please see [Development Guide](https://docs.feast.dev/project/development-guide) for project level development instructions, including instructions for Maintainers. diff --git a/docs/SUMMARY.md b/docs/SUMMARY.md index 8ee48677302..18d1aeb2806 100644 --- a/docs/SUMMARY.md +++ b/docs/SUMMARY.md @@ -120,6 +120,7 @@ * [Contribution process](project/contributing.md) * [Development guide](project/development-guide.md) +* [Backwards Compatibility Policy](project/compatibility.md) * [Maintainer Docs](project/maintainers.md) * [Versioning policy](project/versioning-policy.md) * [Release process](project/release-process.md) diff --git a/docs/project/compatibility.md b/docs/project/compatibility.md new file mode 100644 index 00000000000..9db4d3a14a4 --- /dev/null +++ b/docs/project/compatibility.md @@ -0,0 +1,13 @@ +# API Compatibility in Feast + +Feast follows [semantic versions](./versioning-policy.md). Being pre-1.0, Feast is still considered in active initial development as per the [SemVer spec](https://semver.org/#faq). + +That being said, Feast takes backwards compatibility seriously, to ensure that introducing new functionality across minor versions does not break current users. + +When possible, API changes should always be made in a backwards compatible way. +If this is not possible, the maintainers introduce new APIs alongside existing, now-deprecated APIs, with the intention of supporting the existing APIs for at least 3 minor versions, before deprecating and removing them. +In some cases, the deprecated APIs may be supported for longer than 3 minor versions, if necessary to give users a longer time for migrations. + +When deprecating existing APIs, deprecation warnings should be introduced early, with the expected version at which the deprecated API would be removed. + +At this point, core functionality in Feast is considered "stable" and ready for usage. However, there are still some components that are considered "Alpha". Please check the [roadmap](../roadmap.md) for a full list of all capabilities and their status. diff --git a/docs/project/development-guide.md b/docs/project/development-guide.md index 5aae0628f67..d06ace327cb 100644 --- a/docs/project/development-guide.md +++ b/docs/project/development-guide.md @@ -1,72 +1,438 @@ -# Development guide +# Development Guide: Main Feast Repository -## Overview +## Table of Contents -This guide is targeted at developers looking to contribute to Feast: +- [Overview](#overview) +- [Community](#community) +- [Making a pull request](#making-a-pull-request) + - [Pull request checklist](#pull-request-checklist) + - [Good Practices](#good-practices-to-keep-in-mind) + - [Forking the repo](#forking-the-repo) + - [Pre-commit Hooks](#pre-commit-hooks) + - [Signing off commits](#signing-off-commits) + - [Incorporating upstream changes from master](#incorporating-upstream-changes-from-master) +- [Feast Python SDK / CLI](#feast-python-sdk--cli) + - [Environment Setup](#environment-setup) + - [Code Style & Linting](#code-style--linting) + - [Unit Tests](#unit-tests) + - [Integration Tests](#integration-tests) + - [Local integration tests](#local-integration-tests) + - [(Advanced) Full integration tests](#advanced-full-integration-tests) + - [(Advanced) Running specific provider tests or running your test against specific online or offline stores](#advanced-running-specific-provider-tests-or-running-your-test-against-specific-online-or-offline-stores) + - [(Experimental) Run full integration tests against containerized services](#experimental-run-full-integration-tests-against-containerized-services) + - [Contrib integration tests](#contrib-integration-tests) + - [(Contrib) Running tests for Spark offline store](#contrib-running-tests-for-spark-offline-store) + - [(Contrib) Running tests for Trino offline store](#contrib-running-tests-for-trino-offline-store) + - [(Contrib) Running tests for Postgres offline store](#contrib-running-tests-for-postgres-offline-store) + - [(Contrib) Running tests for Postgres online store](#contrib-running-tests-for-postgres-online-store) + - [(Contrib) Running tests for HBase online store](#contrib-running-tests-for-hbase-online-store) +- [(Experimental) Feast UI](#experimental-feast-ui) +- [Feast Java Serving](#feast-java-serving) +- [Developing the Feast Helm charts](#developing-the-feast-helm-charts) + - [Feast Java Feature Server Helm Chart](#feast-java-feature-server-helm-chart) + - [Feast Python / Go Feature Server Helm Chart](#feast-python--go-feature-server-helm-chart) +- [Feast Go Client](#feast-go-client) + - [Environment Setup](#go-environment-setup) + - [Building](#building-go) + - [Code Style & Linting](#go-code-style--linting) + - [Unit Tests](#go-unit-tests) + - [Testing with GitHub Actions workflows](#testing-with-github-actions-workflows) +- [Data Storage Formats](#feast-data-storage-format) +## Overview +This guide is targeted at developers looking to contribute to Feast components in +the main Feast repository: +- [Feast Python SDK / CLI](#feast-python-sdk--cli) +- [Feast Java Serving](#feast-java-serving) +- [Feast Go Client](#feast-go-client) -* [Project Structure](development-guide.md#repository-structure) -* [Making a Pull Request](development-guide.md#making-a-pull-request) -* [Feast Data Storage Format](development-guide.md#feast-data-storage-format) -* [Feast Protobuf API](development-guide.md#feast-protobuf-api) -* [Maintainer Guide](./maintainers.md) +Please see [this page](../reference/codebase-structure.md) for more details on the structure of the entire codebase. -> Learn How the Feast [Contributing Process](contributing.md) works. +## Compatibility -## Making a Pull Request +The compatibility policy for Feast can be found [here](compatibility.md), and should be followed for all changes proposed, by maintainers or contributors. -{% hint style="info" %} -See also the CONTRIBUTING.md in the corresponding GitHub repository \(e.g. [main repo doc](https://github.com/feast-dev/feast/blob/master/CONTRIBUTING.md)\) -{% endhint %} +## Community +See [Contribution process](./contributing.md) and [Community](../community.md) for details on how to get more involved in the community. -### Incorporating upstream changes from master +A quick few highlights: +- [RFCs](https://drive.google.com/drive/u/0/folders/0AAe8j7ZK3sxSUk9PVA) +- [Community Slack](https://slack.feast.dev/) +- [Feast Dev Mailing List](https://groups.google.com/g/feast-dev) +- [Community Calendar](https://calendar.google.com/calendar/u/0?cid=ZTFsZHVhdGM3MDU3YTJucTBwMzNqNW5rajBAZ3JvdXAuY2FsZW5kYXIuZ29vZ2xlLmNvbQ) + - Includes biweekly community calls at 10AM PST -Our preference is the use of `git rebase` instead of `git merge` : `git pull -r` +## Making a pull request +We use the convention that the assignee of a PR is the person with the next action. -### Signing commits +If the assignee is empty it means that no reviewer has been found yet. +If a reviewer has been found, they should also be the assigned the PR. +Finally, if there are comments to be addressed, the PR author should be the one assigned the PR. -Commits have to be signed before they are allowed to be merged into the Feast codebase: - -```bash -# Include -s flag to signoff -git commit -s -m "My first commit" -``` +### Pull request checklist +A quick list of things to keep in mind as you're making changes: +- As you make changes + - Make your changes in a [forked repo](#forking-the-repo) (instead of making a branch on the main Feast repo) + - [Sign your commits](#signing-off-commits) as you go (to avoid DCO checks failing) + - [Rebase from master](#incorporating-upstream-changes-from-master) instead of using `git pull` on your PR branch + - Install [pre-commit hooks](#pre-commit-hooks) to ensure all the default linters / formatters are run when you push. +- When you make the PR + - Make a pull request from the forked repo you made + - Ensure you add a GitHub **label** (i.e. a kind tag to the PR (e.g. `kind/bug` or `kind/housekeeping`)) or else checks will fail. + - Ensure you leave a release note for any user facing changes in the PR. There is a field automatically generated in the PR request. You can write `NONE` in that field if there are no user facing changes. + - Please run tests locally before submitting a PR (e.g. for Python, the [local integration tests](#local-integration-tests)) + - Try to keep PRs smaller. This makes them easier to review. ### Good practices to keep in mind - * Fill in the description based on the default template configured when you first open the PR * What this PR does/why we need it * Which issue\(s\) this PR fixes * Does this PR introduce a user-facing change -* Include `kind` label when opening the PR * Add `WIP:` to PR name if more work needs to be done prior to review -* Avoid `force-pushing` as it makes reviewing difficult -**Managing CI-test failures** -* GitHub runner tests - * Click `checks` tab to analyse failed tests -* Prow tests - * Visit [Prow status page ](http://prow.feast.ai/)to analyse failed tests +### Forking the repo +Fork the Feast Github repo and clone your fork locally. Then make changes to a local branch to the fork. -## Feast Data Storage Format +See [Creating a pull request from a fork](https://docs.github.com/en/github/collaborating-with-pull-requests/proposing-changes-to-your-work-with-pull-requests/creating-a-pull-request-from-a-fork) -Feast data storage contracts are documented in the following locations: +### Pre-commit Hooks +Setup [`pre-commit`](https://pre-commit.com/) to automatically lint and format the codebase on commit: +1. Ensure that you have Python (3.7 and above) with `pip`, installed. +2. Install `pre-commit` with `pip` & install pre-push hooks +```sh +pip install pre-commit +pre-commit install --hook-type pre-commit --hook-type pre-push +``` +3. On push, the pre-commit hook will run. This runs `make format` and `make lint`. -* [Feast Offline Storage Format](https://github.com/feast-dev/feast/blob/master/docs/specs/offline_store_format.md): Used by BigQuery, Snowflake \(Future\), Redshift \(Future\). -* [Feast Online Storage Format](https://github.com/feast-dev/feast/blob/master/docs/specs/online_store_format.md): Used by Redis, Google Datastore. +### Signing off commits +> :warning: Warning: using the default integrations with IDEs like VSCode or IntelliJ will not sign commits. +> When you submit a PR, you'll have to re-sign commits to pass the DCO check. + +Use git signoffs to sign your commits. See +https://docs.github.com/en/github/authenticating-to-github/managing-commit-signature-verification for details + +Then, you can sign off commits with the `-s` flag: +``` +git commit -s -m "My first commit" +``` + +GPG-signing commits with `-S` is optional. + +### Incorporating upstream changes from master +Our preference is the use of `git rebase [master]` instead of `git merge` : `git pull -r`. + +Note that this means if you are midway through working through a PR and rebase, you'll have to force push: +`git push --force-with-lease origin [branch name]` + +## Feast Python SDK / CLI +### Environment Setup +Setting up your development environment for Feast Python SDK / CLI: +1. Ensure that you have Docker installed in your environment. Docker is used to provision service dependencies during testing, and build images for feature servers and other components. + 1. Please note that we use [Docker with BuiltKit](https://docs.docker.com/develop/develop-images/build_enhancements/). +2. Ensure that you have `make`, Python (3.8 and above) with `pip`, installed. +3. _Recommended:_ Create a virtual environment to isolate development dependencies to be installed + ```sh + # create & activate a virtual environment + python -m venv venv/ + source venv/bin/activate + ``` +4. Upgrade `pip` if outdated + ```sh + pip install --upgrade pip + ``` + +5. (Optional): Install Node & Yarn. Then run the following to build Feast UI artifacts for use in `feast ui` +``` +make build-ui +``` + +6. Install development dependencies for Feast Python SDK / CLI +```sh +pip install -e ".[dev]" +``` + +This will allow the installed feast version to automatically reflect changes to your local development version of Feast without needing to reinstall everytime you make code changes. + +### Code Style & Linting +Feast Python SDK / CLI codebase: +- Conforms to [Black code style](https://black.readthedocs.io/en/stable/the_black_code_style.html) +- Has type annotations as enforced by `mypy` +- Has imports sorted by `isort` +- Is lintable by `flake8` + +To ensure your Python code conforms to Feast Python code standards: +- Autoformat your code to conform to the code style: +```sh +make format-python +``` + +- Lint your Python code before submitting it for review: +```sh +make lint-python +``` + +> Setup [pre-commit hooks](#pre-commit-hooks) to automatically format and lint on commit. + +### Unit Tests +Unit tests (`pytest`) for the Feast Python SDK / CLI can run as follows: +```sh +make test-python +``` + +> :warning: Local configuration can interfere with Unit tests and cause them to fail: +> - Ensure [no AWS configuration is present](https://boto3.amazonaws.com/v1/documentation/api/latest/guide/configuration.html) + > and [no AWS credentials can be accessed](https://boto3.amazonaws.com/v1/documentation/api/latest/guide/credentials.html#configuring-credentials) by `boto3` +> - Ensure Feast Python SDK / CLI is not configured with configuration overrides (ie `~/.feast/config` should be empty). + +### Integration Tests +There are two sets of tests you can run: +1. Local integration tests (for faster development, tests file offline store & key online stores) +2. Full integration tests (requires cloud environment setups) + +#### Local integration tests +For this approach of running tests, you'll need to have docker set up locally: [Get Docker](https://docs.docker.com/get-docker/) + +It leverages a file based offline store to test against emulated versions of Datastore, DynamoDB, and Redis, using ephemeral containers. + +These tests create new temporary tables / datasets locally only, and they are cleaned up. when the containers are torn down. + +```sh +make test-python-integration-local +``` + +#### (Advanced) Full integration tests +To test across clouds, on top of setting up Redis, you also need GCP / AWS / Snowflake setup. + +> Note: you can manually control what tests are run today by inspecting +> [RepoConfiguration](https://github.com/feast-dev/feast/blob/master/sdk/python/tests/integration/feature_repos/repo_configuration.py) +> and commenting out tests that are added to `DEFAULT_FULL_REPO_CONFIGS` + +**GCP** +1. You can get free credits [here](https://cloud.google.com/free/docs/free-cloud-features#free-trial). +2. You will need to setup a service account, enable the BigQuery API, and create a staging location for a bucket. + +* Setup your service account and project using steps 1-5 [here](https://codelabs.developers.google.com/codelabs/cloud-bigquery-python#0). + * Remember to save your `PROJECT_ID` and your `key.json`. These will be your secrets that you will need to configure in Github actions. Namely, `secrets.GCP_PROJECT_ID` and `secrets.GCP_SA_KEY`. The `GCP_SA_KEY` value is the contents of your `key.json` file. +* Follow these [instructions](https://cloud.google.com/storage/docs/creating-buckets) in your project to create a bucket for running GCP tests and remember to save the bucket name. + * Make sure to add the service account email that you created in the previous step to the users that can access your bucket. Then, make sure to give the account the correct access roles, namely `objectCreator`, `objectViewer`, `objectAdmin`, and `admin`, so that your tests can use the bucket. + +3. Install the [Cloud SDK](https://cloud.google.com/sdk/docs/install). +4. Login to gcloud if you haven't already: + ``` + gcloud auth login + gcloud auth application-default login + ``` +- When you run `gcloud auth application-default login`, you should see some output of the form: + ``` + Credentials saved to file: [$HOME/.config/gcloud/application_default_credentials.json] + ``` +- You should run `export GOOGLE_APPLICATION_CREDENTIALS="$HOME/.config/gcloud/application_default_credentials.json”` to add the application credentials to your .zshrc or .bashrc. +5. Run `export GCLOUD_PROJECT=[your project id from step 2]` to your .zshrc or .bashrc. +6. Running `gcloud config list` should give you something like this: + ```sh + $ gcloud config list + [core] + account = [your email] + disable_usage_reporting = True + project = [your project id] + + Your active configuration is: [default] + ``` +7. Export GCP specific environment variables in your workflow. Namely, + ```sh + export GCS_REGION='[your gcs region e.g US]' + export GCS_STAGING_LOCATION='[your gcs staging location]' + ``` +**NOTE**: Your `GCS_STAGING_LOCATION` should be in the form `gs://` where the bucket name is from step 2. + +8. Once authenticated, you should be able to run the integration tests for BigQuery without any failures. -## Feast Protobuf API +**AWS** +1. Setup AWS by creating an account, database, and cluster. You will need to enable Redshift and Dynamo. +* You can get free credits [here](https://aws.amazon.com/free/?all-free-tier.sort-by=item.additionalFields.SortRank&al[…]f.Free%20Tier%20Types=*all&awsf.Free%20Tier%20Categories=*all). +2. To run the AWS Redshift and Dynamo integration tests you will have to export your own AWS credentials. Namely, -Feast Protobuf API defines the common API used by Feast's Components: +```sh +export AWS_REGION='[your aws region]' +export AWS_CLUSTER_ID='[your aws cluster id]' +export AWS_USER='[your aws user]' +export AWS_DB='[your aws database]' +export AWS_STAGING_LOCATION='[your s3 staging location uri]' +export AWS_IAM_ROLE='[redshift and s3 access role]' +export AWS_LAMBDA_ROLE='[your aws lambda execution role]' +export AWS_REGISTRY_PATH='[your aws registry path]' +``` + +**Snowflake** +1. See https://signup.snowflake.com/ to setup a trial. +2. Setup your account and if you are not an `ACCOUNTADMIN` (if you created your own account, you should be), give yourself the `SYSADMIN` role. + ```sql + grant role accountadmin, sysadmin to user user2; + ``` +* Also remember to save your [account name](https://docs.snowflake.com/en/user-guide/admin-account-identifier.html#:~:text=organization_name%20is%20the%20name%20of,your%20account%20within%20your%20organization), username, and role. +* Your account name can be found under +3. Create Dashboard and add a Tile. +4. Create a warehouse and database named `FEAST` with the schemas `OFFLINE` and `ONLINE`. + ```sql + create or replace warehouse feast_tests_wh with + warehouse_size='MEDIUM' --set your warehouse size to whatever your budget allows-- + auto_suspend = 180 + auto_resume = true + initially_suspended=true; + + create or replace database FEAST; + use database FEAST; + create schema OFFLINE; + create schema ONLINE; + ``` +5. You will need to create a data unloading location(either on S3, GCP, or Azure). Detailed instructions [here](https://docs.snowflake.com/en/user-guide/data-unload-overview.html). You will need to save the storage export location and the storage export name. You will need to create a [storage integration ](https://docs.snowflake.com/en/sql-reference/sql/create-storage-integration.html) in your warehouse to make this work. Name this storage integration `FEAST_S3`. +6. Then to run successfully, you'll need some environment variables setup: + ```sh + export SNOWFLAKE_CI_DEPLOYMENT='[your snowflake account name]' + export SNOWFLAKE_CI_USER='[your snowflake username]' + export SNOWFLAKE_CI_PASSWORD='[your snowflake pw]' + export SNOWFLAKE_CI_ROLE='[your CI role e.g. SYSADMIN]' + export SNOWFLAKE_CI_WAREHOUSE='[your warehouse]' + export BLOB_EXPORT_STORAGE_NAME='[your data unloading storage name]' + export BLOB_EXPORT_URI='[your data unloading blob uri]` + ``` +7. Once everything is setup, running snowflake integration tests should pass without failures. + +Note that for Snowflake / GCP / AWS, running `make test-python-integration` will create new temporary tables / datasets in your cloud storage tables. + +#### (Advanced) Running specific provider tests or running your test against specific online or offline stores + +1. If you don't need to have your test run against all of the providers(`gcp`, `aws`, and `snowflake`) or don't need to run against all of the online stores, you can tag your test with specific providers or stores that you need(`@pytest.mark.universal_online_stores` or `@pytest.mark.universal_online_stores` with the `only` parameter). The `only` parameter selects specific offline providers and online stores that your test will test against. Example: + +```python +# Only parametrizes this test with the sqlite online store +@pytest.mark.universal_online_stores(only=["sqlite"]) +def test_feature_get_online_features_types_match(): +``` + +2. You can also filter tests to run by using pytest's cli filtering. Instead of using the make commands to test Feast, you can filter tests by name with the `-k` parameter. The parametrized integration tests are all uniquely identified by their provider and online store so the `-k` option can select only the tests that you need to run. For example, to run only Redshift related tests, you can use the following command: + +```sh +python -m pytest -n 8 --integration -k Redshift sdk/python/tests +``` + +#### (Experimental) Run full integration tests against containerized services +Test across clouds requires existing accounts on GCP / AWS / Snowflake, and may incur costs when using these services. + +For this approach of running tests, you'll need to have docker set up locally: [Get Docker](https://docs.docker.com/get-docker/) + +It's possible to run some integration tests against emulated local versions of these services, using ephemeral containers. +These tests create new temporary tables / datasets locally only, and they are cleaned up. when the containers are torn down. + +The services with containerized replacements currently implemented are: +- Datastore +- DynamoDB +- Redis +- Trino +- HBase +- Postgres +- Cassandra + +You can run `make test-python-integration-container` to run tests against the containerized versions of dependencies. + +### Contrib integration tests +#### (Contrib) Running tests for Spark offline store +You can run `make test-python-universal-spark` to run all tests against the Spark offline store. (Note: you'll have to run `pip install -e ".[dev]"` first). + +Not all tests are passing yet + +#### (Contrib) Running tests for Trino offline store +You can run `make test-python-universal-trino` to run all tests against the Trino offline store. (Note: you'll have to run `pip install -e ".[dev]"` first) + +#### (Contrib) Running tests for Postgres offline store +You can run `test-python-universal-postgres-offline` to run all tests against the Postgres offline store. (Note: you'll have to run `pip install -e ".[dev]"` first) + +#### (Contrib) Running tests for Postgres online store +You can run `test-python-universal-postgres-online` to run all tests against the Postgres offline store. (Note: you'll have to run `pip install -e ".[dev]"` first) + +#### (Contrib) Running tests for HBase online store +TODO + +## (Experimental) Feast UI +See [Feast contributing guide](ui/CONTRIBUTING.md) + +## Feast Java Serving +See [Java contributing guide](java/CONTRIBUTING.md) + +See also development instructions related to the helm chart below at [Developing the Feast Helm charts](#developing-the-feast-helm-charts) -* Feast Protobuf API specifications are written in [proto3](https://developers.google.com/protocol-buffers/docs/proto3) in the Main Feast Repository. -* Changes to the API should be proposed via a [GitHub Issue](https://github.com/feast-dev/feast/issues/new/choose) for discussion first. +## Developing the Feast Helm charts +There are 3 helm charts: +- Feast Java feature server +- Feast Python / Go feature server +- (deprecated) Feast Python feature server -### Generating Language Bindings +Generally, you can override the images in the helm charts with locally built Docker images, and install the local helm +chart. -The language specific bindings have to be regenerated when changes are made to the Feast Protobuf API: +All README's for helm charts are generated using [helm-docs](https://github.com/norwoodj/helm-docs). You can install it +(e.g. with `brew install norwoodj/tap/helm-docs`) and then run `make build-helm-docs`. + +### Feast Java Feature Server Helm Chart +See the Java demo example (it has development instructions too using minikube) [here](examples/java-demo/README.md) + +It will: +- run `make build-java-docker-dev` to build local Java feature server binaries +- configure the included `application-override.yaml` to override the image tag to use the locally built dev images. +- install the local chart with `helm install feast-release ../../../infra/charts/feast --values application-override.yaml` + +### Feast Python / Go Feature Server Helm Chart +See the Python demo example (it has development instructions too using minikube) [here](examples/python-helm-demo/README.md) + +It will: +- run `make build-feature-server-dev` to build a local python feature server binary +- install the local chart with `helm install feast-release ../../../infra/charts/feast-feature-server --set image.tag=dev --set feature_store_yaml_base64=$(base64 feature_store.yaml)` + +## Feast Go Client +### Go Environment Setup +Setting up your development environment for Feast Go SDK: + +- Install Golang, [`protoc` with the Golang & grpc plugins](https://developers.google.com/protocol-buffers/docs/gotutorial#compiling-your-protocol-buffers) + +### Building Go +Build the Feast Go Client with the `go` toolchain: +```sh +make compile-go-lib +``` + +### Go Code Style & Linting +Feast Go Client codebase: +- Conforms to the code style enforced by `go fmt`. +- Is lintable by `go vet`. + +Autoformat your Go code to satisfy the Code Style standard: +```sh +go fmt +``` + +Lint your Go code: +```sh +go vet +``` + +> Setup [pre-commit hooks](#pre-commit-hooks) to automatically format and lint on commit. + +### Go Unit Tests +Unit tests for the Feast Go Client can be run as follows: +```sh +make test-go +``` + +### Testing with Github Actions workflows + +Please refer to the maintainers [doc](./docs/project/maintainers.md) if you would like to locally test out the github actions workflow changes. +This document will help you setup your fork to test the ci integration tests and other workflows without needing to make a pull request against feast-dev master. + +## Feast Data Storage Format + +Feast data storage contracts are documented in the following locations: + +* [Feast Offline Storage Format](https://github.com/feast-dev/feast/blob/master/docs/specs/offline_store_format.md): Used by BigQuery, Snowflake \(Future\), Redshift \(Future\). +* [Feast Online Storage Format](https://github.com/feast-dev/feast/blob/master/docs/specs/online_store_format.md): Used by Redis, Google Datastore. -| Repository | Language | Regenerating Language Bindings | -| :--- | :--- | :--- | -| [Main Feast Repository](https://github.com/feast-dev/feast) | Python | Run `make compile-protos-python` to generate bindings | -| [Main Feast Repository](https://github.com/feast-dev/feast) | Golang | Run `make compile-protos-go` to generate bindings | diff --git a/docs/reference/codebase-structure.md b/docs/reference/codebase-structure.md index b75227860bf..8eb55726793 100644 --- a/docs/reference/codebase-structure.md +++ b/docs/reference/codebase-structure.md @@ -125,6 +125,11 @@ Within `go/`, the `internal/feast/` directory contains most of the core logic: Feast uses [protobuf](https://github.com/protocolbuffers/protobuf) to store serialized versions of the core Feast objects. The protobuf definitions are stored in `protos/feast`. +The [registry](../getting-started/concepts/registry.md) consists of the serialized representations of the Feast objects. + +Typically, changes being made to the Feast objects require changes to their corresponding protobuf representations. +The usual best practices for making changes to protobufs should be followed ensure backwards and forwards compatibility. + ## Web UI The `ui/` directory contains the Web UI. From e4c89805c94466f00943a30477b030f187df4462 Mon Sep 17 00:00:00 2001 From: Achal Shah Date: Tue, 30 Aug 2022 11:58:25 -0700 Subject: [PATCH 09/46] chore: Remove rest settings from java app properties (#3155) The rest setting was not being used since the java server moved to being gRPC only. Specifying it is only likely to cause warnings/errors and confusion. Signed-off-by: Achal Shah Signed-off-by: Achal Shah --- .../service/config/ApplicationProperties.java | 13 ------------- .../service/config/ApplicationPropertiesModule.java | 2 ++ 2 files changed, 2 insertions(+), 13 deletions(-) diff --git a/java/serving/src/main/java/feast/serving/service/config/ApplicationProperties.java b/java/serving/src/main/java/feast/serving/service/config/ApplicationProperties.java index e4c33434a10..7cef10e61a8 100644 --- a/java/serving/src/main/java/feast/serving/service/config/ApplicationProperties.java +++ b/java/serving/src/main/java/feast/serving/service/config/ApplicationProperties.java @@ -38,7 +38,6 @@ public class ApplicationProperties { private static final Logger log = org.slf4j.LoggerFactory.getLogger(ApplicationProperties.class); private FeastProperties feast; private GrpcServer grpc; - private RestServer rest; public FeastProperties getFeast() { return feast; @@ -331,18 +330,6 @@ public void setServer(Server server) { } } - public static class RestServer { - private Server server; - - public Server getServer() { - return server; - } - - public void setServer(Server server) { - this.server = server; - } - } - /** Trace metric collection properties */ public static class TracingProperties { diff --git a/java/serving/src/main/java/feast/serving/service/config/ApplicationPropertiesModule.java b/java/serving/src/main/java/feast/serving/service/config/ApplicationPropertiesModule.java index 35757330736..588a17d269c 100644 --- a/java/serving/src/main/java/feast/serving/service/config/ApplicationPropertiesModule.java +++ b/java/serving/src/main/java/feast/serving/service/config/ApplicationPropertiesModule.java @@ -16,6 +16,7 @@ */ package feast.serving.service.config; +import com.fasterxml.jackson.databind.DeserializationFeature; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.ObjectReader; import com.fasterxml.jackson.dataformat.yaml.YAMLFactory; @@ -40,6 +41,7 @@ public ApplicationProperties provideApplicationProperties() throws IOException { ObjectMapper mapper = new ObjectMapper(new YAMLFactory()); mapper.findAndRegisterModules(); mapper.setDefaultMergeable(Boolean.TRUE); + mapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false); ApplicationProperties properties = new ApplicationProperties(); ObjectReader objectReader = mapper.readerForUpdating(properties); From 5ddb83b14817f55e51e5c89014a3439ec3ef5a59 Mon Sep 17 00:00:00 2001 From: Petra Rebernjak Date: Tue, 30 Aug 2022 23:01:25 +0200 Subject: [PATCH 10/46] fix: Handle complex Spark data types in SparkSource (#3154) * Make sure data types are strings Signed-off-by: Petra Rebernjak * Lint Signed-off-by: Petra Rebernjak Signed-off-by: Petra Rebernjak --- .../contrib/spark_offline_store/spark_source.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/sdk/python/feast/infra/offline_stores/contrib/spark_offline_store/spark_source.py b/sdk/python/feast/infra/offline_stores/contrib/spark_offline_store/spark_source.py index 5b9f5621813..a27065fb5ed 100644 --- a/sdk/python/feast/infra/offline_stores/contrib/spark_offline_store/spark_source.py +++ b/sdk/python/feast/infra/offline_stores/contrib/spark_offline_store/spark_source.py @@ -159,10 +159,7 @@ def get_table_column_names_and_types( store_config=config.offline_store ) df = spark_session.sql(f"SELECT * FROM {self.get_table_query_string()}") - return ( - (fields["name"], fields["type"]) - for fields in df.schema.jsonValue()["fields"] - ) + return ((field.name, field.dataType.simpleString()) for field in df.schema) def get_table_query_string(self) -> str: """Returns a string that can directly be used to reference this table in SQL""" From 857876bb07bdd6d007ea167c76bb84033fb9c8b9 Mon Sep 17 00:00:00 2001 From: Felix Wang Date: Wed, 31 Aug 2022 11:44:32 -0700 Subject: [PATCH 11/46] chore: Remove `join_keys` attribute (#3159) Remove `join_keys` attribute Signed-off-by: Felix Wang Signed-off-by: Felix Wang --- sdk/python/feast/entity.py | 14 +++----------- sdk/python/feast/feature_view.py | 4 ++-- sdk/python/feast/inference.py | 16 +++------------- 3 files changed, 8 insertions(+), 26 deletions(-) diff --git a/sdk/python/feast/entity.py b/sdk/python/feast/entity.py index 55de68a4041..de8fac9451b 100644 --- a/sdk/python/feast/entity.py +++ b/sdk/python/feast/entity.py @@ -33,9 +33,6 @@ class Entity: Attributes: name: The unique name of the entity. value_type: The type of the entity, such as string or float. - join_keys: A list of properties that uniquely identifies different entities within the - collection. This currently only supports a list of size one, but is intended to - eventually support multiple join keys. join_key: A property that uniquely identifies different entities within the collection. The join_key property is typically used for joining entities with their associated features. If not specified, defaults to the name. @@ -48,7 +45,6 @@ class Entity: name: str value_type: ValueType - join_keys: List[str] join_key: str description: str tags: Dict[str, str] @@ -84,21 +80,17 @@ def __init__( self.name = name self.value_type = ValueType.UNKNOWN - # For now, both the `join_key` and `join_keys` attributes are set correctly, - # so both are usable. - # TODO(felixwang9817): Fully remove the usage of `join_key` throughout the codebase, - # at which point the `join_key` attribute no longer needs to be set. if join_keys and len(join_keys) > 1: + # TODO(felixwang9817): When multiple join keys are supported, add a `join_keys` attribute + # and deprecate the `join_key` attribute. raise ValueError( - "An entity may only have single join key. " + "An entity may only have a single join key. " "Multiple join keys will be supported in the future." ) elif join_keys and len(join_keys) == 1: - self.join_keys = join_keys self.join_key = join_keys[0] else: self.join_key = self.name - self.join_keys = [self.join_key] self.description = description self.tags = tags if tags is not None else {} diff --git a/sdk/python/feast/feature_view.py b/sdk/python/feast/feature_view.py index 41bad5828a0..3125f319a9c 100644 --- a/sdk/python/feast/feature_view.py +++ b/sdk/python/feast/feature_view.py @@ -155,10 +155,10 @@ def __init__( features: List[Field] = [] self.entity_columns = [] - join_keys = [] + join_keys: List[str] = [] if entities: for entity in entities: - join_keys += entity.join_keys + join_keys.append(entity.join_key) for field in self.schema: if field.name in join_keys: diff --git a/sdk/python/feast/inference.py b/sdk/python/feast/inference.py index 84e8321c122..267d0d50261 100644 --- a/sdk/python/feast/inference.py +++ b/sdk/python/feast/inference.py @@ -115,15 +115,11 @@ def update_feature_views_with_inferred_features_and_entities( config: The config for the current feature store. """ entity_name_to_entity_map = {e.name: e for e in entities} - entity_name_to_join_keys_map = {e.name: e.join_keys for e in entities} + entity_name_to_join_key_map = {e.name: e.join_key for e in entities} for fv in fvs: join_keys = set( - [ - join_key - for entity_name in fv.entities - for join_key in entity_name_to_join_keys_map[entity_name] - ] + [entity_name_to_join_key_map[entity_name] for entity_name in fv.entities] ) # Fields whose names match a join key are considered to be entity columns; all @@ -164,13 +160,7 @@ def update_feature_views_with_inferred_features_and_entities( fv.entity_columns.append(Field(name=DUMMY_ENTITY_ID, dtype=String)) # Run inference for entity columns if there are fewer entity fields than expected. - num_expected_join_keys = sum( - [ - len(entity_name_to_join_keys_map[entity_name]) - for entity_name in fv.entities - ] - ) - run_inference_for_entities = len(fv.entity_columns) < num_expected_join_keys + run_inference_for_entities = len(fv.entity_columns) < len(join_keys) # Run inference for feature columns if there are no feature fields. run_inference_for_features = len(fv.features) == 0 From bf6e7f4ec2a465a590338f167e226996ed4bfb23 Mon Sep 17 00:00:00 2001 From: Achal Shah Date: Wed, 31 Aug 2022 13:36:33 -0700 Subject: [PATCH 12/46] docs: Update Running Feast in Production guide (#3160) * docs: Update Running Feast in Production guide Signed-off-by: Achal Shah * remove bad import: Signed-off-by: Achal Shah * fix ordering Signed-off-by: Achal Shah Signed-off-by: Achal Shah --- .../production-spark-bytewax.png | Bin 0 -> 168272 bytes .../running-feast-in-production.md | 103 +++++++++++------- 2 files changed, 65 insertions(+), 38 deletions(-) create mode 100644 docs/how-to-guides/production-spark-bytewax.png diff --git a/docs/how-to-guides/production-spark-bytewax.png b/docs/how-to-guides/production-spark-bytewax.png new file mode 100644 index 0000000000000000000000000000000000000000..ce3f7017cdfa4c12f6abe8e8d089626ad71efb66 GIT binary patch literal 168272 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�Z^}~Q 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 SqxwPwf3(!}REw3*1pYr8%U1UQ literal 0 HcmV?d00001 diff --git a/docs/how-to-guides/running-feast-in-production.md b/docs/how-to-guides/running-feast-in-production.md index 61b7b1fe40a..278b7edfb1b 100644 --- a/docs/how-to-guides/running-feast-in-production.md +++ b/docs/how-to-guides/running-feast-in-production.md @@ -9,9 +9,13 @@ Overview of typical production configuration is given below: ![Overview](production-simple.png) {% hint style="success" %} -**Important note:** Feast is highly customizable and modular. Most Feast blocks are loosely connected and can be used independently. Hence, you are free to build your own production configuration. +**Important note:** Feast is highly customizable and modular. + +Most Feast blocks are loosely connected and can be used independently. Hence, you are free to build your own production configuration. For example, you might not have a stream source and, thus, no need to write features in real-time to an online store. Or you might not need to retrieve online features. Feast also often provides multiple options to achieve the same goal. We discuss tradeoffs below. + +Additionally, please check the how-to guide for some specific recommendations on [how to scale Feast](./scaling-feast.md). {% endhint %} In this guide we will show you how to: @@ -24,15 +28,19 @@ In this guide we will show you how to: ## 1. Automatically deploying changes to your feature definitions -### Setting up a feature repository +### 1.1 Setting up a feature repository The first step to setting up a deployment of Feast is to create a Git repository that contains your feature definitions. The recommended way to version and track your feature definitions is by committing them to a repository and tracking changes through commits. If you recall, running `feast apply` commits feature definitions to a **registry**, which users can then read elsewhere. -### Setting up CI/CD to automatically update the registry +### 1.2 Setting up a database-backed registry + +Out of the box, Feast serializes all of its state into a file-based registry. When running Feast in production, we recommend using the more scalable SQL-based registry that is backed by a database. Details are available [here](./scaling-feast.md#scaling-feast-registry). + +### 1.3 Setting up CI/CD to automatically update the registry We recommend typically setting up CI/CD to automatically run `feast plan` and `feast apply` when pull requests are opened / merged. -### Setting up multiple environments +### 1.4 Setting up multiple environments A common scenario when using Feast in production is to want to test changes to Feast object definitions. For this, we recommend setting up a _staging_ environment for your offline and online stores, which mirrors _production_ (with potentially a smaller data set). Having this separate environment allows users to test changes by first applying them to staging, and then promoting the changes to production after verifying the changes on staging. @@ -43,7 +51,37 @@ Different options are presented in the [how-to guide](structuring-repos.md). To keep your online store up to date, you need to run a job that loads feature data from your feature view sources into your online store. In Feast, this loading operation is called materialization. -### 2.1. Manual materializations +### 2.1 Scalable Materialization + +Out of the box, Feast's materialization process uses an in-process materialization engine. This engine loads all the data being materialized into memory from the offline store, and writes it into the online store. + +This approach may not scale to large amounts of data, which users of Feast may be dealing with in production. +In this case, we recommend using one of the more [scalable materialization engines](./scaling-feast.md#scaling-materialization), such as the [Bytewax Materialization Engine](../reference/batch-materialization/bytewax.md), or the [Snowflake Materialization Engine](../reference/batch-materialization/snowflake.md). +Users may also need to [write a custom materialization engine](../how-to-guides/customizing-feast/creating-a-custom-materialization-engine.md) to work on their existing infrastructure. + +The Bytewax materialization engine can run materialization on existing kubernetes cluster. An example configuration of this in a `feature_store.yaml` is as follows: + +```yaml +batch_engine: + type: bytewax + namespace: bytewax + image: bytewax/bytewax-feast:latest + env: + - name: AWS_ACCESS_KEY_ID + valueFrom: + secretKeyRef: + name: aws-credentials + key: aws-access-key-id + - name: AWS_SECRET_ACCESS_KEY + valueFrom: + secretKeyRef: + name: aws-credentials + key: aws-secret-access-key +``` + + + +### 2.2 Manual materialization The simplest way to schedule materialization is to run an **incremental** materialization using the Feast CLI: @@ -53,7 +91,7 @@ feast materialize-incremental 2022-01-01T00:00:00 The above command will load all feature values from all feature view sources into the online store up to the time `2022-01-01T00:00:00`. -A timestamp is required to set the end date for materialization. If your source is fully up to date then the end date would be the current time. However, if you are querying a source where data is not yet available, then you do not want to set the timestamp to the current time. You would want to use a timestamp that ends at a date for which data is available. The next time `materialize-incremental` is run, Feast will load data that starts from the previous end date, so it is important to ensure that the materialization interval does not overlap with time periods for which data has not been made available. This is commonly the case when your source is an ETL pipeline that is scheduled on a daily basis. +A timestamp is required to set the end date for materialization. If your source is fully up-to-date then the end date would be the current time. However, if you are querying a source where data is not yet available, then you do not want to set the timestamp to the current time. You would want to use a timestamp that ends at a date for which data is available. The next time `materialize-incremental` is run, Feast will load data that starts from the previous end date, so it is important to ensure that the materialization interval does not overlap with time periods for which data has not been made available. This is commonly the case when your source is an ETL pipeline that is scheduled on a daily basis. An alternative approach to incremental materialization (where Feast tracks the intervals of data that need to be ingested), is to call Feast directly from your scheduler like Airflow. In this case, Airflow is the system that tracks the intervals that have been ingested. @@ -65,13 +103,15 @@ In the above example we are materializing the source data from the `driver_hourl The timestamps above should match the interval of data that has been computed by the data transformation system. -### 2.2. Automate periodic materializations +### 2.3 Automate periodic materialization It is up to you which orchestration/scheduler to use to periodically run `$ feast materialize`. Feast keeps the history of materialization in its registry so that the choice could be as simple as a [unix cron util](https://en.wikipedia.org/wiki/Cron). Cron util should be sufficient when you have just a few materialization jobs (it's usually one materialization job per feature view) triggered infrequently. However, the amount of work can quickly outgrow the resources of a single machine. That happens because the materialization job needs to repackage all rows before writing them to an online store. That leads to high utilization of CPU and memory. In this case, you might want to use a job orchestrator to run multiple jobs in parallel using several workers. Kubernetes Jobs or Airflow are good choices for more comprehensive job orchestration. If you are using Airflow as a scheduler, Feast can be invoked through the [BashOperator](https://airflow.apache.org/docs/apache-airflow/stable/howto/operator/bash.html) after the [Python SDK](https://pypi.org/project/feast/) has been installed into a virtual environment and your feature repo has been synced: ```python +import datetime + materialize = BashOperator( task_id='materialize', bash_command=f'feast materialize-incremental {datetime.datetime.now().replace(microsecond=0).isoformat()}', @@ -82,6 +122,8 @@ materialize = BashOperator( Important note: Airflow worker must have read and write permissions to the registry file on GS / S3 since it pulls configuration and updates materialization history. {% endhint %} + + ## 3. How to use Feast for model training After we've defined our features and data sources in the repository, we can generate training datasets. @@ -91,6 +133,8 @@ The first thing we need to do in our training code is to create a `FeatureStore` One way to ensure your production clients have access to the feature store is to provide a copy of the `feature_store.yaml` to those pipelines. This `feature_store.yaml` file will have a reference to the feature store registry, which allows clients to retrieve features from offline or online stores. ```python +from feast import FeatureStore + fs = FeatureStore(repo_path="production/") ``` @@ -114,11 +158,12 @@ model = ml.fit(training_df) The most common way to productionize ML models is by storing and versioning models in a "model store", and then deploying these models into production. When using Feast, it is recommended that the list of feature references also be saved alongside the model. This ensures that models and the features they are trained on are paired together when being shipped into production: ```python +import json # Save model model.save('my_model.bin') # Save features -open('feature_refs.json', 'w') as f: +with open('feature_refs.json', 'w') as f: json.dump(feature_refs, f) ``` @@ -217,6 +262,10 @@ from feast import FeatureStore store = FeatureStore(...) +spark = SparkSession.builder.getOrCreate() + +streamingDF = spark.readStream.format(...).load() + def feast_writer(spark_df): pandas_df = spark_df.to_pandas() store.push("driver_hourly_stats", pandas_df) @@ -230,15 +279,7 @@ Alternatively, if you want to ingest features directly from a broker (eg, Kafka If you are using Kafka, [HTTP Sink](https://docs.confluent.io/kafka-connect-http/current/overview.html) could be utilized as a middleware. In this case, the "push service" can be deployed on Kubernetes or as a Serverless function. -## 6. Monitoring - -Feast services can report their metrics to a StatsD-compatible collector. To activate this function, you'll need to provide a StatsD IP address and a port when deploying the helm chart (in future, this will be added to `feature_store.yaml`). - -We use an [InfluxDB-style extension](https://github.com/prometheus/statsd\_exporter#tagging-extensions) for StatsD format to be able to send tags along with metrics. Keep that in mind while selecting the collector ([telegraph](https://www.influxdata.com/blog/getting-started-with-sending-statsd-metrics-to-telegraf-influxdb/#introducing-influx-statsd) will work for sure). - -We chose StatsD since it's a de-facto standard with various implementations (eg, [1](https://github.com/prometheus/statsd\_exporter), [2](https://github.com/influxdata/telegraf/blob/master/plugins/inputs/statsd/README.md)) and metrics can be easily exported to Prometheus, InfluxDB, AWS CloudWatch, etc. - -## 7. Using environment variables in your yaml configuration +## 6. Using environment variables in your yaml configuration You might want to dynamically set parts of your configuration from your environment. For instance to deploy Feast to production and development with the same configuration, but a different server. Or to inject secrets without exposing them in your git repo. To do this, it is possible to use the `${ENV_VAR}` syntax in your `feature_store.yaml` file. For instance: @@ -268,30 +309,16 @@ online_store: Summarizing it all together we want to show several options of architecture that will be most frequently used in production: -### Option #1 (currently preferred) +### Current Recommendation * Feast SDK is being triggered by CI (eg, Github Actions). It applies the latest changes from the feature repo to the Feast registry * Airflow manages materialization jobs to ingest data from DWH to the online store periodically * For the stream ingestion Feast Python SDK is used in the existing Spark / Beam pipeline -* Online features are served via either a Python feature server or a high performance Go feature server - * The Go feature server can be deployed on a Kubernetes cluster (via Helm charts) +* For Batch Materialization Engine: + * If your offline and online workloads are in Snowflake, the Snowflake Engine is likely the best option. + * If your offline and online workloads are not using Snowflake, but using Kubernetes is an option, the Bytewax engine is likely the best option. + * If none of these engines suite your needs, you may continue using the in-process engine, or write a custom engine. +* Online features are served via the Python feature server over HTTP, or consumed using the Feast Python SDK. * Feast Python SDK is called locally to generate a training dataset -![From Repository to Production: Feast Production Architecture](production-spark.png) - -### Option #2 _(still in development)_ - -Same as Option #1, except: - -* Push service is deployed as AWS Lambda / Google Cloud Run and is configured as a sink for Kinesis or PubSub to ingest features directly from a stream broker. Lambda / Cloud Run is being managed by Feast SDK (from CI environment) -* Materialization jobs are managed inside Kubernetes via Kubernetes Job (currently not managed by Helm) - -![With Push Service as Lambda](production-lambda.png) - -### Option #3 _(still in development)_ - -Same as Option #2, except: - -* Push service is deployed on Kubernetes cluster and exposes an HTTP API that can be used as a sink for Kafka (via kafka-http connector) or accessed directly. - -![With Push Service in Kubernetes](production-kube.png) +![From Repository to Production: Feast Production Architecture](production-spark-bytewax.png) From 1b097198ab9953be3154925a4d23d1a260c8388a Mon Sep 17 00:00:00 2001 From: Felix Wang Date: Thu, 1 Sep 2022 11:44:48 -0700 Subject: [PATCH 13/46] chore: Add `value_type` parameter back to entities (#3161) Add `value_type` parameter back to entities Signed-off-by: Felix Wang Signed-off-by: Felix Wang --- sdk/python/feast/entity.py | 5 +- sdk/python/feast/feature_view.py | 23 ++++++ sdk/python/feast/inference.py | 3 +- .../unit/infra/test_inference_unit_tests.py | 73 +++++++++++++++++++ sdk/python/tests/unit/test_feature_views.py | 14 ++++ 5 files changed, 115 insertions(+), 3 deletions(-) diff --git a/sdk/python/feast/entity.py b/sdk/python/feast/entity.py index de8fac9451b..30f04e9c068 100644 --- a/sdk/python/feast/entity.py +++ b/sdk/python/feast/entity.py @@ -58,6 +58,7 @@ def __init__( *, name: str, join_keys: Optional[List[str]] = None, + value_type: Optional[ValueType] = None, description: str = "", tags: Optional[Dict[str, str]] = None, owner: str = "", @@ -70,6 +71,8 @@ def __init__( join_keys (optional): A list of properties that uniquely identifies different entities within the collection. This currently only supports a list of size one, but is intended to eventually support multiple join keys. + value_type (optional): The type of the entity, such as string or float. If not specified, + it will be inferred from the schema of the underlying data source. description (optional): A human-readable description. tags (optional): A dictionary of key-value pairs to store arbitrary metadata. owner (optional): The owner of the entity, typically the email of the primary maintainer. @@ -78,7 +81,7 @@ def __init__( ValueError: Parameters are specified incorrectly. """ self.name = name - self.value_type = ValueType.UNKNOWN + self.value_type = value_type or ValueType.UNKNOWN if join_keys and len(join_keys) > 1: # TODO(felixwang9817): When multiple join keys are supported, add a `join_keys` attribute diff --git a/sdk/python/feast/feature_view.py b/sdk/python/feast/feature_view.py index 3125f319a9c..cdd507868df 100644 --- a/sdk/python/feast/feature_view.py +++ b/sdk/python/feast/feature_view.py @@ -35,7 +35,9 @@ from feast.protos.feast.core.FeatureView_pb2 import ( MaterializationInterval as MaterializationIntervalProto, ) +from feast.types import from_value_type from feast.usage import log_exceptions +from feast.value_type import ValueType warnings.simplefilter("once", DeprecationWarning) @@ -115,6 +117,7 @@ def __init__( If a stream source, the source should contain a batch_source for backfills & batch materialization. schema (optional): The schema of the feature view, including feature, timestamp, and entity columns. + # TODO: clarify that schema is only useful here... entities (optional): The list of entities with which this group of features is associated. ttl (optional): The amount of time this group of features lives. A ttl of 0 indicates that this group of features lives forever. Note that large ttl's or a ttl of 0 @@ -160,9 +163,29 @@ def __init__( for entity in entities: join_keys.append(entity.join_key) + # Ensure that entities have unique join keys. + if len(set(join_keys)) < len(join_keys): + raise ValueError( + "A feature view should not have entities that share a join key." + ) + for field in self.schema: if field.name in join_keys: self.entity_columns.append(field) + + # Confirm that the inferred type matches the specified entity type, if it exists. + matching_entities = ( + [e for e in entities if e.join_key == field.name] + if entities + else [] + ) + assert len(matching_entities) == 1 + entity = matching_entities[0] + if entity.value_type != ValueType.UNKNOWN: + if from_value_type(entity.value_type) != field.dtype: + raise ValueError( + f"Entity {entity.name} has type {entity.value_type}, which does not match the inferred type {field.dtype}." + ) else: features.append(field) diff --git a/sdk/python/feast/inference.py b/sdk/python/feast/inference.py index 267d0d50261..d416763bd39 100644 --- a/sdk/python/feast/inference.py +++ b/sdk/python/feast/inference.py @@ -135,8 +135,7 @@ def update_feature_views_with_inferred_features_and_entities( if field.name not in [feature.name for feature in fv.features]: fv.features.append(field) - # Since the `value_type` parameter has not yet been fully deprecated for - # entities, we respect the `value_type` attribute if it still exists. + # Respect the `value_type` attribute of the entity, if it is specified. for entity_name in fv.entities: entity = entity_name_to_entity_map[entity_name] if ( diff --git a/sdk/python/tests/unit/infra/test_inference_unit_tests.py b/sdk/python/tests/unit/infra/test_inference_unit_tests.py index c5ed83c12f0..b4a0da7e47a 100644 --- a/sdk/python/tests/unit/infra/test_inference_unit_tests.py +++ b/sdk/python/tests/unit/infra/test_inference_unit_tests.py @@ -15,6 +15,7 @@ from feast.on_demand_feature_view import on_demand_feature_view from feast.repo_config import RepoConfig from feast.types import Float32, Float64, Int64, String, UnixTimestamp +from feast.value_type import ValueType from tests.utils.data_source_test_creator import prep_file_source @@ -216,6 +217,78 @@ def test_feature_view_inference_respects_basic_inference(): assert len(feature_view_2.entity_columns) == 2 +def test_feature_view_inference_on_entity_value_types(): + """ + Tests that feature view inference correctly uses the entity `value_type` attribute. + """ + entity1 = Entity( + name="test1", join_keys=["id_join_key"], value_type=ValueType.INT64 + ) + file_source = FileSource(path="some path") + feature_view_1 = FeatureView( + name="test1", + entities=[entity1], + schema=[Field(name="int64_col", dtype=Int64)], + source=file_source, + ) + + assert len(feature_view_1.schema) == 1 + assert len(feature_view_1.features) == 1 + assert len(feature_view_1.entity_columns) == 0 + + update_feature_views_with_inferred_features_and_entities( + [feature_view_1], + [entity1], + RepoConfig( + provider="local", project="test", entity_key_serialization_version=2 + ), + ) + + # The schema is only used as a parameter, as is therefore not updated during inference. + assert len(feature_view_1.schema) == 1 + + # Since there is already a feature specified, additional features are not inferred. + assert len(feature_view_1.features) == 1 + + # The single entity column is inferred correctly and has the expected type. + assert len(feature_view_1.entity_columns) == 1 + assert feature_view_1.entity_columns[0].dtype == Int64 + + +def test_conflicting_entity_value_types(): + """ + Tests that an error is thrown when the entity value types conflict. + """ + entity1 = Entity( + name="test1", join_keys=["id_join_key"], value_type=ValueType.INT64 + ) + file_source = FileSource(path="some path") + + with pytest.raises(ValueError): + _ = FeatureView( + name="test1", + entities=[entity1], + schema=[ + Field(name="int64_col", dtype=Int64), + Field( + name="id_join_key", dtype=Float64 + ), # Conflicts with the defined entity + ], + source=file_source, + ) + + # There should be no error here. + _ = FeatureView( + name="test1", + entities=[entity1], + schema=[ + Field(name="int64_col", dtype=Int64), + Field(name="id_join_key", dtype=Int64), # Conflicts with the defined entity + ], + source=file_source, + ) + + def test_feature_view_inference_on_entity_columns(simple_dataset_1): """ Tests that feature view inference correctly infers entity columns. diff --git a/sdk/python/tests/unit/test_feature_views.py b/sdk/python/tests/unit/test_feature_views.py index 0fe3f839e1f..379396e5c63 100644 --- a/sdk/python/tests/unit/test_feature_views.py +++ b/sdk/python/tests/unit/test_feature_views.py @@ -15,6 +15,20 @@ from feast.types import Float32 +def test_create_feature_view_with_conflicting_entities(): + user1 = Entity(name="user1", join_keys=["user_id"]) + user2 = Entity(name="user2", join_keys=["user_id"]) + batch_source = FileSource(path="some path") + + with pytest.raises(ValueError): + _ = FeatureView( + name="test", + entities=[user1, user2], + ttl=timedelta(days=30), + source=batch_source, + ) + + def test_create_batch_feature_view(): batch_source = FileSource(path="some path") BatchFeatureView( From d7b0c5231526b5acd3de63f37a0fe0aecfafefbd Mon Sep 17 00:00:00 2001 From: Danny Chiao Date: Thu, 1 Sep 2022 11:45:45 -0700 Subject: [PATCH 14/46] docs: Cleanup running Feast in production, fix links, call out Slack more (#3163) * docs: Cleanup running Feast in production, fix links, and call out Slack more Signed-off-by: Danny Chiao * fix title Signed-off-by: Danny Chiao * fix title Signed-off-by: Danny Chiao * remove workshop Signed-off-by: Danny Chiao * remove toc Signed-off-by: Danny Chiao Signed-off-by: Danny Chiao --- docs/README.md | 2 +- docs/SUMMARY.md | 3 +- docs/community.md | 27 ++- docs/getting-started/feast-workshop.md | 44 ---- .../feast-snowflake-gcp-aws/README.md | 4 + .../running-feast-in-production.md | 216 +++++++----------- docs/reference/data-sources/push.md | 23 +- 7 files changed, 136 insertions(+), 183 deletions(-) delete mode 100644 docs/getting-started/feast-workshop.md diff --git a/docs/README.md b/docs/README.md index b838e5fe5b1..f387406c3fd 100644 --- a/docs/README.md +++ b/docs/README.md @@ -57,7 +57,7 @@ Many companies have used Feast to power real-world ML use cases such as: ## How can I get started? {% hint style="info" %} -The best way to learn Feast is to use it. Head over to our [Quickstart](getting-started/quickstart.md) and try it out! +The best way to learn Feast is to use it. Join our [Slack channel](http://slack.feast.dev) and head over to our [Quickstart](getting-started/quickstart.md) and try it out! {% endhint %} Explore the following resources to get started with Feast: diff --git a/docs/SUMMARY.md b/docs/SUMMARY.md index 18d1aeb2806..6ef1c9c49eb 100644 --- a/docs/SUMMARY.md +++ b/docs/SUMMARY.md @@ -1,7 +1,7 @@ # Table of contents * [Introduction](README.md) -* [Community](community.md) +* [Community & getting help](community.md) * [Roadmap](roadmap.md) * [Changelog](https://github.com/feast-dev/feast/blob/master/CHANGELOG.md) @@ -24,7 +24,6 @@ * [Online store](getting-started/architecture-and-components/online-store.md) * [Batch Materialization Engine](getting-started/architecture-and-components/batch-materialization-engine.md) * [Provider](getting-started/architecture-and-components/provider.md) -* [Learning by example](getting-started/feast-workshop.md) * [Third party integrations](getting-started/third-party-integrations.md) * [FAQ](getting-started/faq.md) diff --git a/docs/community.md b/docs/community.md index dc1cc8a0fe8..bd01b2a64b8 100644 --- a/docs/community.md +++ b/docs/community.md @@ -1,17 +1,19 @@ -# Community +# Community & Getting Help ## Links & Resources -* [Slack](https://slack.feast.dev): Feel free to ask questions or say hello! +* [GitHub Repository](https://github.com/feast-dev/feast/): Find the complete Feast codebase on GitHub. +* [Slack](https://slack.feast.dev): Feel free to ask questions or say hello! This is the main place where maintainers and contributors brainstorm and where users ask questions or discuss best practices. + * Feast users should join `#feast-general` or `#feast-beginners` to ask questions + * Feast developers / contributors should join `#feast-development` * [Mailing list](https://groups.google.com/d/forum/feast-dev): We have both a user and developer mailing list. * Feast users should join [feast-discuss@googlegroups.com](mailto:feast-discuss@googlegroups.com) group by clicking [here](https://groups.google.com/g/feast-discuss). - * Feast developers should join [feast-dev@googlegroups.com](mailto:feast-dev@googlegroups.com) group by clicking [here](https://groups.google.com/d/forum/feast-dev). + * Feast developers / contributors should join [feast-dev@googlegroups.com](mailto:feast-dev@googlegroups.com) group by clicking [here](https://groups.google.com/d/forum/feast-dev). * [Community Calendar](https://calendar.google.com/calendar/u/0?cid=ZTFsZHVhdGM3MDU3YTJucTBwMzNqNW5rajBAZ3JvdXAuY2FsZW5kYXIuZ29vZ2xlLmNvbQ): Includes community calls and design meetings. * [Google Folder](https://drive.google.com/drive/u/0/folders/1jgMHOPDT2DvBlJeO9LCM79DP4lm4eOrR): This folder is used as a central repository for all Feast resources. For example: * Design proposals in the form of Request for Comments (RFC). * User surveys and meeting minutes. * Slide decks of conferences our contributors have spoken at. -* [Feast GitHub Repository](https://github.com/feast-dev/feast/): Find the complete Feast codebase on GitHub. * [Feast Linux Foundation Wiki](https://wiki.lfaidata.foundation/display/FEAST/Feast+Home): Our LFAI wiki page contains links to resources for contributors and maintainers. ## How can I get help? @@ -22,17 +24,30 @@ ## Community Calls +### General community call (biweekly) We have a user and contributor community call every two weeks (US & EU friendly). {% hint style="info" %} Please join the above Feast user groups in order to see calendar invites to the community calls {% endhint %} -### Frequency (every 2 weeks) +#### Frequency (every 2 weeks) * Tuesday 10:00 am to 10:30 am PST -### Links +#### Links * Zoom: [https://zoom.us/j/6325193230](https://zoom.us/j/6325193230) * Meeting notes (incl recordings): [https://bit.ly/feast-notes](https://bit.ly/feast-notes) + +### Developers call (biweekly) +We also have a `#feast-development` community call every two weeks, where we discuss contributions + brainstorm best practices. + +#### Frequency (every 2 weeks) + +* Tuesday 8:00 am to 8:30 am PST + +#### Links + +* Meeting notes (incl recordings): [Feast Development Biweekly](https://docs.google.com/document/d/1zUbIWFWjaBEVlToOdupnmKQwgAtFYx41sPoEEEdd2io/edit#) +* Zoom: [https://zoom.us/j/93657748160?pwd=K3ZpdzhqejgrcXNhc3BlSjFMdzUxdz09](https://zoom.us/j/93657748160?pwd=K3ZpdzhqejgrcXNhc3BlSjFMdzUxdz09) diff --git a/docs/getting-started/feast-workshop.md b/docs/getting-started/feast-workshop.md deleted file mode 100644 index 0d648452223..00000000000 --- a/docs/getting-started/feast-workshop.md +++ /dev/null @@ -1,44 +0,0 @@ -# Learning by example - -This workshop aims to teach users about Feast. - -We explain concepts & best practices by example, and also showcase how to address common use cases. - -### Pre-requisites - -This workshop assumes you have the following installed: - -* A local development environment that supports running Jupyter notebooks (e.g. VSCode with Jupyter plugin) -* Python 3.7+ -* Java 11 (for Spark, e.g. `brew install java11`) -* pip -* Docker & Docker Compose (e.g. `brew install docker docker-compose`) -* Terraform ([docs](https://learn.hashicorp.com/tutorials/terraform/install-cli#install-terraform)) -* AWS CLI -* An AWS account setup with credentials via `aws configure` (e.g see [AWS credentials quickstart](https://docs.aws.amazon.com/cli/latest/userguide/cli-configure-quickstart.html#cli-configure-quickstart-creds)) - -Since we'll be learning how to leverage Feast in CI/CD, you'll also need to fork this workshop repository. - -#### **Caveats** - -* M1 Macbook development is untested with this flow. See also [How to run / develop for Feast on M1 Macs](https://github.com/feast-dev/feast/issues/2105). -* Windows development has only been tested with WSL. You will need to follow this [guide](https://docs.docker.com/desktop/windows/wsl/) to have Docker play nicely. - -### Modules - -_See also:_ [_Feast quickstart_](https://docs.feast.dev/getting-started/quickstart)_,_ [_Feast x Great Expectations tutorial_](https://docs.feast.dev/tutorials/validating-historical-features) - -These are meant mostly to be done in order, with examples building on previous concepts. - -See [https://github.com/feast-dev/feast-workshop](https://github.com/feast-dev/feast-workshop) - -| Time (min) | Description | Module | -| :--------: | ----------------------------------------------------------------------- | -------- | -| 30-45 | Setting up Feast projects & CI/CD + powering batch predictions | Module 0 | -| 15-20 | Streaming ingestion & online feature retrieval with Kafka, Spark, Redis | Module 1 | -| 10-15 | Real-time feature engineering with on demand transformations | Module 2 | -| TBD | Feature server deployment (embed, as a service, AWS Lambda) | TBD | -| TBD | Versioning features / models in Feast | TBD | -| TBD | Data quality monitoring in Feast | TBD | -| TBD | Batch transformations | TBD | -| TBD | Stream transformations | TBD | diff --git a/docs/how-to-guides/feast-snowflake-gcp-aws/README.md b/docs/how-to-guides/feast-snowflake-gcp-aws/README.md index 753650080b0..0f6d099349c 100644 --- a/docs/how-to-guides/feast-snowflake-gcp-aws/README.md +++ b/docs/how-to-guides/feast-snowflake-gcp-aws/README.md @@ -12,3 +12,7 @@ {% page-ref page="read-features-from-the-online-store.md" %} +{% page-ref page="../scaling-feast.md" %} + +{% page-ref page="../structuring-repos.md" %} + diff --git a/docs/how-to-guides/running-feast-in-production.md b/docs/how-to-guides/running-feast-in-production.md index 278b7edfb1b..b7cd108bc3b 100644 --- a/docs/how-to-guides/running-feast-in-production.md +++ b/docs/how-to-guides/running-feast-in-production.md @@ -4,7 +4,7 @@ After learning about Feast concepts and playing with Feast locally, you're now ready to use Feast in production. This guide aims to help with the transition from a sandbox project to production-grade deployment in the cloud or on-premise. -Overview of typical production configuration is given below: +A typical production architecture looks like: ![Overview](production-simple.png) @@ -21,10 +21,8 @@ Additionally, please check the how-to guide for some specific recommendations on In this guide we will show you how to: 1. Deploy your feature store and keep your infrastructure in sync with your feature repository -2. Keep the data in your online store up to date +2. Keep the data in your online store up to date (from batch and stream sources) 3. Use Feast for model training and serving -4. Ingest features from a stream source -5. Monitor your production deployment ## 1. Automatically deploying changes to your feature definitions @@ -43,6 +41,7 @@ We recommend typically setting up CI/CD to automatically run `feast plan` and `f ### 1.4 Setting up multiple environments A common scenario when using Feast in production is to want to test changes to Feast object definitions. For this, we recommend setting up a _staging_ environment for your offline and online stores, which mirrors _production_ (with potentially a smaller data set). + Having this separate environment allows users to test changes by first applying them to staging, and then promoting the changes to production after verifying the changes on staging. Different options are presented in the [how-to guide](structuring-repos.md). @@ -79,110 +78,108 @@ batch_engine: key: aws-secret-access-key ``` +### 2.2 Scheduled materialization +> See also [data ingestion](../getting-started/concepts/data-ingestion.md#batch-data-ingestion) for code snippets -### 2.2 Manual materialization - -The simplest way to schedule materialization is to run an **incremental** materialization using the Feast CLI: - -``` -feast materialize-incremental 2022-01-01T00:00:00 -``` - -The above command will load all feature values from all feature view sources into the online store up to the time `2022-01-01T00:00:00`. - -A timestamp is required to set the end date for materialization. If your source is fully up-to-date then the end date would be the current time. However, if you are querying a source where data is not yet available, then you do not want to set the timestamp to the current time. You would want to use a timestamp that ends at a date for which data is available. The next time `materialize-incremental` is run, Feast will load data that starts from the previous end date, so it is important to ensure that the materialization interval does not overlap with time periods for which data has not been made available. This is commonly the case when your source is an ETL pipeline that is scheduled on a daily basis. - -An alternative approach to incremental materialization (where Feast tracks the intervals of data that need to be ingested), is to call Feast directly from your scheduler like Airflow. In this case, Airflow is the system that tracks the intervals that have been ingested. - -``` -feast materialize -v driver_hourly_stats 2020-01-01T00:00:00 2020-01-02T00:00:00 -``` - -In the above example we are materializing the source data from the `driver_hourly_stats` feature view over a day. This command can be scheduled as the final operation in your Airflow ETL, which runs after you have computed your features and stored them in the source location. Feast will then load your feature data into your online store. - -The timestamps above should match the interval of data that has been computed by the data transformation system. +It is up to you to orchestrate and schedule runs of materialization. -### 2.3 Automate periodic materialization +Feast keeps the history of materialization in its registry so that the choice could be as simple as a [unix cron util](https://en.wikipedia.org/wiki/Cron). Cron util should be sufficient when you have just a few materialization jobs (it's usually one materialization job per feature view) triggered infrequently. -It is up to you which orchestration/scheduler to use to periodically run `$ feast materialize`. Feast keeps the history of materialization in its registry so that the choice could be as simple as a [unix cron util](https://en.wikipedia.org/wiki/Cron). Cron util should be sufficient when you have just a few materialization jobs (it's usually one materialization job per feature view) triggered infrequently. However, the amount of work can quickly outgrow the resources of a single machine. That happens because the materialization job needs to repackage all rows before writing them to an online store. That leads to high utilization of CPU and memory. In this case, you might want to use a job orchestrator to run multiple jobs in parallel using several workers. Kubernetes Jobs or Airflow are good choices for more comprehensive job orchestration. +However, the amount of work can quickly outgrow the resources of a single machine. That happens because the materialization job needs to repackage all rows before writing them to an online store. That leads to high utilization of CPU and memory. In this case, you might want to use a job orchestrator to run multiple jobs in parallel using several workers. Kubernetes Jobs or Airflow are good choices for more comprehensive job orchestration. -If you are using Airflow as a scheduler, Feast can be invoked through the [BashOperator](https://airflow.apache.org/docs/apache-airflow/stable/howto/operator/bash.html) after the [Python SDK](https://pypi.org/project/feast/) has been installed into a virtual environment and your feature repo has been synced: +If you are using Airflow as a scheduler, Feast can be invoked through a [PythonOperator](https://airflow.apache.org/docs/apache-airflow/stable/howto/operator/python.html) after the [Python SDK](https://pypi.org/project/feast/) has been installed into a virtual environment and your feature repo has been synced: ```python import datetime - -materialize = BashOperator( - task_id='materialize', - bash_command=f'feast materialize-incremental {datetime.datetime.now().replace(microsecond=0).isoformat()}', +from airflow.operators.python_operator import PythonOperator +from feast import RepoConfig, FeatureStore +from feast.infra.online_stores.dynamodb import DynamoDBOnlineStoreConfig +from feast.repo_config import RegistryConfig + +# Define Python callable +def materialize(): + repo_config = RepoConfig( + registry=RegistryConfig(path="s3://[YOUR BUCKET]/registry.pb"), + project="feast_demo_aws", + provider="aws", + offline_store="file", + online_store=DynamoDBOnlineStoreConfig(region="us-west-2") + ) + store = FeatureStore(config=repo_config) + # Option 1: materialize just one feature view + # store.materialize_incremental(datetime.datetime.now(), feature_views=["my_fv_name"]) + # Option 2: materialize all feature views incrementally + store.materialize_incremental(datetime.datetime.now()) + +# Use Airflow PythonOperator +materialize_python = PythonOperator( + task_id='materialize_python', + python_callable=materialize, ) ``` {% hint style="success" %} -Important note: Airflow worker must have read and write permissions to the registry file on GS / S3 since it pulls configuration and updates materialization history. +Important note: Airflow worker must have read and write permissions to the registry file on GCS / S3 since it pulls configuration and updates materialization history. {% endhint %} +### 2.3 Stream feature ingestion +See more details at [data ingestion](../getting-started/concepts/data-ingestion.md), which shows how to ingest streaming features or 3rd party feature data via a push API. + +This supports pushing feature values into Feast to both online or offline stores. ## 3. How to use Feast for model training -After we've defined our features and data sources in the repository, we can generate training datasets. +### 3.1. Generating training data +After we've defined our features and data sources in the repository, we can generate training datasets. We highly recommend you use a `FeatureService` to version the features that go into a specific model version. -The first thing we need to do in our training code is to create a `FeatureStore` object with a path to the registry. +1. The first thing we need to do in our training code is to create a `FeatureStore` object with a path to the registry. + - One way to ensure your production clients have access to the feature store is to provide a copy of the `feature_store.yaml` to those pipelines. This `feature_store.yaml` file will have a reference to the feature store registry, which allows clients to retrieve features from offline or online stores. -One way to ensure your production clients have access to the feature store is to provide a copy of the `feature_store.yaml` to those pipelines. This `feature_store.yaml` file will have a reference to the feature store registry, which allows clients to retrieve features from offline or online stores. + ```python + from feast import FeatureStore -```python -from feast import FeatureStore + fs = FeatureStore(repo_path="production/") + ``` +2. Then, you need to generate an **entity dataframe**. You have two options + - Create an entity dataframe manually and pass it in + - Use a SQL query to dynamically generate lists of entities (e.g. all entities within a time range) and timestamps to pass into Feast +3. Then, training data can be retrieved as follows: -fs = FeatureStore(repo_path="production/") -``` + ```python + training_retrieval_job = fs.get_historical_features( + entity_df=entity_df_or_sql_string, + features=fs.get_feature_service("driver_activity_v1"), + ) -Then, training data can be retrieved as follows: + # Option 1: In memory model training + model = ml.fit(training_retrieval_job.to_df()) -```python -feature_refs = [ - 'driver_hourly_stats:conv_rate', - 'driver_hourly_stats:acc_rate', - 'driver_hourly_stats:avg_daily_trips' -] - -training_df = fs.get_historical_features( - entity_df=entity_df, - features=feature_refs, -).to_df() + # Option 2: Unloading to blob storage. Further post-processing can occur before kicking off distributed training. + training_retrieval_job.to_remote_storage() + ``` -model = ml.fit(training_df) -``` +### 3.2 Versioning features that power ML models +The most common way to productionize ML models is by storing and versioning models in a "model store", and then deploying these models into production. When using Feast, it is recommended that the feature service name and the model versions have some established convention. -The most common way to productionize ML models is by storing and versioning models in a "model store", and then deploying these models into production. When using Feast, it is recommended that the list of feature references also be saved alongside the model. This ensures that models and the features they are trained on are paired together when being shipped into production: +For example, in MLflow: ```python -import json -# Save model -model.save('my_model.bin') - -# Save features -with open('feature_refs.json', 'w') as f: - json.dump(feature_refs, f) -``` - -To test your model locally, you can simply create a `FeatureStore` object, fetch online features, and then make a prediction: +import mlflow.pyfunc -```python -# Load model -model = ml.load('my_model.bin') - -# Load feature references -with open('feature_refs.json', 'r') as f: - feature_refs = json.load(f) +# Load model from MLflow +model_name = "my-model" +model_version = 1 +model = mlflow.pyfunc.load_model( + model_uri=f"models:/{model_name}/{model_version}" +) -# Create feature store object fs = FeatureStore(repo_path="production/") -# Read online features +# Read online features using the same model name and model version feature_vector = fs.get_online_features( - features=feature_refs, + features=fs.get_feature_service(f"{model_name}_v{model_version}"), entity_rows=[{"driver_id": 1001}] ).to_dict() @@ -196,7 +193,7 @@ It is important to note that both the training pipeline and model serving servic ## 4. Retrieving online features for prediction -Once you have successfully loaded (or in Feast terminology materialized) your data from batch sources into the online store, you can start consuming features for model inference. There are three approaches for that purpose sorted from the most simple one (in an operational sense) to the most performant (benchmarks to be published soon): +Once you have successfully loaded data from batch / streaming sources into the online store, you can start consuming features for model inference. ### 4.1. Use the Python SDK within an existing Python service @@ -219,18 +216,11 @@ feature_vector = fs.get_online_features( ).to_dict() ``` -### 4.2. Consume features via HTTP API from Serverless Feature Server - -If you don't want to add the Feast Python SDK as a dependency, or your feature retrieval service is written in a non-Python language, Feast can deploy a simple feature server on serverless infrastructure (eg, AWS Lambda, Google Cloud Run) for you. This service will provide an HTTP API with JSON I/O, which can be easily used with any programming language. - -[Read more about this feature](../reference/feature-servers/alpha-aws-lambda-feature-server.md) +### 4.2. Deploy Feast feature servers on Kubernetes -### 4.3. Go feature server deployed on Kubernetes - -For users with very latency-sensitive and high QPS use-cases, Feast offers a high-performance [Go feature server](../reference/feature-servers/go-feature-server.md). It can use either HTTP or gRPC. - -The Go feature server can be deployed to a Kubernetes cluster via Helm charts in a few simple steps: +To deploy a Feast feature server on Kubernetes, you can use the included helm chart. +See [helm chart](https://github.com/feast-dev/feast/tree/master/infra/charts/feast-feature-server) for configuration details. 1. Install [kubectl](https://kubernetes.io/docs/tasks/tools/install-kubectl/) and [helm 3](https://helm.sh/) 2. Add the Feast Helm repository and download the latest charts: @@ -243,43 +233,12 @@ helm repo update ``` helm install feast-release feast-charts/feast-feature-server \ - --set global.registry.path=s3://feast/registries/prod \ - --set global.project= + --set feature_store_yaml_base64=$(base64 feature_store.yaml) ``` -This chart will deploy a single service. The service must have read access to the registry file on cloud storage. It will keep a copy of the registry in their memory and periodically refresh it, so expect some delays in update propagation in exchange for better performance. In order for the Go feature server to be enabled, you should set `go_feature_serving: True` in the `feature_store.yaml`. - -## 5. Ingesting features from a stream source - -Recently Feast added functionality for [stream ingestion](../reference/data-sources/push.md). Please note that this is still in an early phase and new incompatible changes may be introduced. - -### 5.1. Using Python SDK in your Apache Spark / Beam pipeline - -The default option to write features from a stream is to add the Python SDK into your existing PySpark / Beam pipeline. Feast SDK provides writer implementation that can be called from `foreachBatch` stream writer in PySpark like this: - -```python -from feast import FeatureStore - -store = FeatureStore(...) - -spark = SparkSession.builder.getOrCreate() - -streamingDF = spark.readStream.format(...).load() - -def feast_writer(spark_df): - pandas_df = spark_df.to_pandas() - store.push("driver_hourly_stats", pandas_df) - -streamingDF.writeStream.foreachBatch(feast_writer).start() -``` - -### 5.2. Push Service (Alpha) - -Alternatively, if you want to ingest features directly from a broker (eg, Kafka or Kinesis), you can use the "push service", which will write to an online store and/or offline store. This service will expose an HTTP API or when deployed on Serverless platforms like AWS Lambda or Google Cloud Run, this service can be directly connected to Kinesis or PubSub. - -If you are using Kafka, [HTTP Sink](https://docs.confluent.io/kafka-connect-http/current/overview.html) could be utilized as a middleware. In this case, the "push service" can be deployed on Kubernetes or as a Serverless function. +This chart will deploy a single service. The service must have read access to the registry file on cloud storage. It will keep a copy of the registry in their memory and periodically refresh it, so expect some delays in update propagation in exchange for better performance. -## 6. Using environment variables in your yaml configuration +## 5. Using environment variables in your yaml configuration You might want to dynamically set parts of your configuration from your environment. For instance to deploy Feast to production and development with the same configuration, but a different server. Or to inject secrets without exposing them in your git repo. To do this, it is possible to use the `${ENV_VAR}` syntax in your `feature_store.yaml` file. For instance: @@ -307,17 +266,16 @@ online_store: ## Summary -Summarizing it all together we want to show several options of architecture that will be most frequently used in production: +In summary, the overall architecture in production may look like: -### Current Recommendation +* Feast SDK is being triggered by CI (eg, Github Actions). It applies the latest changes from the feature repo to the Feast database-backed registry +* Data ingestion + * **Batch data**: Airflow manages materialization jobs to ingest batch data from DWH to the online store periodically. When working with large datasets to materialize, we recommend using a batch materialization engine + * If your offline and online workloads are in Snowflake, the Snowflake materialization engine is likely the best option. + * If your offline and online workloads are not using Snowflake, but using Kubernetes is an option, the Bytewax materialization engine is likely the best option. + * If none of these engines suite your needs, you may continue using the in-process engine, or write a custom engine (e.g with Spark or Ray). + * **Stream data**: The Feast Push API is used within existing Spark / Beam pipelines to push feature values to offline / online stores -* Feast SDK is being triggered by CI (eg, Github Actions). It applies the latest changes from the feature repo to the Feast registry -* Airflow manages materialization jobs to ingest data from DWH to the online store periodically -* For the stream ingestion Feast Python SDK is used in the existing Spark / Beam pipeline -* For Batch Materialization Engine: - * If your offline and online workloads are in Snowflake, the Snowflake Engine is likely the best option. - * If your offline and online workloads are not using Snowflake, but using Kubernetes is an option, the Bytewax engine is likely the best option. - * If none of these engines suite your needs, you may continue using the in-process engine, or write a custom engine. * Online features are served via the Python feature server over HTTP, or consumed using the Feast Python SDK. * Feast Python SDK is called locally to generate a training dataset diff --git a/docs/reference/data-sources/push.md b/docs/reference/data-sources/push.md index 035ee583604..a585de5688d 100644 --- a/docs/reference/data-sources/push.md +++ b/docs/reference/data-sources/push.md @@ -22,7 +22,7 @@ Streaming data sources are important sources of feature values. A typical setup Feast allows users to push features previously registered in a feature view to the online store for fresher features. It also allows users to push batches of stream data to the offline store by specifying that the push be directed to the offline store. This will push the data to the offline store declared in the repository configuration used to initialize the feature store. -## Example +## Example (basic) ### Defining a push source Note that the push schema needs to also include the entity. @@ -59,3 +59,24 @@ fs.push("push_source_name", feature_data_frame, to=PushMode.ONLINE_AND_OFFLINE) See also [Python feature server](../feature-servers/python-feature-server.md) for instructions on how to push data to a deployed feature server. +## Example (Spark Streaming) + +The default option to write features from a stream is to add the Python SDK into your existing PySpark pipeline. + +```python +from feast import FeatureStore + +store = FeatureStore(...) + +spark = SparkSession.builder.getOrCreate() + +streamingDF = spark.readStream.format(...).load() + +def feast_writer(spark_df): + pandas_df = spark_df.to_pandas() + store.push("driver_hourly_stats", pandas_df) + +streamingDF.writeStream.foreachBatch(feast_writer).start() +``` + +This can also be used under the hood by a contrib stream processor (see [Tutorial: Building streaming features](../../tutorials/building-streaming-features.md)) \ No newline at end of file From 7613342823ac493352d7030321a800206c15df23 Mon Sep 17 00:00:00 2001 From: Danny Chiao Date: Fri, 2 Sep 2022 11:55:24 -0700 Subject: [PATCH 15/46] docs: Link to tutorials in helm charts (#3169) * docs: Link to tutorials in helm charts Signed-off-by: Danny Chiao * fix release Signed-off-by: Danny Chiao Signed-off-by: Danny Chiao --- .releaserc.js | 4 ++-- docs/how-to-guides/running-feast-in-production.md | 8 ++++---- infra/charts/feast-feature-server/README.md | 5 ++++- infra/charts/feast-feature-server/README.md.gotmpl | 3 +++ infra/charts/feast/README.md | 3 +++ infra/charts/feast/README.md.gotmpl | 4 ++++ 6 files changed, 20 insertions(+), 7 deletions(-) diff --git a/.releaserc.js b/.releaserc.js index aadc4373e91..124ad8801c2 100644 --- a/.releaserc.js +++ b/.releaserc.js @@ -40,8 +40,8 @@ module.exports = { // Validate the type of release we are doing "verifyReleaseCmd": "./infra/scripts/validate-release.sh ${nextRelease.type} " + current_branch, - // Bump all version files and build UI / update yarn.lock - "prepareCmd": "python ./infra/scripts/release/bump_file_versions.py ${lastRelease.version} ${nextRelease.version}; make build-ui" + // Bump all version files and build UI / update yarn.lock / helm charts + "prepareCmd": "python ./infra/scripts/release/bump_file_versions.py ${lastRelease.version} ${nextRelease.version}; make build-ui; make build-helm-docs" }], ["@semantic-release/release-notes-generator", { diff --git a/docs/how-to-guides/running-feast-in-production.md b/docs/how-to-guides/running-feast-in-production.md index b7cd108bc3b..906445c1436 100644 --- a/docs/how-to-guides/running-feast-in-production.md +++ b/docs/how-to-guides/running-feast-in-production.md @@ -218,9 +218,9 @@ feature_vector = fs.get_online_features( ### 4.2. Deploy Feast feature servers on Kubernetes -To deploy a Feast feature server on Kubernetes, you can use the included helm chart. +To deploy a Feast feature server on Kubernetes, you can use the included [helm chart + tutorial](https://github.com/feast-dev/feast/tree/master/infra/charts/feast-feature-server) (which also has detailed instructions and an example tutorial). -See [helm chart](https://github.com/feast-dev/feast/tree/master/infra/charts/feast-feature-server) for configuration details. +**Basic steps** 1. Install [kubectl](https://kubernetes.io/docs/tasks/tools/install-kubectl/) and [helm 3](https://helm.sh/) 2. Add the Feast Helm repository and download the latest charts: @@ -229,14 +229,14 @@ helm repo add feast-charts https://feast-helm-charts.storage.googleapis.com helm repo update ``` -1. Run Helm Install +3. Run Helm Install ``` helm install feast-release feast-charts/feast-feature-server \ --set feature_store_yaml_base64=$(base64 feature_store.yaml) ``` -This chart will deploy a single service. The service must have read access to the registry file on cloud storage. It will keep a copy of the registry in their memory and periodically refresh it, so expect some delays in update propagation in exchange for better performance. +This will deploy a single service. The service must have read access to the registry file on cloud storage. It will keep a copy of the registry in their memory and periodically refresh it, so expect some delays in update propagation in exchange for better performance. ## 5. Using environment variables in your yaml configuration diff --git a/infra/charts/feast-feature-server/README.md b/infra/charts/feast-feature-server/README.md index 1ee114d9c88..2d229523cfb 100644 --- a/infra/charts/feast-feature-server/README.md +++ b/infra/charts/feast-feature-server/README.md @@ -18,6 +18,9 @@ A base64 encoded version of the `feature_store.yaml` file is needed. Helm instal helm install feast-feature-server feast-charts/feast-feature-server --set feature_store_yaml_base64=$(base64 feature_store.yaml) ``` +## Tutorial +See [here](https://github.com/feast-dev/feast/tree/master/examples/python-helm-demo) for a sample tutorial on testing this helm chart with a demo feature repository and a local Redis instance. + ## Values | Key | Type | Default | Description | @@ -27,7 +30,7 @@ helm install feast-feature-server feast-charts/feast-feature-server --set featur | fullnameOverride | string | `""` | | | image.pullPolicy | string | `"IfNotPresent"` | | | image.repository | string | `"feastdev/feature-server"` | Docker image for Feature Server repository | -| image.tag | string | `"0.23.0"` | The Docker image tag (can be overwritten if custom feature server deps are needed for on demand transforms) | +| image.tag | string | `"0.24.0"` | The Docker image tag (can be overwritten if custom feature server deps are needed for on demand transforms) | | imagePullSecrets | list | `[]` | | | livenessProbe.initialDelaySeconds | int | `30` | | | livenessProbe.periodSeconds | int | `30` | | diff --git a/infra/charts/feast-feature-server/README.md.gotmpl b/infra/charts/feast-feature-server/README.md.gotmpl index 75f28274663..a380113b2f2 100644 --- a/infra/charts/feast-feature-server/README.md.gotmpl +++ b/infra/charts/feast-feature-server/README.md.gotmpl @@ -18,6 +18,9 @@ A base64 encoded version of the `feature_store.yaml` file is needed. Helm instal helm install feast-feature-server feast-charts/feast-feature-server --set feature_store_yaml_base64=$(base64 feature_store.yaml) ``` +## Tutorial +See [here](https://github.com/feast-dev/feast/tree/master/examples/python-helm-demo) for a sample tutorial on testing this helm chart with a demo feature repository and a local Redis instance. + {{ template "chart.requirementsSection" . }} {{ template "chart.valuesSection" . }} \ No newline at end of file diff --git a/infra/charts/feast/README.md b/infra/charts/feast/README.md index 7a0f5f77aa1..2dd8c62250d 100644 --- a/infra/charts/feast/README.md +++ b/infra/charts/feast/README.md @@ -50,6 +50,9 @@ For the default configuration, please see the [Feature Server Configuration](htt For more details, please see: https://docs.feast.dev/how-to-guides/running-feast-in-production +## Example +See [here](https://github.com/feast-dev/feast/tree/master/examples/java-demo) for a sample tutorial on testing this helm chart with a demo feature repository and a local Redis instance. + ## Requirements | Repository | Name | Version | diff --git a/infra/charts/feast/README.md.gotmpl b/infra/charts/feast/README.md.gotmpl index e215858fe01..007dc854531 100644 --- a/infra/charts/feast/README.md.gotmpl +++ b/infra/charts/feast/README.md.gotmpl @@ -50,6 +50,10 @@ For the default configuration, please see the [Feature Server Configuration](htt For more details, please see: https://docs.feast.dev/how-to-guides/running-feast-in-production +## Example +See [here](https://github.com/feast-dev/feast/tree/master/examples/java-demo) for a sample tutorial on testing this helm chart with a demo feature repository and a local Redis instance. + + {{ template "chart.requirementsSection" . }} {{ template "chart.valuesSection" . }} \ No newline at end of file From fafa3e38211103e047649ed3664d5c7bece75c7f Mon Sep 17 00:00:00 2001 From: Danny Chiao Date: Fri, 2 Sep 2022 15:42:26 -0700 Subject: [PATCH 16/46] chore: Add more references to kubernetes for SEO for k8s (#3170) * chore: Add more references to kubernetes for SEO for k8s Signed-off-by: Danny Chiao --- docs/SUMMARY.md | 2 +- docs/how-to-guides/running-feast-in-production.md | 6 +++--- infra/charts/feast-feature-server/README.md | 2 +- infra/charts/feast-feature-server/README.md.gotmpl | 2 +- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/docs/SUMMARY.md b/docs/SUMMARY.md index 6ef1c9c49eb..e23af9bae42 100644 --- a/docs/SUMMARY.md +++ b/docs/SUMMARY.md @@ -49,7 +49,7 @@ * [Read features from the online store](how-to-guides/feast-snowflake-gcp-aws/read-features-from-the-online-store.md) * [Scaling Feast](how-to-guides/scaling-feast.md) * [Structuring Feature Repos](how-to-guides/structuring-repos.md) -* [Running Feast in production](how-to-guides/running-feast-in-production.md) +* [Running Feast in production (e.g. on Kubernetes)](how-to-guides/running-feast-in-production.md) * [Upgrading for Feast 0.20+](how-to-guides/automated-feast-upgrade.md) * [Customizing Feast](how-to-guides/customizing-feast/README.md) * [Adding a custom batch materialization engine](how-to-guides/customizing-feast/creating-a-custom-materialization-engine.md) diff --git a/docs/how-to-guides/running-feast-in-production.md b/docs/how-to-guides/running-feast-in-production.md index 906445c1436..95f3e99581e 100644 --- a/docs/how-to-guides/running-feast-in-production.md +++ b/docs/how-to-guides/running-feast-in-production.md @@ -1,8 +1,8 @@ -# Running Feast in production +# Running Feast in production (e.g. on Kubernetes) ## Overview -After learning about Feast concepts and playing with Feast locally, you're now ready to use Feast in production. This guide aims to help with the transition from a sandbox project to production-grade deployment in the cloud or on-premise. +After learning about Feast concepts and playing with Feast locally, you're now ready to use Feast in production. This guide aims to help with the transition from a sandbox project to production-grade deployment in the cloud or on-premise (e.g. on Kubernetes). A typical production architecture looks like: @@ -58,7 +58,7 @@ This approach may not scale to large amounts of data, which users of Feast may b In this case, we recommend using one of the more [scalable materialization engines](./scaling-feast.md#scaling-materialization), such as the [Bytewax Materialization Engine](../reference/batch-materialization/bytewax.md), or the [Snowflake Materialization Engine](../reference/batch-materialization/snowflake.md). Users may also need to [write a custom materialization engine](../how-to-guides/customizing-feast/creating-a-custom-materialization-engine.md) to work on their existing infrastructure. -The Bytewax materialization engine can run materialization on existing kubernetes cluster. An example configuration of this in a `feature_store.yaml` is as follows: +The Bytewax materialization engine can run materialization on an existing Kubernetes cluster. An example configuration of this in a `feature_store.yaml` is as follows: ```yaml batch_engine: diff --git a/infra/charts/feast-feature-server/README.md b/infra/charts/feast-feature-server/README.md index 2d229523cfb..b52e78e4f8b 100644 --- a/infra/charts/feast-feature-server/README.md +++ b/infra/charts/feast-feature-server/README.md @@ -11,7 +11,7 @@ helm repo add feast-charts https://feast-helm-charts.storage.googleapis.com helm repo update ``` -Install Feast +Install Feast Feature Server on Kubernetes A base64 encoded version of the `feature_store.yaml` file is needed. Helm install example: ``` diff --git a/infra/charts/feast-feature-server/README.md.gotmpl b/infra/charts/feast-feature-server/README.md.gotmpl index a380113b2f2..fb877208e06 100644 --- a/infra/charts/feast-feature-server/README.md.gotmpl +++ b/infra/charts/feast-feature-server/README.md.gotmpl @@ -11,7 +11,7 @@ helm repo add feast-charts https://feast-helm-charts.storage.googleapis.com helm repo update ``` -Install Feast +Install Feast Feature Server on Kubernetes A base64 encoded version of the `feature_store.yaml` file is needed. Helm install example: ``` From 41cb476fcf1a8664fb421886feb89dd1dc638a31 Mon Sep 17 00:00:00 2001 From: Felix Wang Date: Fri, 2 Sep 2022 16:00:30 -0700 Subject: [PATCH 17/46] chore: Use push source in templates (#3164) * Modify local templates to actually use the push source Signed-off-by: Felix Wang * Fix AWS template Signed-off-by: Felix Wang * Fix GCP template Signed-off-by: Felix Wang * Fix Postgres template Signed-off-by: Felix Wang * Fix Cassandra template Signed-off-by: Felix Wang * Fix Hbase template Signed-off-by: Felix Wang * Fix Snowflake template Signed-off-by: Felix Wang * Fix bug with duplicate features requested Signed-off-by: Danny Chiao Signed-off-by: Felix Wang Signed-off-by: Danny Chiao Co-authored-by: Danny Chiao --- .../aws/feature_repo/example_repo.py | 51 ++++++++++++++++--- .../aws/feature_repo/test_workflow.py | 22 +++++--- .../cassandra/feature_repo/example_repo.py | 51 ++++++++++++++++--- .../cassandra/feature_repo/test_workflow.py | 26 ++++++---- .../gcp/feature_repo/example_repo.py | 51 ++++++++++++++++--- .../gcp/feature_repo/test_workflow.py | 27 ++++++---- .../hbase/feature_repo/example_repo.py | 51 ++++++++++++++++--- .../hbase/feature_repo/test_workflow.py | 22 +++++--- sdk/python/feast/templates/local/README.md | 8 +-- .../local/feature_repo/example_repo.py | 51 ++++++++++++++++--- .../local/feature_repo/test_workflow.py | 22 +++++--- .../postgres/feature_repo/example_repo.py | 51 ++++++++++++++++--- .../postgres/feature_repo/test_workflow.py | 22 +++++--- .../feast/templates/snowflake/README.md | 6 ++- .../snowflake/feature_repo/driver_repo.py | 51 ++++++++++++++++--- .../templates/snowflake/test_workflow.py | 24 +++++---- 16 files changed, 428 insertions(+), 108 deletions(-) diff --git a/sdk/python/feast/templates/aws/feature_repo/example_repo.py b/sdk/python/feast/templates/aws/feature_repo/example_repo.py index eaa1a1bfd4c..dd5a9c925b7 100644 --- a/sdk/python/feast/templates/aws/feature_repo/example_repo.py +++ b/sdk/python/feast/templates/aws/feature_repo/example_repo.py @@ -59,12 +59,6 @@ tags={"team": "driver_performance"}, ) -# Defines a way to push data (to be available offline, online or both) into Feast. -driver_stats_push_source = PushSource( - name="driver_stats_push_source", - batch_source=driver_stats_source, -) - # Define a request data source which encodes features / information only # available at request time (e.g. part of the user initiated HTTP request) input_request = RequestSource( @@ -103,3 +97,48 @@ def transformed_conv_rate(inputs: pd.DataFrame) -> pd.DataFrame: driver_activity_v2 = FeatureService( name="driver_activity_v2", features=[driver_stats_fv, transformed_conv_rate] ) + +# Defines a way to push data (to be available offline, online or both) into Feast. +driver_stats_push_source = PushSource( + name="driver_stats_push_source", + batch_source=driver_stats_source, +) + +# Defines a slightly modified version of the feature view from above, where the source +# has been changed to the push source. This allows fresh features to be directly pushed +# to the online store for this feature view. +driver_stats_fresh_fv = FeatureView( + name="driver_hourly_stats_fresh", + entities=[driver], + ttl=timedelta(days=1), + schema=[ + Field(name="conv_rate", dtype=Float32), + Field(name="acc_rate", dtype=Float32), + Field(name="avg_daily_trips", dtype=Int64), + ], + online=True, + source=driver_stats_push_source, # Changed from above + tags={"team": "driver_performance"}, +) + + +# Define an on demand feature view which can generate new features based on +# existing feature views and RequestSource features +@on_demand_feature_view( + sources=[driver_stats_fresh_fv, input_request], # relies on fresh version of FV + schema=[ + Field(name="conv_rate_plus_val1", dtype=Float64), + Field(name="conv_rate_plus_val2", dtype=Float64), + ], +) +def transformed_conv_rate_fresh(inputs: pd.DataFrame) -> pd.DataFrame: + df = pd.DataFrame() + df["conv_rate_plus_val1"] = inputs["conv_rate"] + inputs["val_to_add"] + df["conv_rate_plus_val2"] = inputs["conv_rate"] + inputs["val_to_add_2"] + return df + + +driver_activity_v3 = FeatureService( + name="driver_activity_v3", + features=[driver_stats_fresh_fv, transformed_conv_rate_fresh], +) diff --git a/sdk/python/feast/templates/aws/feature_repo/test_workflow.py b/sdk/python/feast/templates/aws/feature_repo/test_workflow.py index 0d5b2714d9b..494b1914f92 100644 --- a/sdk/python/feast/templates/aws/feature_repo/test_workflow.py +++ b/sdk/python/feast/templates/aws/feature_repo/test_workflow.py @@ -22,20 +22,25 @@ def run_demo(): store.materialize_incremental(end_date=datetime.now()) print("\n--- Online features ---") - fetch_online_features(store, use_feature_service=False) + fetch_online_features(store) print("\n--- Online features retrieved (instead) through a feature service---") - fetch_online_features(store, use_feature_service=True) + fetch_online_features(store, source="feature_service") + + print( + "\n--- Online features retrieved (using feature service v3, which uses a feature view with a push source---" + ) + fetch_online_features(store, source="push") print("\n--- Simulate a stream event ingestion of the hourly stats df ---") event_df = pd.DataFrame.from_dict( { "driver_id": [1001], "event_timestamp": [ - datetime(2021, 5, 13, 10, 59, 42), + datetime.now(), ], "created": [ - datetime(2021, 5, 13, 10, 59, 42), + datetime.now(), ], "conv_rate": [1.0], "acc_rate": [1.0], @@ -46,7 +51,7 @@ def run_demo(): store.push("driver_stats_push_source", event_df, to=PushMode.ONLINE_AND_OFFLINE) print("\n--- Online features again with updated values from a stream push---") - fetch_online_features(store, use_feature_service=True) + fetch_online_features(store, source="push") print("\n--- Run feast teardown ---") subprocess.run(["feast", "teardown"]) @@ -89,7 +94,7 @@ def fetch_historical_features_entity_df(store: FeatureStore, for_batch_scoring: print(training_df.head()) -def fetch_online_features(store, use_feature_service: bool): +def fetch_online_features(store, source: str = ""): entity_rows = [ # {join_key: entity_value} { @@ -103,12 +108,13 @@ def fetch_online_features(store, use_feature_service: bool): "val_to_add_2": 2002, }, ] - if use_feature_service: + if source == "feature_service": features_to_fetch = store.get_feature_service("driver_activity_v1") + elif source == "push": + features_to_fetch = store.get_feature_service("driver_activity_v3") else: features_to_fetch = [ "driver_hourly_stats:acc_rate", - "driver_hourly_stats:avg_daily_trips", "transformed_conv_rate:conv_rate_plus_val1", "transformed_conv_rate:conv_rate_plus_val2", ] diff --git a/sdk/python/feast/templates/cassandra/feature_repo/example_repo.py b/sdk/python/feast/templates/cassandra/feature_repo/example_repo.py index b3c71154825..131f1bcaa61 100644 --- a/sdk/python/feast/templates/cassandra/feature_repo/example_repo.py +++ b/sdk/python/feast/templates/cassandra/feature_repo/example_repo.py @@ -54,12 +54,6 @@ tags={"team": "driver_performance"}, ) -# Defines a way to push data (to be available offline, online or both) into Feast. -driver_stats_push_source = PushSource( - name="driver_stats_push_source", - batch_source=driver_stats_source, -) - # Define a request data source which encodes features / information only # available at request time (e.g. part of the user initiated HTTP request) input_request = RequestSource( @@ -98,3 +92,48 @@ def transformed_conv_rate(inputs: pd.DataFrame) -> pd.DataFrame: driver_activity_v2 = FeatureService( name="driver_activity_v2", features=[driver_stats_fv, transformed_conv_rate] ) + +# Defines a way to push data (to be available offline, online or both) into Feast. +driver_stats_push_source = PushSource( + name="driver_stats_push_source", + batch_source=driver_stats_source, +) + +# Defines a slightly modified version of the feature view from above, where the source +# has been changed to the push source. This allows fresh features to be directly pushed +# to the online store for this feature view. +driver_stats_fresh_fv = FeatureView( + name="driver_hourly_stats_fresh", + entities=[driver], + ttl=timedelta(days=1), + schema=[ + Field(name="conv_rate", dtype=Float32), + Field(name="acc_rate", dtype=Float32), + Field(name="avg_daily_trips", dtype=Int64), + ], + online=True, + source=driver_stats_push_source, # Changed from above + tags={"team": "driver_performance"}, +) + + +# Define an on demand feature view which can generate new features based on +# existing feature views and RequestSource features +@on_demand_feature_view( + sources=[driver_stats_fresh_fv, input_request], # relies on fresh version of FV + schema=[ + Field(name="conv_rate_plus_val1", dtype=Float64), + Field(name="conv_rate_plus_val2", dtype=Float64), + ], +) +def transformed_conv_rate_fresh(inputs: pd.DataFrame) -> pd.DataFrame: + df = pd.DataFrame() + df["conv_rate_plus_val1"] = inputs["conv_rate"] + inputs["val_to_add"] + df["conv_rate_plus_val2"] = inputs["conv_rate"] + inputs["val_to_add_2"] + return df + + +driver_activity_v3 = FeatureService( + name="driver_activity_v3", + features=[driver_stats_fresh_fv, transformed_conv_rate_fresh], +) diff --git a/sdk/python/feast/templates/cassandra/feature_repo/test_workflow.py b/sdk/python/feast/templates/cassandra/feature_repo/test_workflow.py index 2c388deea9e..eebeb113115 100644 --- a/sdk/python/feast/templates/cassandra/feature_repo/test_workflow.py +++ b/sdk/python/feast/templates/cassandra/feature_repo/test_workflow.py @@ -22,17 +22,25 @@ def run_demo(): store.materialize_incremental(end_date=datetime.now()) print("\n--- Online features ---") - fetch_online_features(store, use_feature_service=False) + fetch_online_features(store) + + print("\n--- Online features retrieved (instead) through a feature service---") + fetch_online_features(store, source="feature_service") + + print( + "\n--- Online features retrieved (using feature service v3, which uses a feature view with a push source---" + ) + fetch_online_features(store, source="push") print("\n--- Simulate a stream event ingestion of the hourly stats df ---") event_df = pd.DataFrame.from_dict( { "driver_id": [1001], "event_timestamp": [ - datetime(2021, 5, 13, 10, 59, 42), + datetime.now(), ], "created": [ - datetime(2021, 5, 13, 10, 59, 42), + datetime.now(), ], "conv_rate": [1.0], "acc_rate": [1.0], @@ -43,10 +51,7 @@ def run_demo(): store.push("driver_stats_push_source", event_df, to=PushMode.ONLINE_AND_OFFLINE) print("\n--- Online features again with updated values from a stream push---") - fetch_online_features(store, use_feature_service=True) - - print("\n--- Online features retrieved (instead) through a feature service---") - fetch_online_features(store, use_feature_service=True) + fetch_online_features(store, source="push") print("\n--- Run feast teardown ---") subprocess.run(["feast", "teardown"]) @@ -89,7 +94,7 @@ def fetch_historical_features_entity_df(store: FeatureStore, for_batch_scoring: print(training_df.head()) -def fetch_online_features(store, use_feature_service: bool): +def fetch_online_features(store, source: str = ""): entity_rows = [ # {join_key: entity_value} { @@ -103,12 +108,13 @@ def fetch_online_features(store, use_feature_service: bool): "val_to_add_2": 2002, }, ] - if use_feature_service: + if source == "feature_service": features_to_fetch = store.get_feature_service("driver_activity_v1") + elif source == "push": + features_to_fetch = store.get_feature_service("driver_activity_v3") else: features_to_fetch = [ "driver_hourly_stats:acc_rate", - "driver_hourly_stats:avg_daily_trips", "transformed_conv_rate:conv_rate_plus_val1", "transformed_conv_rate:conv_rate_plus_val2", ] diff --git a/sdk/python/feast/templates/gcp/feature_repo/example_repo.py b/sdk/python/feast/templates/gcp/feature_repo/example_repo.py index ab2f696ef23..81e06c72018 100644 --- a/sdk/python/feast/templates/gcp/feature_repo/example_repo.py +++ b/sdk/python/feast/templates/gcp/feature_repo/example_repo.py @@ -63,12 +63,6 @@ tags={"team": "driver_performance"}, ) -# Defines a way to push data (to be available offline, online or both) into Feast. -driver_stats_push_source = PushSource( - name="driver_stats_push_source", - batch_source=driver_stats_source, -) - # Define a request data source which encodes features / information only # available at request time (e.g. part of the user initiated HTTP request) input_request = RequestSource( @@ -107,3 +101,48 @@ def transformed_conv_rate(inputs: pd.DataFrame) -> pd.DataFrame: driver_activity_v2 = FeatureService( name="driver_activity_v2", features=[driver_stats_fv, transformed_conv_rate] ) + +# Defines a way to push data (to be available offline, online or both) into Feast. +driver_stats_push_source = PushSource( + name="driver_stats_push_source", + batch_source=driver_stats_source, +) + +# Defines a slightly modified version of the feature view from above, where the source +# has been changed to the push source. This allows fresh features to be directly pushed +# to the online store for this feature view. +driver_stats_fresh_fv = FeatureView( + name="driver_hourly_stats_fresh", + entities=[driver], + ttl=timedelta(weeks=52 * 10), # Set to be very long for example purposes only + schema=[ + Field(name="conv_rate", dtype=Float32), + Field(name="acc_rate", dtype=Float32), + Field(name="avg_daily_trips", dtype=Int64), + ], + online=True, + source=driver_stats_push_source, # Changed from above + tags={"team": "driver_performance"}, +) + + +# Define an on demand feature view which can generate new features based on +# existing feature views and RequestSource features +@on_demand_feature_view( + sources=[driver_stats_fresh_fv, input_request], # relies on fresh version of FV + schema=[ + Field(name="conv_rate_plus_val1", dtype=Float64), + Field(name="conv_rate_plus_val2", dtype=Float64), + ], +) +def transformed_conv_rate_fresh(inputs: pd.DataFrame) -> pd.DataFrame: + df = pd.DataFrame() + df["conv_rate_plus_val1"] = inputs["conv_rate"] + inputs["val_to_add"] + df["conv_rate_plus_val2"] = inputs["conv_rate"] + inputs["val_to_add_2"] + return df + + +driver_activity_v3 = FeatureService( + name="driver_activity_v3", + features=[driver_stats_fresh_fv, transformed_conv_rate_fresh], +) diff --git a/sdk/python/feast/templates/gcp/feature_repo/test_workflow.py b/sdk/python/feast/templates/gcp/feature_repo/test_workflow.py index 0f8d8894772..51ce1eb05be 100644 --- a/sdk/python/feast/templates/gcp/feature_repo/test_workflow.py +++ b/sdk/python/feast/templates/gcp/feature_repo/test_workflow.py @@ -1,3 +1,4 @@ +import random import subprocess from datetime import datetime @@ -22,24 +23,29 @@ def run_demo(): store.materialize_incremental(end_date=datetime.now()) print("\n--- Online features ---") - fetch_online_features(store, use_feature_service=False) + fetch_online_features(store) print("\n--- Online features retrieved (instead) through a feature service---") - fetch_online_features(store, use_feature_service=True) + fetch_online_features(store, source="feature_service") + + print( + "\n--- Online features retrieved (using feature service v3, which uses a feature view with a push source---" + ) + fetch_online_features(store, source="push") print("\n--- Simulate a stream event ingestion of the hourly stats df ---") event_df = pd.DataFrame.from_dict( { "driver_id": [1001], "event_timestamp": [ - datetime(2021, 5, 13, 10, 59, 42), + datetime.now(), ], "created": [ - datetime(2021, 5, 13, 10, 59, 42), + datetime.now(), ], "conv_rate": [1.0], - "acc_rate": [1.0], - "avg_daily_trips": [1000], + "acc_rate": [1.0 + random.random()], + "avg_daily_trips": [int(1000 * random.random())], } ) print(event_df) @@ -48,7 +54,7 @@ def run_demo(): store.push("driver_stats_push_source", event_df, to=PushMode.ONLINE) print("\n--- Online features again with updated values from a stream push---") - fetch_online_features(store, use_feature_service=True) + fetch_online_features(store, source="push") print("\n--- Run feast teardown ---") subprocess.run(["feast", "teardown"]) @@ -91,7 +97,7 @@ def fetch_historical_features_entity_df(store: FeatureStore, for_batch_scoring: print(training_df.head()) -def fetch_online_features(store, use_feature_service: bool): +def fetch_online_features(store, source: str = ""): entity_rows = [ # {join_key: entity_value} { @@ -105,12 +111,13 @@ def fetch_online_features(store, use_feature_service: bool): "val_to_add_2": 2002, }, ] - if use_feature_service: + if source == "feature_service": features_to_fetch = store.get_feature_service("driver_activity_v1") + elif source == "push": + features_to_fetch = store.get_feature_service("driver_activity_v3") else: features_to_fetch = [ "driver_hourly_stats:acc_rate", - "driver_hourly_stats:avg_daily_trips", "transformed_conv_rate:conv_rate_plus_val1", "transformed_conv_rate:conv_rate_plus_val2", ] diff --git a/sdk/python/feast/templates/hbase/feature_repo/example_repo.py b/sdk/python/feast/templates/hbase/feature_repo/example_repo.py index b3c71154825..131f1bcaa61 100644 --- a/sdk/python/feast/templates/hbase/feature_repo/example_repo.py +++ b/sdk/python/feast/templates/hbase/feature_repo/example_repo.py @@ -54,12 +54,6 @@ tags={"team": "driver_performance"}, ) -# Defines a way to push data (to be available offline, online or both) into Feast. -driver_stats_push_source = PushSource( - name="driver_stats_push_source", - batch_source=driver_stats_source, -) - # Define a request data source which encodes features / information only # available at request time (e.g. part of the user initiated HTTP request) input_request = RequestSource( @@ -98,3 +92,48 @@ def transformed_conv_rate(inputs: pd.DataFrame) -> pd.DataFrame: driver_activity_v2 = FeatureService( name="driver_activity_v2", features=[driver_stats_fv, transformed_conv_rate] ) + +# Defines a way to push data (to be available offline, online or both) into Feast. +driver_stats_push_source = PushSource( + name="driver_stats_push_source", + batch_source=driver_stats_source, +) + +# Defines a slightly modified version of the feature view from above, where the source +# has been changed to the push source. This allows fresh features to be directly pushed +# to the online store for this feature view. +driver_stats_fresh_fv = FeatureView( + name="driver_hourly_stats_fresh", + entities=[driver], + ttl=timedelta(days=1), + schema=[ + Field(name="conv_rate", dtype=Float32), + Field(name="acc_rate", dtype=Float32), + Field(name="avg_daily_trips", dtype=Int64), + ], + online=True, + source=driver_stats_push_source, # Changed from above + tags={"team": "driver_performance"}, +) + + +# Define an on demand feature view which can generate new features based on +# existing feature views and RequestSource features +@on_demand_feature_view( + sources=[driver_stats_fresh_fv, input_request], # relies on fresh version of FV + schema=[ + Field(name="conv_rate_plus_val1", dtype=Float64), + Field(name="conv_rate_plus_val2", dtype=Float64), + ], +) +def transformed_conv_rate_fresh(inputs: pd.DataFrame) -> pd.DataFrame: + df = pd.DataFrame() + df["conv_rate_plus_val1"] = inputs["conv_rate"] + inputs["val_to_add"] + df["conv_rate_plus_val2"] = inputs["conv_rate"] + inputs["val_to_add_2"] + return df + + +driver_activity_v3 = FeatureService( + name="driver_activity_v3", + features=[driver_stats_fresh_fv, transformed_conv_rate_fresh], +) diff --git a/sdk/python/feast/templates/hbase/feature_repo/test_workflow.py b/sdk/python/feast/templates/hbase/feature_repo/test_workflow.py index 76b8d7836c2..eebeb113115 100644 --- a/sdk/python/feast/templates/hbase/feature_repo/test_workflow.py +++ b/sdk/python/feast/templates/hbase/feature_repo/test_workflow.py @@ -22,20 +22,25 @@ def run_demo(): store.materialize_incremental(end_date=datetime.now()) print("\n--- Online features ---") - fetch_online_features(store, use_feature_service=False) + fetch_online_features(store) print("\n--- Online features retrieved (instead) through a feature service---") - fetch_online_features(store, use_feature_service=True) + fetch_online_features(store, source="feature_service") + + print( + "\n--- Online features retrieved (using feature service v3, which uses a feature view with a push source---" + ) + fetch_online_features(store, source="push") print("\n--- Simulate a stream event ingestion of the hourly stats df ---") event_df = pd.DataFrame.from_dict( { "driver_id": [1001], "event_timestamp": [ - datetime(2021, 5, 13, 10, 59, 42), + datetime.now(), ], "created": [ - datetime(2021, 5, 13, 10, 59, 42), + datetime.now(), ], "conv_rate": [1.0], "acc_rate": [1.0], @@ -46,7 +51,7 @@ def run_demo(): store.push("driver_stats_push_source", event_df, to=PushMode.ONLINE_AND_OFFLINE) print("\n--- Online features again with updated values from a stream push---") - fetch_online_features(store, use_feature_service=True) + fetch_online_features(store, source="push") print("\n--- Run feast teardown ---") subprocess.run(["feast", "teardown"]) @@ -89,7 +94,7 @@ def fetch_historical_features_entity_df(store: FeatureStore, for_batch_scoring: print(training_df.head()) -def fetch_online_features(store, use_feature_service: bool): +def fetch_online_features(store, source: str = ""): entity_rows = [ # {join_key: entity_value} { @@ -103,12 +108,13 @@ def fetch_online_features(store, use_feature_service: bool): "val_to_add_2": 2002, }, ] - if use_feature_service: + if source == "feature_service": features_to_fetch = store.get_feature_service("driver_activity_v1") + elif source == "push": + features_to_fetch = store.get_feature_service("driver_activity_v3") else: features_to_fetch = [ "driver_hourly_stats:acc_rate", - "driver_hourly_stats:avg_daily_trips", "transformed_conv_rate:conv_rate_plus_val1", "transformed_conv_rate:conv_rate_plus_val2", ] diff --git a/sdk/python/feast/templates/local/README.md b/sdk/python/feast/templates/local/README.md index 8133b6e84e3..daf3a686fbc 100644 --- a/sdk/python/feast/templates/local/README.md +++ b/sdk/python/feast/templates/local/README.md @@ -3,13 +3,15 @@ If you haven't already, check out the quickstart guide on Feast's website (http: uses this repo. A quick view of what's in this repository's `feature_repo/` directory: * `data/` contains raw demo parquet data -* `example_repo.py` contains demo feature definitions -* `feature_store.yaml` contains a demo setup configuring where data sources are -* `test_workflow.py` showcases how to run all key Feast commands, including defining, retrieving, and pushing features. +* `feature_repo/example_repo.py` contains demo feature definitions +* `feature_repo/feature_store.yaml` contains a demo setup configuring where data sources are +* `feature_repo/test_workflow.py` showcases how to run all key Feast commands, including defining, retrieving, and pushing features. You can run the overall workflow with `python test_workflow.py`. ## To move from this into a more production ready workflow: +> See more details in [Running Feast in production](https://docs.feast.dev/how-to-guides/running-feast-in-production) + 1. First: you should start with a different Feast template, which delegates to a more scalable offline store. - For example, running `feast init -t gcp` or `feast init -t aws` or `feast init -t snowflake`. diff --git a/sdk/python/feast/templates/local/feature_repo/example_repo.py b/sdk/python/feast/templates/local/feature_repo/example_repo.py index b3c71154825..131f1bcaa61 100644 --- a/sdk/python/feast/templates/local/feature_repo/example_repo.py +++ b/sdk/python/feast/templates/local/feature_repo/example_repo.py @@ -54,12 +54,6 @@ tags={"team": "driver_performance"}, ) -# Defines a way to push data (to be available offline, online or both) into Feast. -driver_stats_push_source = PushSource( - name="driver_stats_push_source", - batch_source=driver_stats_source, -) - # Define a request data source which encodes features / information only # available at request time (e.g. part of the user initiated HTTP request) input_request = RequestSource( @@ -98,3 +92,48 @@ def transformed_conv_rate(inputs: pd.DataFrame) -> pd.DataFrame: driver_activity_v2 = FeatureService( name="driver_activity_v2", features=[driver_stats_fv, transformed_conv_rate] ) + +# Defines a way to push data (to be available offline, online or both) into Feast. +driver_stats_push_source = PushSource( + name="driver_stats_push_source", + batch_source=driver_stats_source, +) + +# Defines a slightly modified version of the feature view from above, where the source +# has been changed to the push source. This allows fresh features to be directly pushed +# to the online store for this feature view. +driver_stats_fresh_fv = FeatureView( + name="driver_hourly_stats_fresh", + entities=[driver], + ttl=timedelta(days=1), + schema=[ + Field(name="conv_rate", dtype=Float32), + Field(name="acc_rate", dtype=Float32), + Field(name="avg_daily_trips", dtype=Int64), + ], + online=True, + source=driver_stats_push_source, # Changed from above + tags={"team": "driver_performance"}, +) + + +# Define an on demand feature view which can generate new features based on +# existing feature views and RequestSource features +@on_demand_feature_view( + sources=[driver_stats_fresh_fv, input_request], # relies on fresh version of FV + schema=[ + Field(name="conv_rate_plus_val1", dtype=Float64), + Field(name="conv_rate_plus_val2", dtype=Float64), + ], +) +def transformed_conv_rate_fresh(inputs: pd.DataFrame) -> pd.DataFrame: + df = pd.DataFrame() + df["conv_rate_plus_val1"] = inputs["conv_rate"] + inputs["val_to_add"] + df["conv_rate_plus_val2"] = inputs["conv_rate"] + inputs["val_to_add_2"] + return df + + +driver_activity_v3 = FeatureService( + name="driver_activity_v3", + features=[driver_stats_fresh_fv, transformed_conv_rate_fresh], +) diff --git a/sdk/python/feast/templates/local/feature_repo/test_workflow.py b/sdk/python/feast/templates/local/feature_repo/test_workflow.py index 76b8d7836c2..eebeb113115 100644 --- a/sdk/python/feast/templates/local/feature_repo/test_workflow.py +++ b/sdk/python/feast/templates/local/feature_repo/test_workflow.py @@ -22,20 +22,25 @@ def run_demo(): store.materialize_incremental(end_date=datetime.now()) print("\n--- Online features ---") - fetch_online_features(store, use_feature_service=False) + fetch_online_features(store) print("\n--- Online features retrieved (instead) through a feature service---") - fetch_online_features(store, use_feature_service=True) + fetch_online_features(store, source="feature_service") + + print( + "\n--- Online features retrieved (using feature service v3, which uses a feature view with a push source---" + ) + fetch_online_features(store, source="push") print("\n--- Simulate a stream event ingestion of the hourly stats df ---") event_df = pd.DataFrame.from_dict( { "driver_id": [1001], "event_timestamp": [ - datetime(2021, 5, 13, 10, 59, 42), + datetime.now(), ], "created": [ - datetime(2021, 5, 13, 10, 59, 42), + datetime.now(), ], "conv_rate": [1.0], "acc_rate": [1.0], @@ -46,7 +51,7 @@ def run_demo(): store.push("driver_stats_push_source", event_df, to=PushMode.ONLINE_AND_OFFLINE) print("\n--- Online features again with updated values from a stream push---") - fetch_online_features(store, use_feature_service=True) + fetch_online_features(store, source="push") print("\n--- Run feast teardown ---") subprocess.run(["feast", "teardown"]) @@ -89,7 +94,7 @@ def fetch_historical_features_entity_df(store: FeatureStore, for_batch_scoring: print(training_df.head()) -def fetch_online_features(store, use_feature_service: bool): +def fetch_online_features(store, source: str = ""): entity_rows = [ # {join_key: entity_value} { @@ -103,12 +108,13 @@ def fetch_online_features(store, use_feature_service: bool): "val_to_add_2": 2002, }, ] - if use_feature_service: + if source == "feature_service": features_to_fetch = store.get_feature_service("driver_activity_v1") + elif source == "push": + features_to_fetch = store.get_feature_service("driver_activity_v3") else: features_to_fetch = [ "driver_hourly_stats:acc_rate", - "driver_hourly_stats:avg_daily_trips", "transformed_conv_rate:conv_rate_plus_val1", "transformed_conv_rate:conv_rate_plus_val2", ] diff --git a/sdk/python/feast/templates/postgres/feature_repo/example_repo.py b/sdk/python/feast/templates/postgres/feature_repo/example_repo.py index a7ba9d7eace..0d1783e1e5e 100644 --- a/sdk/python/feast/templates/postgres/feature_repo/example_repo.py +++ b/sdk/python/feast/templates/postgres/feature_repo/example_repo.py @@ -46,12 +46,6 @@ tags={"team": "driver_performance"}, ) -# Defines a way to push data (to be available offline, online or both) into Feast. -driver_stats_push_source = PushSource( - name="driver_stats_push_source", - batch_source=driver_stats_source, -) - # Define a request data source which encodes features / information only # available at request time (e.g. part of the user initiated HTTP request) input_request = RequestSource( @@ -90,3 +84,48 @@ def transformed_conv_rate(inputs: pd.DataFrame) -> pd.DataFrame: driver_activity_v2 = FeatureService( name="driver_activity_v2", features=[driver_stats_fv, transformed_conv_rate] ) + +# Defines a way to push data (to be available offline, online or both) into Feast. +driver_stats_push_source = PushSource( + name="driver_stats_push_source", + batch_source=driver_stats_source, +) + +# Defines a slightly modified version of the feature view from above, where the source +# has been changed to the push source. This allows fresh features to be directly pushed +# to the online store for this feature view. +driver_stats_fresh_fv = FeatureView( + name="driver_hourly_stats_fresh", + entities=[driver], + ttl=timedelta(days=1), + schema=[ + Field(name="conv_rate", dtype=Float32), + Field(name="acc_rate", dtype=Float32), + Field(name="avg_daily_trips", dtype=Int64), + ], + online=True, + source=driver_stats_push_source, # Changed from above + tags={"team": "driver_performance"}, +) + + +# Define an on demand feature view which can generate new features based on +# existing feature views and RequestSource features +@on_demand_feature_view( + sources=[driver_stats_fresh_fv, input_request], # relies on fresh version of FV + schema=[ + Field(name="conv_rate_plus_val1", dtype=Float64), + Field(name="conv_rate_plus_val2", dtype=Float64), + ], +) +def transformed_conv_rate_fresh(inputs: pd.DataFrame) -> pd.DataFrame: + df = pd.DataFrame() + df["conv_rate_plus_val1"] = inputs["conv_rate"] + inputs["val_to_add"] + df["conv_rate_plus_val2"] = inputs["conv_rate"] + inputs["val_to_add_2"] + return df + + +driver_activity_v3 = FeatureService( + name="driver_activity_v3", + features=[driver_stats_fresh_fv, transformed_conv_rate_fresh], +) diff --git a/sdk/python/feast/templates/postgres/feature_repo/test_workflow.py b/sdk/python/feast/templates/postgres/feature_repo/test_workflow.py index ca5c1ccf42d..f657aba15f7 100644 --- a/sdk/python/feast/templates/postgres/feature_repo/test_workflow.py +++ b/sdk/python/feast/templates/postgres/feature_repo/test_workflow.py @@ -22,20 +22,25 @@ def run_demo(): store.materialize_incremental(end_date=datetime.now()) print("\n--- Online features ---") - fetch_online_features(store, use_feature_service=False) + fetch_online_features(store) print("\n--- Online features retrieved (instead) through a feature service---") - fetch_online_features(store, use_feature_service=True) + fetch_online_features(store, source="feature_service") + + print( + "\n--- Online features retrieved (using feature service v3, which uses a feature view with a push source---" + ) + fetch_online_features(store, source="push") print("\n--- Simulate a stream event ingestion of the hourly stats df ---") event_df = pd.DataFrame.from_dict( { "driver_id": [1001], "event_timestamp": [ - datetime(2021, 5, 13, 10, 59, 42), + datetime.now(), ], "created": [ - datetime(2021, 5, 13, 10, 59, 42), + datetime.now(), ], "conv_rate": [1.0], "acc_rate": [1.0], @@ -46,7 +51,7 @@ def run_demo(): store.push("driver_stats_push_source", event_df, to=PushMode.ONLINE_AND_OFFLINE) print("\n--- Online features again with updated values from a stream push---") - fetch_online_features(store, use_feature_service=True) + fetch_online_features(store, source="push") print("\n--- Run feast teardown ---") subprocess.run(["feast", "teardown"]) @@ -89,7 +94,7 @@ def fetch_historical_features_entity_df(store: FeatureStore, for_batch_scoring: print(training_df.head()) -def fetch_online_features(store, use_feature_service: bool): +def fetch_online_features(store, source: str = ""): entity_rows = [ # {join_key: entity_value} { @@ -103,12 +108,13 @@ def fetch_online_features(store, use_feature_service: bool): "val_to_add_2": 2002, }, ] - if use_feature_service: + if source == "feature_service": features_to_fetch = store.get_feature_service("driver_activity_v1") + elif source == "push": + features_to_fetch = store.get_feature_service("driver_activity_v3") else: features_to_fetch = [ "driver_hourly_stats:acc_rate", - "driver_hourly_stats:avg_daily_trips", "transformed_conv_rate:conv_rate_plus_val1", "transformed_conv_rate:conv_rate_plus_val2", ] diff --git a/sdk/python/feast/templates/snowflake/README.md b/sdk/python/feast/templates/snowflake/README.md index 0c950de4358..d4f1ef6faf4 100644 --- a/sdk/python/feast/templates/snowflake/README.md +++ b/sdk/python/feast/templates/snowflake/README.md @@ -2,13 +2,15 @@ A quick view of what's in this repository: * `data/` contains raw demo parquet data -* `driver_repo.py` contains demo feature definitions -* `feature_store.yaml` contains a demo setup configuring where data sources are +* `feature_repo/driver_repo.py` contains demo feature definitions +* `feature_repo/feature_store.yaml` contains a demo setup configuring where data sources are * `test_workflow.py` showcases how to run all key Feast commands, including defining, retrieving, and pushing features. You can run the overall workflow with `python test_workflow.py`. ## To move from this into a more production ready workflow: +> See more details in [Running Feast in production](https://docs.feast.dev/how-to-guides/running-feast-in-production) + 1. `feature_store.yaml` points to a local file as a registry. You'll want to setup a remote file (e.g. in S3/GCS) or a SQL registry. See [registry docs](https://docs.feast.dev/getting-started/concepts/registry) for more details. 2. Setup CI/CD + dev vs staging vs prod environments to automatically update the registry as you change Feast feature definitions. See [docs](https://docs.feast.dev/how-to-guides/running-feast-in-production#1.-automatically-deploying-changes-to-your-feature-definitions). diff --git a/sdk/python/feast/templates/snowflake/feature_repo/driver_repo.py b/sdk/python/feast/templates/snowflake/feature_repo/driver_repo.py index 4befa693f99..dd05dac8455 100644 --- a/sdk/python/feast/templates/snowflake/feature_repo/driver_repo.py +++ b/sdk/python/feast/templates/snowflake/feature_repo/driver_repo.py @@ -66,12 +66,6 @@ tags={"team": "driver_performance"}, ) -# Defines a way to push data (to be available offline, online or both) into Feast. -driver_stats_push_source = PushSource( - name="driver_stats_push_source", - batch_source=driver_stats_source, -) - # Define a request data source which encodes features / information only # available at request time (e.g. part of the user initiated HTTP request) input_request = RequestSource( @@ -110,3 +104,48 @@ def transformed_conv_rate(inputs: pd.DataFrame) -> pd.DataFrame: driver_activity_v2 = FeatureService( name="driver_activity_v2", features=[driver_stats_fv, transformed_conv_rate] ) + +# Defines a way to push data (to be available offline, online or both) into Feast. +driver_stats_push_source = PushSource( + name="driver_stats_push_source", + batch_source=driver_stats_source, +) + +# Defines a slightly modified version of the feature view from above, where the source +# has been changed to the push source. This allows fresh features to be directly pushed +# to the online store for this feature view. +driver_stats_fresh_fv = FeatureView( + name="driver_hourly_stats_fresh", + entities=[driver], + ttl=timedelta(weeks=52 * 10), # Set to be very long for example purposes only + schema=[ + Field(name="conv_rate", dtype=Float32), + Field(name="acc_rate", dtype=Float32), + Field(name="avg_daily_trips", dtype=Int64), + ], + online=True, + source=driver_stats_push_source, # Changed from above + tags={"team": "driver_performance"}, +) + + +# Define an on demand feature view which can generate new features based on +# existing feature views and RequestSource features +@on_demand_feature_view( + sources=[driver_stats_fresh_fv, input_request], # relies on fresh version of FV + schema=[ + Field(name="conv_rate_plus_val1", dtype=Float64), + Field(name="conv_rate_plus_val2", dtype=Float64), + ], +) +def transformed_conv_rate_fresh(inputs: pd.DataFrame) -> pd.DataFrame: + df = pd.DataFrame() + df["conv_rate_plus_val1"] = inputs["conv_rate"] + inputs["val_to_add"] + df["conv_rate_plus_val2"] = inputs["conv_rate"] + inputs["val_to_add_2"] + return df + + +driver_activity_v3 = FeatureService( + name="driver_activity_v3", + features=[driver_stats_fresh_fv, transformed_conv_rate_fresh], +) diff --git a/sdk/python/feast/templates/snowflake/test_workflow.py b/sdk/python/feast/templates/snowflake/test_workflow.py index 6f5e33622a8..adeed279af5 100644 --- a/sdk/python/feast/templates/snowflake/test_workflow.py +++ b/sdk/python/feast/templates/snowflake/test_workflow.py @@ -23,20 +23,25 @@ def run_demo(): store.materialize_incremental(end_date=datetime.now()) print("\n--- Online features ---") - fetch_online_features(store, use_feature_service=False) + fetch_online_features(store) print("\n--- Online features retrieved (instead) through a feature service---") - fetch_online_features(store, use_feature_service=True) + fetch_online_features(store, source="feature_service") + + print( + "\n--- Online features retrieved (using feature service v3, which uses a feature view with a push source---" + ) + fetch_online_features(store, source="push") print("\n--- Simulate a stream event ingestion of the hourly stats df ---") event_df = pd.DataFrame.from_dict( { "driver_id": [1001], "event_timestamp": [ - datetime(2021, 5, 13, 10, 59, 42), + datetime.now(), ], "created": [ - datetime(2021, 5, 13, 10, 59, 42), + datetime.now(), ], "conv_rate": [1.0], "acc_rate": [1.0], @@ -46,8 +51,8 @@ def run_demo(): print(event_df) store.push("driver_stats_push_source", event_df, to=PushMode.ONLINE_AND_OFFLINE) - print("\n--- Online features again with updated values from a stream push---") - fetch_online_features(store, use_feature_service=True) + print("\n--- Online features again with updated values from a stream push ---") + fetch_online_features(store, source="push") print("\n--- Run feast teardown ---") command = "cd feature_repo; feast teardown" @@ -91,7 +96,7 @@ def fetch_historical_features_entity_df(store: FeatureStore, for_batch_scoring: print(training_df.head()) -def fetch_online_features(store, use_feature_service: bool): +def fetch_online_features(store, source: str = ""): entity_rows = [ # {join_key: entity_value} { @@ -105,12 +110,13 @@ def fetch_online_features(store, use_feature_service: bool): "val_to_add_2": 2002, }, ] - if use_feature_service: + if source == "feature_service": features_to_fetch = store.get_feature_service("driver_activity_v1") + elif source == "push": + features_to_fetch = store.get_feature_service("driver_activity_v3") else: features_to_fetch = [ "driver_hourly_stats:acc_rate", - "driver_hourly_stats:avg_daily_trips", "transformed_conv_rate:conv_rate_plus_val1", "transformed_conv_rate:conv_rate_plus_val2", ] From 219d04adbfa7966ff6037ee1766171538e5599d4 Mon Sep 17 00:00:00 2001 From: Felix Wang Date: Fri, 2 Sep 2022 16:02:26 -0700 Subject: [PATCH 18/46] docs: Functionality matrix for online stores (#3007) * Cleanup online store docs Signed-off-by: Felix Wang * Add overview page Signed-off-by: Felix Wang * Add Snowflake online store to RTD Signed-off-by: Felix Wang * Online store docs Signed-off-by: Felix Wang * add overiew to README in online store Signed-off-by: Danny Chiao Signed-off-by: Felix Wang Signed-off-by: Danny Chiao Co-authored-by: Danny Chiao --- docs/SUMMARY.md | 1 + .../online-store.md | 13 +++-- docs/reference/online-stores/README.md | 4 ++ docs/reference/online-stores/cassandra.md | 29 +++++++++- docs/reference/online-stores/datastore.md | 28 +++++++++- docs/reference/online-stores/dynamodb.md | 28 +++++++++- docs/reference/online-stores/overview.md | 54 +++++++++++++++++++ docs/reference/online-stores/postgres.md | 28 +++++++++- docs/reference/online-stores/redis.md | 34 ++++++++++-- docs/reference/online-stores/snowflake.md | 28 ++++++++++ docs/reference/online-stores/sqlite.md | 28 +++++++++- sdk/python/docs/index.rst | 9 ++++ sdk/python/docs/source/index.rst | 9 ++++ 13 files changed, 279 insertions(+), 14 deletions(-) create mode 100644 docs/reference/online-stores/overview.md diff --git a/docs/SUMMARY.md b/docs/SUMMARY.md index e23af9bae42..430f9a745d7 100644 --- a/docs/SUMMARY.md +++ b/docs/SUMMARY.md @@ -86,6 +86,7 @@ * [Trino (contrib)](reference/offline-stores/trino.md) * [Azure Synapse + Azure SQL (contrib)](reference/offline-stores/mssql.md) * [Online stores](reference/online-stores/README.md) + * [Overview](reference/online-stores/overview.md) * [SQLite](reference/online-stores/sqlite.md) * [Snowflake](reference/online-stores/snowflake.md) * [Redis](reference/online-stores/redis.md) diff --git a/docs/getting-started/architecture-and-components/online-store.md b/docs/getting-started/architecture-and-components/online-store.md index 21b4dbcb9c7..980089c4fe8 100644 --- a/docs/getting-started/architecture-and-components/online-store.md +++ b/docs/getting-started/architecture-and-components/online-store.md @@ -1,15 +1,18 @@ # Online store -The Feast online store is used for low-latency online feature value lookups. Feature values are loaded into the online store from data sources in feature views using the `materialize` command. +Feast uses online stores to serve features at low latency. +Feature values are loaded from data sources into the online store through _materialization_, which can be triggered through the `materialize` command. -The storage schema of features within the online store mirrors that of the data source used to populate the online store. One key difference between the online store and data sources is that only the latest feature values are stored per entity key. No historical values are stored. +The storage schema of features within the online store mirrors that of the original data source. +One key difference is that for each [entity key](../concepts/entity.md), only the latest feature values are stored. +No historical values are stored. -Example batch data source +Here is an example batch data source: ![](../../.gitbook/assets/image%20%286%29.png) -Once the above data source is materialized into Feast \(using `feast materialize`\), the feature values will be stored as follows: +Once the above data source is materialized into Feast (using `feast materialize`), the feature values will be stored as follows: ![](../../.gitbook/assets/image%20%285%29.png) -Features can also be written to the online store via [push sources](../../reference/data-sources/push.md) \ No newline at end of file +Features can also be written directly to the online store via [push sources](../../reference/data-sources/push.md) . \ No newline at end of file diff --git a/docs/reference/online-stores/README.md b/docs/reference/online-stores/README.md index 6d616b46f25..58c8705e9ee 100644 --- a/docs/reference/online-stores/README.md +++ b/docs/reference/online-stores/README.md @@ -2,6 +2,10 @@ Please see [Online Store](../../getting-started/architecture-and-components/online-store.md) for an explanation of online stores. +{% content-ref url="overview.md" %} +[overview.md](overview.md) +{% endcontent-ref %} + {% content-ref url="sqlite.md" %} [sqlite.md](sqlite.md) {% endcontent-ref %} diff --git a/docs/reference/online-stores/cassandra.md b/docs/reference/online-stores/cassandra.md index 3355c3728ce..48b7b73f439 100644 --- a/docs/reference/online-stores/cassandra.md +++ b/docs/reference/online-stores/cassandra.md @@ -55,7 +55,34 @@ online_store: ``` {% endcode %} +The full set of configuration options is available in [CassandraOnlineStoreConfig](https://rtd.feast.dev/en/master/#feast.infra.online_stores.contrib.cassandra_online_store.cassandra_online_store.CassandraOnlineStoreConfig). For a full explanation of configuration options please look at file `sdk/python/feast/infra/online_stores/contrib/cassandra_online_store/README.md`. -Storage specifications can be found at `docs/specs/online_store_format.md`. \ No newline at end of file +Storage specifications can be found at `docs/specs/online_store_format.md`. + +## Functionality Matrix + +The set of functionality supported by online stores is described in detail [here](overview.md#functionality). +Below is a matrix indicating which functionality is supported by the Cassandra online store. + +| | Cassandra | +| :-------------------------------------------------------- | :-- | +| write feature values to the online store | yes | +| read feature values from the online store | yes | +| update infrastructure (e.g. tables) in the online store | yes | +| teardown infrastructure (e.g. tables) in the online store | yes | +| generate a plan of infrastructure changes | yes | +| support for on-demand transforms | yes | +| readable by Python SDK | yes | +| readable by Java | no | +| readable by Go | no | +| support for entityless feature views | yes | +| support for concurrent writing to the same key | no | +| support for ttl (time to live) at retrieval | no | +| support for deleting expired data | no | +| collocated by feature view | yes | +| collocated by feature service | no | +| collocated by entity key | no | + +To compare this set of functionality against other online stores, please see the full [functionality matrix](overview.md#functionality-matrix). diff --git a/docs/reference/online-stores/datastore.md b/docs/reference/online-stores/datastore.md index ed1425abb68..0867853f15d 100644 --- a/docs/reference/online-stores/datastore.md +++ b/docs/reference/online-stores/datastore.md @@ -18,4 +18,30 @@ online_store: ``` {% endcode %} -Configuration options are available [here](https://rtd.feast.dev/en/latest/#feast.repo_config.DatastoreOnlineStoreConfig). +The full set of configuration options is available in [DatastoreOnlineStoreConfig](https://rtd.feast.dev/en/latest/#feast.infra.online_stores.datastore.DatastoreOnlineStoreConfig). + +## Functionality Matrix + +The set of functionality supported by online stores is described in detail [here](overview.md#functionality). +Below is a matrix indicating which functionality is supported by the Datastore online store. + +| | Datastore | +| :-------------------------------------------------------- | :-- | +| write feature values to the online store | yes | +| read feature values from the online store | yes | +| update infrastructure (e.g. tables) in the online store | yes | +| teardown infrastructure (e.g. tables) in the online store | yes | +| generate a plan of infrastructure changes | no | +| support for on-demand transforms | yes | +| readable by Python SDK | yes | +| readable by Java | no | +| readable by Go | no | +| support for entityless feature views | yes | +| support for concurrent writing to the same key | no | +| support for ttl (time to live) at retrieval | no | +| support for deleting expired data | no | +| collocated by feature view | yes | +| collocated by feature service | no | +| collocated by entity key | no | + +To compare this set of functionality against other online stores, please see the full [functionality matrix](overview.md#functionality-matrix). diff --git a/docs/reference/online-stores/dynamodb.md b/docs/reference/online-stores/dynamodb.md index f9f8b4339d7..2f94c768199 100644 --- a/docs/reference/online-stores/dynamodb.md +++ b/docs/reference/online-stores/dynamodb.md @@ -17,7 +17,7 @@ online_store: ``` {% endcode %} -Configuration options are available [here](https://github.com/feast-dev/feast/blob/17bfa6118d6658d2bff53d7de8e2ccef5681714d/sdk/python/feast/infra/online_stores/dynamodb.py#L36). +The full set of configuration options is available in [DynamoDBOnlineStoreConfig](https://rtd.feast.dev/en/master/#feast.infra.online_stores.dynamodb.DynamoDBOnlineStoreConfig). ## Permissions @@ -53,3 +53,29 @@ The following inline policy can be used to grant Feast the necessary permissions ``` Lastly, this IAM role needs to be associated with the desired Redshift cluster. Please follow the official AWS guide for the necessary steps [here](https://docs.aws.amazon.com/redshift/latest/dg/c-getting-started-using-spectrum-add-role.html). + +## Functionality Matrix + +The set of functionality supported by online stores is described in detail [here](overview.md#functionality). +Below is a matrix indicating which functionality is supported by the DynamoDB online store. + +| | DynamoDB | +| :-------------------------------------------------------- | :-- | +| write feature values to the online store | yes | +| read feature values from the online store | yes | +| update infrastructure (e.g. tables) in the online store | yes | +| teardown infrastructure (e.g. tables) in the online store | yes | +| generate a plan of infrastructure changes | no | +| support for on-demand transforms | yes | +| readable by Python SDK | yes | +| readable by Java | no | +| readable by Go | no | +| support for entityless feature views | yes | +| support for concurrent writing to the same key | no | +| support for ttl (time to live) at retrieval | no | +| support for deleting expired data | no | +| collocated by feature view | yes | +| collocated by feature service | no | +| collocated by entity key | no | + +To compare this set of functionality against other online stores, please see the full [functionality matrix](overview.md#functionality-matrix). diff --git a/docs/reference/online-stores/overview.md b/docs/reference/online-stores/overview.md new file mode 100644 index 00000000000..981a1aeeed0 --- /dev/null +++ b/docs/reference/online-stores/overview.md @@ -0,0 +1,54 @@ +# Overview + +## Functionality + +Here are the methods exposed by the `OnlineStore` interface, along with the core functionality supported by the method: +* `online_write_batch`: write feature values to the online store +* `online_read`: read feature values from the online store +* `update`: update infrastructure (e.g. tables) in the online store +* `teardown`: teardown infrastructure (e.g. tables) in the online store +* `plan`: generate a plan of infrastructure changes based on feature repo changes + +There is also additional functionality not properly captured by these interface methods: +* support for on-demand transforms +* readable by Python SDK +* readable by Java +* readable by Go +* support for entityless feature views +* support for concurrent writing to the same key +* support for ttl (time to live) at retrieval +* support for deleting expired data + +Finally, there are multiple data models for storing the features in the online store. For example, features could be: +* collocated by feature view +* collocated by feature service +* collocated by entity key + +See this [issue](https://github.com/feast-dev/feast/issues/2254) for a discussion around the tradeoffs of each of these data models. + +## Functionality Matrix + +There are currently five core online store implementations: `SqliteOnlineStore`, `RedisOnlineStore`, `DynamoDBOnlineStore`, `SnowflakeOnlineStore`, and `DatastoreOnlineStore`. +There are several additional implementations contributed by the Feast community (`PostgreSQLOnlineStore`, `HbaseOnlineStore`, and `CassandraOnlineStore`), which are not guaranteed to be stable or to match the functionality of the core implementations. +Details for each specific online store, such as how to configure it in a `feature_store.yaml`, can be found [here](README.md). + +Below is a matrix indicating which online stores support what functionality. + +| | Sqlite | Redis | DynamoDB | Snowflake | Datastore | Postgres | Hbase | Cassandra | +| :-------------------------------------------------------- | :-- | :-- | :-- | :-- | :-- | :-- | :-- | :-- | +| write feature values to the online store | yes | yes | yes | yes | yes | yes | yes | yes | +| read feature values from the online store | yes | yes | yes | yes | yes | yes | yes | yes | +| update infrastructure (e.g. tables) in the online store | yes | yes | yes | yes | yes | yes | yes | yes | +| teardown infrastructure (e.g. tables) in the online store | yes | yes | yes | yes | yes | yes | yes | yes | +| generate a plan of infrastructure changes | yes | no | no | no | no | no | no | yes | +| support for on-demand transforms | yes | yes | yes | yes | yes | yes | yes | yes | +| readable by Python SDK | yes | yes | yes | yes | yes | yes | yes | yes | +| readable by Java | no | yes | no | no | no | no | no | no | +| readable by Go | yes | yes | no | no | no | no | no | no | +| support for entityless feature views | yes | yes | yes | yes | yes | yes | yes | yes | +| support for concurrent writing to the same key | no | yes | no | no | no | no | no | no | +| support for ttl (time to live) at retrieval | no | yes | no | no | no | no | no | no | +| support for deleting expired data | no | yes | no | no | no | no | no | no | +| collocated by feature view | yes | no | yes | yes | yes | yes | yes | yes | +| collocated by feature service | no | no | no | no | no | no | no | no | +| collocated by entity key | no | yes | no | no | no | no | no | no | diff --git a/docs/reference/online-stores/postgres.md b/docs/reference/online-stores/postgres.md index 4f51dff6172..083c0006359 100644 --- a/docs/reference/online-stores/postgres.md +++ b/docs/reference/online-stores/postgres.md @@ -30,4 +30,30 @@ online_store: ``` {% endcode %} -Configuration options are available [here](https://rtd.feast.dev/en/latest/feast.infra.utils.postgres.html#module-feast.infra.utils.postgres.postgres_config). +The full set of configuration options is available in [PostgreSQLOnlineStoreConfig](https://rtd.feast.dev/en/master/#feast.infra.online_stores.contrib.postgres.PostgreSQLOnlineStoreConfig). + +## Functionality Matrix + +The set of functionality supported by online stores is described in detail [here](overview.md#functionality). +Below is a matrix indicating which functionality is supported by the Postgres online store. + +| | Postgres | +| :-------------------------------------------------------- | :-- | +| write feature values to the online store | yes | +| read feature values from the online store | yes | +| update infrastructure (e.g. tables) in the online store | yes | +| teardown infrastructure (e.g. tables) in the online store | yes | +| generate a plan of infrastructure changes | no | +| support for on-demand transforms | yes | +| readable by Python SDK | yes | +| readable by Java | no | +| readable by Go | no | +| support for entityless feature views | yes | +| support for concurrent writing to the same key | no | +| support for ttl (time to live) at retrieval | no | +| support for deleting expired data | no | +| collocated by feature view | yes | +| collocated by feature service | no | +| collocated by entity key | no | + +To compare this set of functionality against other online stores, please see the full [functionality matrix](overview.md#functionality-matrix). diff --git a/docs/reference/online-stores/redis.md b/docs/reference/online-stores/redis.md index 4388ccfa0ab..80e90348c55 100644 --- a/docs/reference/online-stores/redis.md +++ b/docs/reference/online-stores/redis.md @@ -4,12 +4,12 @@ The [Redis](https://redis.io) online store provides support for materializing feature values into Redis. -* Both Redis and Redis Cluster are supported +* Both Redis and Redis Cluster are supported. * The data model used to store feature values in Redis is described in more detail [here](../../specs/online\_store\_format.md). ## Examples -Connecting to a single Redis instance +Connecting to a single Redis instance: {% code title="feature_store.yaml" %} ```yaml @@ -22,7 +22,7 @@ online_store: ``` {% endcode %} -Connecting to a Redis Cluster with SSL enabled and password authentication +Connecting to a Redis Cluster with SSL enabled and password authentication: {% code title="feature_store.yaml" %} ```yaml @@ -36,4 +36,30 @@ online_store: ``` {% endcode %} -Configuration options are available [here](https://rtd.feast.dev/en/master/#feast.infra.online\_stores.redis.RedisOnlineStoreConfig). +The full set of configuration options is available in [RedisOnlineStoreConfig](https://rtd.feast.dev/en/latest/#feast.infra.online_stores.redis.RedisOnlineStoreConfig). + +## Functionality Matrix + +The set of functionality supported by online stores is described in detail [here](overview.md#functionality). +Below is a matrix indicating which functionality is supported by the Redis online store. + +| | Redis | +| :-------------------------------------------------------- | :-- | +| write feature values to the online store | yes | +| read feature values from the online store | yes | +| update infrastructure (e.g. tables) in the online store | yes | +| teardown infrastructure (e.g. tables) in the online store | yes | +| generate a plan of infrastructure changes | no | +| support for on-demand transforms | yes | +| readable by Python SDK | yes | +| readable by Java | yes | +| readable by Go | yes | +| support for entityless feature views | yes | +| support for concurrent writing to the same key | yes | +| support for ttl (time to live) at retrieval | yes | +| support for deleting expired data | yes | +| collocated by feature view | no | +| collocated by feature service | no | +| collocated by entity key | yes | + +To compare this set of functionality against other online stores, please see the full [functionality matrix](overview.md#functionality-matrix). diff --git a/docs/reference/online-stores/snowflake.md b/docs/reference/online-stores/snowflake.md index bf975fa7eab..7db45dd7bd8 100644 --- a/docs/reference/online-stores/snowflake.md +++ b/docs/reference/online-stores/snowflake.md @@ -33,3 +33,31 @@ online_store: database: SNOWFLAKE_DATABASE ``` {% endcode %} + +The full set of configuration options is available in [SnowflakeOnlineStoreConfig](https://rtd.feast.dev/en/latest/#feast.infra.online_stores.snowflake.SnowflakeOnlineStoreConfig). + +## Functionality Matrix + +The set of functionality supported by online stores is described in detail [here](overview.md#functionality). +Below is a matrix indicating which functionality is supported by the Snowflake online store. + +| | Snowflake | +| :-------------------------------------------------------- | :-- | +| write feature values to the online store | yes | +| read feature values from the online store | yes | +| update infrastructure (e.g. tables) in the online store | yes | +| teardown infrastructure (e.g. tables) in the online store | yes | +| generate a plan of infrastructure changes | no | +| support for on-demand transforms | yes | +| readable by Python SDK | yes | +| readable by Java | no | +| readable by Go | no | +| support for entityless feature views | yes | +| support for concurrent writing to the same key | no | +| support for ttl (time to live) at retrieval | no | +| support for deleting expired data | no | +| collocated by feature view | yes | +| collocated by feature service | no | +| collocated by entity key | no | + +To compare this set of functionality against other online stores, please see the full [functionality matrix](overview.md#functionality-matrix). diff --git a/docs/reference/online-stores/sqlite.md b/docs/reference/online-stores/sqlite.md index 668e6024e3c..859702c07f1 100644 --- a/docs/reference/online-stores/sqlite.md +++ b/docs/reference/online-stores/sqlite.md @@ -20,4 +20,30 @@ online_store: ``` {% endcode %} -Configuration options are available [here](https://rtd.feast.dev/en/latest/#feast.repo_config.SqliteOnlineStoreConfig). +The full set of configuration options is available in [SqliteOnlineStoreConfig](https://rtd.feast.dev/en/latest/#feast.infra.online_stores.sqlite.SqliteOnlineStoreConfig). + +## Functionality Matrix + +The set of functionality supported by online stores is described in detail [here](overview.md#functionality). +Below is a matrix indicating which functionality is supported by the Sqlite online store. + +| | Sqlite | +| :-------------------------------------------------------- | :-- | +| write feature values to the online store | yes | +| read feature values from the online store | yes | +| update infrastructure (e.g. tables) in the online store | yes | +| teardown infrastructure (e.g. tables) in the online store | yes | +| generate a plan of infrastructure changes | yes | +| support for on-demand transforms | yes | +| readable by Python SDK | yes | +| readable by Java | no | +| readable by Go | yes | +| support for entityless feature views | yes | +| support for concurrent writing to the same key | no | +| support for ttl (time to live) at retrieval | no | +| support for deleting expired data | no | +| collocated by feature view | yes | +| collocated by feature service | no | +| collocated by entity key | no | + +To compare this set of functionality against other online stores, please see the full [functionality matrix](overview.md#functionality-matrix). diff --git a/sdk/python/docs/index.rst b/sdk/python/docs/index.rst index beca384137d..ca96782db8a 100644 --- a/sdk/python/docs/index.rst +++ b/sdk/python/docs/index.rst @@ -353,6 +353,15 @@ Redis Online Store .. autoclass:: feast.infra.online_stores.redis.RedisOnlineStoreConfig :members: +Snowflake Online Store +------------------ + +.. autoclass:: feast.infra.online_stores.snowflake.SnowflakeOnlineStore + :members: + +.. autoclass:: feast.infra.online_stores.snowflake.SnowflakeOnlineStoreConfig + :members: + PostgreSQL Online Store ----------------------- diff --git a/sdk/python/docs/source/index.rst b/sdk/python/docs/source/index.rst index beca384137d..ca96782db8a 100644 --- a/sdk/python/docs/source/index.rst +++ b/sdk/python/docs/source/index.rst @@ -353,6 +353,15 @@ Redis Online Store .. autoclass:: feast.infra.online_stores.redis.RedisOnlineStoreConfig :members: +Snowflake Online Store +------------------ + +.. autoclass:: feast.infra.online_stores.snowflake.SnowflakeOnlineStore + :members: + +.. autoclass:: feast.infra.online_stores.snowflake.SnowflakeOnlineStoreConfig + :members: + PostgreSQL Online Store ----------------------- From 7274fe3f266e078eacd1a13b1c71938ac6e7cada Mon Sep 17 00:00:00 2001 From: Kevin Zhang Date: Fri, 2 Sep 2022 16:10:24 -0700 Subject: [PATCH 19/46] ci: Fix e2e test consistency flake (#3109) * Attemp fix Signed-off-by: Kevin Zhang * Fix Signed-off-by: Kevin Zhang * Fix Signed-off-by: Kevin Zhang * Revert Signed-off-by: Kevin Zhang * Revert Signed-off-by: Kevin Zhang * don't increase default materialization time Signed-off-by: Danny Chiao Signed-off-by: Kevin Zhang Signed-off-by: Danny Chiao Co-authored-by: Danny Chiao --- sdk/python/tests/utils/e2e_test_validation.py | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/sdk/python/tests/utils/e2e_test_validation.py b/sdk/python/tests/utils/e2e_test_validation.py index e2b8b14eb47..43bdbefc004 100644 --- a/sdk/python/tests/utils/e2e_test_validation.py +++ b/sdk/python/tests/utils/e2e_test_validation.py @@ -112,8 +112,17 @@ def _check_offline_and_online_features( full_feature_names=full_feature_names, ).to_dict() - if full_feature_names: + # Wait for materialization to occur + if not response_dict[f"{fv.name}__value"][0]: + # Deal with flake with a retry + time.sleep(10) + response_dict = fs.get_online_features( + [f"{fv.name}:value"], + [{"driver_id": driver_id}], + full_feature_names=full_feature_names, + ).to_dict() + if full_feature_names: if expected_value: assert response_dict[f"{fv.name}__value"][0], f"Response: {response_dict}" assert ( From b4ef834b4eb01937ade5302de478240e8e0a2651 Mon Sep 17 00:00:00 2001 From: Danny Chiao Date: Fri, 2 Sep 2022 20:15:08 -0700 Subject: [PATCH 20/46] ci: Fix bump files release after helm chart change Signed-off-by: Danny Chiao --- infra/scripts/release/files_to_bump.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/infra/scripts/release/files_to_bump.txt b/infra/scripts/release/files_to_bump.txt index e94ec88db0a..e43d637ede2 100644 --- a/infra/scripts/release/files_to_bump.txt +++ b/infra/scripts/release/files_to_bump.txt @@ -6,7 +6,7 @@ infra/charts/feast/charts/transformation-service/values.yaml 8 infra/charts/feast/charts/feature-server/Chart.yaml 4 5 infra/charts/feast/charts/feature-server/README.md 3 20 infra/charts/feast/charts/feature-server/values.yaml 8 -infra/charts/feast/README.md 11 58 59 +infra/charts/feast/README.md 11 61 62 infra/charts/feast-python-server/Chart.yaml 5 infra/charts/feast-python-server/README.md 5 infra/charts/feast-feature-server/Chart.yaml 5 From 7c50ab510633c11646b6ff04853f3f26942ad646 Mon Sep 17 00:00:00 2001 From: Danny Chiao Date: Wed, 7 Sep 2022 16:57:26 -0400 Subject: [PATCH 21/46] fix: Fix push API to respect feature view's already inferred entity types (#3172) * fix: Fix push API to respect feature view's already inferred entity types Signed-off-by: Danny Chiao * fix roadmap Signed-off-by: Danny Chiao * fix helm Signed-off-by: Danny Chiao Signed-off-by: Danny Chiao --- Makefile | 6 +----- README.md | 6 +++--- docs/reference/data-sources/push.md | 2 -- docs/roadmap.md | 9 +++++---- infra/charts/feast/README.md | 4 ++-- infra/charts/feast/README.md.gotmpl | 4 ++-- sdk/python/feast/feature_store.py | 7 +------ sdk/python/feast/infra/passthrough_provider.py | 6 ++++-- sdk/python/feast/infra/provider.py | 2 -- 9 files changed, 18 insertions(+), 28 deletions(-) diff --git a/Makefile b/Makefile index 990604da575..ef90cc393cb 100644 --- a/Makefile +++ b/Makefile @@ -356,15 +356,11 @@ lint-go: compile-protos-go compile-go-lib # Docker -build-docker: build-ci-docker build-feature-server-python-docker build-feature-server-python-aws-docker build-feature-transformation-server-docker build-feature-server-java-docker +build-docker: build-feature-server-python-docker build-feature-server-python-aws-docker build-feature-transformation-server-docker build-feature-server-java-docker push-ci-docker: docker push $(REGISTRY)/feast-ci:$(VERSION) -# TODO(adchia): consider removing. This doesn't run successfully right now -build-ci-docker: - docker buildx build -t $(REGISTRY)/feast-ci:$(VERSION) -f infra/docker/ci/Dockerfile --load . - push-feature-server-python-docker: docker push $(REGISTRY)/feature-server:$$VERSION diff --git a/README.md b/README.md index b663533710b..c8adfa5f22c 100644 --- a/README.md +++ b/README.md @@ -185,8 +185,8 @@ The list below contains the functionality that contributors are planning to deve * [ ] Batch transformation (In progress. See [RFC](https://docs.google.com/document/d/1964OkzuBljifDvkV-0fakp2uaijnVzdwWNGdz7Vz50A/edit)) * **Streaming** * [x] [Custom streaming ingestion job support](https://docs.feast.dev/how-to-guides/creating-a-custom-provider) - * [x] [Push based streaming data ingestion to online store (Alpha)](https://docs.feast.dev/reference/data-sources/push) - * [x] [Push based streaming data ingestion to offline store (Alpha)](https://docs.feast.dev/reference/data-sources/push) + * [x] [Push based streaming data ingestion to online store](https://docs.feast.dev/reference/data-sources/push) + * [x] [Push based streaming data ingestion to offline store](https://docs.feast.dev/reference/data-sources/push) * **Deployments** * [x] AWS Lambda (Alpha release. See [RFC](https://docs.google.com/document/d/1eZWKWzfBif66LDN32IajpaG-j82LSHCCOzY6R7Ax7MI/edit)) * [x] Kubernetes (See [guide](https://docs.feast.dev/how-to-guides/running-feast-in-production#4.3.-java-based-feature-server-deployed-on-kubernetes)) @@ -202,7 +202,7 @@ The list below contains the functionality that contributors are planning to deve * [x] Model-centric feature tracking (feature services) * [x] Amundsen integration (see [Feast extractor](https://github.com/amundsen-io/amundsen/blob/main/databuilder/databuilder/extractor/feast_extractor.py)) * [x] DataHub integration (see [DataHub Feast docs](https://datahubproject.io/docs/generated/ingestion/sources/feast/)) - * [x] Feast Web UI (Alpha release. See [docs](https://docs.feast.dev/reference/alpha-web-ui)) + * [x] Feast Web UI (Beta release. See [docs](https://docs.feast.dev/reference/alpha-web-ui)) ## 🎓 Important Resources diff --git a/docs/reference/data-sources/push.md b/docs/reference/data-sources/push.md index a585de5688d..6dad690c16a 100644 --- a/docs/reference/data-sources/push.md +++ b/docs/reference/data-sources/push.md @@ -1,7 +1,5 @@ # Push source -**Warning**: This is an _experimental_ feature. It's intended for early testing and feedback, and could change without warnings in future releases. - ## Description Push sources allow feature values to be pushed to the online store and offline store in real time. This allows fresh feature values to be made available to applications. Push sources supercede the diff --git a/docs/roadmap.md b/docs/roadmap.md index dc1d9ae1ab8..30f4317054b 100644 --- a/docs/roadmap.md +++ b/docs/roadmap.md @@ -43,15 +43,16 @@ The list below contains the functionality that contributors are planning to deve * [ ] Batch transformation (In progress. See [RFC](https://docs.google.com/document/d/1964OkzuBljifDvkV-0fakp2uaijnVzdwWNGdz7Vz50A/edit)) * **Streaming** * [x] [Custom streaming ingestion job support](https://docs.feast.dev/how-to-guides/creating-a-custom-provider) - * [x] [Push based streaming data ingestion to online store (Alpha)](https://docs.feast.dev/reference/data-sources/push) - * [x] [Push based streaming data ingestion to offline store (Alpha)](https://docs.feast.dev/reference/data-sources/push) + * [x] [Push based streaming data ingestion to online store](https://docs.feast.dev/reference/data-sources/push) + * [x] [Push based streaming data ingestion to offline store](https://docs.feast.dev/reference/data-sources/push) * **Deployments** * [x] AWS Lambda (Alpha release. See [RFC](https://docs.google.com/document/d/1eZWKWzfBif66LDN32IajpaG-j82LSHCCOzY6R7Ax7MI/edit)) * [x] Kubernetes (See [guide](https://docs.feast.dev/how-to-guides/running-feast-in-production#4.3.-java-based-feature-server-deployed-on-kubernetes)) * **Feature Serving** * [x] Python Client * [x] [Python feature server](https://docs.feast.dev/reference/feature-servers/python-feature-server) - * [x] [Go feature server](https://docs.feast.dev/reference/feature-servers/go-feature-server) + * [x] [Java feature server (alpha)](https://github.com/feast-dev/feast/blob/master/infra/charts/feast/README.md) + * [x] [Go feature server (alpha)](https://docs.feast.dev/reference/feature-servers/go-feature-server) * **Data Quality Management (See [RFC](https://docs.google.com/document/d/110F72d4NTv80p35wDSONxhhPBqWRwbZXG4f9mNEMd98/edit))** * [x] Data profiling and validation (Great Expectations) * **Feature Discovery and Governance** @@ -60,4 +61,4 @@ The list below contains the functionality that contributors are planning to deve * [x] Model-centric feature tracking (feature services) * [x] Amundsen integration (see [Feast extractor](https://github.com/amundsen-io/amundsen/blob/main/databuilder/databuilder/extractor/feast_extractor.py)) * [x] DataHub integration (see [DataHub Feast docs](https://datahubproject.io/docs/generated/ingestion/sources/feast/)) - * [x] Feast Web UI (Alpha release. See [docs](https://docs.feast.dev/reference/alpha-web-ui)) + * [x] Feast Web UI (Beta release. See [docs](https://docs.feast.dev/reference/alpha-web-ui)) diff --git a/infra/charts/feast/README.md b/infra/charts/feast/README.md index 2dd8c62250d..484c19e6d8d 100644 --- a/infra/charts/feast/README.md +++ b/infra/charts/feast/README.md @@ -1,6 +1,6 @@ -# Feast Helm Charts +# Feast Java Helm Charts (alpha) -This repo contains Helm charts for Feast components that are being installed on Kubernetes: +This repo contains Helm charts for Feast Java components that are being installed on Kubernetes: * Feast (root chart): The complete Helm chart containing all Feast components and dependencies. Most users will use this chart, but can selectively enable/disable subcharts using the values.yaml file. * [Feature Server](charts/feature-server): High performant JVM-based implementation of feature server. * [Transformation Service](charts/transformation-service): Transformation server for calculating on-demand features diff --git a/infra/charts/feast/README.md.gotmpl b/infra/charts/feast/README.md.gotmpl index 007dc854531..362f921d7ef 100644 --- a/infra/charts/feast/README.md.gotmpl +++ b/infra/charts/feast/README.md.gotmpl @@ -1,6 +1,6 @@ -# Feast Helm Charts +# Feast Java Helm Charts (alpha) -This repo contains Helm charts for Feast components that are being installed on Kubernetes: +This repo contains Helm charts for Feast Java components that are being installed on Kubernetes: * Feast (root chart): The complete Helm chart containing all Feast components and dependencies. Most users will use this chart, but can selectively enable/disable subcharts using the values.yaml file. * [Feature Server](charts/feature-server): High performant JVM-based implementation of feature server. * [Transformation Service](charts/transformation-service): Transformation server for calculating on-demand features diff --git a/sdk/python/feast/feature_store.py b/sdk/python/feast/feature_store.py index 23600e7c64f..9350220c21c 100644 --- a/sdk/python/feast/feature_store.py +++ b/sdk/python/feast/feature_store.py @@ -1478,13 +1478,8 @@ def write_to_online_store( feature_view = self.get_feature_view( feature_view_name, allow_registry_cache=allow_registry_cache ) - entities = [] - for entity_name in feature_view.entities: - entities.append( - self.get_entity(entity_name, allow_registry_cache=allow_registry_cache) - ) provider = self._get_provider() - provider.ingest_df(feature_view, entities, df) + provider.ingest_df(feature_view, df) @log_exceptions_and_usage def write_to_offline_store( diff --git a/sdk/python/feast/infra/passthrough_provider.py b/sdk/python/feast/infra/passthrough_provider.py index bb5cd38a835..28b10c12595 100644 --- a/sdk/python/feast/infra/passthrough_provider.py +++ b/sdk/python/feast/infra/passthrough_provider.py @@ -193,7 +193,6 @@ def online_read( def ingest_df( self, feature_view: FeatureView, - entities: List[Entity], df: pd.DataFrame, ): set_usage_attribute("provider", self.__class__.__name__) @@ -204,7 +203,10 @@ def ingest_df( table, feature_view.batch_source.field_mapping ) - join_keys = {entity.join_key: entity.value_type for entity in entities} + join_keys = { + entity.name: entity.dtype.to_value_type() + for entity in feature_view.entity_columns + } rows_to_write = _convert_arrow_to_proto(table, feature_view, join_keys) self.online_write_batch( diff --git a/sdk/python/feast/infra/provider.py b/sdk/python/feast/infra/provider.py index 7d3c37e4c2e..82879b264af 100644 --- a/sdk/python/feast/infra/provider.py +++ b/sdk/python/feast/infra/provider.py @@ -123,7 +123,6 @@ def online_write_batch( def ingest_df( self, feature_view: FeatureView, - entities: List[Entity], df: pd.DataFrame, ): """ @@ -131,7 +130,6 @@ def ingest_df( Args: feature_view: The feature view to which the dataframe corresponds. - entities: The entities that are referenced by the dataframe. df: The dataframe to be persisted. """ pass From 2107ce295f191eb1339c8670f963d39e66c4ccf6 Mon Sep 17 00:00:00 2001 From: Niklas von Maltzahn Date: Wed, 7 Sep 2022 22:58:24 +0200 Subject: [PATCH 22/46] feat: Add `to_remote_storage` functionality to `SparkOfflineStore` (#3175) implement to_remote_storage method Signed-off-by: niklasvm Signed-off-by: niklasvm --- .../contrib/spark_offline_store/spark.py | 77 ++++++++++++++++++- .../spark_offline_store/tests/data_source.py | 4 + 2 files changed, 80 insertions(+), 1 deletion(-) diff --git a/sdk/python/feast/infra/offline_stores/contrib/spark_offline_store/spark.py b/sdk/python/feast/infra/offline_stores/contrib/spark_offline_store/spark.py index 58519014b44..0a4ec05c23d 100644 --- a/sdk/python/feast/infra/offline_stores/contrib/spark_offline_store/spark.py +++ b/sdk/python/feast/infra/offline_stores/contrib/spark_offline_store/spark.py @@ -1,4 +1,6 @@ +import os import tempfile +import uuid import warnings from datetime import datetime from typing import Any, Callable, Dict, List, Optional, Tuple, Union @@ -13,6 +15,7 @@ from pyspark import SparkConf from pyspark.sql import SparkSession from pytz import utc +from sdk.python.feast.infra.utils import aws_utils from feast import FeatureView, OnDemandFeatureView from feast.data_source import DataSource @@ -46,6 +49,12 @@ class SparkOfflineStoreConfig(FeastConfigBaseModel): """ Configuration overlay for the spark session """ # sparksession is not serializable and we dont want to pass it around as an argument + staging_location: Optional[StrictStr] = None + """ Remote path for batch materialization jobs""" + + region: Optional[StrictStr] = None + """ AWS Region if applicable for s3-based staging locations""" + class SparkOfflineStore(OfflineStore): @staticmethod @@ -105,6 +114,7 @@ def pull_latest_from_table_or_query( return SparkRetrievalJob( spark_session=spark_session, query=query, + config=config, full_feature_names=False, on_demand_feature_views=None, ) @@ -129,6 +139,7 @@ def get_historical_features( "Some functionality may still be unstable so functionality can change in the future.", RuntimeWarning, ) + spark_session = get_spark_session_or_start_new_with_repoconfig( store_config=config.offline_store ) @@ -192,6 +203,7 @@ def get_historical_features( min_event_timestamp=entity_df_event_timestamp_range[0], max_event_timestamp=entity_df_event_timestamp_range[1], ), + config=config, ) @staticmethod @@ -286,7 +298,10 @@ def pull_all_from_table_or_query( """ return SparkRetrievalJob( - spark_session=spark_session, query=query, full_feature_names=False + spark_session=spark_session, + query=query, + full_feature_names=False, + config=config, ) @@ -296,6 +311,7 @@ def __init__( spark_session: SparkSession, query: str, full_feature_names: bool, + config: RepoConfig, on_demand_feature_views: Optional[List[OnDemandFeatureView]] = None, metadata: Optional[RetrievalMetadata] = None, ): @@ -305,6 +321,7 @@ def __init__( self._full_feature_names = full_feature_names self._on_demand_feature_views = on_demand_feature_views or [] self._metadata = metadata + self._config = config @property def full_feature_names(self) -> bool: @@ -342,6 +359,53 @@ def persist(self, storage: SavedDatasetStorage, allow_overwrite: bool = False): raise ValueError("Cannot persist, table_name is not defined") self.to_spark_df().createOrReplaceTempView(table_name) + def supports_remote_storage_export(self) -> bool: + return self._config.offline_store.staging_location is not None + + def to_remote_storage(self) -> List[str]: + """Currently only works for local and s3-based staging locations""" + if self.supports_remote_storage_export(): + + sdf: pyspark.sql.DataFrame = self.to_spark_df() + + if self._config.offline_store.staging_location.startswith("file://"): + local_file_staging_location = os.path.abspath( + self._config.offline_store.staging_location + ) + + # write to staging location + output_uri = os.path.join( + str(local_file_staging_location), str(uuid.uuid4()) + ) + sdf.write.parquet(output_uri) + + return _list_files_in_folder(output_uri) + elif self._config.offline_store.staging_location.startswith("s3://"): + + spark_compatible_s3_staging_location = ( + self._config.offline_store.staging_location.replace( + "s3://", "s3a://" + ) + ) + + # write to staging location + output_uri = os.path.join( + str(spark_compatible_s3_staging_location), str(uuid.uuid4()) + ) + sdf.write.parquet(output_uri) + + return aws_utils.list_s3_files( + self._config.offline_store.region, output_uri + ) + + else: + raise NotImplementedError( + "to_remote_storage is only implemented for file:// and s3:// uri schemes" + ) + + else: + raise NotImplementedError() + @property def metadata(self) -> Optional[RetrievalMetadata]: """ @@ -444,6 +508,17 @@ def _format_datetime(t: datetime) -> str: return dt +def _list_files_in_folder(folder): + """List full filenames in a folder""" + files = [] + for file in os.listdir(folder): + filename = os.path.join(folder, file) + if os.path.isfile(filename): + files.append(filename) + + return files + + def _cast_data_frame( df_new: pyspark.sql.DataFrame, df_existing: pyspark.sql.DataFrame ) -> pyspark.sql.DataFrame: diff --git a/sdk/python/feast/infra/offline_stores/contrib/spark_offline_store/tests/data_source.py b/sdk/python/feast/infra/offline_stores/contrib/spark_offline_store/tests/data_source.py index ab1acbef73e..64a2a01cee4 100644 --- a/sdk/python/feast/infra/offline_stores/contrib/spark_offline_store/tests/data_source.py +++ b/sdk/python/feast/infra/offline_stores/contrib/spark_offline_store/tests/data_source.py @@ -58,6 +58,10 @@ def create_offline_store_config(self): self.spark_offline_store_config = SparkOfflineStoreConfig() self.spark_offline_store_config.type = "spark" self.spark_offline_store_config.spark_conf = self.spark_conf + self.spark_offline_store_config.staging_location = "file://" + str( + tempfile.TemporaryDirectory() + ) + self.spark_offline_store_config.region = "eu-west-1" return self.spark_offline_store_config def create_data_source( From de75971e27357a8fb4a882bd7ec4212148256616 Mon Sep 17 00:00:00 2001 From: Kevin Zhang Date: Wed, 7 Sep 2022 14:54:25 -0700 Subject: [PATCH 23/46] fix: Fix Shopify timestamp bug and add warnings to help with debugging entity registration (#3191) * Fix Signed-off-by: Kevin Zhang * Foix lint Signed-off-by: Kevin Zhang Signed-off-by: Kevin Zhang --- sdk/python/feast/feature_view.py | 6 ++++++ sdk/python/feast/type_map.py | 2 ++ 2 files changed, 8 insertions(+) diff --git a/sdk/python/feast/feature_view.py b/sdk/python/feast/feature_view.py index cdd507868df..344171745be 100644 --- a/sdk/python/feast/feature_view.py +++ b/sdk/python/feast/feature_view.py @@ -409,6 +409,12 @@ def from_proto(cls, feature_view_proto: FeatureViewProto): for field_proto in feature_view_proto.spec.entity_columns ] + if len(feature_view.entities) != len(feature_view.entity_columns): + warnings.warn( + f"There are some mismatches in your feature view's registered entities. Please check if you have applied your entities correctly." + f"Entities: {feature_view.entities} vs Entity Columns: {feature_view.entity_columns}" + ) + # FeatureViewProjections are not saved in the FeatureView proto. # Create the default projection. feature_view.projection = FeatureViewProjection.from_definition(feature_view) diff --git a/sdk/python/feast/type_map.py b/sdk/python/feast/type_map.py index 3bec457dcbb..466993bb3d7 100644 --- a/sdk/python/feast/type_map.py +++ b/sdk/python/feast/type_map.py @@ -320,6 +320,8 @@ def _python_datetime_to_int_timestamp( int_timestamps.append(int(value.ToSeconds())) elif isinstance(value, np.datetime64): int_timestamps.append(value.astype("datetime64[s]").astype(np.int_)) + elif isinstance(value, type(np.nan)): + int_timestamps.append(NULL_TIMESTAMP_INT_VALUE) else: int_timestamps.append(int(value)) return int_timestamps From 62cd20ca5e21dcedc8b57d6707fbf3e8bcd3a9ed Mon Sep 17 00:00:00 2001 From: Danny Chiao Date: Wed, 7 Sep 2022 23:02:48 -0400 Subject: [PATCH 24/46] ci: Fix release process to setup helm-docs Signed-off-by: Danny Chiao --- .github/workflows/release.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 83eb5bcdd5f..ec56d60e4c5 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -92,6 +92,9 @@ jobs: uses: actions/setup-node@v2 with: node-version: '16' + - name: Setup Helm-docs + run: | + brew install norwoodj/tap/helm-docs - name: Release (Dry Run) if: github.event.inputs.dry_run == 'true' run: | From b1b1568e3db500f9f176466116767c814061752b Mon Sep 17 00:00:00 2001 From: Danny Chiao Date: Wed, 7 Sep 2022 23:18:36 -0400 Subject: [PATCH 25/46] ci: Fix unique versions count for helm chart validation Signed-off-by: Danny Chiao --- infra/scripts/helm/validate-helm-chart-versions.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/infra/scripts/helm/validate-helm-chart-versions.sh b/infra/scripts/helm/validate-helm-chart-versions.sh index 17a87e163aa..cd8317222bd 100755 --- a/infra/scripts/helm/validate-helm-chart-versions.sh +++ b/infra/scripts/helm/validate-helm-chart-versions.sh @@ -3,7 +3,7 @@ set -e # Amount of file locations that need to be bumped in unison when versions increment -UNIQUE_VERSIONS_COUNT=21 # Change in release 0.24.0 +UNIQUE_VERSIONS_COUNT=22 # Change in release 0.24.0 if [ $# -ne 1 ]; then echo "Please provide a single semver version (without a \"v\" prefix) to test the repository against, e.g 0.99.0" From 11a0e1a6c63b35644d6bcc9a93a8db89cef8c1f3 Mon Sep 17 00:00:00 2001 From: Danny Chiao Date: Wed, 7 Sep 2022 23:48:41 -0400 Subject: [PATCH 26/46] ci: Fix feature server docker image release workflow Signed-off-by: Danny Chiao --- .github/workflows/publish.yml | 2 +- Makefile | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 46e16657543..9587044a84d 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -49,7 +49,7 @@ jobs: needs: get-version strategy: matrix: - component: [feature-server-python, feature-server-python-aws, feature-server-java, feature-transformation-server] + component: [feature-server, feature-server-python-aws, feature-server-java, feature-transformation-server] env: MAVEN_CACHE: gs://feast-templocation-kf-feast/.m2.2020-08-19.tar REGISTRY: feastdev diff --git a/Makefile b/Makefile index ef90cc393cb..0bf0c82669f 100644 --- a/Makefile +++ b/Makefile @@ -361,10 +361,10 @@ build-docker: build-feature-server-python-docker build-feature-server-python-aws push-ci-docker: docker push $(REGISTRY)/feast-ci:$(VERSION) -push-feature-server-python-docker: +push-feature-server-docker: docker push $(REGISTRY)/feature-server:$$VERSION -build-feature-server-python-docker: +build-feature-server-docker: docker buildx build --build-arg VERSION=$$VERSION \ -t $(REGISTRY)/feature-server:$$VERSION \ -f sdk/python/feast/infra/feature_servers/multicloud/Dockerfile --load . From 189afb9313d071c7b6492e0e8a996e6d073a2c6c Mon Sep 17 00:00:00 2001 From: sfc-gh-madkins <82121043+sfc-gh-madkins@users.noreply.github.com> Date: Thu, 8 Sep 2022 14:44:30 -0500 Subject: [PATCH 27/46] fix: Snowflake config file search error (#3193) Signed-off-by: Miles Adkins Signed-off-by: Miles Adkins --- sdk/python/feast/infra/materialization/snowflake_engine.py | 5 +---- sdk/python/feast/infra/offline_stores/snowflake.py | 4 +--- sdk/python/feast/infra/online_stores/snowflake.py | 5 +---- 3 files changed, 3 insertions(+), 11 deletions(-) diff --git a/sdk/python/feast/infra/materialization/snowflake_engine.py b/sdk/python/feast/infra/materialization/snowflake_engine.py index 80deb13f96e..de498fe4e1f 100644 --- a/sdk/python/feast/infra/materialization/snowflake_engine.py +++ b/sdk/python/feast/infra/materialization/snowflake_engine.py @@ -2,7 +2,6 @@ import shutil from dataclasses import dataclass from datetime import datetime -from pathlib import Path from typing import Callable, List, Literal, Optional, Sequence, Union import click @@ -45,9 +44,7 @@ class SnowflakeMaterializationEngineConfig(FeastConfigBaseModel): type: Literal["snowflake.engine"] = "snowflake.engine" """ Type selector""" - config_path: Optional[str] = ( - Path(os.environ["HOME"]) / ".snowsql/config" - ).__str__() + config_path: Optional[str] = os.path.expanduser("~/.snowsql/config") """ Snowflake config path -- absolute path required (Cant use ~)""" account: Optional[str] = None diff --git a/sdk/python/feast/infra/offline_stores/snowflake.py b/sdk/python/feast/infra/offline_stores/snowflake.py index aab68718653..23959797023 100644 --- a/sdk/python/feast/infra/offline_stores/snowflake.py +++ b/sdk/python/feast/infra/offline_stores/snowflake.py @@ -64,9 +64,7 @@ class SnowflakeOfflineStoreConfig(FeastConfigBaseModel): type: Literal["snowflake.offline"] = "snowflake.offline" """ Offline store type selector""" - config_path: Optional[str] = ( - Path(os.environ["HOME"]) / ".snowsql/config" - ).__str__() + config_path: Optional[str] = os.path.expanduser("~/.snowsql/config") """ Snowflake config path -- absolute path required (Cant use ~)""" account: Optional[str] = None diff --git a/sdk/python/feast/infra/online_stores/snowflake.py b/sdk/python/feast/infra/online_stores/snowflake.py index a52beb73f76..be914043f24 100644 --- a/sdk/python/feast/infra/online_stores/snowflake.py +++ b/sdk/python/feast/infra/online_stores/snowflake.py @@ -2,7 +2,6 @@ import os from binascii import hexlify from datetime import datetime -from pathlib import Path from typing import Any, Callable, Dict, List, Optional, Sequence, Tuple import pandas as pd @@ -31,9 +30,7 @@ class SnowflakeOnlineStoreConfig(FeastConfigBaseModel): type: Literal["snowflake.online"] = "snowflake.online" """ Online store type selector""" - config_path: Optional[str] = ( - Path(os.environ["HOME"]) / ".snowsql/config" - ).__str__() + config_path: Optional[str] = os.path.expanduser("~/.snowsql/config") """ Snowflake config path -- absolute path required (Can't use ~)""" account: Optional[str] = None From af355bc2542d61265785c818a21221e5b5bd8678 Mon Sep 17 00:00:00 2001 From: Danny Chiao Date: Thu, 8 Sep 2022 15:45:28 -0400 Subject: [PATCH 28/46] docs: Update Java helm chart to point to up to date configuration (#3194) Signed-off-by: Danny Chiao Signed-off-by: Danny Chiao --- infra/charts/feast/README.md | 9 ++++++++- infra/charts/feast/README.md.gotmpl | 9 ++++++++- 2 files changed, 16 insertions(+), 2 deletions(-) diff --git a/infra/charts/feast/README.md b/infra/charts/feast/README.md index 484c19e6d8d..58b0357a9a6 100644 --- a/infra/charts/feast/README.md +++ b/infra/charts/feast/README.md @@ -43,10 +43,17 @@ feature-server: config: host: localhost port: 6379 + entityKeySerializationVersion: 2 + +global: + registry: + path: gs://[YOUR GCS BUCKET]/demo-repo/registry.db + cache_ttl_seconds: 60 + project: feast_java_demo ``` -For the default configuration, please see the [Feature Server Configuration](https://github.com/feast-dev/feast-java/blob/master/serving/src/main/resources/application.yml). +For the default configuration, please see the [Feature Server Configuration](https://github.com/feast-dev/feast/blob/master/java/serving/src/main/resources/application.yml). For more details, please see: https://docs.feast.dev/how-to-guides/running-feast-in-production diff --git a/infra/charts/feast/README.md.gotmpl b/infra/charts/feast/README.md.gotmpl index 362f921d7ef..7a32d22c7f1 100644 --- a/infra/charts/feast/README.md.gotmpl +++ b/infra/charts/feast/README.md.gotmpl @@ -43,10 +43,17 @@ feature-server: config: host: localhost port: 6379 + entityKeySerializationVersion: 2 + +global: + registry: + path: gs://[YOUR GCS BUCKET]/demo-repo/registry.db + cache_ttl_seconds: 60 + project: feast_java_demo ``` -For the default configuration, please see the [Feature Server Configuration](https://github.com/feast-dev/feast-java/blob/master/serving/src/main/resources/application.yml). +For the default configuration, please see the [Feature Server Configuration](https://github.com/feast-dev/feast/blob/master/java/serving/src/main/resources/application.yml). For more details, please see: https://docs.feast.dev/how-to-guides/running-feast-in-production From 2535024b0f6867f61af74014ee31608b59e86d08 Mon Sep 17 00:00:00 2001 From: Danny Chiao Date: Thu, 8 Sep 2022 15:46:23 -0400 Subject: [PATCH 29/46] chore: Move community governance into main Feast repo (#3198) * chore: Move community governance into main Feast repo. Update contributing docs Signed-off-by: Danny Chiao * lint Signed-off-by: Danny Chiao Signed-off-by: Danny Chiao --- community/README.md | 8 + community/governance.excalidraw | 913 ++++++++++++++++++++++++++++++ community/governance.md | 272 +++++++++ community/governance.png | Bin 0 -> 258405 bytes community/maintainers.md | 42 ++ docs/community.md | 1 + docs/project/contributing.md | 31 +- docs/project/development-guide.md | 78 +-- 8 files changed, 1306 insertions(+), 39 deletions(-) create mode 100644 community/README.md create mode 100644 community/governance.excalidraw create mode 100644 community/governance.md create mode 100644 community/governance.png create mode 100644 community/maintainers.md diff --git a/community/README.md b/community/README.md new file mode 100644 index 00000000000..3ffe7f46296 --- /dev/null +++ b/community/README.md @@ -0,0 +1,8 @@ +# Feast Community + +Welcome to the Feast community! + +Please see the Community section on [Feast.dev](https://docs.feast.dev/) for more details on getting involved. + +- [Governance](governance.md): The Feast governance structure +- [Maintainers](maintainers.md): List of members acting as maintainers diff --git a/community/governance.excalidraw b/community/governance.excalidraw new file mode 100644 index 00000000000..f4c8dad9a4f --- /dev/null +++ b/community/governance.excalidraw @@ -0,0 +1,913 @@ +{ + "type": "excalidraw", + "version": 2, + "source": "https://excalidraw.com", + "elements": [ + { + "type": "rectangle", + "version": 620, + "versionNonce": 853777363, + "isDeleted": false, + "id": "pr0yIJcUDXb4nFgowH9_r", + "fillStyle": "hachure", + "strokeWidth": 1, + "strokeStyle": "dashed", + "roughness": 1, + "opacity": 100, + "angle": 0, + "x": 409.5, + "y": 620.5, + "strokeColor": "#000000", + "backgroundColor": "transparent", + "width": 194, + "height": 83, + "seed": 1695250557, + "groupIds": [], + "strokeSharpness": "sharp", + "boundElements": [ + { + "id": "YfmPferxgVKoP70zGfYDK", + "type": "text" + }, + { + "id": "YfmPferxgVKoP70zGfYDK", + "type": "text" + }, + { + "type": "text", + "id": "YfmPferxgVKoP70zGfYDK" + }, + { + "id": "IsihlXUGDSklv2RsxX6wO", + "type": "arrow" + }, + { + "id": "G5s2AUFJ730fyPsIbA8xP", + "type": "arrow" + }, + { + "id": "j9ZVC3ZgHTsAGe3hJQecp", + "type": "arrow" + } + ], + "updated": 1662582134601, + "link": null, + "locked": false + }, + { + "type": "text", + "version": 623, + "versionNonce": 328400605, + "isDeleted": false, + "id": "YfmPferxgVKoP70zGfYDK", + "fillStyle": "hachure", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "angle": 0, + "x": 414.5, + "y": 649.5, + "strokeColor": "#000000", + "backgroundColor": "transparent", + "width": 184, + "height": 25, + "seed": 1575229907, + "groupIds": [], + "strokeSharpness": "sharp", + "boundElements": [], + "updated": 1662582134601, + "link": null, + "locked": false, + "fontSize": 20, + "fontFamily": 1, + "text": "CODEOWNERS", + "baseline": 18, + "textAlign": "center", + "verticalAlign": "middle", + "containerId": "pr0yIJcUDXb4nFgowH9_r", + "originalText": "CODEOWNERS" + }, + { + "type": "rectangle", + "version": 756, + "versionNonce": 1648798067, + "isDeleted": false, + "id": "XDy4VWWtJ9sd6hzPJDdFe", + "fillStyle": "hachure", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "angle": 0, + "x": 409.5, + "y": 779.5, + "strokeColor": "#000000", + "backgroundColor": "transparent", + "width": 194, + "height": 83, + "seed": 1925179667, + "groupIds": [], + "strokeSharpness": "sharp", + "boundElements": [ + { + "id": "gUz4p_oPytb5-ejbYb81N", + "type": "text" + }, + { + "id": "gUz4p_oPytb5-ejbYb81N", + "type": "text" + }, + { + "id": "gUz4p_oPytb5-ejbYb81N", + "type": "text" + }, + { + "type": "text", + "id": "gUz4p_oPytb5-ejbYb81N" + }, + { + "id": "G5s2AUFJ730fyPsIbA8xP", + "type": "arrow" + } + ], + "updated": 1662582134601, + "link": null, + "locked": false + }, + { + "type": "text", + "version": 781, + "versionNonce": 1240013629, + "isDeleted": false, + "id": "gUz4p_oPytb5-ejbYb81N", + "fillStyle": "hachure", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "angle": 0, + "x": 414.5, + "y": 808.5, + "strokeColor": "#000000", + "backgroundColor": "transparent", + "width": 184, + "height": 25, + "seed": 140322205, + "groupIds": [], + "strokeSharpness": "sharp", + "boundElements": [], + "updated": 1662582134601, + "link": null, + "locked": false, + "fontSize": 20, + "fontFamily": 1, + "text": "Contributors", + "baseline": 18, + "textAlign": "center", + "verticalAlign": "middle", + "containerId": "XDy4VWWtJ9sd6hzPJDdFe", + "originalText": "Contributors" + }, + { + "type": "text", + "version": 463, + "versionNonce": 2109720179, + "isDeleted": false, + "id": "AYJKq2RGJrSIpbfiJOf_4", + "fillStyle": "hachure", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "angle": 0, + "x": 526, + "y": 517.5, + "strokeColor": "#000000", + "backgroundColor": "transparent", + "width": 274, + "height": 75, + "seed": 1616513981, + "groupIds": [], + "strokeSharpness": "sharp", + "boundElements": [], + "updated": 1662582134602, + "link": null, + "locked": false, + "fontSize": 20, + "fontFamily": 1, + "text": "1. organize contributors\n2. influence roadmap\n3. own direction of an area", + "baseline": 68, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "1. organize contributors\n2. influence roadmap\n3. own direction of an area" + }, + { + "type": "rectangle", + "version": 776, + "versionNonce": 519656573, + "isDeleted": false, + "id": "z5LT5d710gSTA9DjwiL3O", + "fillStyle": "hachure", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 70, + "angle": 0, + "x": 1013.7117834394903, + "y": 187.5000000000001, + "strokeColor": "#000000", + "backgroundColor": "#4c6ef5", + "width": 132, + "height": 682, + "seed": 1424710877, + "groupIds": [], + "strokeSharpness": "sharp", + "boundElements": [ + { + "id": "RscqyQXicYOkFsE_zvran", + "type": "text" + }, + { + "id": "J7IG4T5j15pB3b_K0Cpd9", + "type": "arrow" + }, + { + "id": "XEohLLmfFl0L9Wi2ew5AU", + "type": "arrow" + }, + { + "id": "o3Pp-94PORhEiEauRcZW_", + "type": "arrow" + }, + { + "type": "text", + "id": "RscqyQXicYOkFsE_zvran" + }, + { + "id": "j9ZVC3ZgHTsAGe3hJQecp", + "type": "arrow" + }, + { + "id": "Klq-VJGZiolZnGuaNJ8k9", + "type": "arrow" + } + ], + "updated": 1662582138112, + "link": null, + "locked": false + }, + { + "type": "text", + "version": 896, + "versionNonce": 1733426643, + "isDeleted": false, + "id": "RscqyQXicYOkFsE_zvran", + "fillStyle": "hachure", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "angle": 0, + "x": 1018.7117834394903, + "y": 476.0000000000001, + "strokeColor": "#000000", + "backgroundColor": "transparent", + "width": 122, + "height": 105, + "seed": 1202400115, + "groupIds": [], + "strokeSharpness": "sharp", + "boundElements": [], + "updated": 1662582138113, + "link": null, + "locked": false, + "fontSize": 28, + "fontFamily": 1, + "text": "Feast \nGitHub \nproject", + "baseline": 95, + "textAlign": "center", + "verticalAlign": "middle", + "containerId": "z5LT5d710gSTA9DjwiL3O", + "originalText": "Feast GitHub project" + }, + { + "id": "IsihlXUGDSklv2RsxX6wO", + "type": "arrow", + "x": 506.997671158975, + "y": 619, + "width": 0, + "height": 132, + "angle": 0, + "strokeColor": "#000000", + "backgroundColor": "#868e96", + "fillStyle": "hachure", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "strokeSharpness": "sharp", + "seed": 345290749, + "version": 680, + "versionNonce": 787007421, + "isDeleted": false, + "boundElements": null, + "updated": 1662582134602, + "link": null, + "locked": false, + "points": [ + [ + 0, + 0 + ], + [ + 0, + -132 + ] + ], + "lastCommittedPoint": null, + "startBinding": { + "elementId": "pr0yIJcUDXb4nFgowH9_r", + "focus": 0.005130630504896713, + "gap": 1.5 + }, + "endBinding": { + "elementId": "TBYpmrW2OsKEqbpZfEeJg", + "focus": 0.461338833375829, + "gap": 1 + }, + "startArrowhead": null, + "endArrowhead": "arrow" + }, + { + "id": "G5s2AUFJ730fyPsIbA8xP", + "type": "arrow", + "x": 506.9985864097345, + "y": 776, + "width": 0.9914493467237548, + "height": 71, + "angle": 0, + "strokeColor": "#000000", + "backgroundColor": "#868e96", + "fillStyle": "hachure", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "strokeSharpness": "sharp", + "seed": 241364467, + "version": 241, + "versionNonce": 649485971, + "isDeleted": false, + "boundElements": null, + "updated": 1662582134602, + "link": null, + "locked": false, + "points": [ + [ + 0, + 0 + ], + [ + -0.9914493467237548, + -71 + ] + ], + "lastCommittedPoint": null, + "startBinding": { + "elementId": "XDy4VWWtJ9sd6hzPJDdFe", + "focus": 0.011569796958606356, + "gap": 3.5 + }, + "endBinding": { + "elementId": "pr0yIJcUDXb4nFgowH9_r", + "focus": 0.011204382815075232, + "gap": 1.5 + }, + "startArrowhead": null, + "endArrowhead": "arrow" + }, + { + "id": "TBYpmrW2OsKEqbpZfEeJg", + "type": "rectangle", + "x": 409.5, + "y": 188, + "width": 361.99999999999994, + "height": 298, + "angle": 0, + "strokeColor": "#000000", + "backgroundColor": "#868e96", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 30, + "groupIds": [ + "mcHoJ-dlfU3T8l_C93UPa" + ], + "strokeSharpness": "sharp", + "seed": 1515491581, + "version": 231, + "versionNonce": 593345661, + "isDeleted": false, + "boundElements": [ + { + "id": "IsihlXUGDSklv2RsxX6wO", + "type": "arrow" + }, + { + "id": "Klq-VJGZiolZnGuaNJ8k9", + "type": "arrow" + } + ], + "updated": 1662582134602, + "link": null, + "locked": false + }, + { + "id": "YEEHpa4RXaR8G9YW55v25", + "type": "rectangle", + "x": 428.5, + "y": 398, + "width": 163.61445783132532, + "height": 70.00000000000001, + "angle": 0, + "strokeColor": "#000000", + "backgroundColor": "transparent", + "fillStyle": "hachure", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [ + "mcHoJ-dlfU3T8l_C93UPa" + ], + "strokeSharpness": "sharp", + "seed": 932648787, + "version": 319, + "versionNonce": 398988755, + "isDeleted": false, + "boundElements": [ + { + "type": "text", + "id": "8iyUZwSph5yMVrXehf6vg" + }, + { + "id": "o3Pp-94PORhEiEauRcZW_", + "type": "arrow" + }, + { + "id": "IsihlXUGDSklv2RsxX6wO", + "type": "arrow" + } + ], + "updated": 1662582134602, + "link": null, + "locked": false + }, + { + "id": "8iyUZwSph5yMVrXehf6vg", + "type": "text", + "x": 433.5, + "y": 422.5, + "width": 153.61445783132532, + "height": 21, + "angle": 0, + "strokeColor": "#000000", + "backgroundColor": "transparent", + "fillStyle": "hachure", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [ + "mcHoJ-dlfU3T8l_C93UPa" + ], + "strokeSharpness": "sharp", + "seed": 1803538003, + "version": 365, + "versionNonce": 952837341, + "isDeleted": false, + "boundElements": null, + "updated": 1662582134602, + "link": null, + "locked": false, + "text": "Area maintainers", + "fontSize": 16.697223677317968, + "fontFamily": 1, + "textAlign": "center", + "verticalAlign": "middle", + "baseline": 15, + "containerId": "YEEHpa4RXaR8G9YW55v25", + "originalText": "Area maintainers" + }, + { + "type": "rectangle", + "version": 355, + "versionNonce": 1753998195, + "isDeleted": false, + "id": "Wh-PpzmGy1bWJko0akD-a", + "fillStyle": "hachure", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "angle": 0, + "x": 429.8072289156627, + "y": 257.1185567010309, + "strokeColor": "#000000", + "backgroundColor": "transparent", + "width": 161, + "height": 68.88144329896907, + "seed": 1844448573, + "groupIds": [ + "mcHoJ-dlfU3T8l_C93UPa" + ], + "strokeSharpness": "sharp", + "boundElements": [ + { + "id": "OJCS1hAx71BD6u1jesJzR", + "type": "text" + }, + { + "type": "text", + "id": "OJCS1hAx71BD6u1jesJzR" + }, + { + "id": "o3Pp-94PORhEiEauRcZW_", + "type": "arrow" + } + ], + "updated": 1662582134602, + "link": null, + "locked": false + }, + { + "type": "text", + "version": 409, + "versionNonce": 556564797, + "isDeleted": false, + "id": "OJCS1hAx71BD6u1jesJzR", + "fillStyle": "hachure", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "angle": 0, + "x": 434.8072289156627, + "y": 271.55927835051546, + "strokeColor": "#000000", + "backgroundColor": "transparent", + "width": 151, + "height": 40, + "seed": 852504851, + "groupIds": [ + "mcHoJ-dlfU3T8l_C93UPa" + ], + "strokeSharpness": "sharp", + "boundElements": [], + "updated": 1662582134602, + "link": null, + "locked": false, + "fontSize": 16.413043478260864, + "fontFamily": 1, + "text": "Project \nmaintainers", + "baseline": 34, + "textAlign": "center", + "verticalAlign": "middle", + "containerId": "Wh-PpzmGy1bWJko0akD-a", + "originalText": "Project maintainers" + }, + { + "id": "o3Pp-94PORhEiEauRcZW_", + "type": "arrow", + "x": 510.1952932956257, + "y": 396.60017389144207, + "width": 0.34508644012566947, + "height": 69.20034778288408, + "angle": 0, + "strokeColor": "#000000", + "backgroundColor": "transparent", + "fillStyle": "hachure", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [ + "mcHoJ-dlfU3T8l_C93UPa" + ], + "strokeSharpness": "sharp", + "seed": 1889236627, + "version": 572, + "versionNonce": 918879507, + "isDeleted": false, + "boundElements": null, + "updated": 1662582134602, + "link": null, + "locked": false, + "points": [ + [ + 0, + 0 + ], + [ + 0.34508644012566947, + -69.20034778288408 + ] + ], + "lastCommittedPoint": null, + "startBinding": { + "elementId": "YEEHpa4RXaR8G9YW55v25", + "focus": -0.0035794947090358044, + "gap": 1.3998261085579315 + }, + "endBinding": { + "elementId": "Wh-PpzmGy1bWJko0akD-a", + "focus": -0.0051056226396315905, + "gap": 1.3998261085579884 + }, + "startArrowhead": null, + "endArrowhead": "arrow" + }, + { + "id": "4CHi-UfB3oI1PAfcFm2o_", + "type": "text", + "x": 528.5, + "y": 354.5, + "width": 218, + "height": 20, + "angle": 0, + "strokeColor": "#000000", + "backgroundColor": "transparent", + "fillStyle": "hachure", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [ + "mcHoJ-dlfU3T8l_C93UPa" + ], + "strokeSharpness": "sharp", + "seed": 2054408115, + "version": 238, + "versionNonce": 1105416605, + "isDeleted": false, + "boundElements": null, + "updated": 1662582134602, + "link": null, + "locked": false, + "text": "break ties by majority vote", + "fontSize": 16, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "baseline": 14, + "containerId": null, + "originalText": "break ties by majority vote" + }, + { + "id": "gHvMhIQl4S1SxPE8kzHLx", + "type": "text", + "x": 431.8072289156627, + "y": 201.5, + "width": 157, + "height": 35, + "angle": 0, + "strokeColor": "#000000", + "backgroundColor": "#868e96", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [ + "mcHoJ-dlfU3T8l_C93UPa" + ], + "strokeSharpness": "sharp", + "seed": 1597289651, + "version": 154, + "versionNonce": 1038738099, + "isDeleted": false, + "boundElements": null, + "updated": 1662582134602, + "link": null, + "locked": false, + "text": "Maintainers", + "fontSize": 28, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "baseline": 25, + "containerId": null, + "originalText": "Maintainers" + }, + { + "type": "text", + "version": 545, + "versionNonce": 1478563325, + "isDeleted": false, + "id": "_qJ5MtLgnvmF1-EDKX6qg", + "fillStyle": "hachure", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "angle": 0, + "x": 533, + "y": 732.5, + "strokeColor": "#000000", + "backgroundColor": "transparent", + "width": 106, + "height": 25, + "seed": 1870614973, + "groupIds": [], + "strokeSharpness": "sharp", + "boundElements": [], + "updated": 1662582134602, + "link": null, + "locked": false, + "fontSize": 20, + "fontFamily": 1, + "text": "review PRs", + "baseline": 18, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "review PRs" + }, + { + "id": "j9ZVC3ZgHTsAGe3hJQecp", + "type": "arrow", + "x": 610.590909090909, + "y": 673.6931323855418, + "width": 394.7818338530517, + "height": 1.1368683772161603e-13, + "angle": 0, + "strokeColor": "#000000", + "backgroundColor": "#868e96", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "strokeSharpness": "sharp", + "seed": 1115132605, + "version": 594, + "versionNonce": 1612334739, + "isDeleted": false, + "boundElements": null, + "updated": 1662582138112, + "link": null, + "locked": false, + "points": [ + [ + 0, + 0 + ], + [ + 394.7818338530517, + 1.1368683772161603e-13 + ] + ], + "lastCommittedPoint": null, + "startBinding": { + "elementId": "pr0yIJcUDXb4nFgowH9_r", + "gap": 7.0909090909090455, + "focus": 0.2817622261576348 + }, + "endBinding": { + "elementId": "z5LT5d710gSTA9DjwiL3O", + "gap": 9.339040495529549, + "focus": -0.4257863119810608 + }, + "startArrowhead": null, + "endArrowhead": "arrow" + }, + { + "id": "Klq-VJGZiolZnGuaNJ8k9", + "type": "arrow", + "x": 775.7385321100917, + "y": 334, + "width": 233.2672383568049, + "height": 0, + "angle": 0, + "strokeColor": "#000000", + "backgroundColor": "#868e96", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "strokeSharpness": "sharp", + "seed": 632787667, + "version": 198, + "versionNonce": 1893334067, + "isDeleted": false, + "boundElements": null, + "updated": 1662582138112, + "link": null, + "locked": false, + "points": [ + [ + 0, + 0 + ], + [ + 233.2672383568049, + 0 + ] + ], + "lastCommittedPoint": null, + "startBinding": { + "elementId": "TBYpmrW2OsKEqbpZfEeJg", + "gap": 4.238532110091741, + "focus": -0.020134228187919462 + }, + "endBinding": { + "elementId": "z5LT5d710gSTA9DjwiL3O", + "gap": 5.7060129725937765, + "focus": 0.5703812316715546 + }, + "startArrowhead": null, + "endArrowhead": "arrow" + }, + { + "type": "text", + "version": 651, + "versionNonce": 1375877757, + "isDeleted": false, + "id": "diazwl57WWW_7gfm7wMea", + "fillStyle": "hachure", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "angle": 0, + "x": 678, + "y": 637.5, + "strokeColor": "#000000", + "backgroundColor": "transparent", + "width": 262, + "height": 25, + "seed": 2077675699, + "groupIds": [], + "strokeSharpness": "sharp", + "boundElements": [], + "updated": 1662582152851, + "link": null, + "locked": false, + "fontSize": 20, + "fontFamily": 1, + "text": "merge PRs if no objections", + "baseline": 18, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "merge PRs if no objections" + }, + { + "type": "text", + "version": 658, + "versionNonce": 1558756051, + "isDeleted": false, + "id": "_T1wMHFNqfA6a8Ku2OLDl", + "fillStyle": "hachure", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "angle": 0, + "x": 840, + "y": 296.5, + "strokeColor": "#000000", + "backgroundColor": "transparent", + "width": 102, + "height": 25, + "seed": 1987233139, + "groupIds": [], + "strokeSharpness": "sharp", + "boundElements": [], + "updated": 1662582148465, + "link": null, + "locked": false, + "fontSize": 20, + "fontFamily": 1, + "text": "merge PRs", + "baseline": 18, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "merge PRs" + } + ], + "appState": { + "gridSize": null, + "viewBackgroundColor": "#ffffff" + }, + "files": {} +} \ No newline at end of file diff --git a/community/governance.md b/community/governance.md new file mode 100644 index 00000000000..89cf800bc88 --- /dev/null +++ b/community/governance.md @@ -0,0 +1,272 @@ +

Feast Governance

+ +- [Introduction](#introduction) +- [Feast community overview](#feast-community-overview) +- [Feast governance model overview](#feast-governance-model-overview) +- [Roles And Responsibilities](#roles-and-responsibilities) + - [Users](#users) + - [Contributors](#contributors) + - [CODEOWNERS](#codeowners) + - [Maintainers (project + area)](#maintainers-project--area) + - [Types of maintainers](#types-of-maintainers) + - [Optional maintainer responsibilities](#optional-maintainer-responsibilities) + - [Becoming a Maintainer](#becoming-a-maintainer) + - [Who is eligible to become a maintainer?](#who-is-eligible-to-become-a-maintainer) + - [Process](#process) + - [Earning a Nomination](#earning-a-nomination) + - [Losing Maintainer Status](#losing-maintainer-status) +- [Decision Making Process](#decision-making-process) + - [Lazy consensus](#lazy-consensus) + - [Voting](#voting) + - [Changes to Governance](#changes-to-governance) +- [Roadmap Creation](#roadmap-creation) + - [RFCs Process](#rfcs-process) + - [When to Use RFCs](#when-to-use-rfcs) +- [Resources](#resources) + + +# Introduction + +Feast is an open-source feature store for machine learning that allows teams to define, manage, store, and serve features to operational ML systems. + +The Feast project aims for open and transparent governance and decision-making, thus encouraging community building and contribution. + +A formal governance structure helps us to + +* Provide a structure for individuals to become involved in the project. +* Communicate all processes for members to operate within the project. +* Document a system for open product development, roadmapping, and planning. +* Provide a means for making decisions if consensus cannot be reached. + +# Feast community overview + +On a high level, the key moving parts of the community are: +- **GitHub activity** (issues + pull requests) +- **Slack community** ([slack.feast.dev](slack.feast.dev)) + - `#feast-development` is where design discussions happen amongst contributors + - Other Slack channels exist for users to ask and answer questions. +- **RFCs** ([drive folder](https://drive.google.com/drive/u/0/folders/1msUsgmDbVBaysmhBlg9lklYLLTMk4bC3)) for detailed discussions +- **Community calls** (biweekly) to discuss best practices, contributions, and announce changes +- **Maintainer syncs** (monthly) for [maintainers](maintainers.md) to discuss project direction and health + +With this structure, users and contributors largely self-organize and contribute changes as per [lazy consensus](#lazy-consensus). If there is active opposition and unresolvable conflict, then maintainers step in to break ties or make decisions. + +We dive more deeply into the governance model below. + +# Feast governance model overview + +Feast is a meritocratic, consensus-based community project. + +Anyone interested in the project can join the community to: +- contribute to the project design +- participate in the decision-making process. + +The general decision making workflow is as follows: + + + +> **Note**: There may not always a corresponding CODEOWNER for the affected code, in which case the responsibility falls on other maintainers or contributors with write access to review + merge the PR + +# Roles And Responsibilities + +## Users + +Users are community members who require the operational ML functionality of Feast. They are the most important community members, and without them, the project would have no purpose. Anyone can be a user; there are no special requirements. + +Feast asks its users to participate in the project and community as much as possible. User contributions enable the project team to ensure that they satisfy the needs of those users. Frequently, user contributions include (but are not limited to): + +* Providing developers with feedback on the project (user experience) +* Providing feature requests +* Filing bug reports or flagging issues +* Providing moral support +* Evangelizing the project + +Users who continue to engage with the project and its community will often become more and more involved. Such users may find themselves becoming contributors, as described in the next section. + + +## Contributors + +Contributors are community members who contribute in concrete ways to the project. Anyone can become a contributor, and contributions can take many forms, as detailed in the [all-contributors project](https://allcontributors.org/docs/en/emoji-key#table). There is no expectation of commitment to the project, no specific skill requirements, and no selection process. + +In addition to their actions as users, contributors may also find themselves doing one or more of the following: + +* Supporting new users (existing users are often the best people to help new users) +* Creating, triaging or commenting on Issues +* Doing code reviews or commenting on technical documents +* Writing, editing, translating or reviewing the documentation +* Organizing events or evangelizing the project + +Contributors engage with the project through the issue tracker and slack community, or by writing or editing documentation. They submit changes to the project itself via Pull Requests (PRs), which will be considered for inclusion in the project by existing maintainers (see next section). + +Contributors should follow the following guides when creating PRs: +- [Contribution Process](https://docs.feast.dev/project/contributing) +- [Development Guide](https://docs.feast.dev/project/development-guide). + +As contributors gain experience and familiarity with the project, their profile and commitment within the community will increase. At some stage, they may find themselves being nominated for being a maintainer. + +## CODEOWNERS + +On top of maintainers who will be in the CODEOWNERS file, other contributors can also be added as a lower commitment way to contribute by reviewing / responding to PRs. + +CODEOWNERS will generally be the first point of contacts in reviewing pull requests and will have commit privileges. + +## Maintainers (project + area) + +Maintainers are community members who have shown that they are committed to Feast’s continued development through ongoing engagement with the community. Because of this, maintainers have the right to merge PRs and have voting rights. + +> **Note**: maintainers, like other contributors, must make changes to Feast via pull requests (with code review). This applies to all changes to documentation, code, configuration, governance, etc. + +### Types of maintainers + +There are two kinds of maintainers + +1. **Project maintainers** control overall project organization and resolving disputes. They also + - Attend a regular maintainers sync + - Participate in strategic planning, approve changes to the governance model, and manage the copyrights within the project outputs. + - (optional) Attend community calls + - (optional) Planning project roadmaps and articulating vision + - (optional) Guide design decisions to reinforce key project values (e.g. simplicity) +2. **Area maintainers** own a specific technical area (which may span code modules), often specifically targeting a user journey or tech stack. They + - are generally point people in GitHub or Slack on discussions in that area (e.g. tagged in `#feast-development`) + - (optional) help drive roadmap decisions + +> **Note:** project maintainers may also be area maintainers, but this does not give their ideas increased weight over other area maintainers. + +Decisions that need tie breakers may require intervention via project maintainers majority consensus. + +### Optional maintainer responsibilities +Other optional activites a maintainer (project or area maintainer) may participate in: + * Monitor email aliases and our Slack (#feast-general, #feast-development, #feast-beginners). + * Perform code reviews for other maintainers and the community. The areas of specialization listed in [OWNERS.md](OWNERS.md) can be used to help with routing an issue/question to the right person. + * Triage GitHub issues, applying [labels]([https://github.com/feast-dev/feast/labels](https://github.com/feast-dev/feast/labels)) to each new item. Labels are extremely useful for future issue follow ups. Adding labels is somewhat subjective, so please use your best judgment. + * Triage build issues, filing issues for known flaky builds or bugs, fixing or finding someone to fix any master build breakages. + * Make sure that ongoing PRs are moving forward at the right pace or closing them. + +## Becoming a Maintainer + +### Who is eligible to become a maintainer? +Anyone can become a maintainer. Typically, a potential maintainer will need to show that they understand the project, its objectives, and its strategy. They will also have provided valuable contributions to the project over a period of time. Maintainers must also act in the interest of the community. + +### Process +Any existing maintainer can nominate new maintainers. Once they have been nominated, there will be a vote by the rest of the maintainers. Maintainer voting is one of the few activities that takes place in private. This is to allow maintainers to express their opinions about a nominee without causing embarrassment freely. The approval requires **three maintainers +1 vote** and **no -1 vote from a maintainer**. + +The nominee is entitled to request an explanation of any ‘no’ votes against them, regardless of the vote's outcome. This explanation will be provided by the maintainers and will be anonymous and constructive. + +Nominees may decline their appointment as a maintainer. Becoming a maintainer means that they will be spending a substantial time working on Feast for the foreseeable future. It is essential to recognize that being a maintainer is a privilege, not a right. That privilege must be earned, and once earned, the rest of the maintainers can remove it in extreme circumstances. + +Lazy consensus does not apply to becoming a maintainer. A vote must be held. Voting takes place through the [maintainer mailing list](https://groups.google.com/g/feast-maintainers). A vote must stay open for at least 7 days. + +### Earning a Nomination + +There is not a single path of earning a nomination for maintainer at Feast, however, we can give some guidance about some actions that would help: + +* Start by expressing interest to the maintainers that you are interested in becoming a maintainer. +* You can start tackling issues labeled as ‘help wanted’, or if you are new to the project, some of the ‘good first issue’ tickets. +* As you gain experience with the codebase and our standards, we will ask you to do code reviews for incoming PRs (i.e., all maintainers are expected to shoulder a proportional share of community reviews). +* We will expect you to start contributing increasingly complicated PRs, under the guidance of the existing maintainers. + +## Losing Maintainer Status + +If a maintainer is no longer interested and cannot perform the maintainer duties listed above, they can volunteer to be moved to emeritus status. The maintainer status is attributed for life otherwise. An emeritus maintainer may request reinstatement of commit access from the rest of maintainers. Such reinstatement is subject to lazy consensus approval of active maintainers. + +Emeritus status is a nominal title, and confers no special rights (like voting) or access. Emeritus members are functionally identical to normal contributors, with the exception that they can request for reinstatement of their commit access. + +In extreme cases, maintainers can lose their status by a vote of the maintainers per the voting process below. + + +# Decision Making Process + +Decisions about the future of Feast are made through discussion with all community members, from the newest user to the most experienced maintainer. All non-sensitive project management discussion takes place on the project issue tracker system. Occasionally, sensitive discussion occurs on a private channel of our Slack. + +To ensure that the project is not bogged down by endless discussion and continual voting, the project operates a policy of lazy consensus. This allows the majority of decisions to be made without resorting to a formal vote. + + +## Lazy consensus + +Decision making typically involves the following steps: +* Proposal *(via GitHub issue + GitHub PR)* +* Discussion *(in Slack channels at #feast-development and GitHub)* +* (optional) Maintainers voting (if there is active opposition + consensus is not reached through discussion) +* Decision + +Any community member can make a proposal for consideration by the community. To initiate a discussion about a new idea, they should create an issue or submit a PR implementing the idea to the issue tracker. This will prompt a review and, if necessary, a discussion of the idea. The goal of this review and discussion is to gain approval for the contribution. Since most people in the project community have a shared vision, there is often little discussion to reach consensus. + +In general, as long as nobody explicitly opposes a proposal or PR, it is recognized as having the support of the community. This is called lazy consensus - that is, those who have not stated their opinion explicitly have implicitly agreed to the proposal's implementation. + +Lazy consensus is a fundamental concept within the project. This process allows a large group of people to reach consensus efficiently as someone with no objections to a proposal need not spend time stating their position. + +For lazy consensus to be effective, it is necessary to allow at least 48 hours before assuming that there are no objections to the proposal. This requirement ensures that everyone is given enough time to read, digest, and respond to the proposal. This time period is chosen to be as inclusive as possible of all participants, regardless of their location and time commitments. + + +## Voting + +Not all decisions can be made using lazy consensus. Issues such as those affecting the strategic direction or legal standing of the project must gain explicit approval in the form of a vote. Every member of the community is encouraged to express their opinions in all discussions and all votes. However, only project maintainers have binding votes for the purposes of decision making. + + +## Changes to Governance + +We believe governance needs to adapt in order to be effective long term. This governance document itself can be extended or modified as our community and project grows and our needs change. + +A change in our governance structure should be a rare occurrence and should face sufficient scrutiny and review. To this end, the rules that apply to modifications to the Feast Governance structure are more stringent: + +* Governance changes are made through PRs to the [feast-dev/community](https://github.com/feast-dev/community) repository. +* Lazy consensus applies to governance changes, but the proposed changes must be public for at least 7 days instead of 48 hours before they are accepted. +* If there is opposition to a change, a vote will be held by maintainers. +* Voting is asynchronous. All maintainers must be notified of a vote through the [maintainer mailing list](https://groups.google.com/g/feast-maintainers). +* Maintainers must be given at least 7 days to respond. +* Voting requires a super-majority in order to pass a decision, and maintainers do not hold veto power for these votes. A super-majority is defined as at least 60% of votes in favor. +* The total pool of votes does not include those who abstain from voting. +* A quorum is required for voting. A quorum is 60% of maintainers. + + +# Roadmap Creation + +Our [roadmap](https://docs.feast.dev/roadmap) gives an overview of what we are currently working on and what we want to tackle next. This helps potential contributors understand your project's current status and where it's going next, as well as giving a chance to be part of the planning. + +In this section, we describe the process we follow to create it, using request for comments documents (RFCs). + + +## RFCs Process + +Most of the issues we see can be handled with regular GitHub issues. However, some changes are "substantial", and we ask that these go through a design process and produce a consensus among the Feast community. + +The "RFC" (request for comments) process is intended to provide a consistent and controlled path for new features to enter the roadmap. The high-level process looks like this: + + + +1. Contributor creates an RFC draft in the repository +2. Users, Contributors, and Maintainers discuss and upvote the draft +3. If confident on its success, contributor completes the RFC with more in-detail technical specifications +4. Maintainers approve RFC when it is ready +5. Maintainers meet every quarter and choose three or five items based on popularity and alignment with project vision and goals +6. Those selected items become part of the Mid-term goals + + +### When to Use RFCs + +What constitutes a "substantial" change is evolving based on the community, but may include the following: +* New features that require configuration options to activate/deactivate +* Remove features +* Architecture changes + +Some changes do not require an RFC: +* Reorganizing or refactoring code or documentation +* Improvements that tackle objective quality criteria (speedup, better browser support) +* Changes noticeable only by contributors or maintainers +* Examples: + * Adding programmatic descriptions + * Adding support for tags at a column level + +If you submit a pull request to implement a new feature without going through the RFC process, it may be closed with a polite request to submit an RFC first. That said, if most of the work is done, we'd accelerate the process. + +We will keep our RFC documents in a separate repo on the feast-dev organization, where a detailed step by step process will be documented. + + +# Resources + +* [Envoy’s Governance Document](https://github.com/envoyproxy/envoy/blob/master/GOVERNANCE.md) +* [OSS Watch, Meritocratic Governance](http://oss-watch.ac.uk/resources/meritocraticgovernancemodel) +* [The Apache Software Foundation meritocratic model](http://www.apache.org/foundation/how-it-works.html#meritocracy) +* [Ember RFCs](https://github.com/emberjs/rfcs) + +Attribution: The Feast governance structure is based on the Amundsen Governance structure. diff --git a/community/governance.png b/community/governance.png new file mode 100644 index 0000000000000000000000000000000000000000..c2b00930e3a3d4e4676ab80817e0cb364ff84777 GIT binary patch literal 258405 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
+For code examples of how the below work, inspect the generated repository from `feast init -t [YOUR TEMPLATE]` (`gcp`, `snowflake`, and `aws` are the most fully fleshed). -How to: generate training data +## Concepts +Before diving into how to retrieve features, we need to understand some high level concepts in Feast. -Feast abstracts away point-in-time join complexities with the `get_historical_features` API. +### Feature Services -It expects an **entity dataframe (or SQL query to retrieve a list of entities)** and a **list of feature references (or a feature service)** +A feature service is an object that represents a logical group of features from one or more [feature views](feature-view.md#feature-view). Feature Services allows features from within a feature view to be used as needed by an ML model. Users can expect to create one feature service per model version, allowing for tracking of the features used by models. -#### **Option 1: using feature references (to pick individual features when exploring data)** +{% tabs %} +{% tab title="driver_trips_feature_service.py" %} +```python +from driver_ratings_feature_view import driver_ratings_fv +from driver_trips_feature_view import driver_stats_fv + +driver_stats_fs = FeatureService( + name="driver_activity", + features=[driver_stats_fv, driver_ratings_fv[["lifetime_rating"]]] +) +``` +{% endtab %} +{% endtabs %} + +Feature services are used during + +* The generation of training datasets when querying feature views in order to find historical feature values. A single training dataset may consist of features from multiple feature views. +* Retrieval of features for batch scoring from the offline store (e.g. with an entity dataframe where all timestamps are `now()`) +* Retrieval of features from the online store for online inference (with smaller batch sizes). The features retrieved from the online store may also belong to multiple feature views. + +{% hint style="info" %} +Applying a feature service does not result in an actual service being deployed. +{% endhint %} + +Feature services enable referencing all or some features from a feature view. + +Retrieving from the online store with a feature service ```python -entity_df = pd.DataFrame.from_dict( - { - "driver_id": [1001, 1002, 1003, 1004, 1001], - "event_timestamp": [ - datetime(2021, 4, 12, 10, 59, 42), - datetime(2021, 4, 12, 8, 12, 10), - datetime(2021, 4, 12, 16, 40, 26), - datetime(2021, 4, 12, 15, 1, 12), - datetime.now() - ] - } +from feast import FeatureStore +feature_store = FeatureStore('.') # Initialize the feature store + +feature_service = feature_store.get_feature_service("driver_activity") +features = feature_store.get_online_features( + features=feature_service, entity_rows=[entity_dict] ) -training_df = store.get_historical_features( - entity_df=entity_df, +``` + +Retrieving from the offline store with a feature service + +```python +from feast import FeatureStore +feature_store = FeatureStore('.') # Initialize the feature store + +feature_service = feature_store.get_feature_service("driver_activity") +feature_store.get_historical_features(features=feature_service, entity_df=entity_df) +``` + +### Feature References + +This mechanism of retrieving features is only recommended as you're experimenting. Once you want to launch experiments or serve models, feature services are recommended. + +Feature references uniquely identify feature values in Feast. The structure of a feature reference in string form is as follows: `:` + +Feature references are used for the retrieval of features from Feast: + +```python +online_features = fs.get_online_features( features=[ - "driver_hourly_stats:conv_rate", - "driver_hourly_stats:acc_rate", - "driver_daily_features:daily_miles_driven" + 'driver_locations:lon', + 'drivers_activity:trips_today' ], -).to_df() -print(training_df.head()) + entity_rows=[ + # {join_key: entity_value} + {'driver': 'driver_1001'} + ] +) ``` -#### Option 2: using feature services (to version models) +It is possible to retrieve features from multiple feature views with a single request, and Feast is able to join features from multiple tables in order to build a training dataset. However, it is not possible to reference (or retrieve) features from multiple projects at the same time. + +{% hint style="info" %} +Note, if you're using [Feature views without entities](feature-view.md#feature-views-without-entities), then those features can be added here without additional entity values in the `entity_rows` parameter. +{% endhint %} + +### Event timestamp + +The timestamp on which an event occurred, as found in a feature view's data source. The event timestamp describes the event time at which a feature was observed or generated. + +Event timestamps are used during point-in-time joins to ensure that the latest feature values are joined from feature views onto entity rows. Event timestamps are also used to ensure that old feature values aren't served to models during online serving. + +### Dataset + +A dataset is a collection of rows that is produced by a historical retrieval from Feast in order to train a model. A dataset is produced by a join from one or more feature views onto an entity dataframe. Therefore, a dataset may consist of features from multiple feature views. + +**Dataset vs Feature View:** Feature views contain the schema of data and a reference to where data can be found (through its data source). Datasets are the actual data manifestation of querying those data sources. + +**Dataset vs Data Source:** Datasets are the output of historical retrieval, whereas data sources are the inputs. One or more data sources can be used in the creation of a dataset. + +## Retrieving historical features (for training data or batch scoring) +Feast abstracts away point-in-time join complexities with the `get_historical_features` API. + +We go through the major steps, and also show example code. Note that the quickstart templates generally have end-to-end working examples for all these cases. + +
+ +Full example: generate training data ```python entity_df = pd.DataFrame.from_dict( @@ -77,60 +148,118 @@ print(training_df.head())
-How to: retrieve offline features for batch scoring +Full example: retrieve offline features for batch scoring The main difference here compared to training data generation is how to handle timestamps in the entity dataframe. You want to pass in the **current time** to get the latest feature values for all your entities. -#### Option 1: fetching features with entity dataframe - ```python from feast import FeatureStore -import pandas as pd store = FeatureStore(repo_path=".") # Get the latest feature values for unique entities -entity_df = pd.DataFrame.from_dict({"driver_id": [1001, 1002, 1003, 1004, 1005],}) -entity_df["event_timestamp"] = pd.to_datetime("now", utc=True) +entity_sql = f""" + SELECT + driver_id, + CURRENT_TIMESTAMP() as event_timestamp + FROM {store.get_data_source("driver_hourly_stats_source").get_table_query_string()} + WHERE event_timestamp BETWEEN '2021-01-01' and '2021-12-31' + GROUP BY driver_id +""" batch_scoring_features = store.get_historical_features( - entity_df=entity_df, features=store.get_feature_service("model_v2"), + entity_df=entity_sql, + features=store.get_feature_service("model_v2"), ).to_df() # predictions = model.predict(batch_scoring_features) ``` -#### Option 2: fetching features using a SQL query to generate entities +
-```python -from feast import FeatureStore +### Step 1: Specifying Features +Feast accepts either: +- [feature services](feature-retrieval.md#feature-services), which group features needed for a model version +- [feature references](feature-retrieval.md#feature-references) -store = FeatureStore(repo_path=".") +### Example: querying a feature service (recommended) +```python +training_df = store.get_historical_features( + entity_df=entity_df, + features=store.get_feature_service("model_v1"), +).to_df() +``` -# Get the latest feature values for unique entities -batch_scoring_features = store.get_historical_features( - entity_df=""" - SELECT - user_id, - CURRENT_TIME() as event_timestamp - FROM entity_source_table - WHERE user_last_active_time BETWEEN '2019-01-01' and '2020-12-31' - GROUP BY user_id - """ - , - features=store.get_feature_service("model_v2"), +### Example: querying a list of feature references +```python +training_df = store.get_historical_features( + entity_df=entity_df, + features=[ + "driver_hourly_stats:conv_rate", + "driver_hourly_stats:acc_rate", + "driver_daily_features:daily_miles_driven" + ], ).to_df() -# predictions = model.predict(batch_scoring_features) ``` +### Step 2: Specifying Entities +Feast accepts either a **Pandas dataframe** as the entity dataframe (including entity keys and timestamps) or a **SQL query** to generate the entities. -
+Both approaches must specify the full **entity key** needed as well as the **timestamps**. Feast then joins features onto this dataframe. -
+### Example: entity dataframe for generating training data +```python +entity_df = pd.DataFrame.from_dict( + { + "driver_id": [1001, 1002, 1003, 1004, 1001], + "event_timestamp": [ + datetime(2021, 4, 12, 10, 59, 42), + datetime(2021, 4, 12, 8, 12, 10), + datetime(2021, 4, 12, 16, 40, 26), + datetime(2021, 4, 12, 15, 1, 12), + datetime.now() + ] + } +) +training_df = store.get_historical_features( + entity_df=entity_df, + features=[ + "driver_hourly_stats:conv_rate", + "driver_hourly_stats:acc_rate", + "driver_daily_features:daily_miles_driven" + ], +).to_df() +``` -How to: retrieve online features for real-time model inference (Python SDK) +### Example: entity SQL query for generating training data +You can also pass a SQL string to generate the above dataframe. This is useful for getting all entities in a timeframe from some data source. + +```python +entity_sql = f""" + SELECT + driver_id, + event_timestamp + FROM {store.get_data_source("driver_hourly_stats_source").get_table_query_string()} + WHERE event_timestamp BETWEEN '2021-01-01' and '2021-12-31' +""" +training_df = store.get_historical_features( + entity_df=entity_sql, + features=[ + "driver_hourly_stats:conv_rate", + "driver_hourly_stats:acc_rate", + "driver_daily_features:daily_miles_driven" + ], +).to_df() +``` +## Retrieving online features (for model inference) Feast will ensure the latest feature values for registered features are available. At retrieval time, you need to supply a list of **entities** and the corresponding **features** to be retrieved. Similar to `get_historical_features`, we recommend using feature services as a mechanism for grouping features in a model version. _Note: unlike `get_historical_features`, the `entity_rows` **do not need timestamps** since you only want one feature value per entity key._ +There are several options for retrieving online features: through the SDK, or through a feature server + +
+ +Full example: retrieve online features for real-time model inference (Python SDK) + ```python from feast import RepoConfig, FeatureStore from feast.repo_config import RegistryConfig @@ -160,11 +289,7 @@ features = store.get_online_features(
-How to: retrieve online features for real-time model inference (Feature Server) - -Feast will ensure the latest feature values for registered features are available. At retrieval time, you need to supply a list of **entities** and the corresponding **features** to be retrieved. Similar to `get_historical_features`, we recommend using feature services as a mechanism for grouping features in a model version. - -_Note: unlike `get_historical_features`, the `entity_rows` **do not need timestamps** since you only want one feature value per entity key._ +Full example: retrieve online features for real-time model inference (Feature Server) This approach requires you to deploy a feature server (see [Python feature server](../../reference/feature-servers/python-feature-server)). @@ -183,96 +308,3 @@ print(json.dumps(r.json(), indent=4, sort_keys=True)) ```
- -## Feature Services - -A feature service is an object that represents a logical group of features from one or more [feature views](feature-view.md#feature-view). Feature Services allows features from within a feature view to be used as needed by an ML model. Users can expect to create one feature service per model version, allowing for tracking of the features used by models. - -{% tabs %} -{% tab title="driver_trips_feature_service.py" %} -```python -from driver_ratings_feature_view import driver_ratings_fv -from driver_trips_feature_view import driver_stats_fv - -driver_stats_fs = FeatureService( - name="driver_activity", - features=[driver_stats_fv, driver_ratings_fv[["lifetime_rating"]]] -) -``` -{% endtab %} -{% endtabs %} - -Feature services are used during - -* The generation of training datasets when querying feature views in order to find historical feature values. A single training dataset may consist of features from multiple feature views. -* Retrieval of features for batch scoring from the offline store (e.g. with an entity dataframe where all timestamps are `now()`) -* Retrieval of features from the online store for online inference (with smaller batch sizes). The features retrieved from the online store may also belong to multiple feature views. - -{% hint style="info" %} -Applying a feature service does not result in an actual service being deployed. -{% endhint %} - -Feature services enable referencing all or some features from a feature view. - -Retrieving from the online store with a feature service - -```python -from feast import FeatureStore -feature_store = FeatureStore('.') # Initialize the feature store - -feature_service = feature_store.get_feature_service("driver_activity") -features = feature_store.get_online_features( - features=feature_service, entity_rows=[entity_dict] -) -``` - -Retrieving from the offline store with a feature service - -```python -from feast import FeatureStore -feature_store = FeatureStore('.') # Initialize the feature store - -feature_service = feature_store.get_feature_service("driver_activity") -feature_store.get_historical_features(features=feature_service, entity_df=entity_df) -``` - -## Feature References - -This mechanism of retrieving features is only recommended as you're experimenting. Once you want to launch experiments or serve models, feature services are recommended. - -Feature references uniquely identify feature values in Feast. The structure of a feature reference in string form is as follows: `:` - -Feature references are used for the retrieval of features from Feast: - -```python -online_features = fs.get_online_features( - features=[ - 'driver_locations:lon', - 'drivers_activity:trips_today' - ], - entity_rows=[ - # {join_key: entity_value} - {'driver': 'driver_1001'} - ] -) -``` - -It is possible to retrieve features from multiple feature views with a single request, and Feast is able to join features from multiple tables in order to build a training dataset. However, it is not possible to reference (or retrieve) features from multiple projects at the same time. - -{% hint style="info" %} -Note, if you're using [Feature views without entities](feature-view.md#feature-views-without-entities), then those features can be added here without additional entity values in the `entity_rows` parameter. -{% endhint %} - -## Event timestamp - -The timestamp on which an event occurred, as found in a feature view's data source. The event timestamp describes the event time at which a feature was observed or generated. - -Event timestamps are used during point-in-time joins to ensure that the latest feature values are joined from feature views onto entity rows. Event timestamps are also used to ensure that old feature values aren't served to models during online serving. - -## Dataset - -A dataset is a collection of rows that is produced by a historical retrieval from Feast in order to train a model. A dataset is produced by a join from one or more feature views onto an entity dataframe. Therefore, a dataset may consist of features from multiple feature views. - -**Dataset vs Feature View:** Feature views contain the schema of data and a reference to where data can be found (through its data source). Datasets are the actual data manifestation of querying those data sources. - -**Dataset vs Data Source:** Datasets are the output of historical retrieval, whereas data sources are the inputs. One or more data sources can be used in the creation of a dataset. diff --git a/docs/how-to-guides/running-feast-in-production.md b/docs/how-to-guides/running-feast-in-production.md index 95f3e99581e..ef903b68c4b 100644 --- a/docs/how-to-guides/running-feast-in-production.md +++ b/docs/how-to-guides/running-feast-in-production.md @@ -132,6 +132,8 @@ This supports pushing feature values into Feast to both online or offline stores ## 3. How to use Feast for model training ### 3.1. Generating training data +> For more details, see [feature retrieval](../getting-started/concepts/feature-retrieval.md#retrieving-historical-features-for-training-data-or-batch-scoring) + After we've defined our features and data sources in the repository, we can generate training datasets. We highly recommend you use a `FeatureService` to version the features that go into a specific model version. 1. The first thing we need to do in our training code is to create a `FeatureStore` object with a path to the registry. diff --git a/sdk/python/feast/errors.py b/sdk/python/feast/errors.py index 15fda6ac7d0..834df0e5c48 100644 --- a/sdk/python/feast/errors.py +++ b/sdk/python/feast/errors.py @@ -384,3 +384,17 @@ def __init__(self, features: Any): super().__init__( f"Invalid `features` parameter type {type(features)}. Expected one of List[str] and FeatureService." ) + + +class EntitySQLEmptyResults(Exception): + def __init__(self, entity_sql: str): + super().__init__( + f"No entity values found from the specified SQL query to generate the entity dataframe: {entity_sql}." + ) + + +class EntityDFNotDateTime(Exception): + def __init__(self): + super().__init__( + "The entity dataframe specified does not have the timestamp field as a datetime." + ) diff --git a/sdk/python/feast/infra/offline_stores/bigquery.py b/sdk/python/feast/infra/offline_stores/bigquery.py index 8b2773fb657..c570b8d38ab 100644 --- a/sdk/python/feast/infra/offline_stores/bigquery.py +++ b/sdk/python/feast/infra/offline_stores/bigquery.py @@ -28,6 +28,8 @@ from feast.errors import ( BigQueryJobCancelled, BigQueryJobStillRunning, + EntityDFNotDateTime, + EntitySQLEmptyResults, FeastProviderLoginError, InvalidEntityType, ) @@ -665,6 +667,13 @@ def _get_entity_df_event_timestamp_range( res.get("min"), res.get("max"), ) + if ( + entity_df_event_timestamp_range[0] is None + or entity_df_event_timestamp_range[1] is None + ): + raise EntitySQLEmptyResults(entity_df) + if type(entity_df_event_timestamp_range[0]) != datetime: + raise EntityDFNotDateTime() elif isinstance(entity_df, pd.DataFrame): entity_df_event_timestamp = entity_df.loc[ :, entity_df_event_timestamp_col diff --git a/sdk/python/feast/infra/offline_stores/snowflake.py b/sdk/python/feast/infra/offline_stores/snowflake.py index c835eb939ff..2d621de50ff 100644 --- a/sdk/python/feast/infra/offline_stores/snowflake.py +++ b/sdk/python/feast/infra/offline_stores/snowflake.py @@ -25,7 +25,7 @@ from feast import OnDemandFeatureView from feast.data_source import DataSource -from feast.errors import InvalidEntityType +from feast.errors import EntitySQLEmptyResults, InvalidEntityType from feast.feature_logging import LoggingConfig, LoggingSource from feast.feature_view import DUMMY_ENTITY_ID, DUMMY_ENTITY_VAL, FeatureView from feast.infra.offline_stores import offline_utils @@ -574,6 +574,11 @@ def _get_entity_df_event_timestamp_range( results = execute_snowflake_statement(snowflake_conn, query).fetchall() entity_df_event_timestamp_range = cast(Tuple[datetime, datetime], results[0]) + if ( + entity_df_event_timestamp_range[0] is None + or entity_df_event_timestamp_range[1] is None + ): + raise EntitySQLEmptyResults(entity_df) else: raise InvalidEntityType(type(entity_df)) diff --git a/sdk/python/feast/templates/aws/feature_repo/test_workflow.py b/sdk/python/feast/templates/aws/feature_repo/test_workflow.py index 494b1914f92..59ac1f0ee73 100644 --- a/sdk/python/feast/templates/aws/feature_repo/test_workflow.py +++ b/sdk/python/feast/templates/aws/feature_repo/test_workflow.py @@ -1,7 +1,9 @@ +import random import subprocess -from datetime import datetime +from datetime import datetime, timedelta import pandas as pd +from pytz import utc from feast import FeatureStore from feast.data_source import PushMode @@ -18,6 +20,16 @@ def run_demo(): print("\n--- Historical features for batch scoring ---") fetch_historical_features_entity_df(store, for_batch_scoring=True) + print( + "\n--- Historical features for training (all entities in a window using SQL entity dataframe) ---" + ) + fetch_historical_features_entity_sql(store, for_batch_scoring=False) + + print( + "\n--- Historical features for batch scoring (all entities in a window using SQL entity dataframe) ---" + ) + fetch_historical_features_entity_sql(store, for_batch_scoring=True) + print("\n--- Load features into online store ---") store.materialize_incremental(end_date=datetime.now()) @@ -43,8 +55,8 @@ def run_demo(): datetime.now(), ], "conv_rate": [1.0], - "acc_rate": [1.0], - "avg_daily_trips": [1000], + "acc_rate": [1.0 + random.random()], + "avg_daily_trips": [int(1000 * random.random())], } ) print(event_df) @@ -57,6 +69,47 @@ def run_demo(): subprocess.run(["feast", "teardown"]) +def fetch_historical_features_entity_sql(store: FeatureStore, for_batch_scoring): + end_date = ( + datetime.now().replace(microsecond=0, second=0, minute=0).astimezone(tz=utc) + ) + start_date = (end_date - timedelta(days=60)).astimezone(tz=utc) + # For batch scoring, we want the latest timestamps + if for_batch_scoring: + print( + "Generating a list of all unique entities in a time window for batch scoring" + ) + # We use a group by since we want all distinct driver_ids. + entity_sql = f""" + SELECT + driver_id, + GETDATE() as event_timestamp + FROM {store.get_data_source("feast_driver_hourly_stats").get_table_query_string()} + WHERE event_timestamp BETWEEN TIMESTAMP '{start_date}' AND TIMESTAMP '{end_date}' + GROUP BY driver_id + """ + else: + print("Generating training data for all entities in a time window") + # We don't need a group by if we want to generate training data + entity_sql = f""" + SELECT + driver_id, + event_timestamp + FROM {store.get_data_source("feast_driver_hourly_stats").get_table_query_string()} + WHERE event_timestamp BETWEEN TIMESTAMP '{start_date}' AND TIMESTAMP '{end_date}' + """ + + training_df = store.get_historical_features( + entity_df=entity_sql, + features=[ + "driver_hourly_stats:conv_rate", + "driver_hourly_stats:acc_rate", + "driver_hourly_stats:avg_daily_trips", + ], + ).to_df() + print(training_df.head()) + + def fetch_historical_features_entity_df(store: FeatureStore, for_batch_scoring: bool): # Note: see https://docs.feast.dev/getting-started/concepts/feature-retrieval for more details on how to retrieve # for all entities in the offline store instead diff --git a/sdk/python/feast/templates/gcp/feature_repo/test_workflow.py b/sdk/python/feast/templates/gcp/feature_repo/test_workflow.py index 51ce1eb05be..95ca080012b 100644 --- a/sdk/python/feast/templates/gcp/feature_repo/test_workflow.py +++ b/sdk/python/feast/templates/gcp/feature_repo/test_workflow.py @@ -19,6 +19,16 @@ def run_demo(): print("\n--- Historical features for batch scoring ---") fetch_historical_features_entity_df(store, for_batch_scoring=True) + print( + "\n--- Historical features for training (all entities in a window using SQL entity dataframe) ---" + ) + fetch_historical_features_entity_sql(store, for_batch_scoring=False) + + print( + "\n--- Historical features for batch scoring (all entities in a window using SQL entity dataframe) ---" + ) + fetch_historical_features_entity_sql(store, for_batch_scoring=True) + print("\n--- Load features into online store ---") store.materialize_incremental(end_date=datetime.now()) @@ -60,6 +70,43 @@ def run_demo(): subprocess.run(["feast", "teardown"]) +def fetch_historical_features_entity_sql(store: FeatureStore, for_batch_scoring): + # For batch scoring, we want the latest timestamps + if for_batch_scoring: + print( + "Generating a list of all unique entities in a time window for batch scoring" + ) + # We use a group by since we want all distinct driver_ids. + entity_sql = f""" + SELECT + driver_id, + CURRENT_TIMESTAMP() as event_timestamp + FROM {store.get_data_source("driver_hourly_stats_source").get_table_query_string()} + WHERE event_timestamp BETWEEN '2021-01-01' and '2021-12-31' + GROUP BY driver_id + """ + else: + print("Generating training data for all entities in a time window") + # We don't need a group by if we want to generate training data + entity_sql = f""" + SELECT + driver_id, + event_timestamp + FROM {store.get_data_source("driver_hourly_stats_source").get_table_query_string()} + WHERE event_timestamp BETWEEN '2021-01-01' and '2021-12-31' + """ + + training_df = store.get_historical_features( + entity_df=entity_sql, + features=[ + "driver_hourly_stats:conv_rate", + "driver_hourly_stats:acc_rate", + "driver_hourly_stats:avg_daily_trips", + ], + ).to_df() + print(training_df.head()) + + def fetch_historical_features_entity_df(store: FeatureStore, for_batch_scoring: bool): # Note: see https://docs.feast.dev/getting-started/concepts/feature-retrieval for more details on how to retrieve # for all entities in the offline store instead diff --git a/sdk/python/feast/templates/snowflake/test_workflow.py b/sdk/python/feast/templates/snowflake/test_workflow.py index adeed279af5..904d1e1f3e5 100644 --- a/sdk/python/feast/templates/snowflake/test_workflow.py +++ b/sdk/python/feast/templates/snowflake/test_workflow.py @@ -1,7 +1,9 @@ +import random import subprocess -from datetime import datetime +from datetime import datetime, timedelta import pandas as pd +from pytz import utc from feast import FeatureStore from feast.data_source import PushMode @@ -19,6 +21,16 @@ def run_demo(): print("\n--- Historical features for batch scoring ---") fetch_historical_features_entity_df(store, for_batch_scoring=True) + print( + "\n--- Historical features for training (all entities in a window using SQL entity dataframe) ---" + ) + fetch_historical_features_entity_sql(store, for_batch_scoring=False) + + print( + "\n--- Historical features for batch scoring (all entities in a window using SQL entity dataframe) ---" + ) + fetch_historical_features_entity_sql(store, for_batch_scoring=True) + print("\n--- Load features into online store ---") store.materialize_incremental(end_date=datetime.now()) @@ -44,8 +56,8 @@ def run_demo(): datetime.now(), ], "conv_rate": [1.0], - "acc_rate": [1.0], - "avg_daily_trips": [1000], + "acc_rate": [1.0 + random.random()], + "avg_daily_trips": [int(1000 * random.random())], } ) print(event_df) @@ -59,6 +71,47 @@ def run_demo(): subprocess.run(command, shell=True) +def fetch_historical_features_entity_sql(store: FeatureStore, for_batch_scoring): + end_date = ( + datetime.now().replace(microsecond=0, second=0, minute=0).astimezone(tz=utc) + ) + start_date = (end_date - timedelta(days=60)).astimezone(tz=utc) + # For batch scoring, we want the latest timestamps + if for_batch_scoring: + print( + "Generating a list of all unique entities in a time window for batch scoring" + ) + # We use a group by since we want all distinct driver_ids. + entity_sql = f""" + SELECT + "driver_id", + CURRENT_TIMESTAMP() as "event_timestamp" + FROM {store.list_data_sources()[-1].get_table_query_string()} + WHERE "event_timestamp" BETWEEN '{start_date}' AND '{end_date}' + GROUP BY "driver_id" + """ + else: + print("Generating training data for all entities in a time window") + # We don't need a group by if we want to generate training data + entity_sql = f""" + SELECT + "driver_id", + "event_timestamp" + FROM {store.list_data_sources()[-1].get_table_query_string()} + WHERE "event_timestamp" BETWEEN '{start_date}' AND '{end_date}' + """ + + training_df = store.get_historical_features( + entity_df=entity_sql, + features=[ + "driver_hourly_stats:conv_rate", + "driver_hourly_stats:acc_rate", + "driver_hourly_stats:avg_daily_trips", + ], + ).to_df() + print(training_df.head()) + + def fetch_historical_features_entity_df(store: FeatureStore, for_batch_scoring: bool): # Note: see https://docs.feast.dev/getting-started/concepts/feature-retrieval for more details on how to retrieve # for all entities in the offline store instead From ae37b2058e59a722c45324f5b43668ae4e74657d Mon Sep 17 00:00:00 2001 From: ptran32 <11425176+ptran32@users.noreply.github.com> Date: Thu, 15 Sep 2022 17:07:21 -0400 Subject: [PATCH 40/46] fix: Feature-server image is missing mysql dependency for mysql registry (#3223) * add requirements.txt and add mysqlclient package Signed-off-by: 11425176+ptran32@users.noreply.github.com <11425176+ptran32@users.noreply.github.com> Author: 11425176+ptran32@users.noreply.github.com <11425176+ptran32@users.noreply.github.com> Signed-off-by: 11425176+ptran32@users.noreply.github.com <11425176+ptran32@users.noreply.github.com> * use feast[mysql] in requirements.txt Signed-off-by: 11425176+ptran32@users.noreply.github.com <11425176+ptran32@users.noreply.github.com> * add feast components Signed-off-by: 11425176+ptran32@users.noreply.github.com <11425176+ptran32@users.noreply.github.com> * add native libraries needed by mysqlclient Signed-off-by: 11425176+ptran32@users.noreply.github.com <11425176+ptran32@users.noreply.github.com> Signed-off-by: 11425176+ptran32@users.noreply.github.com <11425176+ptran32@users.noreply.github.com> --- .../feast/infra/feature_servers/multicloud/Dockerfile | 11 +++++++++-- .../infra/feature_servers/multicloud/Dockerfile.dev | 9 +++++++-- .../infra/feature_servers/multicloud/requirements.txt | 1 + 3 files changed, 17 insertions(+), 4 deletions(-) create mode 100644 sdk/python/feast/infra/feature_servers/multicloud/requirements.txt diff --git a/sdk/python/feast/infra/feature_servers/multicloud/Dockerfile b/sdk/python/feast/infra/feature_servers/multicloud/Dockerfile index b853411e273..751a398ad4f 100644 --- a/sdk/python/feast/infra/feature_servers/multicloud/Dockerfile +++ b/sdk/python/feast/infra/feature_servers/multicloud/Dockerfile @@ -1,9 +1,16 @@ FROM python:3.8 RUN apt update && \ - apt install -y jq + apt install -y \ + jq \ + python3-dev \ + default-libmysqlclient-dev \ + build-essential + RUN pip install pip --upgrade -RUN pip install "feast[aws,gcp,snowflake,redis,go]" +COPY . . + +RUN pip install -r requirements.txt RUN apt update RUN apt install -y -V ca-certificates lsb-release wget RUN wget https://apache.jfrog.io/artifactory/arrow/$(lsb_release --id --short | tr 'A-Z' 'a-z')/apache-arrow-apt-source-latest-$(lsb_release --codename --short).deb diff --git a/sdk/python/feast/infra/feature_servers/multicloud/Dockerfile.dev b/sdk/python/feast/infra/feature_servers/multicloud/Dockerfile.dev index f1dd7cc390b..751a398ad4f 100644 --- a/sdk/python/feast/infra/feature_servers/multicloud/Dockerfile.dev +++ b/sdk/python/feast/infra/feature_servers/multicloud/Dockerfile.dev @@ -1,11 +1,16 @@ FROM python:3.8 RUN apt update && \ - apt install -y jq + apt install -y \ + jq \ + python3-dev \ + default-libmysqlclient-dev \ + build-essential + RUN pip install pip --upgrade COPY . . -RUN pip install ".[aws,gcp,snowflake,redis,go]" +RUN pip install -r requirements.txt RUN apt update RUN apt install -y -V ca-certificates lsb-release wget RUN wget https://apache.jfrog.io/artifactory/arrow/$(lsb_release --id --short | tr 'A-Z' 'a-z')/apache-arrow-apt-source-latest-$(lsb_release --codename --short).deb diff --git a/sdk/python/feast/infra/feature_servers/multicloud/requirements.txt b/sdk/python/feast/infra/feature_servers/multicloud/requirements.txt new file mode 100644 index 00000000000..01d08a4effa --- /dev/null +++ b/sdk/python/feast/infra/feature_servers/multicloud/requirements.txt @@ -0,0 +1 @@ +feast[aws,gcp,snowflake,redis,go,mysql] From a9060188713e34d07fd82cf3469061fdd2220956 Mon Sep 17 00:00:00 2001 From: Max Zwiessele Date: Fri, 16 Sep 2022 17:58:43 +0100 Subject: [PATCH 41/46] fix: Remove opening file object when validating S3 parquet source (#3217) * Remove opening the file object Let pyarrow handle opening the path using the filesystem. Signed-off-by: Max Z * fix: linting error Signed-off-by: Max Z Signed-off-by: Max Z --- sdk/python/feast/infra/offline_stores/file_source.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/sdk/python/feast/infra/offline_stores/file_source.py b/sdk/python/feast/infra/offline_stores/file_source.py index 135409ed04a..81a83c22457 100644 --- a/sdk/python/feast/infra/offline_stores/file_source.py +++ b/sdk/python/feast/infra/offline_stores/file_source.py @@ -160,9 +160,7 @@ def get_table_column_names_and_types( if filesystem is None: schema = ParquetDataset(path).schema.to_arrow_schema() else: - schema = ParquetDataset( - filesystem.open_input_file(path), filesystem=filesystem - ).schema + schema = ParquetDataset(path, filesystem=filesystem).schema return zip(schema.names, map(str, schema.types)) From e1170822bd3de8e1bfe803d9e2757c760fa5df2f Mon Sep 17 00:00:00 2001 From: Nikolaus Sonntag <3696356+nsonntag@users.noreply.github.com> Date: Fri, 16 Sep 2022 19:45:41 -0600 Subject: [PATCH 42/46] fix: Broken Feature Service Link (#3227) Fixing Broken Link Signed-off-by: nsonntag <3696356+nsonntag@users.noreply.github.com> Signed-off-by: nsonntag <3696356+nsonntag@users.noreply.github.com> --- ui/src/pages/feature-services/FeatureServiceIndexEmptyState.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ui/src/pages/feature-services/FeatureServiceIndexEmptyState.tsx b/ui/src/pages/feature-services/FeatureServiceIndexEmptyState.tsx index 40778680a3f..b5120325d08 100644 --- a/ui/src/pages/feature-services/FeatureServiceIndexEmptyState.tsx +++ b/ui/src/pages/feature-services/FeatureServiceIndexEmptyState.tsx @@ -17,7 +17,7 @@ const FeatureServiceIndexEmptyState = () => { { window.open( - "https://docs.feast.dev/getting-started/concepts/feature-service", + "https://docs.feast.dev/getting-started/concepts/feature-retrieval#feature-services", "_blank" ); }} From f020630c0144ab366f50c29dc3c97b8501687d3b Mon Sep 17 00:00:00 2001 From: William Horton Date: Mon, 19 Sep 2022 11:05:19 -0700 Subject: [PATCH 43/46] fix: Fix handling of TTL in Go server (#3232) Fix handling of TTL in Go server According to the FeatureView docstring, 'A ttl of 0 indicates that this group of features lives forever.' However, the Go feature server doesn't currently respect this, returning OUTSIDE_MAX_AGE for FeatureViews that have a TTL of 0. This fixes that issue by always returning false for checkOutsideTtl if the TTL is 0. Signed-off-by: William Horton Signed-off-by: William Horton --- go/internal/feast/onlineserving/serving.go | 3 +++ 1 file changed, 3 insertions(+) diff --git a/go/internal/feast/onlineserving/serving.go b/go/internal/feast/onlineserving/serving.go index 131cc6d1f14..dc7124fc8b8 100644 --- a/go/internal/feast/onlineserving/serving.go +++ b/go/internal/feast/onlineserving/serving.go @@ -633,6 +633,9 @@ func getUniqueEntityRows(joinKeysProto []*prototypes.EntityKey) ([]*prototypes.E } func checkOutsideTtl(featureTimestamp *timestamppb.Timestamp, currentTimestamp *timestamppb.Timestamp, ttl *durationpb.Duration) bool { + if ttl.Seconds == 0 { + return false + } return currentTimestamp.GetSeconds()-featureTimestamp.GetSeconds() > ttl.Seconds } From fbc6a2c48303424ef08f9b206e406fc0448e5c6f Mon Sep 17 00:00:00 2001 From: Max Zwiessele Date: Tue, 20 Sep 2022 04:03:57 +0100 Subject: [PATCH 44/46] feat: Add ability to give boto extra args for registry config (#3219) * feature: add ability to give boto extra args in config Signed-off-by: Max Z * rename: s3_additional_kwargs Signed-off-by: Max Z Signed-off-by: Max Z --- sdk/python/feast/infra/registry/s3.py | 5 ++++- sdk/python/feast/repo_config.py | 3 +++ 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/sdk/python/feast/infra/registry/s3.py b/sdk/python/feast/infra/registry/s3.py index d3772910f56..0a94c942e18 100644 --- a/sdk/python/feast/infra/registry/s3.py +++ b/sdk/python/feast/infra/registry/s3.py @@ -25,6 +25,7 @@ def __init__(self, registry_config: RegistryConfig, repo_path: Path): self._uri = urlparse(uri) self._bucket = self._uri.hostname self._key = self._uri.path.lstrip("/") + self._boto_extra_args = registry_config.s3_additional_kwargs or {} self.s3_client = boto3.resource( "s3", endpoint_url=os.environ.get("FEAST_S3_ENDPOINT_URL") @@ -77,4 +78,6 @@ def _write_registry(self, registry_proto: RegistryProto): file_obj = TemporaryFile() file_obj.write(registry_proto.SerializeToString()) file_obj.seek(0) - self.s3_client.Bucket(self._bucket).put_object(Body=file_obj, Key=self._key) + self.s3_client.Bucket(self._bucket).put_object( + Body=file_obj, Key=self._key, **self._boto_extra_args + ) diff --git a/sdk/python/feast/repo_config.py b/sdk/python/feast/repo_config.py index d5f68a8db61..fab0f25b410 100644 --- a/sdk/python/feast/repo_config.py +++ b/sdk/python/feast/repo_config.py @@ -113,6 +113,9 @@ class RegistryConfig(FeastBaseModel): set to infinity by setting TTL to 0 seconds, which means the cache will only be loaded once and will never expire. Users can manually refresh the cache by calling feature_store.refresh_registry() """ + s3_additional_kwargs: Optional[Dict[str, str]] + """ Dict[str, str]: Extra arguments to pass to boto3 when writing the registry file to S3. """ + class RepoConfig(FeastBaseModel): """Repo config. Typically loaded from `feature_store.yaml`""" From ae727b815ef06d71cbf0c984d9c2e23d42005456 Mon Sep 17 00:00:00 2001 From: Felix Wang Date: Mon, 19 Sep 2022 22:35:44 -0700 Subject: [PATCH 45/46] chore: Fix bump files (#3233) Fix Signed-off-by: Felix Wang Signed-off-by: Felix Wang --- infra/scripts/release/files_to_bump.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/infra/scripts/release/files_to_bump.txt b/infra/scripts/release/files_to_bump.txt index e43d637ede2..d7588185ded 100644 --- a/infra/scripts/release/files_to_bump.txt +++ b/infra/scripts/release/files_to_bump.txt @@ -6,7 +6,7 @@ infra/charts/feast/charts/transformation-service/values.yaml 8 infra/charts/feast/charts/feature-server/Chart.yaml 4 5 infra/charts/feast/charts/feature-server/README.md 3 20 infra/charts/feast/charts/feature-server/values.yaml 8 -infra/charts/feast/README.md 11 61 62 +infra/charts/feast/README.md 11 68 69 infra/charts/feast-python-server/Chart.yaml 5 infra/charts/feast-python-server/README.md 5 infra/charts/feast-feature-server/Chart.yaml 5 From 769c31869eb8d9bb693f8a2876cc68b8cdd16521 Mon Sep 17 00:00:00 2001 From: feast-ci-bot Date: Tue, 20 Sep 2022 05:47:43 +0000 Subject: [PATCH 46/46] chore(release): release 0.25.0 # [0.25.0](https://github.com/feast-dev/feast/compare/v0.24.0...v0.25.0) (2022-09-20) ### Bug Fixes * Broken Feature Service Link ([#3227](https://github.com/feast-dev/feast/issues/3227)) ([e117082](https://github.com/feast-dev/feast/commit/e1170822bd3de8e1bfe803d9e2757c760fa5df2f)) * Feature-server image is missing mysql dependency for mysql registry ([#3223](https://github.com/feast-dev/feast/issues/3223)) ([ae37b20](https://github.com/feast-dev/feast/commit/ae37b2058e59a722c45324f5b43668ae4e74657d)) * Fix handling of TTL in Go server ([#3232](https://github.com/feast-dev/feast/issues/3232)) ([f020630](https://github.com/feast-dev/feast/commit/f020630c0144ab366f50c29dc3c97b8501687d3b)) * Fix materialization when running on Spark cluster. ([#3166](https://github.com/feast-dev/feast/issues/3166)) ([175fd25](https://github.com/feast-dev/feast/commit/175fd256e0d21f6539f68708705bddf1caa3d975)) * Fix push API to respect feature view's already inferred entity types ([#3172](https://github.com/feast-dev/feast/issues/3172)) ([7c50ab5](https://github.com/feast-dev/feast/commit/7c50ab510633c11646b6ff04853f3f26942ad646)) * Fix release workflow ([#3144](https://github.com/feast-dev/feast/issues/3144)) ([20a9dd9](https://github.com/feast-dev/feast/commit/20a9dd98550ad8daf291381a771b3da798e4c1a4)) * Fix Shopify timestamp bug and add warnings to help with debugging entity registration ([#3191](https://github.com/feast-dev/feast/issues/3191)) ([de75971](https://github.com/feast-dev/feast/commit/de75971e27357a8fb4a882bd7ec4212148256616)) * Handle complex Spark data types in SparkSource ([#3154](https://github.com/feast-dev/feast/issues/3154)) ([5ddb83b](https://github.com/feast-dev/feast/commit/5ddb83b14817f55e51e5c89014a3439ec3ef5a59)) * Local staging location provision ([#3195](https://github.com/feast-dev/feast/issues/3195)) ([cdf0faf](https://github.com/feast-dev/feast/commit/cdf0fafa6939f67cfb13ee3e28ff16a46c2147ae)) * Remove bad snowflake offline store method ([#3204](https://github.com/feast-dev/feast/issues/3204)) ([dfdd0ca](https://github.com/feast-dev/feast/commit/dfdd0ca5fe54b638ac5a268501d67e5621ca8d89)) * Remove opening file object when validating S3 parquet source ([#3217](https://github.com/feast-dev/feast/issues/3217)) ([a906018](https://github.com/feast-dev/feast/commit/a9060188713e34d07fd82cf3469061fdd2220956)) * Snowflake config file search error ([#3193](https://github.com/feast-dev/feast/issues/3193)) ([189afb9](https://github.com/feast-dev/feast/commit/189afb9313d071c7b6492e0e8a996e6d073a2c6c)) * Update Snowflake Online docs ([#3206](https://github.com/feast-dev/feast/issues/3206)) ([7bc1dff](https://github.com/feast-dev/feast/commit/7bc1dff5882c53c7e25f51ddb0b730bd81091a03)) ### Features * Add `to_remote_storage` functionality to `SparkOfflineStore` ([#3175](https://github.com/feast-dev/feast/issues/3175)) ([2107ce2](https://github.com/feast-dev/feast/commit/2107ce295f191eb1339c8670f963d39e66c4ccf6)) * Add ability to give boto extra args for registry config ([#3219](https://github.com/feast-dev/feast/issues/3219)) ([fbc6a2c](https://github.com/feast-dev/feast/commit/fbc6a2c48303424ef08f9b206e406fc0448e5c6f)) * Add health endpoint to py server ([#3202](https://github.com/feast-dev/feast/issues/3202)) ([43222f2](https://github.com/feast-dev/feast/commit/43222f21046c54a68250350c49b4cdf819d41591)) * Add snowflake support for date & number with scale ([#3148](https://github.com/feast-dev/feast/issues/3148)) ([50e8755](https://github.com/feast-dev/feast/commit/50e8755d41ca2eacd41e31fc0be1202c69b61fdd)) * Add tag kwarg to set Snowflake online store table path ([#3176](https://github.com/feast-dev/feast/issues/3176)) ([39aeea3](https://github.com/feast-dev/feast/commit/39aeea3fa77c3b3a789556a1e0fa22ecedcae4ea)) * Add workgroup to athena offline store config ([#3139](https://github.com/feast-dev/feast/issues/3139)) ([a752211](https://github.com/feast-dev/feast/commit/a752211e1d0d6b44901d88f435328fc355d16c20)) * Implement spark materialization engine ([#3184](https://github.com/feast-dev/feast/issues/3184)) ([a59c33a](https://github.com/feast-dev/feast/commit/a59c33ac10760b4029fadd8e377eb36a2c458583)) --- CHANGELOG.md | 30 +++++++++++++++++++ infra/charts/feast-feature-server/Chart.yaml | 2 +- infra/charts/feast-feature-server/README.md | 4 +-- infra/charts/feast-feature-server/values.yaml | 2 +- infra/charts/feast-python-server/Chart.yaml | 2 +- infra/charts/feast-python-server/README.md | 2 +- infra/charts/feast/Chart.yaml | 2 +- infra/charts/feast/README.md | 6 ++-- .../feast/charts/feature-server/Chart.yaml | 4 +-- .../feast/charts/feature-server/README.md | 4 +-- .../feast/charts/feature-server/values.yaml | 2 +- .../charts/transformation-service/Chart.yaml | 4 +-- .../charts/transformation-service/README.md | 4 +-- .../charts/transformation-service/values.yaml | 2 +- infra/charts/feast/requirements.yaml | 4 +-- java/pom.xml | 2 +- ui/package.json | 2 +- 17 files changed, 54 insertions(+), 24 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1cb54565778..b657e9ddd1a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,35 @@ # Changelog +# [0.25.0](https://github.com/feast-dev/feast/compare/v0.24.0...v0.25.0) (2022-09-20) + + +### Bug Fixes + +* Broken Feature Service Link ([#3227](https://github.com/feast-dev/feast/issues/3227)) ([e117082](https://github.com/feast-dev/feast/commit/e1170822bd3de8e1bfe803d9e2757c760fa5df2f)) +* Feature-server image is missing mysql dependency for mysql registry ([#3223](https://github.com/feast-dev/feast/issues/3223)) ([ae37b20](https://github.com/feast-dev/feast/commit/ae37b2058e59a722c45324f5b43668ae4e74657d)) +* Fix handling of TTL in Go server ([#3232](https://github.com/feast-dev/feast/issues/3232)) ([f020630](https://github.com/feast-dev/feast/commit/f020630c0144ab366f50c29dc3c97b8501687d3b)) +* Fix materialization when running on Spark cluster. ([#3166](https://github.com/feast-dev/feast/issues/3166)) ([175fd25](https://github.com/feast-dev/feast/commit/175fd256e0d21f6539f68708705bddf1caa3d975)) +* Fix push API to respect feature view's already inferred entity types ([#3172](https://github.com/feast-dev/feast/issues/3172)) ([7c50ab5](https://github.com/feast-dev/feast/commit/7c50ab510633c11646b6ff04853f3f26942ad646)) +* Fix release workflow ([#3144](https://github.com/feast-dev/feast/issues/3144)) ([20a9dd9](https://github.com/feast-dev/feast/commit/20a9dd98550ad8daf291381a771b3da798e4c1a4)) +* Fix Shopify timestamp bug and add warnings to help with debugging entity registration ([#3191](https://github.com/feast-dev/feast/issues/3191)) ([de75971](https://github.com/feast-dev/feast/commit/de75971e27357a8fb4a882bd7ec4212148256616)) +* Handle complex Spark data types in SparkSource ([#3154](https://github.com/feast-dev/feast/issues/3154)) ([5ddb83b](https://github.com/feast-dev/feast/commit/5ddb83b14817f55e51e5c89014a3439ec3ef5a59)) +* Local staging location provision ([#3195](https://github.com/feast-dev/feast/issues/3195)) ([cdf0faf](https://github.com/feast-dev/feast/commit/cdf0fafa6939f67cfb13ee3e28ff16a46c2147ae)) +* Remove bad snowflake offline store method ([#3204](https://github.com/feast-dev/feast/issues/3204)) ([dfdd0ca](https://github.com/feast-dev/feast/commit/dfdd0ca5fe54b638ac5a268501d67e5621ca8d89)) +* Remove opening file object when validating S3 parquet source ([#3217](https://github.com/feast-dev/feast/issues/3217)) ([a906018](https://github.com/feast-dev/feast/commit/a9060188713e34d07fd82cf3469061fdd2220956)) +* Snowflake config file search error ([#3193](https://github.com/feast-dev/feast/issues/3193)) ([189afb9](https://github.com/feast-dev/feast/commit/189afb9313d071c7b6492e0e8a996e6d073a2c6c)) +* Update Snowflake Online docs ([#3206](https://github.com/feast-dev/feast/issues/3206)) ([7bc1dff](https://github.com/feast-dev/feast/commit/7bc1dff5882c53c7e25f51ddb0b730bd81091a03)) + + +### Features + +* Add `to_remote_storage` functionality to `SparkOfflineStore` ([#3175](https://github.com/feast-dev/feast/issues/3175)) ([2107ce2](https://github.com/feast-dev/feast/commit/2107ce295f191eb1339c8670f963d39e66c4ccf6)) +* Add ability to give boto extra args for registry config ([#3219](https://github.com/feast-dev/feast/issues/3219)) ([fbc6a2c](https://github.com/feast-dev/feast/commit/fbc6a2c48303424ef08f9b206e406fc0448e5c6f)) +* Add health endpoint to py server ([#3202](https://github.com/feast-dev/feast/issues/3202)) ([43222f2](https://github.com/feast-dev/feast/commit/43222f21046c54a68250350c49b4cdf819d41591)) +* Add snowflake support for date & number with scale ([#3148](https://github.com/feast-dev/feast/issues/3148)) ([50e8755](https://github.com/feast-dev/feast/commit/50e8755d41ca2eacd41e31fc0be1202c69b61fdd)) +* Add tag kwarg to set Snowflake online store table path ([#3176](https://github.com/feast-dev/feast/issues/3176)) ([39aeea3](https://github.com/feast-dev/feast/commit/39aeea3fa77c3b3a789556a1e0fa22ecedcae4ea)) +* Add workgroup to athena offline store config ([#3139](https://github.com/feast-dev/feast/issues/3139)) ([a752211](https://github.com/feast-dev/feast/commit/a752211e1d0d6b44901d88f435328fc355d16c20)) +* Implement spark materialization engine ([#3184](https://github.com/feast-dev/feast/issues/3184)) ([a59c33a](https://github.com/feast-dev/feast/commit/a59c33ac10760b4029fadd8e377eb36a2c458583)) + # [0.24.0](https://github.com/feast-dev/feast/compare/v0.23.0...v0.24.0) (2022-08-25) diff --git a/infra/charts/feast-feature-server/Chart.yaml b/infra/charts/feast-feature-server/Chart.yaml index 81970bc1a84..7095866e4f6 100644 --- a/infra/charts/feast-feature-server/Chart.yaml +++ b/infra/charts/feast-feature-server/Chart.yaml @@ -2,7 +2,7 @@ apiVersion: v2 name: feast-feature-server description: Feast Feature Server in Go or Python type: application -version: 0.24.0 +version: 0.25.0 keywords: - machine learning - big data diff --git a/infra/charts/feast-feature-server/README.md b/infra/charts/feast-feature-server/README.md index b52e78e4f8b..9cf5c31a631 100644 --- a/infra/charts/feast-feature-server/README.md +++ b/infra/charts/feast-feature-server/README.md @@ -1,6 +1,6 @@ # Feast Python / Go Feature Server Helm Charts -Current chart version is `0.24.0` +Current chart version is `0.25.0` ## Installation @@ -30,7 +30,7 @@ See [here](https://github.com/feast-dev/feast/tree/master/examples/python-helm-d | fullnameOverride | string | `""` | | | image.pullPolicy | string | `"IfNotPresent"` | | | image.repository | string | `"feastdev/feature-server"` | Docker image for Feature Server repository | -| image.tag | string | `"0.24.0"` | The Docker image tag (can be overwritten if custom feature server deps are needed for on demand transforms) | +| image.tag | string | `"0.25.0"` | The Docker image tag (can be overwritten if custom feature server deps are needed for on demand transforms) | | imagePullSecrets | list | `[]` | | | livenessProbe.initialDelaySeconds | int | `30` | | | livenessProbe.periodSeconds | int | `30` | | diff --git a/infra/charts/feast-feature-server/values.yaml b/infra/charts/feast-feature-server/values.yaml index 257cf03bfa2..90954a23cb1 100644 --- a/infra/charts/feast-feature-server/values.yaml +++ b/infra/charts/feast-feature-server/values.yaml @@ -9,7 +9,7 @@ image: repository: feastdev/feature-server pullPolicy: IfNotPresent # image.tag -- The Docker image tag (can be overwritten if custom feature server deps are needed for on demand transforms) - tag: 0.24.0 + tag: 0.25.0 imagePullSecrets: [] nameOverride: "" diff --git a/infra/charts/feast-python-server/Chart.yaml b/infra/charts/feast-python-server/Chart.yaml index d2b45ee8b60..30d90876182 100644 --- a/infra/charts/feast-python-server/Chart.yaml +++ b/infra/charts/feast-python-server/Chart.yaml @@ -2,7 +2,7 @@ apiVersion: v2 name: feast-python-server description: Feast Feature Server in Python type: application -version: 0.24.0 +version: 0.25.0 keywords: - machine learning - big data diff --git a/infra/charts/feast-python-server/README.md b/infra/charts/feast-python-server/README.md index acdf527531b..c5d5393e29f 100644 --- a/infra/charts/feast-python-server/README.md +++ b/infra/charts/feast-python-server/README.md @@ -2,7 +2,7 @@ > Note: this helm chart is deprecated in favor of [feast-feature-server](../feast-feature-server/README.md) -Current chart version is `0.24.0` +Current chart version is `0.25.0` ## Installation Docker repository and tag are required. Helm install example: diff --git a/infra/charts/feast/Chart.yaml b/infra/charts/feast/Chart.yaml index a657298b52f..02f689f5419 100644 --- a/infra/charts/feast/Chart.yaml +++ b/infra/charts/feast/Chart.yaml @@ -1,7 +1,7 @@ apiVersion: v1 description: Feature store for machine learning name: feast -version: 0.24.0 +version: 0.25.0 keywords: - machine learning - big data diff --git a/infra/charts/feast/README.md b/infra/charts/feast/README.md index 58b0357a9a6..2afdaf645cc 100644 --- a/infra/charts/feast/README.md +++ b/infra/charts/feast/README.md @@ -8,7 +8,7 @@ This repo contains Helm charts for Feast Java components that are being installe ## Chart: Feast -Feature store for machine learning Current chart version is `0.24.0` +Feature store for machine learning Current chart version is `0.25.0` ## Installation @@ -65,8 +65,8 @@ See [here](https://github.com/feast-dev/feast/tree/master/examples/java-demo) fo | Repository | Name | Version | |------------|------|---------| | https://charts.helm.sh/stable | redis | 10.5.6 | -| https://feast-helm-charts.storage.googleapis.com | feature-server(feature-server) | 0.24.0 | -| https://feast-helm-charts.storage.googleapis.com | transformation-service(transformation-service) | 0.24.0 | +| https://feast-helm-charts.storage.googleapis.com | feature-server(feature-server) | 0.25.0 | +| https://feast-helm-charts.storage.googleapis.com | transformation-service(transformation-service) | 0.25.0 | ## Values diff --git a/infra/charts/feast/charts/feature-server/Chart.yaml b/infra/charts/feast/charts/feature-server/Chart.yaml index f238b6aee4b..bdaa9ea1fcc 100644 --- a/infra/charts/feast/charts/feature-server/Chart.yaml +++ b/infra/charts/feast/charts/feature-server/Chart.yaml @@ -1,8 +1,8 @@ apiVersion: v1 description: "Feast Feature Server: Online feature serving service for Feast" name: feature-server -version: 0.24.0 -appVersion: v0.24.0 +version: 0.25.0 +appVersion: v0.25.0 keywords: - machine learning - big data diff --git a/infra/charts/feast/charts/feature-server/README.md b/infra/charts/feast/charts/feature-server/README.md index 465665fb3b0..aef8c0329a6 100644 --- a/infra/charts/feast/charts/feature-server/README.md +++ b/infra/charts/feast/charts/feature-server/README.md @@ -1,6 +1,6 @@ # feature-server -![Version: 0.24.0](https://img.shields.io/badge/Version-0.24.0-informational?style=flat-square) ![AppVersion: v0.24.0](https://img.shields.io/badge/AppVersion-v0.24.0-informational?style=flat-square) +![Version: 0.25.0](https://img.shields.io/badge/Version-0.25.0-informational?style=flat-square) ![AppVersion: v0.25.0](https://img.shields.io/badge/AppVersion-v0.25.0-informational?style=flat-square) Feast Feature Server: Online feature serving service for Feast @@ -17,7 +17,7 @@ Feast Feature Server: Online feature serving service for Feast | envOverrides | object | `{}` | Extra environment variables to set | | image.pullPolicy | string | `"IfNotPresent"` | Image pull policy | | image.repository | string | `"feastdev/feature-server-java"` | Docker image for Feature Server repository | -| image.tag | string | `"0.24.0"` | Image tag | +| image.tag | string | `"0.25.0"` | Image tag | | ingress.grpc.annotations | object | `{}` | Extra annotations for the ingress | | ingress.grpc.auth.enabled | bool | `false` | Flag to enable auth | | ingress.grpc.class | string | `"nginx"` | Which ingress controller to use | diff --git a/infra/charts/feast/charts/feature-server/values.yaml b/infra/charts/feast/charts/feature-server/values.yaml index b014d8cee79..d3b8a33a645 100644 --- a/infra/charts/feast/charts/feature-server/values.yaml +++ b/infra/charts/feast/charts/feature-server/values.yaml @@ -5,7 +5,7 @@ image: # image.repository -- Docker image for Feature Server repository repository: feastdev/feature-server-java # image.tag -- Image tag - tag: 0.24.0 + tag: 0.25.0 # image.pullPolicy -- Image pull policy pullPolicy: IfNotPresent diff --git a/infra/charts/feast/charts/transformation-service/Chart.yaml b/infra/charts/feast/charts/transformation-service/Chart.yaml index 4c650544f58..104b2f24278 100644 --- a/infra/charts/feast/charts/transformation-service/Chart.yaml +++ b/infra/charts/feast/charts/transformation-service/Chart.yaml @@ -1,8 +1,8 @@ apiVersion: v1 description: "Transformation service: to compute on-demand features" name: transformation-service -version: 0.24.0 -appVersion: v0.24.0 +version: 0.25.0 +appVersion: v0.25.0 keywords: - machine learning - big data diff --git a/infra/charts/feast/charts/transformation-service/README.md b/infra/charts/feast/charts/transformation-service/README.md index 7b55e1a10c6..37be5b0f106 100644 --- a/infra/charts/feast/charts/transformation-service/README.md +++ b/infra/charts/feast/charts/transformation-service/README.md @@ -1,6 +1,6 @@ # transformation-service -![Version: 0.24.0](https://img.shields.io/badge/Version-0.24.0-informational?style=flat-square) ![AppVersion: v0.24.0](https://img.shields.io/badge/AppVersion-v0.24.0-informational?style=flat-square) +![Version: 0.25.0](https://img.shields.io/badge/Version-0.25.0-informational?style=flat-square) ![AppVersion: v0.25.0](https://img.shields.io/badge/AppVersion-v0.25.0-informational?style=flat-square) Transformation service: to compute on-demand features @@ -13,7 +13,7 @@ Transformation service: to compute on-demand features | envOverrides | object | `{}` | Extra environment variables to set | | image.pullPolicy | string | `"IfNotPresent"` | Image pull policy | | image.repository | string | `"feastdev/feature-transformation-server"` | Docker image for Transformation Server repository | -| image.tag | string | `"0.24.0"` | Image tag | +| image.tag | string | `"0.25.0"` | Image tag | | nodeSelector | object | `{}` | Node labels for pod assignment | | podLabels | object | `{}` | Labels to be added to Feast Serving pods | | replicaCount | int | `1` | Number of pods that will be created | diff --git a/infra/charts/feast/charts/transformation-service/values.yaml b/infra/charts/feast/charts/transformation-service/values.yaml index 149d613e9fb..5232ac68ca1 100644 --- a/infra/charts/feast/charts/transformation-service/values.yaml +++ b/infra/charts/feast/charts/transformation-service/values.yaml @@ -5,7 +5,7 @@ image: # image.repository -- Docker image for Transformation Server repository repository: feastdev/feature-transformation-server # image.tag -- Image tag - tag: 0.24.0 + tag: 0.25.0 # image.pullPolicy -- Image pull policy pullPolicy: IfNotPresent diff --git a/infra/charts/feast/requirements.yaml b/infra/charts/feast/requirements.yaml index 5dd4a4bce1b..a599ac23a44 100644 --- a/infra/charts/feast/requirements.yaml +++ b/infra/charts/feast/requirements.yaml @@ -1,12 +1,12 @@ dependencies: - name: feature-server alias: feature-server - version: 0.24.0 + version: 0.25.0 condition: feature-server.enabled repository: https://feast-helm-charts.storage.googleapis.com - name: transformation-service alias: transformation-service - version: 0.24.0 + version: 0.25.0 condition: transformation-service.enabled repository: https://feast-helm-charts.storage.googleapis.com - name: redis diff --git a/java/pom.xml b/java/pom.xml index 9cff26daa6e..874daa27984 100644 --- a/java/pom.xml +++ b/java/pom.xml @@ -35,7 +35,7 @@ - 0.24.0 + 0.25.0 https://github.com/feast-dev/feast UTF-8 diff --git a/ui/package.json b/ui/package.json index 7f0e7c3fbe1..73165523252 100644 --- a/ui/package.json +++ b/ui/package.json @@ -1,6 +1,6 @@ { "name": "@feast-dev/feast-ui", - "version": "0.24.0", + "version": "0.25.0", "private": false, "files": [ "dist"

V08LI?>v4AIPGz_w5sVZ ztx08S4!Fi{Qms*OnIP{%`FAAprqRd=CBw!yZ7^*7FP>p2PM&%ANVY-gQi>1^FZ7C7aG$Z4eg4dz*xg{)gL~! zwpKovTU|n>qT=P{5i9!Q^*k)R-NtU#I5^W~5+S)MDraM5kxyh~+0`Cf!2iY}dDLO1 zv0#qXD;7Wc;kN7b`18l=Eib1#kcLzIU)m}DPS>wq>CL&nV_>-3_xRk-yszPF=a=3I6% zw&Qx{-pr&{DI_u;09>$y&%a@dGlPpsp3ajcnrCx6mZZvRZes9z%Qd~iDgu;^IO~kI z{Z8unPn*;0x^!C<%WbElK^rKgpzOf1*Wo2h6eSSbdozN3mDQ zeQAh^U-LrCOM&RREzjANk-m!+$KDfE3L%^}*c0e)+LdPQfas_}YGse+kIe=`;I-6) z^Cs599g{-zq2E0uM7Z#mXr*r0$lul^{>6Yc6c!?*-{`-t7(B6@RHjOJt8)PIY%qr2 z4`}^P{w9N+S0eU!RQE6(OugEU^xyUJss||RGk{+zaJ=}zD|ZdO3@K5e%Mdxwh&tyyXYTnpiM5oo`(0t{r1ge{l95NTgUi6&3-ER%5 z9?+8UNAS!`2)Im)kR&Pn_GrA1ImSbt!>}mIhZ+TGEM;}R`;$z*66)4kDd?>0ce+Jx z;S`pNT4yiJEc>5gx^UfO?jB^Xe562d(SjvA8l^+*DJEU^-fO!Tk>u?@XX#J#(vCeZ zyPrNwGZ_3apDn(gKe&p@$tve1aj=dK2yKzj3}Srr8mU6~uCIzm0LK6^K46zqje|lK)y3Voo?>c+!h425?)J7wf zfH+xh27OYrkG&V}V3tM&y z(a!VXXMpWWdyhG_8gaEgA6{-0kQ#-we)ZUlmL7t$H7t-w!Yv=;C)COLRW#;?N!p)i z6>QY54tZp&Y-7`{%KETxP8-06)-iKv%9QtDx_-YuT@pzcOdw#pXIq5CVbZRL;XqsF zI|bHR?x%V=gjV0Lz&%LZyH)&2yhZh%J=QHt?51Wu9LabmRCqsU5J|{IGg!7h;SG(psac5e-lD$O!-kwIbqWNxc7U;w(;DtvPj7gu7vvIOd z$fXG670CVi3sD!CAY=#HKc{t7V3?=dBF0&BMx;0O3*ysl3?Mj`nYkYi5r^+6&?#u9 z#b9CyMA&d z+K3)w+=xu3R0is)kg!C4E&ClhX8bki@blF1_!1k`B1jv19rPErp*!+{+ovLpEl`_2 zFA{@B2RoVORc{9`uCBAv3Z04+MZYFTgOTgj_OES)&qxMs1Hx60l_66GbJuzKyC!Ys zM5#>Qa$rCX=j09*L;Ss`ALYWB`?%OC;SbL0VLpvY-z49)=3F;vY3Ar5StR(4QR_v#Wn$gf*W zc&i#q!D}`W9#i25FKv5hNHH8`rJgYIOP*)Idp;d&JcWf2VFcM-b`9Tq#RQ=+ERvm_ zAc0YxQr9y0>l+RW0(DC|McU%4B3hPVA>N&Aj)dfQ847G+3zTL#H)D&{Et@6EZpj!c zn+LPiTxi8b%W8i`x!`dn@hyl=I=k_j;UHvF+k-*|8cT)S?8qnWH+=vCA-!i(ADJ-`nk%#siLU+2`{;w1Z)LQBdrfqOBtrfe|aMMI7;!u9GzsS}# z!kRobg9@h5X|g;y8R)|K09Cc~eQcwpoTx(I*rTp9+`sEfR|!)1!wpH+5vP#IyFxQ| zy^JaXMTN8a@*?^7@XYz1dHGn~!0$LN_uu6CKBj~_7rkd{3F);LL}zPA8f-tWt9c{o zw$QE(ZXWOggtOyugsnzjc8hvS+)4Tz>NdxUljiq7*$r9DRvI=1lJfJ7Ybf&MTdvc} zE*By-EWu!oS!TY0iO{e=!RD6%`Jb@|^m|L{VmzNs%Lu151yyUEdl|ErB4dqDkg+MZ z#3^}(MlIF3^xP!2e|ihk*7dFlH_tnE}1h&sKNP~A9<6ya#s2Xw2+5kO}Za4Dd*PqaPqbO}nhpRB=mP&`SsZ%{ai%!nG09;Ao9 zoHNmuyWM%yW|~M+fdMMg){m}T#zSrPkB5B`-GCy<$9%T=8Kuf1Idv)QIoIoOwtDx% z!SG8jx&rW-SNAsR&vIg{*Q|KdqsJ>f+zB}Uh%PDb3#4}Xhsla^XzJdhPZ#~{@zaRk zsmoDnmEx>1qDAIzW9LYVMQStIsp7=7TQzWEKACzKM1iSvH1mkd6lN=O4_8xO>ATe8 z*xFhN&L$TjWPWy`xQxShz$ePW&~{p@=Hb_Hx1X?f32&_j>qaxwVo^~dk+HVdUiLVl zgIiqo>_jt>l&0&;e^k!*|CZ{GXJ$(%sEBcKYEen~ri;DjZdUkhrpS49?q<9w)g;4i z_=xt(qxs>Wf?#43(0;#@nO=2H_1{LP9qA>AaE>15?f{~{!i&({YhzmTF+vI5;#()& z8bWtWdIn_D>1xuhj-mEVI0th=>SanH8?|Zed*dyFX(K+@H&MANrnvm8#5qS!dubAd zg>Usg`h^2`@`Jgl9M|;Xqkg2#F8ke6oPgz=J=DLk9PnE1;KG%+h4h+0XDYO27%26} zuMbxAvLX;?wV_rX$=)AI_L_@Jdrx}9EQ@TDnU8?`Y$MlOA`0cJF)wX=C+isbNXFi- zkSer+@2ssZFPQXrGv7W6MaqpA{@}p@T;t1T(DMta>U#G-#76Auc_6}&a+BNl-WsWTv`je9uDBF(gctU36_}wZ7CJWhLz3?ABHr3l zy?ghYBD5Dbe$fikIab-#v(Fr?2&|2i{N;3uVWCYyo<43<*a8pst)IY)cht7|ERVwo zZBcAK?U%I~(NDmLOCp~RLZP2dfQ3O*tD=tqM5yV9r46O82eK%k;OVn4LF~CXNrBnk z5*wQkiZ^V$*60$pGdxU(H0a}gE#A-}|G!{n`l532sR1u|!eMsz|+ zri(O!KRkz>hJCAZ-Zrb7-TK|U`p#X?H_MDQs2%$gaw62Lz{}8L$j9^3j(^+smmI>M zg?b=?Noz^Y_}vU?qI?{MRYsf}cZOl5dPhcQ_Sj zPw6YGG-kwHj{_NrLMUxIsJ{fd#nUP*a}5p-j&d{o#ouAV4Qd5>Cz*yH6^xo}3dUQG z$U8kdxn{s?!Y(Z?ldj5XtG0|;WONE0E+d$C?orIbzpDr7s6S&3GDfKOJsu(Z&U}GG zrD8$nmPP+vzf*8KEIhy5E%c7*OX;Io8N^V5Ja-y#ViWOAW?Xq=ZxFN-_W*Y`har!` zn$#XRBJn`0W2f>V?7eJWhTEwi?~$;}si;J^hzP+CV|TK?eHs!%vSbz#vQ(SL4nfT& zI;1MwrN^!LvWFwblfVu?$u2Goio#2-j;n;Rzdy};mp?5ThIH!HSX2SQxK%deIpyQ6 zi@#s~d)54>AsL1Ts3mF2J27M|?j`AkkjL=D(n5feGyz9c^tj4NvgMB1YUEbj?MDb46?LKZ~hbbN)*Jv+Sf(5W$j|B)UH-c}wj*ju?*fxnpKF|@x@~wGcIq-b$Wz-s$KT?4PCUcC zn}>uWJ|Jd3t?}Eh>xm#AERY{Sw2hr`K9^P%y9HBFdTucb&I={)Bq;$=uK;@;wVj(= zKkJL&@es7D306*TKV^UOIeETa9oOz)7rtHrh&?x_Kugr90zzB(6dW*(<{<_sw;-PbgXxCSp znEX5V%H7%&*K6XKO>*j%AtR`|pOk;@`GMlKHYv6LB~*_<2f)`&OhA?J#Wf%U{Y;?J z7dOm?OHr2$XeS0-o^?{xT@kUYWy?crterNG4wtxMAg9E&hG7d>w`k*(13g~qF7%8T zX>#-5ccl=r$Q;pj%fAYWY0fHD^j#R)iAq4bpR>~QUt=8=&`+3IMLrCeGldu@1vZRS zEbBT4zku}2cE_@XPh_ygl8)~l;jcal-J}>g4QZ>>EcQG}{o7x`Ie#&-|8fn9b8A2A zV6j4Uk?SIlYmr75!;zB6*OXRXc}vY^mT>R`*bD~X9W(f;XqLiBJ0UD;SL2#rt-M{>!s6qV|f^AlyI+lbZc4iV&&X`t*?+#H5sv8 zHt<&OqD^+nAUgGT`&HSi6SxT8 z>l(|3Vc%ugQ|9*9g!+;^vU;V=9E+6b`?9K|fHokJ$sozDGPHM8nneLI3URgW337Ol z`@lKfMeo?a=(X`zYYg?Gs1Nbqj}|%sV#%YF0cTPjS1`I(XStT_57p(rSlk)mU8Mt{?$3+)#Yl+k2!-`1)~t#j(CO+o=T%iq20CC%6XDQ>Mbd z_qfzm);f3yalH3Lc>5dnezJ79WW4}Orndulz#&W+VH89Ci4}~@;SGE5<2~>0k27|( z8Yrr^DZdBh94vsL0UpYr*FWD9mPc!g_c-At$?{_R7&KgL`RH%p2 zFM0p#IwpD22vDfOCzR&<&U|46CPhHR^pQE7*wO+o^|FN>yt7sZ zSiD+F9Ef}(NR6|mCe2(=37P8`K`}G*m}3V9CkT0iHI<|Aat2hAh1R1kNw+Jzm?m!) z;4kbILNbuM$obL-93%(!uU1#oq9z4$pAMiROoh^O3Vk@nE$S3!KDV|C_ z!{wv=M@~8sn#a$NCpFbLPxoC`UtrvEGYtG-BZkR<%^EGY5hrv@)qD70;VXRhQ00dG z*E<=-)cc)(k()J+v!*(Lq;uGW1z?#M#B+tiOe3$0+Yl;^keAIi=~l{o7$S$Q-jqTS zh|G<_fT&J7lOdPbk>%3QA?#oY$b-u`0>p*b>)4Vo`E{I6suX>1k-(=EU0VcnM&E~k zd}+^$(Z;w6i^#d5^v2Ooh9%_j1ss$hzTa2jf=^A{Nlwro`kU&*Eih!c_HZNC_ungy z55|@o1|S>^La-fwn@qUH0@qQ7yUd8LyhXf*s)Km!D|n;w{%(GW6vrOq9_DV55KE3= zl|q#5uEI#r3QL6G6J`e&67ffBIPWQa%czwur3Y5#|9PwbAt4@UY|G*$HU>9oC3wM0 zd)6x}()m{dEuEf6NbVgQ1p3h`!g8sXS>tbux~&j>IIHIeX4ZPfUm9VA2t`N{=)Q0j zq0)FozX2HvA(gHU((-&3n9m;=E51qBa%uQ5b8yh7&u$5)qmU?*ir{HXL_KTteQC8) z_)&}+QjXn-xV5QGkfO(a#Y8$QWn-}`lla~GSd@wsZM5{@Cy#j}-GgewiU?38+(yMR zXO)2nus`Wmv@z6KQ|JG(5BHqo=&n8{*|a}Z6!~G#`*I`ma4cDN$*S-USH#@mcDv%|5Z9aER~YmAi(dxvcaVZ&BuaKolRRzxf3 zO&;Dt+a}LXj!K1S0#l=@RJ9GAdK#RP=A}Rb*eK6GqRPugWPTfXp-lLr*lvbXzsi3r zFaZKY%_wZX2f**ismK4|Fp1|JD&NONik&tV97sC?tVOBIqtB3Mfig6+Q$FEmQ*@oW zfkl`iiEsx@vkIsQZxHvU!;=b7fhquii2b*RkOFIhKv!DzCG*{{FZ7t1jbeXuo5U4e z>B$Dlhx#k7KU|^$&XxLv7HGWIbL3B!S=PjvV8t*?Zy#{8|F_`=I)m7LxqTJ6q@q^$CHv%T+bf~}J1vSfN3B~++v#E!lYYD7Fi6v5uoN!~ekWir) zu$xfU&d{>uT^(!~l>Ah}_u655Mn#AIej@w_j^`>1b!R1h^{*5e>n4B;RS}-lWKR{< z&GGs|qtm*u$DUcLeZspIl*&Gu9tCrzEyH;vTO+1i&6uAUw`gkvLN(-wxPI%yn{n~H zBV*{8Ves3zY~yNg%EXsMX8a0#5hsFOwgBf$l-RpD!%ei9tTB432A^(Om~`*NA7|iU zs;PQ)KA)0OIFKHY$ zdb(J)RJm^Jw}cWK4^5#{U*8nUKF;&qSv)AtHbWWbF-uD>&%W8d3W9xJ9}nLAGjs9@g~oAUwW|?~~7sQ6V?L!SuyF&XM1$zeMNtn^|NgcP-aP z5QZmXnvi8}6Kxmj7ANngH9 zpe5-*o^(wCNXC3T(%d&?ji~2Xo~CrF(6T9^3p$VF~6I@$06sz`+=cUY-p*_ zyk6=spG)g^+_9FDGn)9W6Y5R0DxklQIT^d3qFP6Es`Q49eYcZi`~=l~!{>Me%u)w= zMISN-7)l8P>J#?cAX)B>6B_b&g*bu1&U+5GgOxKz)BPna_m{heu8=$C<)ut(B9JGo zy8`}F)hazNfyHDoSc1fp7(9Zv>`eVPSE#PI4GoX66e>E@ayC&ATCg@RbJ$WPqb&Bl zZ~5AAYFU;9H*EyrcQ|Xa4)CP*N@ED3Ua*qLre9mQf`C-^Q@|*wV0+DlJuVx-EY}Y4 zR7yZil4BTRJl#M^DBorkbZo_jW;Wo!+X zsIJBSKKk>R0PZCJT2!Kx8;ddtoe_5Ql`cc{2T_{GCEJpg}#p+fr#7# z&37LTX)In(_VB$E1$h9L4xv2ozXxD0+^0zTRFH+9dH|$;i!LHXyIdvvGg^KkA_odt zB4|XE%&O_DaSYGrT`j=4aB3u7ZxuXRt@>B>>za_+FS*6?^-C0v-o_!iA zv;yd(5m_j1@Oc6R37FB2U2V#rCB0O<{vlCpl8wAZ5FU(fB%j={(vZB4=GH$}8hCd1 z-y-<`=iGjxUeyOv^Gd1rKnVEC=}8j7)W%IjX6X>D8;4rrsnYodtfV^q8&NovCtKZas+!?y97I zTKfJ@GQw{p&lN&RH=E4PYAaFTxKKYYc4<=EW{_YF_Y>M_no%YBm_P_N5RIgOk%OF` zu@XL^ZTW`AsafqxWsIP+O8BYxJ)qrAL9BR!`@55&2x|vVFItn7uOF9-T=3)F zyjc5H)ZDXyq*L8|e`z46vPis(s=d%kLq~ni-xLM$t^gs-IAf{54lZVZE8oz$PU&tD z)hHn&!S@q!@{J9J=Cnt=wgSKlt~gpl=uZZ8{?v$zI6hmxD79K{_sz8Y7=&fC18pG< zhGDx)ka$UoaLBW0o&B1x65Pp^v3br#8puR%>DUCAf1)n5|?D`4NBuClfHSI($xYjgi^@Ucpf7 zg^4P!QB4~4l5yA_lcf@wk1`=ROx}7yX&kIz=7yt+(1L;#3by*)BG_sDhtQ;+LaS9d z4cJaBoA>0)drmx1ZuA`*jF~_B(_T_0I}B*;C7wriPAVax28G%+GN<*>~j>0pPy~1o+b)u?4?OW_vBzIl<&uUf@nh zgl?OcqxU$1bL*d~<5S@V*3x?1DyCuOB;p}iT$fnU3;7~=R~nzK2+r&!SebcSKA`70 z6HO-Y-TkTFEKB4ytGwzTa$Gt6eHH90s~XzZS?;Rh*v$z(IN_E6p+2NXXNJzIWr`&Rvk8l?qZMc1G(gIC6q3KTMi~ zt`7x5HhJyTO|tO|rQ);bS%}b8R5UX06=@FvzKMBO%34c3U#RWopr1uw+Nz_N0C->I z`0)AYQ=?M9qgV zbAbPG!>n6xl4+TI)=cA5POqKl(a+v1469rH*M)elt|v`< z9-rOo>iJ32V_4O8G2>8|wq<-0qc2yg&)yRWaDmLIEB?U5;S|A5_;`mZF0ka?yh8tL zEKS^`=LuWl$t<;5OHFJh5T^d)WMKGx{peE6<{Cg0ugadchEpCv{?Y7BWnmI?<;1Xd z`KjNE(9%rVqU0Absnjq^|5?m{OV!!eZO$sY>EeF?K?AJ;#*I^v9y}%ChRt?qPZ~6Q zI~6a2;S_OJQRAY3)(@4V3Tud7dTpGPDiSBnoAs~Z3T2;e@;C3|yL)1NBqnyQrd3mW34Ap~}x)vCTJev=91h$`+)n}}CmQc#Rxz%&ekj{%6j z__zMHmIQBgaq_`coyJKl>8z2&;|HW2!RK|2SD|9Ti%_g`sHR>Ikh27#K*Pu3j0dy8 zEOI9y5pPUMTXCbComyjnuLf^%)_GB&yvfnL8Hz@7%b3DQx6zs z+$=6`Zfhl+U@fEfBSi|cmWupRJ9#)Y=fs$YE$)qT@BRir@&1g#d(p8els64lXcszQ ztjh@`l5?sJLX986?d=tPexlHdG$CU+PlHcTkn)zISY5l?e$2aCd~&SPxDAR){%*YE z?8?~bIwCD$bY-MZRkB5~VQHvA&7H$A>!@6(Vh0`Ho;mdWWhpt`P}$_#Sc1;|(oFUy zOv*MAA2O0iSzY1&bUwfq`l(Xe1c@18PT{ZhF^t&0K+UUJBk$IT<==b%C*>8FQdl#= zB7N6k2$R|kD>qwL#f%`VhZK-&oB-69Kx$A9Gx2?b;W-?*p-uEgJ0I*{EO49MpnhmO3qkv1^a@@FvLn7^fy9J0j<5o5mbiCHIP%!ijld0L;NO0Dqww+Be-r9X&M6Ty z$6&KNZLg4kbVtOt@t2XWh?@XYF2d&Pu)3<@m|3u@+~qT@kvTKNgKSlI3F4t~!sLD* zxi5*r9)kUS+q{mAUSFP#FLy?lVa)7t(5vxP!ApLnTCVuXQQbH+WQHzAP`c^cyWe}0 z1q9UNnF4BwIk4|5MFHJGZD)9_5yqj;o&XbWF0lXR+FqKj@ zpkWv-fx7)`@4}d9L%1Ph3~qeDiF>9Xg&^ujL3Cput)+FptX$fu)LmK4_sOnh2lml6 zh}f&V*FrHS&qm<>Ci@h%eF5eL_V^aS(}-Ol3;m4T)#12ZoNh(`LfWI>H4Y5*WG2dL z;(B=|$$y>gdSQ64Ew=nazb@&c8?}@B?+60)uWUNy^>PF2*R!VH1s|f_lv3C=uvXG- za`sQ!FIBz&YPhN{f6+7avV?C87fl3MMGuO;{?PafA81_f5-WD22NO%$soV{*93xWR zj->M#-(PGm>)T{+M-Xv2$-M6CLeU-3x8Wc(L?%A@2o5&0;pJQ~z0_`jvnpwuAaYfi ztwVzOr2V+asi^l!%Wiqr{18)a`{I+of2Y1c4i)pY+#_iy!{b^zd?jwERcd^vaF_z=HG9 zMvMX8c8n2ZcH8;G3xi9l`E5q(JxFW9?; zvde&WWoEhS_1Ss0!CqJQ=h?fHnfxzpDv;+;EIjmyYcamHy8GGe@)!7JMD;guHqkKH zJ1CJR>neG$fzx(tHwDL2@aEL?I*jjk6Ik|0w4b=MxLqxgP(OQG_Fmm@c+%@~ z4SUo_JzNZ4+~|CDtrKZzJ@Ol2-7eRyK52Z>;MHD&n@de7H2=dTY14Jil8zy_9zFq? zv19p!pv+ouKG>%LdXd|Wb#1XX>yooI$$m==tfP_?QFj! z_tU{26u5vdgUZKVqJC?MYT{^x2gcMFVP63KYq0$Y&cRxRIitqEoM9k<#}3|sB~g&j z@h2Ko)88{3rCyK1S|5U{Y3QE{2L$8}^)q>){%&a%cxjVceSHd(OGER$9s^*= z8A_^&U)ciRp@OUESD2(15%L-Dtc_mP;%qBX4Otp{ito>fBvk2r;EHKiZ>>KLuiX@V zy~qvG0L|Ml*0;aIKsKBIS01Kswzf9@RnA|6YANt`xrYp47kSaEaFOL+gd7q3cA4OZ z5z%W9j=1+(T>zyBhis?!S4nhWXs~kxl@9CMPJf@kWi2(#@p9va%*Yvz2IXzuJo>A}X%{o}cJnTCCUtAYPN! zqtc(U>2)%4q}|JuW0Jg~OeKHwEGqOglIh&3hM5yA!6F|mQM{&Gvuf#livICOG{u(w z#Yp2Tr&oFg?w}KJj`FmL?as;7Ax2=uhi=bvli~}iddTkW4YJyBlaUh~!j);j`R^dp zGUlK?u9gYKmy0q}24)^AKsn>S-5A&~T{_`S-UO;T`c zSm1u}FiS~DU@Dg>rTm5qY7j%4MBHGAFf%}Q{^JNQ(<~WxqZIvc1$B)39HNa3>%tZp_j zB)zbH**AM_4bD+b>=}ggLjsRTWXI3NnZ|V?yXg-0ATpbcsYuhxUPJ zGNN!ujOw>u^V_Q)6e_E1VeXQ{it5f`kC=DOF^`K{i@9CBPn`?Gz68T+)P|k@84FbZ zpmTR&hM+DDuJ$u&muU`fl*Hn`?)vnt^cRJm5v23e8s}|TltKv-Y*Ki{LInT3@0-^h zpcFNrcDF<8>kbF!;1$K&=f|XF|9KYd6i!2mD~c3+BczK>a1Z{~klG3k1Jc{Kauyf1ay1OxyhsB#?ku4GN>YMG;_g{Lbf>klAmlp%Ps`P3$dNNPP--KnK zsYEO=h*CSJ{7}ZA47Ai`1a&HPw*r?CCNr_y6l)!Sp3h!^-T@w=55ZYKylTd8C6)yC ze4i<#P6sHlDQHAemn%@N=Jgp;DR?Bin9icHQY@Uxr4S+!M+4hFbB>TlC%k_I_ZN=Au?i&A-7P*0_m!GQ&Ucg#=A>U3x#VF@SYq{UhtYtPV z)Zp;YR@@uBa;VdE-D2OhVE80*_pad0|7@E%BU|}(1J{=o7?U7z+p=VWIxfDaC#1h~El6q!eYs*(`J480+3O z9l;n*i9|}i%Vfq^UvB8wOJMuZX@U;neeZP^}g%l}^%fI`pL zBD>Z^^I4;%`xCRPRM*`POn>es?}yu@+fZeB^_WD^O~+KxV&{noSU+lroOL`LkF-dQ zXXegOXJs>m%Vl&(85{#Cu2RfUDsMgnb~O_fka?|k5NDiMm*{8m&k^|gAp1i2yz z{4JpSnXGp+-!CgW%&)7e?M3(cARFy&@lM}VEN}dCywTl!w-z*f^F@!}`2F6Bpa%q9P0Af>1|pSqJze}+=R}A> z)~13IItp?bjZ2UtL0$#bc9ybSexXgCJO;!f^y&e(;2I|x8xW-p2(iGUk?u(iV15_% zBE_Rv2@!lzp2JuKkP1VWbgaU=;vcCfu(+UU_k`DuA2UHyY?EPripnB$CE2ptH{kEU$#augG0ueR{n8$4gMwB*%!7s|*%F$lT5V z!&6F7HZjX9M`^svkCHYg5vS3YdhnbzCW_H&8D>HMMG_VC6j-T#PmeUtFHk+<9KJ;) z+#Y^Iqs}jzg$iQ1oCGej+peDXo9zwm_w4d${yHUo%%lipFq1vo-fFEmB^^5#=o()W zlw_QWS>o~7Z`J{&Dtw6ua~2ICWXPi~70%&F@wUO(4)CNQ(So_mowEjBu?ztiHy zL09x)KO_S$9niSiX1NSyO-R@8y|q;8GaaW%WKJRvPmI*NY84J0!Wc+=m$p%7_%k;Ono`#4yCEC({~+s5mPJhh0|; zrCD<|zO~$KL}mj_17jxrcSWN&)Oc~hmEgp!2m-bpD{o2Gv)}mkH#Y%w3($m4Yn$%C zyY;Y`RXX%=kK+ZM;{nmR`2#q0;@^eLLUOp(0XXL$GX+_0KAP4U)&ZPUoObl5Mw}iG zz0)F^M2TFLvjj^YMA%hJ@WOth{q(Nw&A)u0`X4)lK8K_fgU7gkV{GzwS%T1Ca`Y*h zf)Nnq`efR`@ZzWB?2H4OAo-Ux|C&`%78CqbcOcs{*`z^9#6dzXp#l!e&`6S(&pO@G z2$cmo7k-&EbGO0rg3hl(qx|rLbse@&ZyVS!T!1@>9-5D*X@o@5q`a?3^}EqN<_!icgG4>4kTs>+egvD0?k4u04C6H8MVsd&qHBczjYL z6nqO>DQ|`o{Q*mpQjp4nvli*B)R%LW8TF z8N4MKn&>|t_0i3`&D2tq?E)%xlBi`+<7}P*PUU~$pbWHCf$_%)sNtP*k*WQG1+|u+ zIe)D}H=|_hB+LdsPSMvMsJr7lBnG9Rws-DgYeBzffQ^~jkg&MNu3>k8dSM!v1IY!J z#-j=SbMuh?n2&bdhh`cSiyNO{|CV(RC{+LiXq(`UdS;Pk!6y@Kl1YZ0 zqY43XfRG>a#y1QOL9W^wF@mD8Y;+gl9|OXJPNNiEy-oGI`J4hi2_mkx-7ZbcIqEIR z75SC92s<1pHhmtl^&ZusSk4wOCcmlFFG>E~h2U|C>P0TL^XJT{u2|*QEfU5+w{Y;R zGAzcdhf)=zKolcOauYh1o}5D*0jF8`}>7jlaZJ+YIM+2RF8t9L37N&-huB z&ce*Q_C9W%Z}X<_+kV?QRqZXA&hvk-h|BMbFW2F##%tBq^CZlE?bMXLx#7H@q5O+* z_u=X^GXfzb%O;4$By=Y*CX;yipVprl`$F;?OhxR~Rh`r))8Dz1VejD}IFw>GltMC; zEaV8k1uu9W#T35)0vhfiO=t-AOTh66lb5pue79^b`2cU8gl=Iu$Ht4yjER7P^=6>S zz5Ts_r6iX*2l?iTy3f0+de9H$X&IgTSb4I~v>aL9Ro`l5>O2=k&+hfZfC&ht8V)+> zX0uT~%%afo=96^BM=A+PtyjkDhXY_bl-y3d)rGpk}HZF242b>hUm*H!?t%fQTGmLV}CwIX2j6r$h54tCAW8H z@s8(@gOjmbsS*V5LcdP8cP-B7npRq_!VGA_^+5LCS5(;ud=2g=n;k@LMGsN!M)T#f zm9=g;2CVf$cPHlvx{*k9jBdGM9X9Gv8%*_G1N_gnXDjlwUkk=uUwC*-wKp^yU!_+B zc3oqDiTrVUbul>4J;BuEXp%&hi6iJuIVn0wq4%|q0|o&1ohY~u3fiMpaGJ-lpS*VK zC}k$)AZT#oOovN?bS{0?DAKdB$5FTPreldl!&K->Pu2k}c?9w`ML)EV00CK`A0W!# zML*mTQaBX&nZ^Yw9MiLapYq$RNXe|}a!S7V8qy!g$%hVeM!Iph77iw^t!S*{EW~+Y zn(pU%E?-3}MxTtYniq~iOgZUE6J@^A0ms6`@U+O#i4Io((YHKpvYUZkHdMWla#ga( zRwOQBVE`8TBfOz!pLm{oAWQ zzq;bXnI`0Et*gSQ_9=4k9&7>5uSM7izxs@<_(hB;F|@g{ThuBSv#9VTSaS+~uiAHF zz%?Q;%r{NBGVP}Y1dn8k=u#l;ydP2`2PYJ%O^BmnOeimh6&k^N5vJAqM|NHW3fyhV zN{t^nVb8AQUt;#y%Ot`#pCY=WwgRh%P6Bt?PXD$XqX+?MZD+$D{yFBedM`4vE;q6S(4f%IBX{AeBg}#!xcx)6K;>a$Apot(; zGzC_mIe2Oc0EFQW1@MzFtm`Q_C)O3lXBOd%A0g8iC{Ee)g8ABl<|*uFSX1s0P^0;2 z+vzn<9ChKSvDX$25o|wLb9(=Cx&dnoF#Yr0DWBzNIfe>t7w9U7{Nybfj4>ehf;c}i zv(c?c?-71eVMFSJ8)+~V+!*{GY)3QSFkhmL(oMVkvYXxgl6YaWm{V0l|8a+oM)2V= zeR?-D+fkoNgiR2*cW{Z~AI!JDh9Vo;uTAU{;G(T3StD<6?%*KhMwn(E#4}Vgd9y0ujpQCY67VM59l$VY@>S;_ zrbWF=DlyMuyS(RIM>cN#E5`LBe9d<>o)`5FuY`|wpvm3URTAX>I}_=EUy`h>oB-EO z8&t;Uv+0<%^xFjdA8vsO0S#}>Uwoc!+hiLtsSy_Djf?UZE_>#z(hIs?REmQzyHlj`oPKkrms^;UW;(Y<0^%`2czc9GavDowknT-n5o<=*kiG@9ExxF>qK=nI2 zl|wzU6wX+RNurE~U8!pf%su%Md&m2}qa0f+WcaiBs+i;`e z+5ebVX7beE3Vle*wt+8=rL>W=Y!|eHPV5eJ9@Ve4o1>6|Iv@GGk-b+_>k8*Yjp#9M zIfxANVDRTKU2!?d({ZpLo;C3{ZLb_*QHqf>7IZ#e28C%d%wZNim?}tOC^5L`+*BVj zmyyOw{aF1bc@vh6pfw|q@hyHc5?e#=;=@|%Np(cMPzR<11CGDS3PV{sIlLSHmJ(-) z%w5ob0Pk21YLRZ$uf!Rve_PybG^k3~*K`trB$dP%r>&nu5mioj-Q$dg&S5krDjt7X zU#lW((yy5+Esh+Zh>b?ZV<0SOCICU9Gi3Amq|{2cX6w^ztsMykZhUxh1%LDGcc#`q z28^wn!@plt&zszbYDZ5Z@2!{r;hTVZ&0CwDs-82f49zo#2m*P|Tuf$vQ+7g5HWwNl zH?5eA**vu1Qp;`j! z{f9{@zxFd!B0I5D#q+QUvC6X`pelTeG%lpo*Pc4r`_-91=v*T&twabD+~ zbMJj)kHK%ccI~QFtLFUXyu5?+?#sGVw88qJ23wXX>JR_oZGACO_Y0ly4gF!-A*lG! z*CthLvto03cuCNMRQ#&)P7Q}3ivf!p!Q9t)no%{)U%@6zpNgqkFXp>kt#Mi%GZ2OD z&E)S~hFH%Lm=LsJ z;%wQ-fWc1q-veqe;E$cS6Fq7N&9+>ycnrF*b!VV}ABah_#6^W7nl_GnM)q~nTr7}( z-#CNTth6(?{UxXQdFMYbH3%{IWfSf1XR568FTl#nJ%eQwV4fr*Aim26!veSKX*7NF zkh#->VH5X5taN}I=?Hut@Jj7mCLJN(A~aIj5pS5LDS_#4XyjbO$^B`&@>T0E%WaHq zp+f9SJV$juzL9iyFH&6P3>M@21%yTi&ldRDjMc&zX3dU{N0V++_v4FMT41Aikq_^g zG`7^Ve&ZEqkSEy5LI>X1FwyM$ZzqLs32D60%-5%RT5_!s6QomN=HIVwqA7Zfj1HEw zF@g7raNF7y==!@z%z83#hZrV zE9YbZ?f|G$KK4qmy_)L@(yph{LTwA59sk#hwXih9jYl#Ubi&xzLaEE(maV+=kfn5# zx(WW7VbDI-CY!<_r+HzB|NqMFe_Pw}5S$>Q1H9DmZVXTZ-cx-queqLN%Aiu-EFku% zQ8xH~2P;8yX$n_0o@P_T0M{|>0bk%l3^Q{H9~XcPoKOVnO1-US7{ZM;=0M|KZX{4$ zXW^1?KU{oG9CZ6usz+TwC2j;@4`_G^_vBt0ov5RNPnS#ywTQe2LDMMl~3HdS(Hk#9naCpVRdp&Q{C z?m5=)0k={M_gsT%pAG%Up%#H5JINtaqSeF?o6pMX`iT*!0z9^+ya?-l@yBQmorRgC zwG?O7H|-GgBIo{Ez+LTUH(ugl-$gsbu|Z@rb^YOKi%7i4l-gg;`=@hc%}@aC z9pa>;aO9Z;x@a?a0Ut23(89_oDN~D_HG!#IfLdI3Y!z|_hf#Vy97+t2wkpq@Zwf^L zg9jqB?hU03YpqBR&E&d6ZztuYdso3sVB%1du2s{9;En@%egn&cu^C4$nkWCCnu> zL4=8K4gv*XTjIF>-(|o~kyfozuZTi=kEU8BrEIOhvP=fw6P(MEgurc@NypP?nYfBP zj@@Ny_3MRdq8aMwDnct;*MT**LT18QSLb(?4O>50*~Eiq`?gQ?2lkEp~AB zrVf~GQ>Qw*7rlQMdxv6P+U4ZHw)O3D{tEiIy%CGu$$!8D$%tjKoykchnB~QxRfi)cPvQ&V*XLi!3N69XbQqQw?-|3Mv zaMfQXJVhSyS`YrqbTo4&rDXDEG+-c$*pXZgJwU&^6JVYL`JtUxYA6)<@J$Vw<1lUH z?U)chN?$%Z0nB?!IAO-H9kL~%TKJLH%z1kkR;2KRB4jku6N;y_^65^lSg3i1D^$R3hjyOzs;k6e~|~^B<$t! zZ*O7t_z zNFqGfR!*<%lw!<>N(4M-Ed1|F|k8sW`*unsJ99$^AMTrEQ65B5}MRj!!Q_z?MM zaZXi_J=_3O_ki!5@caGA?`Fwd1^kxZCoZM-4FnS-uzzoSnJ#gO`bNPVUbgVY4s?|k zTV2}y8@G{(;Wyc7J9iqEjzEM}i?t6rLK(NewF* zn9-;T&V-Lzxg#O3m3CJlOgD}iH7PB;3~Fy1-qBx9B7C^E#{^rJ8|U@wo8a3B8By}} z0vgx+TD6ib1eiv36DN-gq&cMO9uw`8AfEms|Cj$o*{B1s6FmF?^V4dlHEaAX)V|LXO}Orh&G-5jC^v|j zzpAIzAesagN?IXW@L8dA@G^y_<&sVQI4>e*OG1FT=@2Q?>vz#O5D~+iqsjWIGgxs7 zsgKu}Ga`*tO;Crpl3|M%B8HljPAFXNjc+Nv3(2tR^(5pKR-8+q*Ldwt4#ezD+oqTZ zn-aeO>QDO_CRN}P&3I%XRf~gbj*(RRRV`(jE+iJpMKc$xXbs#&M}`6(F6X2fv#G_5 zti<_vK;>vN*#Z5%0;^={$4cdKS>q9Yd|DG`n9hs!y0`0;;?-K}FklLNLdbh!~9 z>jr#`V3(-WOT%x~-nt|S>8GI-%t$IC^{nWN)2ZfBnTXWL$8~kGl<8p-q%5OY<_&jv z$({kvH|vuUIiomyHoT+!74Ss~-IkVv$g<4z;O^>i|A&UVghZ6g0r$X=&lEukHtN7! z7ALKoY0U|oL(DctUS0wE{y53$r%$iK?fud#>RA3yv3gxpSGQh+1TJCtlz8$G!Lznp zojW!DIoWr`U$m)0vag#oLPu}gK)<~|fRzyL->R8^fPVN}`RBi?+P8Y^WRoGFPFazi z-dfzQYv}~FpD%%9E!ffyH`hNo0s&1NxoD>nX*2ICvNDVHzpl@X-0 z^BI(?PGI(@%TofHL)`=Tbjyx_uXDD7u)xhqBUH>@tfA*U*fnK+ta&L?$)XvRQH)mimDGWiwQSEf|Rmn1K6>SNcb$u z>hyG~J|s>AR*{UeCI;R^_E{T3>qXgGbv}w&vQ=JUek0g=tnmhK-N0oV#>U7+pk0N9 z=60A}uCvkbW#xT+7f4vlzCYB4(`JUD5hjxUm&ih&D8h|xzjrcglmFiSK<~66K!e&% z&2_&0qMxMMJKN9oziTe?5ZrkI<9CT1`Y1v?Rei?ZO;#)7y`WSf+Pa^_*v}!#O5(2H zZP&eUHjr>J@l!A+y%;Fw05fMYe=;Pr;d9fd-{BZM-u686#9|K81SS-5cm3wrnDu{E zk37WQNr%78-qH8;d#RGa-s%wq$px^?!^g%g2OGhZlSpjjtSxt#8>C)TTga$oD>%{U zt@t#3IWq8?i1mcW+}BqciXGg=N*>36FUync#d6=%uD^Z=Y%F$IRaBH=l&#Sdjj^%>Hka=XZ^Bx%OVhM}=)8}TrLMt}>CACGUiJL7!rY3)r?b+^J?2X_enVGG zVT^Ge6Qm*S1k9PraPzZ=xfRYv1)8_hfDb|SX)7qf2?+@h7#}=`ZXc#d{)ex&+O>0s z>FZTvDe9xjDN3U4o0s8a@+K#QZ~qHcT+g%v0+{1m^WIno42|cpW2=7dYLol|JP!Eu zkn>_Xl<3f#y-tH3Th3~Xqx4d!mj3fI8h0D=eh#BDq7Hm6Y3P~~wq33|LYv%7I+6@+ zPh>8@iteM5xqk#;#|b;p@(m1S06Z05rvFZSn%4XN6RoB>9rR-QfK|57 z1;y|HFt6N?D0GLtweZdTEF1W{S1EJG>>-tfk^(-i5&8Fw&aA(Y4-=mOy;x~0OlYh%bA&0#G@})#nfORJKji+3ZAeR$xP<)@Ml{i$k1I8{t zZT>D4YE9G%*MMH7llNgQq4~6lD&zP~mr;X&%3eepJl~5D*6y$@%%|`_ZxV&f-_eO| zaRZoAj#QxT{z^V4B47iCL|kBj(f&Kv+EqX@m{A1wHEqyJetQR2_?aSe?*vRqlAEN1PvC>^#m%ELk09ja z!P5)hPMs5?#LFkzXH-#)bvL&JtjpCwa6dQUr=WP92urH3f|msg#D23n%C5)raIqVH!Czn6 z4DtlR2UF*K)OAYv&5k46s5GeKzipgOtL%0=3gkG940WCIv4Dncl~R$f`RixiAG>D( zpZ{4BS(~@9p|BRL^-QYX^HvOK1;((@PoPsIoz;PA=Ylojs3cdq0^wh0Vr}QjCSP!q zYx+M8>cSoBfQG8L0zl;sCij^edKC=;K(CDK|3S}QUC_6cBY{GQ&2U1%Ar_iqF0|Ni z1@VC_kAz7lz@kI1@=qD=^|$MfxeR>(!&X@fE%<~5_ehCjMuAz~v|uavyh6F{RlbJ_ zWtGTo(Ew98`(cN!|nJJ^m3T( zXhOp%oy$62YRAayfL6Q8Sd?WKR$4#Lt}ASPxZaJAXj%UthRG6$^k8!esmsxpeIewF2+s6tBr<7gzKXrsLPwsO*K;r^k4xisrZ<|(_6a400h$p!is=c56&(!zD6zoNbo2K{{g42P2#QePFnAKosC{dbi*Z zGmzgb&D)^q?)bweM58Xyc(9`?gBU0i66~|-LaN>q;#^zcTi>(;ot@4End7s=#+`we zht3(665M`O`nFGBj}gs;(R*$O9YS z;hQYc-Xh$=HObmY3io3R9!R@*b^!eLd6CrkF;!>}F%fAhv&7_SLfp)RTpzM=gA9ca z1@Rq`@;aEq>*=)>2lEJG5-zq|Wzkj_6$5z0aUVa+0_q`fZf}d;$h&}2%7*p%P&3gX zk`5K82vw{Ae#xW&__AcjtYZDSk4l#B{Q-;$r0?8#0ZHt|nkgNwTkKA)RuC+XAi;rUkeI^^7txd%l-|gtP z<2o$rMdRf8a%Z@;c1(b|tyB1BZQ7AR9i|-gUrFP4Hb-ZZwVInBA2I%Ca`X9H*&_5`1u){`2)u(e zKrJ2|=OytRa$6$NmtFVU>&*b5PDM`u7M)UHiXk@wkd1DyclJX(K>JAB^M*IWiVyRf z4+e9Z#{MP646J%eJ{ zHMpe zClPws&ka1LLxAzNF=>c9mzNL}km=JL#5|s0)>m}hdK)|;7$$b3dY@#n77L+D8tdQ@ zrfYxCpj&6f$9px5DYc*-;N$?a{vd8DSiOq)J|Ms($|%8yI4~<4?X<^;<7PPhXJUEM zBIAvokmQF8QFGEDd=_2vL+0?mf!kcibM8jSRfYR)mc&0E*Vpj0_1}eE;e<_NhtXJd zc^#8NDQ(2M>#V11(#3E?zcE*~eYAqDzLbJAffF?~Czii?ecNnTj~1q!*xdLY;a7vW^N5ep^>g@tr{L!4wy zUWHK_4a~?2GrVjlaqAR(pOF7+$q2FlGKLWfEj`|6d5?PsH9PNppO!-qB;@3#y}K^` zx!y>2u;B>22;9#?<|N2N0#(YRPwmWp|*pczyGs?;Ok33z49F$?cEn$xvH%gkJm&~oYSFQW6H4~4@#72&kp+i zHT=GsqP8`2)`{XHVyl6&ztoR%B(7`#iFxqf!r|jvY62z&8LXGR_1R(MK8tJ+*At z&XoP~##J<_7;>^`#yHh<+mogGKo~5+v)NL$5|xoe;#k=EDhi znheg!1&`9beTX%rK)iR0wgsHPfUePGQ`!O9qw28L47|1qijXp;Km-p8rW*y2wU*bEY-q*ElP!%w8^~310>Bm<+3tk={&MW<&A0cCPsXZhxCaPeRLL_+eES(K`5?NDuB$ zIR^-83+*X|@NP+WHyXgB{BsC?H0X6kQdv%OGvZ^R@Vm=4j7SJoU(6DPzweRJ9zZ2= z7`3fz|J-}TtdyGmqaw;O33A3}8&4XiU|Vx^eE&J6>pz0Ob9}xod_s_)&pGVVMgFG~ zM%f1z0gM0+xE}Daz#w_;LPleAYw>=1=C{$Tur{NmgBqnq>b?nLQcPP1^QSf{KT^uS z9<|%E0KTl(duRgg7b)ae8ISW!UUv();=tLr=NdHr02uZl2V16FWn zOu}PNGI1?&M&+&j&XvHcm`r%Q)(>S;K=B|FcZvR#5WUb{;$tKQ8eN> zJE{@DSKEN;Ax?GfZ>&r_UcbjHF-A#Q=)Ioi!px&ymg?K!h=FxR<3EDNX_=?Lo#CTr zC82O;rAWE7oK-=nUjRB(k2<+W0BGSVo<{m)-ctD~(}Lv>w_0=nN<+Up${KQIo5+W^ zvcv2>ioxmoeDlD+l3RxE2w`^8%X|HvHU7?V(H!`rN9CVvTI4`AFOnTC_0i5q*BMvhm=c&Jd-+Qr!`+be70 zyNO0W#mJL4aJr2_ele?x#)cq8_4&QzT~tQtis1^M8ex6Oaq(a;lv{3K?| z7oWq>qRG&VB!8(t!mvD=F+eb|A+j43;5Yn>1>RYJk51rjN!tml$Bz^_4PGI<T z&4qLoCoWUS(}|DGKJ24%8Xt!cl2rlQD-2lRZrg?-PE9@xDGy3mbjQ`W;@pSQStvRoU6UGT6g6?L!snd`6a&f% zKfu8m!}M886l!Urr{Vs5_FTCiUC&kxKv(d4gK1(FNoi5%r1nh|B(Jy2_E_Vh^Ba6F z)c>c#DnP7(^8)Z1lQVs+G+9ac7B9HAYz0OMKKLMIq-x zUS*GwT8sWRAEu68rhHljxKH^12`=wWJHtoxUCX2wJZA1)$94OLkAOHp%+q@xAkcTk zNgr_EC|RU2M*&{Dh!6}K_7cKZ^FG#ht90z|@cD87OOSxT_ zk~5_8w#Kj^3AYiVdc}?rPzeQ4msZloSXq#CsLccS|wnL@#6H* zsi=p-KKnxkQxnZ>{2wOpJSuq38&{hCI5mM6J74Q@O1p?_(U1!BC;1+c*;yG26Tt(_ zu5)DVXUT9bx!mGf>V4jgd?O5!$&02Fg4tRP#!WbZ3lD~Atn`>anO0Q(MDoB4Y`*mr zJruKWmJK2{G-1y9YK0@C&ILTc&ZM3M_swAb#f-XP&7K*qQ+@_d0wp)Ho~upL&@)+= z20BmR!yMDXlH==t@hcAOECCVdec>`x-2Bosn3In5s(9RZhO)OKCFDV3BLnIZCyM5H zvTvh!(1hZ`mKq$7oA7c4c7hQ!t2qz}%0d4{u!gmijHcXC=VCsw>5pOSX3mseRo7u*8Z>ZbgXSr`BI7^`k z5-LZVG~t?S%IN}{Mzkg~vYUlWQvelJIJj-e?xVkn%=K*1NL?hFGUk- z{hr&3)u@L8PpdlOKom{d6#iP(WgXD}oq-<$nBIJh2>@-sft?V+tPi^k%arPjaaNi! z&DmxzDhDBcTN1vA2SZv>rfA#^pP7 z=y>CvjCbtsYJ!BR(34e=%U|Ux%hAK)QGc&vcjkW z1QZT~<<19cZS?+d@J?P^=Ih`qdvg`yBWv0Edzlu>q@ zjJXIU^xF~tz%%IL4-W83*NB0A!u8H)MpZh5i!p9u#%#hg(g|KMVvSjyD1xrLhP*!@ z2CE*4DSp(XQs@;{GAhlp2!43qj;xD&ThARfQ{1uVF8JUd_{Ze`ZH|#n)A<9w-OOiW zYSHzhAXUEY)Zj{t5QGlFG`k`;6@wg%ihFOd`oI*NuMYJG_N0zQbTlUMWvI|)zI}(b z1IufiqUs6h%9ds$hbxFL+^pKB>RrjDHPX!N1IO})fWZV)P`Z{S#J{_S}FF4*?ewc5LoY zPxWeb=0fC+$kL-l9}TP~{W(>V<6Gmcj4<~f?`U$s+B1aal0@bY7s7r(OIe#|0P#n~ zC#rwvZ{Yj_8~2LU*lTkCoxc5L(DNe{YZX>xLcRlHYsulS8CC&Ysy2&yTOgR9;B7wJ zWh9L-+!CRB5njrdzx?qa*-wcmFrXNwD^e4A$nRIPGokg|k{Xa7PJDpIJ4B#$1R7k< zr>ZRD?r4K!+q3r8(+a!l)`t3u3X5_Ed<02R#CPbXxw1Hj2a4|`C~uzq*B+fd8}`#6 zyr@!n$7Hf|+OM`kJ2=fQ1@~UY&H#j*QDDoSVAPBxr9gV1ctN*fXp#~KUFX|+!Ps~p z_u2)@`K*#e-j~iwf^|Y67ge#3q1TlgJvb_weNC39i5#33TnY0ypRP4nfLU{MbPtLM z6Ae8d#s&y{2okHIdyKWG_MxY?#G#lvy?4(SG5*(kRsH5&QWywpiFx5uIN@G9^FUCBE2VmP8Lca^Pvzag zYI`eIL;U>ry>9q_PiF}7n~6;4Zr91e%s$ELkssgwDh#PnflxEkK=KUjSyBmHt3lM5 z>x#t2WLeJ}(wPsPl25`dduwMUpK1qV2AT-LX;+aH<7aNAqVg}8d;CLj@~;mjB~3p8 z0k#{p2@E+6hsyJ7*y-Lw<&(4Xue=Yt%Zo#_`2nn1OWPc%N~?rC)Q8Z`(n%6(ZLig* zubbJu+`RBVdu<1hVMM?f%roz5Kl?N6ud5wqF54W1(*92a&XZ7iQ&F^vKxBXa&Zmze zo@RslMdPY_(ky!Q&=RM^A?)^rR|^W1^&dYf+e$~)%0|5RZo&%<@uHqQB3On&YW@M{ z^xeORY(CWRg7b;uF4T%)(^ zg&N_=UJ4~YzI*oi_SO$I2g|-I>5Ypd(w#*kr8Phm7G3BjoHYlaBXV*>X2`1l!Y_!A zfQ}%DKvy;dBh|#0mjEMKD0Ly+J{5P-M*WFIfp9Gki6qGc}q;*U~7q zNf?X%F!0cBrE{4x@@FJhOY`rvcoRtZVn!u6$vP}K)izABy!!loh3p9d_`Plk!VK_IC%gBq@A5CSJ>HI^%|}WD(x82ks+wRs!n|Yf{u&-=l~= zhq%U?ATOwC{EZGI0w+VYb_k^XpO4mP2;ZIE(d}*U=O90cSib3MJE4U6i~QTU|EgWm zT@f2*y6s9%>QVd+S3PHSNY)*W+gmp-Wx7zgfK=dns~(_T#d}+f`Q%ON$)%W|&q(pJgJb;N`?wIcMA! zMBkrN$-Hj@S0n@k&nNef!E>eN{oK!=e_|FeWa8B})T{4TJ8&{7Xa6*M9biSo?QC*% zQ1YkqHYCwzf9DU0`xFfnjqz`+;8&2BX4WOH$=nRt$aKTk<&R0(2;z?P5#oR8N_G7T z*bcXwtT;N>wPs>J&Ic3N?gDW`+0hb%{yU5_Tv+FgqET@xSRu3(^O*dn>BO;uY1<VarxD z|3tXhVI{9-b9NAHXN3jc_O24;Z9?cyeet$)gs8nDm_L2|>$>VpF<)|nfBMZ#+B)wJ z%6%w;+x~J8lhg#ekQU;eCHE#fsA##huP}r>oaXP@x!VNquX$hOYeT9F*D}2cS3I`cnyxqZ{1ywX5C$Riq0yHT7;b9kzC7p zm)Tw0Q5jWYANvQX+`}}2QtWWGk1vxy=wf?Xq#87q?@%;n^q7iBM}#IwnOvZY|5*9O zbS}AwyyZMMKw01TVO}AmR@0+6e)MekcEo$j&VpKrEq_8JgS;x&Mmd`0_ALZIz_LnwC#US z_18JNowB9Q(fT^&YnVs3S9?04Au{J%L+!ZRhgH~La}#?Y6`J8yxh+JWc5A6zD7s9Z z%P_lYpJjkh^RN(k_Br2X%Y))?sd?eEPseH2YtxMD0IBbGmDL z>p-XTg}}gH9t52SWtg%(Z<)#}+f_~4okk-HQwt*apQ)C1qE z*&n^xJe;Metz~qD%ldgM6ykocjgVrH*&FQ`O6nO$P2HKqwJq<0DzaLw&H~NP<6Y*C z$=7Bv`#&sz#}#%JKhbJ$SC2Sxp4wD5%qq<(=&Of*^|H&>mrCX}QylBfqX(&*T770& zUtOfNO#O%PEa;E6HhLnzuOl8tl-kMp-m7gIy%}VwC7%G-nio?7A_EM(w;}oi!>H59 zSWlTQ3W>g7Nbl%JcnIuE$Pj0dl7*{$2bd|g#})Ilz%OT7l}i|_n|9z&6IxKw+h_aQy5 zrz?vfHda>w)WmH>>dG-PS!WM>zK#+40;IIV4O5`kpI|-Z~<;U%1tY zv&uNg^f;Q1k{d1yJv-n}%;3==vxPHmH!}j7HJ!!Nx?khZi02;}Z&KD08m%y-PZEwu zC@1$1CbiLgwis+Z5R6v7ERUV2VdV2kor_L0mF;2HuPkt63fQirGz=BbkGad!qVUV6 zTL7oj<}db?Gp-Ytv&G}qqVC4Go}V~gnlFAmt`P059j5%8hO}+*q(9$rQRX>n_}+=)$Hi>~{;rDCPZ@%sg^nG&zI~U=O^D2sxKk>=~fVa7Cpl{6C{Q2Cw|) zgNtDJDe&$OAlij6M5#73g8ArcY=J2l$MdOP%zd=DUYKzE>HV4Ss5nus;8Q;D8)!gV zG+^&v8tL7|mipfHvDSL4^>X0&z_@#>OT`Yl9k3rNq`fX$tsLkQ&(U*80*7aebV%E5QEfLi;+6;ohvy`V`}Rbuzy4r?S8Gq;YLkk#W=|;pVj3 zR=~LVw8a*Z`dsObQ-J41;iQba!}B$5hi4Kia5~st&PRK`crDQiVht*di83O1of%I* z3&P6q=WAGCNxONc+&=odw#Q$}hfcWRk@k0Jg~Z;%^lWuwgTOwv3Of?gEONkj1=4kT zbn5G+m%l0h>mUASr&W|tn`mU%4Cc?2{515i1?0xj@wcN}k^Q$&clNL%TNkv1jX22)s?0<#>SQ?4jE~L-0q~d#&5h?w=O+rg=6eaz^C$B-X@r{Cd$(h95eIxnWrXX`?^mZ^^? zbg>3&pKEI&!tQH3@OTxSXRCTEq12%*)xM7{-$%V$4TW2pc9>mX{|@@|R!ZpK4L;VH zwd!L2jaKJxLQs7DUbK=)l&bKCARgxPZxXD zc^7-J7oEr@gxlWfhVs$Y{Tx6STDX}@Qm(#37*?Zn4S4nKn#9=Hp=I(`0 zGV*IyPoOEp%wr|0pO2g^e=QAQ%I(+lv&G{dU$*arz;eL+o;xobLjN64kJ^#5jGh>h zdmmp?=6IhJe!ksjJ<+phonxxvqPE3i*~1FmF6g4yzqa^`X+QH8PWp?D%6>)L0}?bB z?C1Bw#OtsQ_8tb=)eO-oMZF!gf&pJh87KMl3mwd8IV%dygW7!-^ z{*xxD$_kzAZJ6+I2l}n6-7}Ra8v=1*Vj6gg z+S~wimZ!0P;CQ}yN@SqKjS@rUShvAZWA}E5NyBCM?k=4Flj$-_Lx04NVWA)LR0(Y- zR#4;4SAt-`xQCf+4v!(oHEEs88KDu#6!7k@Gj7x!gHELqSNsws{DfIJH%SenwZc_v zEngd!VfWoDgK^XbMi%Y7U)ric<1fJ8FC7-z?nhr=U6k7edWAjKqE2_*(c3GY8ua?F z99Y|&&@|kSe~nEjKYG<@w=}PBmVV_k0(hT#62Od^v;QLTM;yiP?qH<*@`6;ajrHAqJ4FH_bc{NCm#`8#WlWa1{wcgrWsJt}& z*CDr{CI~?D{mUl9MTPed{Gb;3D})NeHm(5!*VWzAHXLNHh*5V=%T~)!9;c+IY>Q+C zu9XwzLi?kK_Vak}!_Jsy9OcgLsARQGRzXHGW0mbCDIHhqt-_g4zcWqeI@ey>1hoeU z#xf6D8rW3&LF2xyRryW(o*i9vKLAg$F%h_F=H3Et=1m+{d5aQu+$rc3E00omY>WG~ zomaFI3J%C^89Du8U&&%pFmCs$v0A(BF`ZxUmMI{Az6+Lu8=Adc2NfE>OcL@QLwF(k zF`+HA@}~6}TS^^023PdJlamIAfsU#1cAjk{=;UK%Ym9W18)pYO3)6%G=<_9otxy(sqP@MT+m<=5Avf_-r}IN*}+Gg@0p z)QMDAa-s3Bp59pMrOqdU)c`ukhf z{7l5eU48SPeo#?2c~%xq){kHdW!Ff>rPvs={M4YI=>rU4+SX-x$wU|0{8lSpRlX4= z0S|cZLt>sU+pBkV(Sov)$+Ehrl<}uEd(crF2H};HsVx<0lT*#YZLbqgq&s~g2RAen zu0bk#j6^a&!osueRPUENX}F5?#*y0y)&OljC_vxi>T}U(muILO+yLq6BM({QWq{ws zHzK=Z4MfJ2(p0~-QliAQ#GNHrMnucBGHCRB@p<*lXUF*XV^B!N$S_plC;mXyQFNEx)aX6(@Q3A=;!n*A7}atUZZ(>Y5bTM;V+E%nuR8>z#Hp;>iS zG|XC(Es%w0*h#%mvZ7mr<7J?^!8-swDs)N?}eYNzA`93zrfspP>^j$ zM^WF|^XS9bZ(n*Co-Ez#@h@|w%RlhW9uJi)zn6z!m!3QjH*nCFl zB;LWHJySgL}BE%0_8CBU}03?EY-}v!`G>t@k7dVo{$=o!U`?Hl6689+e zbldT`Q&3^R6fu!!FKIcs`V!n6>Z`xA#R@O>Xb9x{=`yeT=+em;R00YN7cWt^kh_tuYP>G z@IuA`9Ri%!F#TcrKlHqQrC~d6Iq$W*?A#R$gbp>%U~-SpyUV2dq=Q8R6uzc098cq) zeogba96C+ZHe~ef7Vto1K_LP*HV!IX@FanhKTxQXB-(=fz!W6sIy%mNX?=wJ6(Xe zSq)DK96X#B&X3Kr{RtlQ#@5(q@PnUa&_ z+)KI}4^2-v&F||&KxJ#sMbbLt`EfRIM4lRMhSg`2;bS*i`Eh{t?1oKSWmRWmLExqf ziR6!-#VqQ6@eEJJ1cjYl95w8*NniEBNE2H0td#tLU*UeXYDl&Y z<0!kpY$}&JUIAD3*5>7mxasBb9t@(tlK1Xlgyd9RG$PEdXURi+ly!q7TO}47j7*wV z?9YaB;6CWwx0w-W@6mtXYRV}vfF{|2?`mjMj+jsQN-Dc`bGu2L8OovBcJw{R>r0zS z19IA9I0#GTX-^&TvdncN4X^huJytZ7k z@312HRaHJd?%)LO<0UoX46vO`1+J`SOb4CHqPzBqdQ0fpE6@%1@~M7+%)P~XBZS}y zTZWjtdTq^Tl94hwXYMwqUxJ@SO@90AQxILc&lNo!(ofg4(vyZ0zqV!17#tx*OKHX> zF11jDH=g6=DVFzSm~aH&-V#f@Y7Xe&I!t^=Ucp&VAYf?hSA8*khBr2%M0=C$c?IG| zJ%2@oYsw(Q?KkC2O9|P=MBAD`#3!EAk^WPUd}m&EcL8wSPLqzXJ1R;7C zH9AqF_d!I6-n-FDMDKMFy@wDYj1s-~?l#m|@QL4xhQsolAJO&lDuO{zuN0_%MErx5xj|{wnMsllEXj^+Wv@357o~1? z`DV+sIU7++0tIYpgD(clQ8Lr-oR5&TC}4Yci0(e!zE@Fo2(Uq9Wa?oV>%kwG+_=7W zrdA0jsz>E%>+^(zq>D^feUp5-t#UQ*jc@yUmP=nyl;iOwUs-eK&IDFf?paSVBTb|J zz^H@y?U{3we8H=pA-E~(r+^EJ2kxwEpsR(x-9H*gc4pkU$rX}TCd~H+tQUG z=(xBAyFW^0eD*YpM_q7na&s+wIf%{uIMEpp{HVbeE8Yl6UFQ>T-_y8kagfLYDF&csWQ1sMZvJf#<#(QpZ?EDU(5RSHE{1mUyd7)7QYU9_cZ-i zy3)lL*TPJaET8RC^&Cn6C+K36S)PkmUtD)#Ft?{!mJc;89q;iPSl&0G^a~mtJUNF0 z7tm2N`_lWPMc;!c!3F0Y-i?zxvZm**^v`E3@H?A?Y5z4#d{6k=>_vQ?x!ZndVDwX_ zG-`R$xJ`zbiinPE{Y=sGo*(fxzcw#Dv%UUIt}v{2y2RkKef|_c%dm04-kEUKu2$LA zHyK~}Rb%#Kt<}4Ca_PPug)yS@IK&K{)U2c^7-Ro&B;;=Y<8)=SwlYGa-@7|!+Eb29 zjv<;oLhIuW3WX3&x$67PVL(1~GD4eqvNL-;MlwV1d_F%$s&nMv$&9CSPJX`7Cw~g% z8N=0LGWj2as#&hPr9yH&QpyhjIgL$Esjn>KOj1Ti!5vvfwEmK%Y(5WP@@%>;nD09l zZ>xB&kBT34R)F!3R%;%0?#J|#$=0qH-wmE!u3ib@=DpDTv6ID{FH9{~Kk%S*S0T(X zZLq);u*^5WKh{+|68bH17&DsfRLV{-iP!HM`Q>U+*RLZotn+B=(YLtqwYK5?aPF41 z`OUUB2d|BE=SXBJyX;yprwh6Qs|&sFzdmzESqsM1v`(7bA5#!o#ha?=cVfY{=`fIB zN|vs$nh{IHYP8rsWXkxg#4 zDqVS%;n-qpM=&#mT6NGu7Se@p=7!N9_qa%vhe9_{O#P@jNPLS*+EPT(vy01gzvt%* zuH)0<>$tw(XN_7tqO_(-OX&+1nuo+b$21etgvqxuNQ&)=-mdWOy}dm7y3^WF^qy#s z^U=5AN@)!jVq`XB2oMbqJ2Nn4#?jbbELbE`Smf*0UD$vSipRDz3K+?3cOO1Q-E-k@ z#Q`{L9Ae1~ataLi<_yP3QsvxBpuI)|jyINZLBF!a)zPJQ}U4RWI+|x3Iz_pFNkI|Rp z#8*spW6xPBWS2%FoPy6t42h7>ZknI+B1c3L(5DmnT5j64kq~q-?p}H+%zI||GJRUc zk=1iHUx`^Fo2ULm))##P-JA6nD_0JNgKBbw7vnKSwpo6k{F2L~ke_e5Ke5A5O>y7+ z4pyUY2j7i6Yzt#R6S~{k=|?P#anD=tg6_F=WUc05oO5Olad(}?=!qW~ZUNEz`-{$< zz}U;-;Y*B}ftgx9(~U*9DdhmiXl75mSEHX_c^_?nEE-TD^uf@M?>=q>4Uyt9P*WMM zFuq{YUyn)a40Ssh6}q7dL9GM*KA?8aG!eB*=1PjplbSA4rU`5>`@2W_FksE@qjriS z58wDM*%oKrddHidO5?GHhvlTxAU@A|=Ml;RPD%oV_u}sLCzS~gM7Py)6<-H0$o4nJ=&?bx&Nvy;DM(q?l{Kbv@B$2X zbmgN6B3`1Y`O)A(KUZH`R%-LhWY~YfXXhKS7)#!GL0`h@#`8~QV+YhBm8eYMwSwI# zIho-HWEwI)JU@EC%VB6WxP+KgyPEfTrmtG(_&3nKT@@(c##0T(48|<$g`~l{X$m9xU~Q+WWm0`7=3|k8x`z*9VzHE_G)VR{s)uwJ$J=of z&G!QHl+ZNIo{v#NgykT?g+U^>L}`Y0f)I{ALh`ij*im}=VK10>796b4DtMH!Vlxqo zJ?Yqb5O-}fOg(%N)<^rpqJ0?4dMp0hY2_gOBHQXmORQ@%W3adUd>212} zjEunM?|r~LPMVc%u&!{}oJSQAw9E&SGR-#ai9`fL`@xSr^i#hW0Ui(@zc)lK`^=v5 zy2kDfZnH+9iHwJUix`-3$68Mq;ltU()*;MqH&P3dq*rPA^}~0ON7T zw7$lJxePl9QAGfy5F`J^UYxly4$^x0P9a!y%yM=al}Rr)N|@b0l>L;{^%M{MnD_DTN5om1trkfx=1oEva^o!0gI?Q6JfKeWU2{V~Fba*Vb4iC0 z&AcU&wrbH~oN$2N3c5@Y(Nfa3V)IA`YRhEUT-$0W{NyQd?PhgN{F6j~+8DO=SKxG+ z|0p|YZ4i8~HpSEFxM@O0CB}eewE&UYJ)}kLhm|5y%*kODqtqPfLbQbcNN>alEx^}7 z;E?ffaQr378i;w$efTvW36w}NdQ#l}T;60b1_S>=vi?L7FnKmI(5xoO(+64zG*O&e zrc9&=cs4cmf)BCFX1Sh#OxcwLNYh7oK?Vp3*MwZb5IHck&njXb3>SzPoA8vAlQgto7QRF8#7pYV{0E)n{!aC>Ybmq6f1J0d0dWG@2AFSJE=^)7>9L&10+ z1gT%}?zkHNtR}>v&V%l3f_%p>=b@RgPU|^MUmBv!K_j7IjcF4b#LJzU}45(GFPSPQgevxAFYkBY0z~e#Idum)9;V_tj z&($P4g(32}LPZ(`_1~-SEP4`)MVR3ZY(8Aic3y9^eF}J@z%R9qatGOnq3K*;FdUV7 zf7}jCn6Ezw%we-RQIW%?AM@r%!+;q{T>6B$z3tnI7aQ~CU3O2s$_IW;n(wJKPQR)c zxvTQ+wYEVWhpP8I=KpkN&b%10H*>tVYB!gjr=a&9cH{;jl!f5f{tzDQOC@T?i!F_7 z{Arbv&$m&QO8FD9X)JtBiF6gH%kM9UvZ^MrmkRZPKjXfrxxpb)gIWWe*kNqH%^MVN( zbe~vocG=8N56c$OV@1$abfQ05z-$H3ULRf^wCpTy#t)Ze=Z=?*{;Yg36_|kxL3MWK zziWHu&3IYH4`Gla&YWwNz?4U8P1)ZXP1bvpAmyR#PIfpqZp(X8{Msx z`E6F@k#i3dcbe$Ar6=RUt9Y#0xo;IWM9HE>C+#vz*gHyzSi?Ei&{Q-x@?Xiw>ly@Z zN8w~UcY;p?9A_dY_o*a2XNFu`2Hv@u^`fQUGSwtq7jEU85Dq>0n+DBIXu%<*vVZ%AK46r>ZFE}*9=VU(@8DN18@hnH6WvoM>gw_JrA`&Adge7<&JGH=LT|kT z4SQ!TXQK97J;0fEXip1uhKJ4b-o+eIu}z#RzHBR!rYbTSy07MVC?C9dDaIP`8szwp zx;t->)XY1++!|SJy!N^tcircFcS}3xuvMWof__~q1v&l=l~W+7*sv?6GkNjFQ!XrW|+Nd~$ zE-zqzKD2cc@FufS~OmLK$odid28Lylz;c zY;;-ei(4aQN>oHzCL9$phT(CeyJgDs&0j7gkFI+0y6PwO>Q}SP;#A3@G&zkqIkUG2 zWfqwT{f6w-5!J5ztbX(O-*^XUyCVE}wlm54LntGJjY@-r@@kipTr*0-9Falegf4lP zPkHJQuX6?Uh@YS038X82BVkCk2CJ32Mv|B;dV7o{y6Zu5D9-Tx7)snt%azQPFGp_C z4i+{kCM>Jg(!II%uCY`aM!T$K-o;1txYUTJ`eSmppK3hJ-#<1VtB6=*`r4L(s7Hk> z_;rmseea@4xo8+R-rJo4GwsHUJlb#KOtfl&TeF%2k*9! zuzZ(`Y;i$!R`y_gMQ{mx?8A7TRkZ{p$<{gTLPU zgJkf9mwl~-7tq)jQnixQc4(nEC#4%T;ReU4$1pS@z-KXA{o;x9^G z$QCgWNOV|8lz$l?^y}IGVl=#Qc9CH=G`i%}!dD*>ODl9DGS_O_2_ZZTYZhO;da&Li z1$Z5`11_1(F$iZXno$y)UFRfvcDq+1PfLG+)n5L=NQ!^@`vJv?EaS0crvoq4ED74dH*d zA1EVbLwA!D6CNG47Dq6jQUPv1%8AI}w!ASOBd&Y0G(p)PZURp<+x%Ly&p-=j8^fA9 zdK5_V>tVe~GhofCWU`87jcruiUyhU>*6cFkO*kg9Lwn>M_k6eB^y3YTy5~A_9^%#a zT*2JfMo`Uopsom$G?z2&ZdQPe`@-C|8L^;PR+J`N@Bn&r>5ep#%3OxD zEy7?;V6F7&M09~OvW{~VO>r-g_q__;G)cPkq(gGEc3`aE`}*;#q$h`)TOwa2nlnjs zF50W!!0)uCV@JkHx0yzhQ&Ya#qQCxY+-D6|4(~Hbnw$5VrF2rBfx`ybH{Eb>vcQ8l zDfxMhswpdIg9f5|_LmQpxkllYJ2qB*4`{p}$J5?l?3cKbi{vpsbDLv?t|#}O{Lzz?{5BzV=W`XbNVOg(15+VO}vrPmhepjwayD&h`M`&4ulmoI=C!x z7`8|Kh9J^9hL);*mhH&7f-UCkZZNY|Dw~j7T;UceogRw#_A^K_g#GLOrq1y7x#lWs z7u}@w6CoKR!!#0VT>i&+WjN+SB|Cn6rK9Oqt)8+8xlFs5<`K7|cDO1NTn*7BoOC~* zel4X|PMg$H6oce_wFR#;SoIPmIm-=9xGLF0&Rl)++N`~+qTyOZD zP=CpIE&6_q=a{nqk(1Lo>>evaFK%&z?SJBj1J{K0kRDQ zhR+ey(N`xQ~+eyTpzwM2OmB}d^5JB+esJL{f zM^zt;ufrk@`mLpQzs&KQoA)0R#x!>qsmNKCE;PS8?!8rlX19-7&tf~P!GthT5Y8Ef z8>;92WRFm^;+Q@N0s;hG1HuV;DP8ln3C^{UKxGah$Q03ZbZ>D7*~k(NlFFuM?bGXT z&Hr+DjU`u}!I@>WbX)a0oNI$X1KHaG+Mi~tvrqpkm_R5+;fy)mbAh=zTblJ(S}F(f z$9lZ;)=~Glb?)zSOTJwF0)C3gT_Bd}FZ+Nv)ia*tw(elbSB*%W|L5R}uGS#nxj@ zk6H@<<=DD+OAyVbz0=OnjgLWiOD~RisFX`*eB*oiu0D}CMq|z(LU@Cl+r^qTI|aPB zFdK+T=7Y#WI{}9JF-U!4@ltatM=pmC$glA_$dcm8Fwj(qCoqwA*NNe(`$0EmmgS%k z%FwJ)A(jM_m2({o?-D=?c8jF9Woy=&vs3q6kfe6XL_D9n!>9R94wT!@i&OJmSyI31A|&S6i0m5m?wZF$h$8n z-Qv>^0#p5-CV$W9+NF{+XF|4x!$nC^0y@+@t`D$a#Xo#V^z+)Yt#9wdual9US9XOA zjH(UjryZb;xzcV!L*Ej*(e9J|rHdvum8LPnL{pqdnUAws;`kl7o}(P;`M7V0Rca2d z?WMHjLZ(7cTy8*!2JLjbW3c+CP4VyNvR(o<@&uOtj0@_XZ1dPDP-FMr`E>;mfXU~yoN_SIEq#RR5!w$wA->~3MZ7#R0H(J6K&8d9*7~iWhzF>NMf~9QH zOd2p~2=ViF`s{UzI=wqV9J8SG<>U*kqVywB|KT=h#Ug9D4zjIJ(I;N(Zlb0vXh-@+ z?gRIQm|WT&!OB>|XkSx90w6jYYZP`4#=;hfFwh zOgVxR^Qm0Tc?caopEZ(>hMNLJmEHOenJ2%vf^NoLBcb5v0PSbMb(vS`r_7LKpZ_g2rAR~5kA26d5{r8h@bd$V>t&>1WFlag9^*fUkpM4 zqpK|R0&Vl>qqme6GI8(1U|HzvjVFVPJ^2Su9K3CKDdAawX*4+M`o~A7QZrpV1OWxT zkv|ya;Zaj>!TS(7o(``p*N{V5Y$AM1FZpfN{01U>Hu@{k=qKK>)r8K=ut+`^8a}{V z+m1VmpJuALd+;kmWaU3G!wIQYr%w2Tb$K>nJu=bq;25x$E--kUNEcz!>K@sATrMF? z^TU69+M5&|a(IM(|9Ie9x(lXWj1`IH^9-xTo5M$`vLKJI`-plq2-ti-B2P^INSx^Z zJ%?Yt;Zc`UB^7OK+jyfnY_9|$XnD^#GgnFbl7YX{D{=KL?ZRn!4PYtS?vRs*8G4f z{&wNUa(hI%owc@`yYPL6H{7`0$5J%*-ijnF{GT@Rf1Ut$!lz%%PSEfMD^PlBw(Cz+ z`irgE?1z8!)Zf2=#6olkcam01Ih;p8welRAgac>dZtaA=8|9y%U0=*fYdTASA;oMn^JUNPnO7(RsVRO^@{39?~)(IYGiUc=ooVcsTb zeh3LFSG+M%s!ioOO{mpUGz}Nl#IOqZ0@2*2Isufh{#J1SodB_EQ+R<&+3LF+a^Emk zTL>Ih83D4i(L#GBF5k-3DNV6Q3-UkiuiS#fUZZzj;z3Nue~w{^;RCLG9+-zhVwe+N zasz~r!kp_WFX{7>i>F-(%1w(%1qN}qF-Prs% zNf!X+i3D1crBi2x-2^xAzP%t6%6Xy{IU*W_*5(l$>L7dYt{Ap)nR>>ucQlLBo)@j2 z)TFz{jE#0!OO-?B=TosLBaIGgml3NuWTro@<-YyGUsC*J>#c{X9$}C=pwbtAD?_x3!Jy0PMu6@Og3sYR=66(IkbUS z;W*ilkdY-pxoFXAoi0(El?T|Bc@xwCZp;yItwbZ52C4bsI18=%*X&#T_VxHOI~)zt zcP6z|8_Xq2IXjY>5g!b(&J6l-T`ckw)Y>~M19=&WF}(JRPd9nrT8ylJOrKF-aWB2( zeL0(~i$C}b^Af#Gy_n1HY8(xRxE1Fz!*@wm5@{^#R08HpEK@cbP<`7QxT0Da!5TI- zlZ9!+L;4IgBK*(uk0dsIS42cVyH@3V4lnjDzh=wdr$|}=Ndf#)>rFqb0%cd?aURoI z3?7=nbV?YD-a_=gH<4!xjiUB2He1atxcQRO4>RmRcRI&+r?fPQeu z{*XrkowbBj$q1Yj!p5o=l*fw`5njPcX-WZ85`>Y%RdP9^@_-f{gyIbS5H6Kx-`B4v z2Cqq$nvOuh~E&eu-bCFU~()G_M&M+)Kgk$zz47cR= znCh_EZ`9!CG5fX<_b1y%`C0A0jP-$Pq;%A7Z%`7U-tL_6KeARKSEs)=gnMm`#I58Jcdm07-2sn2|K@`9n9o( zYEV4?hM+LCPu=1G6i&%c%s&qbnRqqf)<2L6NzCnmkC@fbUmjW{vYT1r12*aMxrb(5 zp_qRPsoW!U26tufj}vLgO5YNfpc9~>shN8G5nMlqv7+9arlV7n0d{p^Py9=(8OqYu5+7Ow3lD~R6ct)tj zjAQX!Qqm(4U7{`Klttf-&qE!H_qZ;3hT~qg6oXwc!eA1C*;LE-GK6D&;VtM<&d=BA zRtuQut$;V~Yp+>eXCf>=KilJ{p+02RJ_VmT92c6ZP4q$OYF+<{L;rlA z@ga+6(cN+3AE9HH_5ai0FdG*@n5r1kr!Fw|Ta5>lS0+Acq$^q2! z%>bXku9u3+t&~x0Mfp1hB|VjY+6Vu75S2&}aMSa=mwI!soU1o;!UXI~&6v-QvIF>ZyR|DaIwaLNfh1bZ9jf&Zqu7`9^3abzetX_ehDHowJWF*^dhck}isEt$U!7u7nT# zi)oBsoG|t<=ZPgld9}&jm>4D1_K56^Krl8bo*TD~%@DB={I>$fqQ5+ofSzA>QQ-QA zlvvD=SG}obGfmkQWTbkF{~-x*7=CslY3Ru5X4#7drp>u@SST1QU^dYtW<%tdsjs(d zMkOLgSRtTI0hs9w_;Ya(Nz29I1%;i2#q6G+mM1c$heimwt^MB3A^&=;Q=a&lTs0e& zQ!vrKb({yrK*UMr+4UJNT3})Gz-MdnLLVe&4p9NnfvCFaGsqQmq!w{ln=-Cttsng8R@(WMTK5LXWND3E zM6Rmqj+pZDI{1f~QFN}#v{wH&SgJR0Rf2Oy6w2Pr+2fm(R)llzdk6(iJHu4j%U}F2 zGVh7UPUi-$LV{%7sRZWa<6Y)kT6e!PU!!R|p25Z;94y>!W1um;n+1&9<7AI!7HNz}s7GvGF3p zzL|b8ARwq|r;L>`)szAV@cGambklh^=;t|~+sr ztB$hzUXLb_P|t^YMWbMG-x6ig`}70gDiSlb-}7dd_9nX-xI&{WJ{>%hth@SpObPgF zw`oDfUO+o-CiAGP(yKoHFS(=Adk+LaD#62o@zdNlpYltCQ)4tfQL-y=rhYsJjef#w zCTppY>@#3?#_jiee^jmPN++TSo!%=K z5lBC{Zqz1!7&QK$oeH1lDJc6`z#sowty|-NLAh)a(hCk!6C+R)oWoNQt0TZW#Nmr-VC{qPmDxU%J)d%oWME^3kU}BfL7h&(E*5FZU=4QtQDXEV?{4063F@5 zt&wOoVB8ajX=4tXe5dh;{Sl>L5|!wW_0nz%Z4pOynVB8g=CvdUK4+c zlw?dPZ$u|TL$_#Do{wwRqc~2qnB;Os-g`$aN*V^Hi@OAt%(xxIuX(&RHr6^bn4_P!{{y5(ZZpWohM4it|M(Fhl_a2DM8qY z$v;|0#|_foxQB}=yxjq`H@RM$W*0xv<0EW&!*{$|TuP}!>nnU@(K{LTKuX`f&dWq{ zcf!HDRXr3rj^KO^&@vriIoD&Tu>ED+d)=r->eMz}$a}rOxyr-c+gc7H5NiwMi>}tG z*=+X=7goF!Zzq2d1^K+_L^>?Vpc&#suW$=O`;rTmlz##sGGD0=5#j0KvA z5wnrd)yU5m(}tPV+k4YaKizuSmSP)jY286)Jo}Uo*hHb*S5etbAxBZ~&3_Xb^voOf zOq7v^Pfa<5H9A*c(k=0F3*6tQD)h}AcotfubBHaKm50KJ@e{44%5+ja3%J*pU-Q|< zMIluiasCJt�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Ð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 L8tYf+IsNf(^|&9| literal 0 HcmV?d00001 diff --git a/community/maintainers.md b/community/maintainers.md new file mode 100644 index 00000000000..cdf78b150cd --- /dev/null +++ b/community/maintainers.md @@ -0,0 +1,42 @@ +# Maintainers + +See [Governance](governance.md) for what each maintainer type is + +## Project maintainers + +In alphabetical order + +| Name | GitHub Username | Email | Organization | +| -------------- | ---------------- | --------------------------- | ------------------ | +| Abhin Chhabra | `chhabrakadabra` | chhabra.abhin@gmail.com | Shopify | +| Achal Shah | `achals` | achals@gmail.com | Tecton | +| Danny Chiao | `adchia` | d.chiao@gmail.com | Tecton | +| David Liu | `mavysavydav` | davidyliuliu@gmail.com | Twitter | +| Felix Wang | `felixwang9817` | wangfelix98@gmail.com | Tecton | +| Kevin Zhang | `kevjumba` | kevin.zhang.13499@gmail.com | Tecton | +| Matt Delacour | `MattDelac` | mdelacour@hey.com | (formerly) Shopify | +| Miles Adkins | `sfc-gh-madkins` | miles.adkins@snowflake.com | Snowflake | +| Willem Pienaar | `woop` | will.pienaar@gmail.com | Tecton | +| Zhiling Chen | `zhilingc` | chnzhlng@gmail.com | GetGround | + +## Area maintainers + +Generally, with contribution questions here, default to `#feast-development` in the [slack.feast.dev](slack.feast.dev) Slack channel, but these may be folks for you to tag in messages + +| Area | Description | Name | +| -------------------- | -------------------------------------------------------------------------- | --------------------------------------------- | +| Data ingestion | ingesting batch + stream data into the online store (materialization) | Achal Shah,
Felix Wang,
Kevin Zhang | +| Developer experience | tooling, testing, documentation, tutorials | Achal Shah | +| Feature serving | optimization, caching, deployment patterns, batch retrieval, range queries | Dvir Dukhan | +| Ops | general deployment concerns, CI/CD, versioning | Keith Adler,
Danny Chiao,
Felix Wang | +| Web UI | i.e. `feast ui` output | Danny Chiao,
David Liu | + +## Emeritus Maintainers + +| Name | GitHub Username | Email | Organization | +| ------------------- | --------------- | --------------------------- | ------------ | +| Oleg Avdeev | oavdeev | oleg.v.avdeev@gmail.com | Tecton | +| Oleksii Moskalenko | pyalex | moskalenko.alexey@gmail.com | Tecton | +| Jay Parthasarthy | jparthasarthy | jparthasarthy@gmail.com | Tecton | +| Pradithya Aria Pura | pradithya | pradithya.aria@gmail.com | Gojek | +| Tsotne Tabidze | tsotnet | tsotnet@gmail.com | Tecton | \ No newline at end of file diff --git a/docs/community.md b/docs/community.md index bd01b2a64b8..098b6b3f90b 100644 --- a/docs/community.md +++ b/docs/community.md @@ -3,6 +3,7 @@ ## Links & Resources * [GitHub Repository](https://github.com/feast-dev/feast/): Find the complete Feast codebase on GitHub. + * [Community Governance Doc](https://github.com/feast-dev/feast/blob/master/community): See the governance model of Feast, including who the maintainers are and how decisions are made. * [Slack](https://slack.feast.dev): Feel free to ask questions or say hello! This is the main place where maintainers and contributors brainstorm and where users ask questions or discuss best practices. * Feast users should join `#feast-general` or `#feast-beginners` to ask questions * Feast developers / contributors should join `#feast-development` diff --git a/docs/project/contributing.md b/docs/project/contributing.md index 933237f204f..9a3e3e1a3ea 100644 --- a/docs/project/contributing.md +++ b/docs/project/contributing.md @@ -1,11 +1,36 @@ # Contribution process -We use [RFCs](https://en.wikipedia.org/wiki/Request_for_Comments) and [GitHub issues](https://github.com/feast-dev/feast/issues) to communicate development ideas. The simplest way to contribute to Feast is to leave comments in our [RFCs](https://drive.google.com/drive/u/0/folders/1Lj1nIeRB868oZvKTPLYqAvKQ4O0BksjY) in the [Feast Google Drive](https://drive.google.com/drive/u/0/folders/0AAe8j7ZK3sxSUk9PVA) or our GitHub issues. You will need to join our [Google Group](../community.md) in order to get access. +## Getting started +After familiarizing yourself with the documentation, the simplest way to get started is to: +1. Join the `#feast-development` [Slack channel](https://tectonfeast.slack.com/archives/C01NTDB88QK), where contributors discuss ideas and PRs +2. Join our Google Groups in order to get access to RFC folders + get invites to community calls. See [community](../community.md) for more details. +3. Setup your developer environment by following [development guide](development-guide.md). +4. Either create a [GitHub issue](https://github.com/feast-dev/feast/issues) or make a draft PR (following [development guide](development-guide.md)) to get the ball rolling! -We follow a process of [lazy consensus](http://community.apache.org/committers/lazyConsensus.html). If you believe you know what the project needs then just start development. If you are unsure about which direction to take with development then please communicate your ideas through a GitHub issue or through our [Slack Channel](../community.md) before starting development. +## Decision making process +*See [governance](../../community/governance.md) for more details here* + +We follow a process of [lazy consensus](http://community.apache.org/committers/lazyConsensus.html). If you believe you know what the project needs then just start development. As long as there is no active opposition and the PR has been approved by maintainers or CODEOWNERS, contributions will be merged. + +We use our `#feast-development` [Slack channel](https://tectonfeast.slack.com/archives/C01NTDB88QK), [GitHub issues](https://github.com/feast-dev/feast/issues), and [GitHub pull requests](https://github.com/feast-dev/feast/pulls) to communicate development ideas. + +The general decision making workflow is as follows: + + + +> **Note**: There may not always a corresponding CODEOWNER for the affected code, in which case the responsibility falls on other maintainers or contributors with write access to review + merge the PR + +## Pull requests Please [submit a PR](https://github.com/feast-dev/feast/pulls) to the master branch of the Feast repository once you are ready to submit your contribution. Code submission to Feast \(including submission from project maintainers\) require review and approval from maintainers or code owners. PRs that are submitted by the general public need to be identified as `ok-to-test`. Once enabled, [Prow](https://github.com/kubernetes/test-infra/tree/master/prow) will run a range of tests to verify the submission, after which community members will help to review the pull request. -See also [Community](../community.md) for other ways to get involved with the community (e.g. joining community calls) \ No newline at end of file +See also [Making a pull request](development-guide.md#making-a-pull-request) for other guidelines on making pull requests in Feast. + +## Resources + +- [Community](../community.md) for other ways to get involved with the community (e.g. joining community calls) +- [Development guide](development-guide.md) for tips on how to contribute +- [Feast GitHub issues](https://github.com/feast-dev/feast/issues) to see what others are working on +- [Feast RFCs](https://drive.google.com/drive/u/0/folders/1msUsgmDbVBaysmhBlg9lklYLLTMk4bC3) for a folder of previously written RFCs \ No newline at end of file diff --git a/docs/project/development-guide.md b/docs/project/development-guide.md index d06ace327cb..39c2088e854 100644 --- a/docs/project/development-guide.md +++ b/docs/project/development-guide.md @@ -2,42 +2,45 @@ ## Table of Contents -- [Overview](#overview) -- [Community](#community) -- [Making a pull request](#making-a-pull-request) - - [Pull request checklist](#pull-request-checklist) - - [Good Practices](#good-practices-to-keep-in-mind) - - [Forking the repo](#forking-the-repo) - - [Pre-commit Hooks](#pre-commit-hooks) - - [Signing off commits](#signing-off-commits) - - [Incorporating upstream changes from master](#incorporating-upstream-changes-from-master) -- [Feast Python SDK / CLI](#feast-python-sdk--cli) - - [Environment Setup](#environment-setup) - - [Code Style & Linting](#code-style--linting) - - [Unit Tests](#unit-tests) - - [Integration Tests](#integration-tests) - - [Local integration tests](#local-integration-tests) - - [(Advanced) Full integration tests](#advanced-full-integration-tests) - - [(Advanced) Running specific provider tests or running your test against specific online or offline stores](#advanced-running-specific-provider-tests-or-running-your-test-against-specific-online-or-offline-stores) - - [(Experimental) Run full integration tests against containerized services](#experimental-run-full-integration-tests-against-containerized-services) - - [Contrib integration tests](#contrib-integration-tests) - - [(Contrib) Running tests for Spark offline store](#contrib-running-tests-for-spark-offline-store) - - [(Contrib) Running tests for Trino offline store](#contrib-running-tests-for-trino-offline-store) - - [(Contrib) Running tests for Postgres offline store](#contrib-running-tests-for-postgres-offline-store) - - [(Contrib) Running tests for Postgres online store](#contrib-running-tests-for-postgres-online-store) - - [(Contrib) Running tests for HBase online store](#contrib-running-tests-for-hbase-online-store) -- [(Experimental) Feast UI](#experimental-feast-ui) -- [Feast Java Serving](#feast-java-serving) -- [Developing the Feast Helm charts](#developing-the-feast-helm-charts) - - [Feast Java Feature Server Helm Chart](#feast-java-feature-server-helm-chart) - - [Feast Python / Go Feature Server Helm Chart](#feast-python--go-feature-server-helm-chart) -- [Feast Go Client](#feast-go-client) - - [Environment Setup](#go-environment-setup) - - [Building](#building-go) - - [Code Style & Linting](#go-code-style--linting) - - [Unit Tests](#go-unit-tests) - - [Testing with GitHub Actions workflows](#testing-with-github-actions-workflows) -- [Data Storage Formats](#feast-data-storage-format) +- [Development Guide: Main Feast Repository](#development-guide-main-feast-repository) + - [Table of Contents](#table-of-contents) + - [Overview](#overview) + - [Compatibility](#compatibility) + - [Community](#community) + - [Making a pull request](#making-a-pull-request) + - [Pull request checklist](#pull-request-checklist) + - [Good practices to keep in mind](#good-practices-to-keep-in-mind) + - [Forking the repo](#forking-the-repo) + - [Pre-commit Hooks](#pre-commit-hooks) + - [Signing off commits](#signing-off-commits) + - [Incorporating upstream changes from master](#incorporating-upstream-changes-from-master) + - [Feast Python SDK / CLI](#feast-python-sdk--cli) + - [Environment Setup](#environment-setup) + - [Code Style & Linting](#code-style--linting) + - [Unit Tests](#unit-tests) + - [Integration Tests](#integration-tests) + - [Local integration tests](#local-integration-tests) + - [(Advanced) Full integration tests](#advanced-full-integration-tests) + - [(Advanced) Running specific provider tests or running your test against specific online or offline stores](#advanced-running-specific-provider-tests-or-running-your-test-against-specific-online-or-offline-stores) + - [(Experimental) Run full integration tests against containerized services](#experimental-run-full-integration-tests-against-containerized-services) + - [Contrib integration tests](#contrib-integration-tests) + - [(Contrib) Running tests for Spark offline store](#contrib-running-tests-for-spark-offline-store) + - [(Contrib) Running tests for Trino offline store](#contrib-running-tests-for-trino-offline-store) + - [(Contrib) Running tests for Postgres offline store](#contrib-running-tests-for-postgres-offline-store) + - [(Contrib) Running tests for Postgres online store](#contrib-running-tests-for-postgres-online-store) + - [(Contrib) Running tests for HBase online store](#contrib-running-tests-for-hbase-online-store) + - [(Experimental) Feast UI](#experimental-feast-ui) + - [Feast Java Serving](#feast-java-serving) + - [Developing the Feast Helm charts](#developing-the-feast-helm-charts) + - [Feast Java Feature Server Helm Chart](#feast-java-feature-server-helm-chart) + - [Feast Python / Go Feature Server Helm Chart](#feast-python--go-feature-server-helm-chart) + - [Feast Go Client](#feast-go-client) + - [Go Environment Setup](#go-environment-setup) + - [Building Go](#building-go) + - [Go Code Style & Linting](#go-code-style--linting) + - [Go Unit Tests](#go-unit-tests) + - [Testing with Github Actions workflows](#testing-with-github-actions-workflows) + - [Feast Data Storage Format](#feast-data-storage-format) ## Overview This guide is targeted at developers looking to contribute to Feast components in the main Feast repository: @@ -68,6 +71,8 @@ If the assignee is empty it means that no reviewer has been found yet. If a reviewer has been found, they should also be the assigned the PR. Finally, if there are comments to be addressed, the PR author should be the one assigned the PR. +PRs that are submitted by the general public need to be identified as `ok-to-test`. Once enabled, [Prow](https://github.com/kubernetes/test-infra/tree/master/prow) will run a range of tests to verify the submission, after which community members will help to review the pull request. + ### Pull request checklist A quick list of things to keep in mind as you're making changes: - As you make changes @@ -77,6 +82,7 @@ A quick list of things to keep in mind as you're making changes: - Install [pre-commit hooks](#pre-commit-hooks) to ensure all the default linters / formatters are run when you push. - When you make the PR - Make a pull request from the forked repo you made + - Ensure the title of the PR matches semantic release conventions (e.g. start with `feat:` or `fix:` or `ci:` or `chore:` or `docs:`). Keep in mind that any PR with `feat:` or `fix:` will directly make it into the change log of a release, so make sure they are understandable! - Ensure you add a GitHub **label** (i.e. a kind tag to the PR (e.g. `kind/bug` or `kind/housekeeping`)) or else checks will fail. - Ensure you leave a release note for any user facing changes in the PR. There is a field automatically generated in the PR request. You can write `NONE` in that field if there are no user facing changes. - Please run tests locally before submitting a PR (e.g. for Python, the [local integration tests](#local-integration-tests)) From 782c75979ee6a00a3efdbc0250ee2cbb7a8a23af Mon Sep 17 00:00:00 2001 From: Felix Wang Date: Thu, 8 Sep 2022 12:47:21 -0700 Subject: [PATCH 30/46] chore: Clean up entity key serialization (#3199) * Clean up entity key serialization Signed-off-by: Felix Wang * Remove incorrect docs Signed-off-by: Felix Wang * Format Signed-off-by: Felix Wang Signed-off-by: Felix Wang --- .../adding-support-for-a-new-online-store.md | 22 +++++++------------ .../cassandra_online_store.py | 6 +++-- .../feast/infra/online_stores/snowflake.py | 18 +++++++++++---- 3 files changed, 26 insertions(+), 20 deletions(-) diff --git a/docs/how-to-guides/customizing-feast/adding-support-for-a-new-online-store.md b/docs/how-to-guides/customizing-feast/adding-support-for-a-new-online-store.md index 52f0897138d..ab88ebaa203 100644 --- a/docs/how-to-guides/customizing-feast/adding-support-for-a-new-online-store.md +++ b/docs/how-to-guides/customizing-feast/adding-support-for-a-new-online-store.md @@ -154,7 +154,10 @@ def online_write_batch( project = config.project for entity_key, values, timestamp, created_ts in data: - entity_key_bin = serialize_entity_key(entity_key).hex() + entity_key_bin = serialize_entity_key( + entity_key, + entity_key_serialization_version=config.entity_key_serialization_version, + ).hex() timestamp = _to_naive_utc(timestamp) if created_ts is not None: created_ts = _to_naive_utc(created_ts) @@ -184,7 +187,10 @@ def online_read( project = config.project for entity_key in entity_keys: - entity_key_bin = serialize_entity_key(entity_key).hex() + entity_key_bin = serialize_entity_key( + entity_key, + entity_key_serialization_version=config.entity_key_serialization_version, + ).hex() print(f"entity_key_bin: {entity_key_bin}") cur.execute( @@ -208,18 +214,6 @@ def online_read( ``` {% endcode %} -### 1.3 Type Mapping - -Most online stores will have to perform some custom mapping of online store datatypes to feast value types. - -* The function to implement here are `source_datatype_to_feast_value_type` and `get_column_names_and_types` in your `DataSource` class. -* `source_datatype_to_feast_value_type` is used to convert your DataSource's datatypes to feast value types. -* `get_column_names_and_types` retrieves the column names and corresponding datasource types. - -Add any helper functions for type conversion to `sdk/python/feast/type_map.py`. - -* Be sure to implement correct type mapping so that Feast can process your feature columns without casting incorrectly that can potentially cause loss of information or incorrect data. - ## 2. Defining an OnlineStoreConfig class Additional configuration may be needed to allow the OnlineStore to talk to the backing store. For example, MySQL may need configuration information like the host at which the MySQL instance is running, credentials for connecting to the database, etc. diff --git a/sdk/python/feast/infra/online_stores/contrib/cassandra_online_store/cassandra_online_store.py b/sdk/python/feast/infra/online_stores/contrib/cassandra_online_store/cassandra_online_store.py index ee0cb19fef5..f89517c41eb 100644 --- a/sdk/python/feast/infra/online_stores/contrib/cassandra_online_store/cassandra_online_store.py +++ b/sdk/python/feast/infra/online_stores/contrib/cassandra_online_store/cassandra_online_store.py @@ -314,7 +314,8 @@ def online_write_batch( project = config.project for entity_key, values, timestamp, created_ts in data: entity_key_bin = serialize_entity_key( - entity_key, entity_key_serialization_version=2 + entity_key, + entity_key_serialization_version=config.entity_key_serialization_version, ).hex() with tracing_span(name="remote_call"): self._write_rows( @@ -353,7 +354,8 @@ def online_read( for entity_key in entity_keys: entity_key_bin = serialize_entity_key( - entity_key, entity_key_serialization_version=2 + entity_key, + entity_key_serialization_version=config.entity_key_serialization_version, ).hex() with tracing_span(name="remote_call"): diff --git a/sdk/python/feast/infra/online_stores/snowflake.py b/sdk/python/feast/infra/online_stores/snowflake.py index be914043f24..a5c03085031 100644 --- a/sdk/python/feast/infra/online_stores/snowflake.py +++ b/sdk/python/feast/infra/online_stores/snowflake.py @@ -94,9 +94,13 @@ def online_write_batch( for j, (feature_name, val) in enumerate(values.items()): df.loc[j, "entity_feature_key"] = serialize_entity_key( - entity_key, 2 + entity_key, + entity_key_serialization_version=config.entity_key_serialization_version, ) + bytes(feature_name, encoding="utf-8") - df.loc[j, "entity_key"] = serialize_entity_key(entity_key, 2) + df.loc[j, "entity_key"] = serialize_entity_key( + entity_key, + entity_key_serialization_version=config.entity_key_serialization_version, + ) df.loc[j, "feature_name"] = feature_name df.loc[j, "value"] = val.SerializeToString() df.loc[j, "event_ts"] = timestamp @@ -162,7 +166,10 @@ def online_read( ( "TO_BINARY(" + hexlify( - serialize_entity_key(combo[0], 2) + serialize_entity_key( + combo[0], + entity_key_serialization_version=config.entity_key_serialization_version, + ) + bytes(combo[1], encoding="utf-8") ).__str__()[1:] + ")" @@ -184,7 +191,10 @@ def online_read( df = execute_snowflake_statement(conn, query).fetch_pandas_all() for entity_key in entity_keys: - entity_key_bin = serialize_entity_key(entity_key, 2) + entity_key_bin = serialize_entity_key( + entity_key, + entity_key_serialization_version=config.entity_key_serialization_version, + ) res = {} res_ts = None for index, row in df[df["entity_key"] == entity_key_bin].iterrows(): From 175fd256e0d21f6539f68708705bddf1caa3d975 Mon Sep 17 00:00:00 2001 From: ckarwicki <71740096+ckarwicki@users.noreply.github.com> Date: Fri, 9 Sep 2022 00:21:22 -0400 Subject: [PATCH 31/46] fix: Fix materialization when running on Spark cluster. (#3166) * Fix materialization when running on Spark cluster. When running materialization and have Spark offline store configured to use cluster (`spark.master` pointing to actual Spark master node) `self.to_spark_df().write.parquet(temp_dir, mode="overwrite")` will create parquet file in worker node but `return pq.read_table(temp_dir)` is executed on driver node and it can't read from worker. Proposed fix makes materialization work when run on Spark cluster. Signed-off-by: ckarwicki <104110169+ckarwicki-deloitte@users.noreply.github.com> Signed-off-by: ckarwicki <71740096+ckarwicki@users.noreply.github.com> * Fix linter. Signed-off-by: ckarwicki Signed-off-by: ckarwicki <104110169+ckarwicki-deloitte@users.noreply.github.com> Signed-off-by: ckarwicki <71740096+ckarwicki@users.noreply.github.com> * Fix linter. Signed-off-by: ckarwicki Signed-off-by: ckarwicki <104110169+ckarwicki-deloitte@users.noreply.github.com> Signed-off-by: ckarwicki <71740096+ckarwicki@users.noreply.github.com> Signed-off-by: ckarwicki <104110169+ckarwicki-deloitte@users.noreply.github.com> Signed-off-by: ckarwicki <71740096+ckarwicki@users.noreply.github.com> Signed-off-by: ckarwicki --- .../offline_stores/contrib/spark_offline_store/spark.py | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/sdk/python/feast/infra/offline_stores/contrib/spark_offline_store/spark.py b/sdk/python/feast/infra/offline_stores/contrib/spark_offline_store/spark.py index 0a4ec05c23d..663c60f3e83 100644 --- a/sdk/python/feast/infra/offline_stores/contrib/spark_offline_store/spark.py +++ b/sdk/python/feast/infra/offline_stores/contrib/spark_offline_store/spark.py @@ -342,11 +342,7 @@ def _to_df_internal(self) -> pd.DataFrame: def _to_arrow_internal(self) -> pyarrow.Table: """Return dataset as pyarrow Table synchronously""" - - # write to temp parquet and then load it as pyarrow table from disk - with tempfile.TemporaryDirectory() as temp_dir: - self.to_spark_df().write.parquet(temp_dir, mode="overwrite") - return pq.read_table(temp_dir) + return pyarrow.Table.from_pandas(self._to_df_internal()) def persist(self, storage: SavedDatasetStorage, allow_overwrite: bool = False): """ From c19d64089cec04c771d24ce2b0034b66070cedb5 Mon Sep 17 00:00:00 2001 From: Danny Chiao Date: Fri, 9 Sep 2022 12:58:38 -0400 Subject: [PATCH 32/46] chore: Add CODEOWNERS file to automate more code reviews (#3192) * chore: Add CODEOWNERS file to automate more code reviews Signed-off-by: Danny Chiao * lint Signed-off-by: Danny Chiao * add java redis logic to CODEOWNERS Signed-off-by: Danny Chiao * remove sam Signed-off-by: Danny Chiao Signed-off-by: Danny Chiao --- CODEOWNERS | 54 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 54 insertions(+) create mode 100644 CODEOWNERS diff --git a/CODEOWNERS b/CODEOWNERS new file mode 100644 index 00000000000..259c13ea3f0 --- /dev/null +++ b/CODEOWNERS @@ -0,0 +1,54 @@ +# See https://help.github.com/articles/about-codeowners/ +# for more info about CODEOWNERS file + +# Core Interfaces +/sdk/python/feast/infra/offline_stores/offline_store.py @feast-dev/maintainers @chhabrakadabra @mavysavydav @sfc-gh-madkins +/sdk/python/feast/infra/online_stores/online_store.py @feast-dev/maintainers @DvirDukhan +/sdk/python/feast/infra/materialization_engine/batch_materialization_engine.py @feast-dev/maintainers @whoahbot @sfc-gh-madkins + +# ==== Offline Stores ==== +# Core utils +/sdk/python/feast/infra/offline_stores/offline_utils.py @feast-dev/maintainers @chhabrakadabra @mavysavydav @sfc-gh-madkins + +# BigQuery +/sdk/python/feast/infra/offline_stores/offline_store.py @feast-dev/maintainers @chhabrakadabra @mavysavydav + +# Snowflake +/sdk/python/feast/infra/offline_stores/snowflake* @sfc-gh-madkins + +# Athena (contrib) +/sdk/python/feast/infra/offline_stores/contrib/athena_offline_store/ @toping4445 + +# Azure SQL (contrib) +/sdk/python/feast/infra/offline_stores/contrib/mssql_offline_store/ @kevjumba + +# Spark (contrib) +/sdk/python/feast/infra/offline_stores/contrib/spark_offline_store/ @niklasvm @kevjumba + +# ==== Online Stores ==== + +# Redis +/sdk/python/feast/infra/online_stores/redis.py @DvirDukhan +/java/feast/serving/connectors/redis/ @DvirDukhan + +# Snowflake +/sdk/python/feast/infra/online_stores/snowflake.py @sfc-gh-madkins + +# Cassandra (contrib) +/sdk/python/feast/infra/online_stores/cassandra_online_store/ @hemidactylus + +# ==== Batch Materialization Engines ==== + +# Snowflake +/sdk/python/feast/infra/materialization/snowflake* @sfc-gh-madkins + +# Bytewax +/sdk/python/feast/infra/materialization/contrib/bytewax/ @whoahbot + +# AWS Lambda +/sdk/python/feast/infra/materialization/contrib/aws_lambda/ @achals + +# ==== Web UI ==== +/ui/ @adchia +/sdk/python/feast/ui/ @adchia +/sdk/python/feast/ui_server.py @adchia From dfdd0ca5fe54b638ac5a268501d67e5621ca8d89 Mon Sep 17 00:00:00 2001 From: sfc-gh-madkins <82121043+sfc-gh-madkins@users.noreply.github.com> Date: Mon, 12 Sep 2022 11:08:25 -0500 Subject: [PATCH 33/46] fix: Remove bad snowflake offline store method (#3204) Signed-off-by: Miles Adkins Signed-off-by: Miles Adkins --- sdk/python/feast/infra/offline_stores/snowflake.py | 9 --------- 1 file changed, 9 deletions(-) diff --git a/sdk/python/feast/infra/offline_stores/snowflake.py b/sdk/python/feast/infra/offline_stores/snowflake.py index 23959797023..c835eb939ff 100644 --- a/sdk/python/feast/infra/offline_stores/snowflake.py +++ b/sdk/python/feast/infra/offline_stores/snowflake.py @@ -447,15 +447,6 @@ def to_sql(self) -> str: with self._query_generator() as query: return query - def to_arrow_chunks(self, arrow_options: Optional[Dict] = None) -> Optional[List]: - with self._query_generator() as query: - - arrow_batches = execute_snowflake_statement( - self.snowflake_conn, query - ).get_result_batches() - - return arrow_batches - def persist(self, storage: SavedDatasetStorage, allow_overwrite: bool = False): assert isinstance(storage, SavedDatasetSnowflakeStorage) self.to_snowflake(table_name=storage.snowflake_options.table) From 39aeea3fa77c3b3a789556a1e0fa22ecedcae4ea Mon Sep 17 00:00:00 2001 From: sfc-gh-madkins <82121043+sfc-gh-madkins@users.noreply.github.com> Date: Mon, 12 Sep 2022 12:34:29 -0500 Subject: [PATCH 34/46] feat: Add tag kwarg to set Snowflake online store table path (#3176) Signed-off-by: Miles Adkins Signed-off-by: Miles Adkins --- docs/reference/online-stores/snowflake.md | 16 +++++++++++-- .../infra/materialization/snowflake_engine.py | 10 +++++--- .../feast/infra/online_stores/snowflake.py | 24 +++++++++---------- .../infra/utils/snowflake/snowflake_utils.py | 16 +++++++++++++ 4 files changed, 49 insertions(+), 17 deletions(-) diff --git a/docs/reference/online-stores/snowflake.md b/docs/reference/online-stores/snowflake.md index 7db45dd7bd8..1f79bbfdeb8 100644 --- a/docs/reference/online-stores/snowflake.md +++ b/docs/reference/online-stores/snowflake.md @@ -17,7 +17,6 @@ The data model for using a Snowflake Transient Table as an online store follows (This model may be subject to change when Snowflake Hybrid Tables are released) ## Example - {% code title="feature_store.yaml" %} ```yaml project: my_feature_repo @@ -34,6 +33,19 @@ online_store: ``` {% endcode %} +## Tags KWARGs Actions: + +"ONLINE_PATH": Adding the "ONLINE_PATH" key to a FeatureView tags parameter allows you to choose the online table path for the online serving table (ex. "{database}"."{schema}"). + +{% code title="example_config.py" %} +```python +driver_stats_fv = FeatureView( + ... + tags={"snowflake-online-store/online_path": '"FEAST"."ONLINE"'}, +) +``` +{% endcode %} + The full set of configuration options is available in [SnowflakeOnlineStoreConfig](https://rtd.feast.dev/en/latest/#feast.infra.online_stores.snowflake.SnowflakeOnlineStoreConfig). ## Functionality Matrix @@ -41,7 +53,7 @@ The full set of configuration options is available in [SnowflakeOnlineStoreConfi The set of functionality supported by online stores is described in detail [here](overview.md#functionality). Below is a matrix indicating which functionality is supported by the Snowflake online store. -| | Snowflake | +| | Snowflake | | :-------------------------------------------------------- | :-- | | write feature values to the online store | yes | | read feature values from the online store | yes | diff --git a/sdk/python/feast/infra/materialization/snowflake_engine.py b/sdk/python/feast/infra/materialization/snowflake_engine.py index de498fe4e1f..0219a7923f6 100644 --- a/sdk/python/feast/infra/materialization/snowflake_engine.py +++ b/sdk/python/feast/infra/materialization/snowflake_engine.py @@ -28,6 +28,7 @@ assert_snowflake_feature_names, execute_snowflake_statement, get_snowflake_conn, + get_snowflake_online_store_path, package_snowpark_zip, ) from feast.protos.feast.types.EntityKey_pb2 import EntityKey as EntityKeyProto @@ -370,8 +371,6 @@ def materialize_to_snowflake_online_store( ) -> None: assert_snowflake_feature_names(feature_view) - online_table = f"""{repo_config .online_store.database}"."{repo_config.online_store.schema_}"."[online-transient] {project}_{feature_view.name}""" - feature_names_str = '", "'.join( [feature.name for feature in feature_view.features] ) @@ -381,8 +380,13 @@ def materialize_to_snowflake_online_store( else: fv_created_str = None + online_path = get_snowflake_online_store_path(repo_config, feature_view) + online_table = ( + f'{online_path}."[online-transient] {project}_{feature_view.name}"' + ) + query = f""" - MERGE INTO "{online_table}" online_table + MERGE INTO {online_table} online_table USING ( SELECT "entity_key" || TO_BINARY("feature_name", 'UTF-8') AS "entity_feature_key", diff --git a/sdk/python/feast/infra/online_stores/snowflake.py b/sdk/python/feast/infra/online_stores/snowflake.py index a5c03085031..c4474dff38d 100644 --- a/sdk/python/feast/infra/online_stores/snowflake.py +++ b/sdk/python/feast/infra/online_stores/snowflake.py @@ -15,6 +15,7 @@ from feast.infra.utils.snowflake.snowflake_utils import ( execute_snowflake_statement, get_snowflake_conn, + get_snowflake_online_store_path, write_pandas_binary, ) from feast.protos.feast.types.EntityKey_pb2 import EntityKey as EntityKeyProto @@ -112,9 +113,7 @@ def online_write_batch( agg_df = pd.concat(dfs) # This combines both the data upload plus the overwrite in the same transaction - table_path = ( - f'"{config.online_store.database}"."{config.online_store.schema_}"' - ) + online_path = get_snowflake_online_store_path(config, table) with get_snowflake_conn(config.online_store, autocommit=False) as conn: write_pandas_binary( conn, @@ -125,7 +124,7 @@ def online_write_batch( ) # special function for writing binary to snowflake query = f""" - INSERT OVERWRITE INTO {table_path}."[online-transient] {config.project}_{table.name}" + INSERT OVERWRITE INTO {online_path}."[online-transient] {config.project}_{table.name}" SELECT "entity_feature_key", "entity_key", @@ -138,7 +137,7 @@ def online_write_batch( *, ROW_NUMBER() OVER(PARTITION BY "entity_key","feature_name" ORDER BY "event_ts" DESC, "created_ts" DESC) AS "_feast_row" FROM - {table_path}."[online-transient] {config.project}_{table.name}") + {online_path}."[online-transient] {config.project}_{table.name}") WHERE "_feast_row" = 1; """ @@ -178,13 +177,13 @@ def online_read( ] ) - table_path = f'"{config.online_store.database}"."{config.online_store.schema_}"' + online_path = get_snowflake_online_store_path(config, table) with get_snowflake_conn(config.online_store) as conn: query = f""" SELECT "entity_key", "feature_name", "value", "event_ts" FROM - {table_path}."[online-transient] {config.project}_{table.name}" + {online_path}."[online-transient] {config.project}_{table.name}" WHERE "entity_feature_key" IN ({entity_fetch_str}) """ @@ -221,11 +220,11 @@ def update( ): assert isinstance(config.online_store, SnowflakeOnlineStoreConfig) - table_path = f'"{config.online_store.database}"."{config.online_store.schema_}"' with get_snowflake_conn(config.online_store) as conn: for table in tables_to_keep: + online_path = get_snowflake_online_store_path(config, table) query = f""" - CREATE TRANSIENT TABLE IF NOT EXISTS {table_path}."[online-transient] {config.project}_{table.name}" ( + CREATE TRANSIENT TABLE IF NOT EXISTS {online_path}."[online-transient] {config.project}_{table.name}" ( "entity_feature_key" BINARY, "entity_key" BINARY, "feature_name" VARCHAR, @@ -237,7 +236,8 @@ def update( execute_snowflake_statement(conn, query) for table in tables_to_delete: - query = f'DROP TABLE IF EXISTS {table_path}."[online-transient] {config.project}_{table.name}"' + online_path = get_snowflake_online_store_path(config, table) + query = f'DROP TABLE IF EXISTS {online_path}."[online-transient] {config.project}_{table.name}"' execute_snowflake_statement(conn, query) def teardown( @@ -248,8 +248,8 @@ def teardown( ): assert isinstance(config.online_store, SnowflakeOnlineStoreConfig) - table_path = f'"{config.online_store.database}"."{config.online_store.schema_}"' with get_snowflake_conn(config.online_store) as conn: for table in tables: - query = f'DROP TABLE IF EXISTS {table_path}."[online-transient] {config.project}_{table.name}"' + online_path = get_snowflake_online_store_path(config, table) + query = f'DROP TABLE IF EXISTS {online_path}."[online-transient] {config.project}_{table.name}"' execute_snowflake_statement(conn, query) diff --git a/sdk/python/feast/infra/utils/snowflake/snowflake_utils.py b/sdk/python/feast/infra/utils/snowflake/snowflake_utils.py index c7b27d8331c..a5d2b05d45d 100644 --- a/sdk/python/feast/infra/utils/snowflake/snowflake_utils.py +++ b/sdk/python/feast/infra/utils/snowflake/snowflake_utils.py @@ -22,6 +22,7 @@ import feast from feast.errors import SnowflakeIncompleteConfig, SnowflakeQueryUnknownError from feast.feature_view import FeatureView +from feast.repo_config import RepoConfig try: import snowflake.connector @@ -104,6 +105,21 @@ def get_snowflake_conn(config, autocommit=True) -> SnowflakeConnection: raise SnowflakeIncompleteConfig(e) +def get_snowflake_online_store_path( + config: RepoConfig, + feature_view: FeatureView, +) -> str: + path_tag = "snowflake-online-store/online_path" + if path_tag in feature_view.tags: + online_path = feature_view.tags[path_tag] + else: + online_path = ( + f'"{config.online_store.database}"."{config.online_store.schema_}"' + ) + + return online_path + + def package_snowpark_zip(project_name) -> Tuple[str, str]: path = os.path.dirname(feast.__file__) copy_path = path + f"/snowflake_feast_{project_name}" From cdf0fafa6939f67cfb13ee3e28ff16a46c2147ae Mon Sep 17 00:00:00 2001 From: Niklas von Maltzahn Date: Mon, 12 Sep 2022 19:35:27 +0200 Subject: [PATCH 35/46] fix: Local staging location provision (#3195) * fix: Local staging location provision Signed-off-by: niklasvm * fix import Signed-off-by: niklasvm * linting fix Signed-off-by: niklasvm Signed-off-by: niklasvm --- .../infra/offline_stores/contrib/spark_offline_store/spark.py | 4 ++-- .../contrib/spark_offline_store/tests/data_source.py | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/sdk/python/feast/infra/offline_stores/contrib/spark_offline_store/spark.py b/sdk/python/feast/infra/offline_stores/contrib/spark_offline_store/spark.py index 663c60f3e83..01f89f80bb7 100644 --- a/sdk/python/feast/infra/offline_stores/contrib/spark_offline_store/spark.py +++ b/sdk/python/feast/infra/offline_stores/contrib/spark_offline_store/spark.py @@ -15,7 +15,6 @@ from pyspark import SparkConf from pyspark.sql import SparkSession from pytz import utc -from sdk.python.feast.infra.utils import aws_utils from feast import FeatureView, OnDemandFeatureView from feast.data_source import DataSource @@ -32,6 +31,7 @@ RetrievalMetadata, ) from feast.infra.registry.registry import Registry +from feast.infra.utils import aws_utils from feast.repo_config import FeastConfigBaseModel, RepoConfig from feast.saved_dataset import SavedDatasetStorage from feast.type_map import spark_schema_to_np_dtypes @@ -364,7 +364,7 @@ def to_remote_storage(self) -> List[str]: sdf: pyspark.sql.DataFrame = self.to_spark_df() - if self._config.offline_store.staging_location.startswith("file://"): + if self._config.offline_store.staging_location.startswith("/"): local_file_staging_location = os.path.abspath( self._config.offline_store.staging_location ) diff --git a/sdk/python/feast/infra/offline_stores/contrib/spark_offline_store/tests/data_source.py b/sdk/python/feast/infra/offline_stores/contrib/spark_offline_store/tests/data_source.py index 64a2a01cee4..71c07b20c27 100644 --- a/sdk/python/feast/infra/offline_stores/contrib/spark_offline_store/tests/data_source.py +++ b/sdk/python/feast/infra/offline_stores/contrib/spark_offline_store/tests/data_source.py @@ -58,8 +58,8 @@ def create_offline_store_config(self): self.spark_offline_store_config = SparkOfflineStoreConfig() self.spark_offline_store_config.type = "spark" self.spark_offline_store_config.spark_conf = self.spark_conf - self.spark_offline_store_config.staging_location = "file://" + str( - tempfile.TemporaryDirectory() + self.spark_offline_store_config.staging_location = ( + tempfile.TemporaryDirectory().name ) self.spark_offline_store_config.region = "eu-west-1" return self.spark_offline_store_config From 43222f21046c54a68250350c49b4cdf819d41591 Mon Sep 17 00:00:00 2001 From: Rob Howley Date: Mon, 12 Sep 2022 14:49:23 -0400 Subject: [PATCH 36/46] feat: Add health endpoint to py server (#3202) feat: add health endpoint to py server Signed-off-by: Rob Howley Signed-off-by: Rob Howley Co-authored-by: Rob Howley --- sdk/python/feast/feature_server.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/sdk/python/feast/feature_server.py b/sdk/python/feast/feature_server.py index bef88923e18..c2596d411c6 100644 --- a/sdk/python/feast/feature_server.py +++ b/sdk/python/feast/feature_server.py @@ -4,7 +4,7 @@ import pandas as pd import uvicorn -from fastapi import FastAPI, HTTPException, Request +from fastapi import FastAPI, HTTPException, Request, Response, status from fastapi.logger import logger from fastapi.params import Depends from google.protobuf.json_format import MessageToDict, Parse @@ -124,6 +124,10 @@ def write_to_online_store(body=Depends(get_body)): # Raise HTTPException to return the error message to the client raise HTTPException(status_code=500, detail=str(e)) + @app.get("/health") + def health(): + return Response(status_code=status.HTTP_200_OK) + return app From 7bc1dff5882c53c7e25f51ddb0b730bd81091a03 Mon Sep 17 00:00:00 2001 From: sfc-gh-madkins <82121043+sfc-gh-madkins@users.noreply.github.com> Date: Mon, 12 Sep 2022 20:27:23 -0500 Subject: [PATCH 37/46] fix: Update Snowflake Online docs (#3206) Signed-off-by: Miles Adkins Signed-off-by: Miles Adkins --- docs/reference/online-stores/snowflake.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/reference/online-stores/snowflake.md b/docs/reference/online-stores/snowflake.md index 1f79bbfdeb8..d114c87144a 100644 --- a/docs/reference/online-stores/snowflake.md +++ b/docs/reference/online-stores/snowflake.md @@ -35,7 +35,7 @@ online_store: ## Tags KWARGs Actions: -"ONLINE_PATH": Adding the "ONLINE_PATH" key to a FeatureView tags parameter allows you to choose the online table path for the online serving table (ex. "{database}"."{schema}"). +"snowflake-online-store/online_path": Adding the "snowflake-online-store/online_path" key to a FeatureView tags parameter allows you to choose the online table path for the online serving table (ex. "{database}"."{schema}"). {% code title="example_config.py" %} ```python From a59c33ac10760b4029fadd8e377eb36a2c458583 Mon Sep 17 00:00:00 2001 From: Niklas von Maltzahn Date: Thu, 15 Sep 2022 16:45:22 +0200 Subject: [PATCH 38/46] feat: Implement spark materialization engine (#3184) * implement spark materialization engine Signed-off-by: niklasvm * remove redundant code Signed-off-by: niklasvm * make function private Signed-off-by: niklasvm * refactor serializing into a class Signed-off-by: niklasvm * switch to using `foreachPartition` Signed-off-by: niklasvm * remove batch_size parameter Signed-off-by: niklasvm * add partitions parameter Signed-off-by: niklasvm * linting Signed-off-by: niklasvm * rename spark to spark.offline and spark.engine Signed-off-by: niklasvm * fix to test Signed-off-by: niklasvm * forgot to stage Signed-off-by: niklasvm * revert spark.offline to spark to ensure backward compatibility Signed-off-by: niklasvm * fix import Signed-off-by: niklasvm * remove code from testing a large data set Signed-off-by: niklasvm * linting Signed-off-by: niklasvm * test without repartition Signed-off-by: niklasvm * test alternate connection string Signed-off-by: niklasvm * use redis online creator Signed-off-by: niklasvm Signed-off-by: niklasvm --- .../spark/spark_materialization_engine.py | 265 ++++++++++++++++++ sdk/python/feast/repo_config.py | 1 + .../contrib/spark/test_spark.py | 77 +++++ 3 files changed, 343 insertions(+) create mode 100644 sdk/python/feast/infra/materialization/contrib/spark/spark_materialization_engine.py create mode 100644 sdk/python/tests/integration/materialization/contrib/spark/test_spark.py diff --git a/sdk/python/feast/infra/materialization/contrib/spark/spark_materialization_engine.py b/sdk/python/feast/infra/materialization/contrib/spark/spark_materialization_engine.py new file mode 100644 index 00000000000..66eb97bca78 --- /dev/null +++ b/sdk/python/feast/infra/materialization/contrib/spark/spark_materialization_engine.py @@ -0,0 +1,265 @@ +import tempfile +from dataclasses import dataclass +from datetime import datetime +from typing import Callable, List, Literal, Optional, Sequence, Union + +import dill +import pandas as pd +import pyarrow +from tqdm import tqdm + +from feast.batch_feature_view import BatchFeatureView +from feast.entity import Entity +from feast.feature_view import FeatureView +from feast.infra.materialization.batch_materialization_engine import ( + BatchMaterializationEngine, + MaterializationJob, + MaterializationJobStatus, + MaterializationTask, +) +from feast.infra.offline_stores.contrib.spark_offline_store.spark import ( + SparkOfflineStore, + SparkRetrievalJob, +) +from feast.infra.online_stores.online_store import OnlineStore +from feast.infra.passthrough_provider import PassthroughProvider +from feast.infra.registry.base_registry import BaseRegistry +from feast.protos.feast.core.FeatureView_pb2 import FeatureView as FeatureViewProto +from feast.repo_config import FeastConfigBaseModel, RepoConfig +from feast.stream_feature_view import StreamFeatureView +from feast.utils import ( + _convert_arrow_to_proto, + _get_column_names, + _run_pyarrow_field_mapping, +) + + +class SparkMaterializationEngineConfig(FeastConfigBaseModel): + """Batch Materialization Engine config for spark engine""" + + type: Literal["spark.engine"] = "spark.engine" + """ Type selector""" + + partitions: int = 0 + """Number of partitions to use when writing data to online store. If 0, no repartitioning is done""" + + +@dataclass +class SparkMaterializationJob(MaterializationJob): + def __init__( + self, + job_id: str, + status: MaterializationJobStatus, + error: Optional[BaseException] = None, + ) -> None: + super().__init__() + self._job_id: str = job_id + self._status: MaterializationJobStatus = status + self._error: Optional[BaseException] = error + + def status(self) -> MaterializationJobStatus: + return self._status + + def error(self) -> Optional[BaseException]: + return self._error + + def should_be_retried(self) -> bool: + return False + + def job_id(self) -> str: + return self._job_id + + def url(self) -> Optional[str]: + return None + + +class SparkMaterializationEngine(BatchMaterializationEngine): + def update( + self, + project: str, + views_to_delete: Sequence[ + Union[BatchFeatureView, StreamFeatureView, FeatureView] + ], + views_to_keep: Sequence[ + Union[BatchFeatureView, StreamFeatureView, FeatureView] + ], + entities_to_delete: Sequence[Entity], + entities_to_keep: Sequence[Entity], + ): + # Nothing to set up. + pass + + def teardown_infra( + self, + project: str, + fvs: Sequence[Union[BatchFeatureView, StreamFeatureView, FeatureView]], + entities: Sequence[Entity], + ): + # Nothing to tear down. + pass + + def __init__( + self, + *, + repo_config: RepoConfig, + offline_store: SparkOfflineStore, + online_store: OnlineStore, + **kwargs, + ): + if not isinstance(offline_store, SparkOfflineStore): + raise TypeError( + "SparkMaterializationEngine is only compatible with the SparkOfflineStore" + ) + super().__init__( + repo_config=repo_config, + offline_store=offline_store, + online_store=online_store, + **kwargs, + ) + + def materialize( + self, registry, tasks: List[MaterializationTask] + ) -> List[MaterializationJob]: + return [ + self._materialize_one( + registry, + task.feature_view, + task.start_time, + task.end_time, + task.project, + task.tqdm_builder, + ) + for task in tasks + ] + + def _materialize_one( + self, + registry: BaseRegistry, + feature_view: Union[BatchFeatureView, StreamFeatureView, FeatureView], + start_date: datetime, + end_date: datetime, + project: str, + tqdm_builder: Callable[[int], tqdm], + ): + entities = [] + for entity_name in feature_view.entities: + entities.append(registry.get_entity(entity_name, project)) + + ( + join_key_columns, + feature_name_columns, + timestamp_field, + created_timestamp_column, + ) = _get_column_names(feature_view, entities) + + job_id = f"{feature_view.name}-{start_date}-{end_date}" + + try: + offline_job: SparkRetrievalJob = ( + self.offline_store.pull_latest_from_table_or_query( + config=self.repo_config, + data_source=feature_view.batch_source, + join_key_columns=join_key_columns, + feature_name_columns=feature_name_columns, + timestamp_field=timestamp_field, + created_timestamp_column=created_timestamp_column, + start_date=start_date, + end_date=end_date, + ) + ) + + spark_serialized_artifacts = _SparkSerializedArtifacts.serialize( + feature_view=feature_view, repo_config=self.repo_config + ) + + spark_df = offline_job.to_spark_df() + if self.repo_config.batch_engine.partitions != 0: + spark_df = spark_df.repartition( + self.repo_config.batch_engine.partitions + ) + + spark_df.foreachPartition( + lambda x: _process_by_partition(x, spark_serialized_artifacts) + ) + + return SparkMaterializationJob( + job_id=job_id, status=MaterializationJobStatus.SUCCEEDED + ) + except BaseException as e: + return SparkMaterializationJob( + job_id=job_id, status=MaterializationJobStatus.ERROR, error=e + ) + + +@dataclass +class _SparkSerializedArtifacts: + """Class to assist with serializing unpicklable artifacts to the spark workers""" + + feature_view_proto: str + repo_config_file: str + + @classmethod + def serialize(cls, feature_view, repo_config): + + # serialize to proto + feature_view_proto = feature_view.to_proto().SerializeToString() + + # serialize repo_config to disk. Will be used to instantiate the online store + repo_config_file = tempfile.NamedTemporaryFile(delete=False).name + with open(repo_config_file, "wb") as f: + dill.dump(repo_config, f) + + return _SparkSerializedArtifacts( + feature_view_proto=feature_view_proto, repo_config_file=repo_config_file + ) + + def unserialize(self): + # unserialize + proto = FeatureViewProto() + proto.ParseFromString(self.feature_view_proto) + feature_view = FeatureView.from_proto(proto) + + # load + with open(self.repo_config_file, "rb") as f: + repo_config = dill.load(f) + + provider = PassthroughProvider(repo_config) + online_store = provider.online_store + return feature_view, online_store, repo_config + + +def _process_by_partition(rows, spark_serialized_artifacts: _SparkSerializedArtifacts): + """Load pandas df to online store""" + + # convert to pyarrow table + dicts = [] + for row in rows: + dicts.append(row.asDict()) + + df = pd.DataFrame.from_records(dicts) + if df.shape[0] == 0: + print("Skipping") + return + + table = pyarrow.Table.from_pandas(df) + + # unserialize artifacts + feature_view, online_store, repo_config = spark_serialized_artifacts.unserialize() + + if feature_view.batch_source.field_mapping is not None: + table = _run_pyarrow_field_mapping( + table, feature_view.batch_source.field_mapping + ) + + join_key_to_value_type = { + entity.name: entity.dtype.to_value_type() + for entity in feature_view.entity_columns + } + + rows_to_write = _convert_arrow_to_proto(table, feature_view, join_key_to_value_type) + online_store.online_write_batch( + repo_config, + feature_view, + rows_to_write, + lambda x: None, + ) diff --git a/sdk/python/feast/repo_config.py b/sdk/python/feast/repo_config.py index 47a5ae321d9..d5f68a8db61 100644 --- a/sdk/python/feast/repo_config.py +++ b/sdk/python/feast/repo_config.py @@ -39,6 +39,7 @@ "snowflake.engine": "feast.infra.materialization.snowflake_engine.SnowflakeMaterializationEngine", "lambda": "feast.infra.materialization.aws_lambda.lambda_engine.LambdaMaterializationEngine", "bytewax": "feast.infra.materialization.contrib.bytewax.bytewax_materialization_engine.BytewaxMaterializationEngine", + "spark.engine": "feast.infra.materialization.contrib.spark.spark_materialization_engine.SparkMaterializationEngine", } ONLINE_STORE_CLASS_FOR_TYPE = { diff --git a/sdk/python/tests/integration/materialization/contrib/spark/test_spark.py b/sdk/python/tests/integration/materialization/contrib/spark/test_spark.py new file mode 100644 index 00000000000..c7028a09ef4 --- /dev/null +++ b/sdk/python/tests/integration/materialization/contrib/spark/test_spark.py @@ -0,0 +1,77 @@ +from datetime import timedelta + +import pytest + +from feast.entity import Entity +from feast.feature_view import FeatureView +from feast.field import Field +from feast.infra.offline_stores.contrib.spark_offline_store.tests.data_source import ( + SparkDataSourceCreator, +) +from feast.types import Float32 +from tests.data.data_creator import create_basic_driver_dataset +from tests.integration.feature_repos.integration_test_repo_config import ( + IntegrationTestRepoConfig, +) +from tests.integration.feature_repos.repo_configuration import ( + construct_test_environment, +) +from tests.integration.feature_repos.universal.online_store.redis import ( + RedisOnlineStoreCreator, +) +from tests.utils.e2e_test_validation import validate_offline_online_store_consistency + + +@pytest.mark.integration +def test_spark_materialization_consistency(): + spark_config = IntegrationTestRepoConfig( + provider="local", + online_store_creator=RedisOnlineStoreCreator, + offline_store_creator=SparkDataSourceCreator, + batch_engine={"type": "spark.engine", "partitions": 10}, + ) + spark_environment = construct_test_environment( + spark_config, None, entity_key_serialization_version=1 + ) + + df = create_basic_driver_dataset() + + ds = spark_environment.data_source_creator.create_data_source( + df, + spark_environment.feature_store.project, + field_mapping={"ts_1": "ts"}, + ) + + fs = spark_environment.feature_store + driver = Entity( + name="driver_id", + join_keys=["driver_id"], + ) + + driver_stats_fv = FeatureView( + name="driver_hourly_stats", + entities=[driver], + ttl=timedelta(weeks=52), + schema=[Field(name="value", dtype=Float32)], + source=ds, + ) + + try: + + fs.apply([driver, driver_stats_fv]) + + print(df) + + # materialization is run in two steps and + # we use timestamp from generated dataframe as a split point + split_dt = df["ts_1"][4].to_pydatetime() - timedelta(seconds=1) + + print(f"Split datetime: {split_dt}") + + validate_offline_online_store_consistency(fs, driver_stats_fv, split_dt) + finally: + fs.teardown() + + +if __name__ == "__main__": + test_spark_materialization_consistency() From 59b4853593d1a3255c9cd409cbe9d2a198832588 Mon Sep 17 00:00:00 2001 From: Danny Chiao Date: Thu, 15 Sep 2022 15:45:20 -0400 Subject: [PATCH 39/46] chore: Add example of getting historical features with entity SQL (#3224) * chore: Add example of getting historical features with entity SQL Signed-off-by: Danny Chiao * chore: Add example of getting historical features with entity SQL Signed-off-by: Danny Chiao * Call this out in running feast in production guide Signed-off-by: Danny Chiao * Call this out in running feast in production guide Signed-off-by: Danny Chiao * Call this out in running feast in production guide Signed-off-by: Danny Chiao * lint Signed-off-by: Danny Chiao * lint Signed-off-by: Danny Chiao Signed-off-by: Danny Chiao --- .../concepts/feature-retrieval.md | 330 ++++++++++-------- .../running-feast-in-production.md | 2 + sdk/python/feast/errors.py | 14 + .../feast/infra/offline_stores/bigquery.py | 9 + .../feast/infra/offline_stores/snowflake.py | 7 +- .../aws/feature_repo/test_workflow.py | 59 +++- .../gcp/feature_repo/test_workflow.py | 47 +++ .../templates/snowflake/test_workflow.py | 59 +++- 8 files changed, 371 insertions(+), 156 deletions(-) diff --git a/docs/getting-started/concepts/feature-retrieval.md b/docs/getting-started/concepts/feature-retrieval.md index f4462d06900..fd216fc71f5 100644 --- a/docs/getting-started/concepts/feature-retrieval.md +++ b/docs/getting-started/concepts/feature-retrieval.md @@ -17,41 +17,112 @@ Each of these retrieval mechanisms accept: Before beginning, you need to instantiate a local `FeatureStore` object that knows how to parse the registry (see [more details](https://docs.feast.dev/getting-started/concepts/registry)) -