From 70f8a37eb59e358002a0c4054d07462228597a57 Mon Sep 17 00:00:00 2001 From: Felix Wang Date: Thu, 25 Aug 2022 11:52:45 -0700 Subject: [PATCH 01/24] 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 0add6a4f6d078c42b6456f4c13465bee9bdc8a8c Mon Sep 17 00:00:00 2001 From: Felix Wang Date: Thu, 25 Aug 2022 12:22:18 -0700 Subject: [PATCH 02/24] 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 c77a655c03499443e253e4cb9c7d7651685c7de3 Mon Sep 17 00:00:00 2001 From: Kevin Zhang Date: Thu, 25 Aug 2022 13:56:08 -0700 Subject: [PATCH 03/24] 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 62d665a0620bdba53d0a6f98b45fda4678683aa3 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 04/24] 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 9f7a69ef2756e5af9c93b40ffa49fd439f2fefbd Mon Sep 17 00:00:00 2001 From: Achal Shah Date: Mon, 29 Aug 2022 15:17:36 -0700 Subject: [PATCH 05/24] 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 cadc91eb7bc2a276420a05cb93ba8786663ba1c2 Mon Sep 17 00:00:00 2001 From: Achal Shah Date: Tue, 30 Aug 2022 10:38:25 -0700 Subject: [PATCH 06/24] =?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 1ee39fe7ff26ecb0816ef2308f9880d0625c93aa Mon Sep 17 00:00:00 2001 From: Achal Shah Date: Tue, 30 Aug 2022 11:58:25 -0700 Subject: [PATCH 07/24] 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 0e135734e313b0b579d7335d008db67dfa14b551 Mon Sep 17 00:00:00 2001 From: Petra Rebernjak Date: Tue, 30 Aug 2022 23:01:25 +0200 Subject: [PATCH 08/24] 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 a3dd52007e0541169b87174f0fde7ce6b5629bd1 Mon Sep 17 00:00:00 2001 From: Achal Shah Date: Wed, 31 Aug 2022 13:36:33 -0700 Subject: [PATCH 09/24] 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 2e9c1c00dfeca8d0e46437994401e2fc89e5a141 Mon Sep 17 00:00:00 2001 From: Danny Chiao Date: Thu, 1 Sep 2022 11:45:45 -0700 Subject: [PATCH 10/24] 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 cf99f5d6f4e84f21ec3beee4a478a0550cfd882e Mon Sep 17 00:00:00 2001 From: Danny Chiao Date: Fri, 2 Sep 2022 11:55:24 -0700 Subject: [PATCH 11/24] 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 ddb9365fc473f9d6fbc677e35a51e11c4f284cc4 Mon Sep 17 00:00:00 2001 From: Danny Chiao Date: Fri, 2 Sep 2022 15:42:26 -0700 Subject: [PATCH 12/24] 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 a3d0ae5b717538e28e36a7a015712a0b5c70a333 Mon Sep 17 00:00:00 2001 From: Felix Wang Date: Thu, 1 Sep 2022 11:44:48 -0700 Subject: [PATCH 13/24] 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 55de68a4041..57d811d9f0f 100644 --- a/sdk/python/feast/entity.py +++ b/sdk/python/feast/entity.py @@ -62,6 +62,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 = "", @@ -74,6 +75,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. @@ -82,7 +85,7 @@ def __init__( ValueError: Parameters are specified incorrectly. """ self.name = name - self.value_type = ValueType.UNKNOWN + self.value_type = value_type or ValueType.UNKNOWN # For now, both the `join_key` and `join_keys` attributes are set correctly, # so both are usable. diff --git a/sdk/python/feast/feature_view.py b/sdk/python/feast/feature_view.py index 41bad5828a0..1aad9e109e5 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 += entity.join_keys + # 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 84e8321c122..223eacb66af 100644 --- a/sdk/python/feast/inference.py +++ b/sdk/python/feast/inference.py @@ -139,8 +139,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 b93bc1b6e704dc9e179ccbba16addc799f0f0644 Mon Sep 17 00:00:00 2001 From: Felix Wang Date: Fri, 2 Sep 2022 16:00:30 -0700 Subject: [PATCH 14/24] 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 c136873bdd1ead35b24f057ba6ad6f5bddbf104e Mon Sep 17 00:00:00 2001 From: Felix Wang Date: Fri, 2 Sep 2022 16:02:26 -0700 Subject: [PATCH 15/24] 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 c6f2666c1d5ad419774fc6ac3f7f94ed5eb2d989 Mon Sep 17 00:00:00 2001 From: Kevin Zhang Date: Fri, 2 Sep 2022 16:10:24 -0700 Subject: [PATCH 16/24] 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 97518294d42b7774bcf6f97571a52167a319cf83 Mon Sep 17 00:00:00 2001 From: Danny Chiao Date: Fri, 2 Sep 2022 20:15:08 -0700 Subject: [PATCH 17/24] 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 5dba4a31c18d938d9d058bf162635c3bdc5781a0 Mon Sep 17 00:00:00 2001 From: Danny Chiao Date: Wed, 7 Sep 2022 16:57:26 -0400 Subject: [PATCH 18/24] 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 8e03ed5349f..6f9df544959 100644 --- a/Makefile +++ b/Makefile @@ -353,15 +353,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 95066ea1be9831cfac923734f3a04fac88e194eb Mon Sep 17 00:00:00 2001 From: Kevin Zhang Date: Wed, 7 Sep 2022 14:54:25 -0700 Subject: [PATCH 19/24] 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 1aad9e109e5..4c4e6391b35 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 2cb1c4fefb0..cc8d991da82 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 49301b350bb872a3160781948078d355c2197485 Mon Sep 17 00:00:00 2001 From: Danny Chiao Date: Wed, 7 Sep 2022 23:02:48 -0400 Subject: [PATCH 20/24] 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 b0939edd1d5aac5759e0855d9bf164cff66b9ec2 Mon Sep 17 00:00:00 2001 From: Danny Chiao Date: Wed, 7 Sep 2022 23:03:53 -0400 Subject: [PATCH 21/24] ci: Temporarily suppress web UI build for release process Signed-off-by: Danny Chiao --- .github/workflows/release.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index ec56d60e4c5..1fc38ff97a4 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -76,7 +76,6 @@ jobs: release: name: release runs-on: ubuntu-latest - needs: publish-web-ui-npm env: GITHUB_TOKEN: ${{ github.event.inputs.token }} GIT_AUTHOR_NAME: feast-ci-bot From b58a6c52e10bd401eba8a631c948707453fa8aae Mon Sep 17 00:00:00 2001 From: Danny Chiao Date: Wed, 7 Sep 2022 23:18:36 -0400 Subject: [PATCH 22/24] 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 ca00503c9ff496ba0bb11486ad0e7e0a812268bb Mon Sep 17 00:00:00 2001 From: Danny Chiao Date: Wed, 7 Sep 2022 23:48:41 -0400 Subject: [PATCH 23/24] 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 6f9df544959..cb20484a7d9 100644 --- a/Makefile +++ b/Makefile @@ -358,10 +358,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 d71fc974ed2c87fecfa8e938f0c09688a841d8b2 Mon Sep 17 00:00:00 2001 From: feast-ci-bot Date: Thu, 8 Sep 2022 03:53:22 +0000 Subject: [PATCH 24/24] chore(release): release 0.24.1 ## [0.24.1](https://github.com/feast-dev/feast/compare/v0.24.0...v0.24.1) (2022-09-08) ### Bug Fixes * Fix push API to respect feature view's already inferred entity types ([#3172](https://github.com/feast-dev/feast/issues/3172)) ([5dba4a3](https://github.com/feast-dev/feast/commit/5dba4a31c18d938d9d058bf162635c3bdc5781a0)) * Fix release workflow ([#3144](https://github.com/feast-dev/feast/issues/3144)) ([c77a655](https://github.com/feast-dev/feast/commit/c77a655c03499443e253e4cb9c7d7651685c7de3)) * Fix Shopify timestamp bug and add warnings to help with debugging entity registration ([#3191](https://github.com/feast-dev/feast/issues/3191)) ([95066ea](https://github.com/feast-dev/feast/commit/95066ea1be9831cfac923734f3a04fac88e194eb)) * Handle complex Spark data types in SparkSource ([#3154](https://github.com/feast-dev/feast/issues/3154)) ([0e13573](https://github.com/feast-dev/feast/commit/0e135734e313b0b579d7335d008db67dfa14b551)) --- CHANGELOG.md | 10 ++++++++++ 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 +++--- infra/charts/feast/charts/feature-server/Chart.yaml | 4 ++-- infra/charts/feast/charts/feature-server/README.md | 4 ++-- infra/charts/feast/charts/feature-server/values.yaml | 2 +- .../feast/charts/transformation-service/Chart.yaml | 4 ++-- .../feast/charts/transformation-service/README.md | 4 ++-- .../feast/charts/transformation-service/values.yaml | 2 +- infra/charts/feast/requirements.yaml | 4 ++-- java/pom.xml | 2 +- ui/package.json | 2 +- 17 files changed, 34 insertions(+), 24 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1cb54565778..2e2f6549db8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,15 @@ # Changelog +## [0.24.1](https://github.com/feast-dev/feast/compare/v0.24.0...v0.24.1) (2022-09-08) + + +### Bug Fixes + +* Fix push API to respect feature view's already inferred entity types ([#3172](https://github.com/feast-dev/feast/issues/3172)) ([5dba4a3](https://github.com/feast-dev/feast/commit/5dba4a31c18d938d9d058bf162635c3bdc5781a0)) +* Fix release workflow ([#3144](https://github.com/feast-dev/feast/issues/3144)) ([c77a655](https://github.com/feast-dev/feast/commit/c77a655c03499443e253e4cb9c7d7651685c7de3)) +* Fix Shopify timestamp bug and add warnings to help with debugging entity registration ([#3191](https://github.com/feast-dev/feast/issues/3191)) ([95066ea](https://github.com/feast-dev/feast/commit/95066ea1be9831cfac923734f3a04fac88e194eb)) +* Handle complex Spark data types in SparkSource ([#3154](https://github.com/feast-dev/feast/issues/3154)) ([0e13573](https://github.com/feast-dev/feast/commit/0e135734e313b0b579d7335d008db67dfa14b551)) + # [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..fc3264b0563 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.24.1 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..24e45712e02 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.24.1` ## 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.24.1"` | 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..5667315f48d 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.24.1 imagePullSecrets: [] nameOverride: "" diff --git a/infra/charts/feast-python-server/Chart.yaml b/infra/charts/feast-python-server/Chart.yaml index d2b45ee8b60..9a8f2f011a5 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.24.1 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..bd1219527e0 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.24.1` ## 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..da5ec639067 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.24.1 keywords: - machine learning - big data diff --git a/infra/charts/feast/README.md b/infra/charts/feast/README.md index 484c19e6d8d..218f18f197e 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.24.1` ## Installation @@ -58,8 +58,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.24.1 | +| https://feast-helm-charts.storage.googleapis.com | transformation-service(transformation-service) | 0.24.1 | ## Values diff --git a/infra/charts/feast/charts/feature-server/Chart.yaml b/infra/charts/feast/charts/feature-server/Chart.yaml index f238b6aee4b..2f0b385a615 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.24.1 +appVersion: v0.24.1 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..cd0da20427b 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.24.1](https://img.shields.io/badge/Version-0.24.1-informational?style=flat-square) ![AppVersion: v0.24.1](https://img.shields.io/badge/AppVersion-v0.24.1-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.24.1"` | 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..fe9e3bee54c 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.24.1 # 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..02f26f1ed95 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.24.1 +appVersion: v0.24.1 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..39ad9154032 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.24.1](https://img.shields.io/badge/Version-0.24.1-informational?style=flat-square) ![AppVersion: v0.24.1](https://img.shields.io/badge/AppVersion-v0.24.1-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.24.1"` | 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..d0584b113ed 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.24.1 # image.pullPolicy -- Image pull policy pullPolicy: IfNotPresent diff --git a/infra/charts/feast/requirements.yaml b/infra/charts/feast/requirements.yaml index 5dd4a4bce1b..59079114dae 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.24.1 condition: feature-server.enabled repository: https://feast-helm-charts.storage.googleapis.com - name: transformation-service alias: transformation-service - version: 0.24.0 + version: 0.24.1 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..77fd3fa16e8 100644 --- a/java/pom.xml +++ b/java/pom.xml @@ -35,7 +35,7 @@ - 0.24.0 + 0.24.1 https://github.com/feast-dev/feast UTF-8 diff --git a/ui/package.json b/ui/package.json index 7f0e7c3fbe1..6bd58663db1 100644 --- a/ui/package.json +++ b/ui/package.json @@ -1,6 +1,6 @@ { "name": "@feast-dev/feast-ui", - "version": "0.24.0", + "version": "0.24.1", "private": false, "files": [ "dist"