-
Notifications
You must be signed in to change notification settings - Fork 1.3k
Refactor OnlineStoreConfig classes into owning modules #1649
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 1 commit
fe441c1
f79a3b9
587b981
b9bc19b
37b5dd8
fa5fd81
32dce48
1060170
39be498
25c9a45
3ee9185
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
Signed-off-by: Achal Shah <achals@gmail.com>
- Loading branch information
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -42,15 +42,15 @@ def __init__(self, provider_name): | |
| super().__init__(f"Provider '{provider_name}' is not implemented") | ||
|
|
||
|
|
||
| class FeastProviderModuleImportError(Exception): | ||
| def __init__(self, module_name): | ||
| super().__init__(f"Could not import provider module '{module_name}'") | ||
| class FeastModuleImportError(Exception): | ||
| def __init__(self, module_name, module_type="provider"): | ||
| super().__init__(f"Could not import {module_type} module '{module_name}'") | ||
|
|
||
|
|
||
| class FeastProviderClassImportError(Exception): | ||
| def __init__(self, module_name, class_name): | ||
| class FeastClassImportError(Exception): | ||
| def __init__(self, module_name, class_name, class_type="provider"): | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. same here
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. done in #1657 |
||
| super().__init__( | ||
| f"Could not import provider '{class_name}' from module '{module_name}'" | ||
| f"Could not import {class_type} '{class_name}' from module '{module_name}'" | ||
| ) | ||
|
|
||
|
|
||
|
|
@@ -71,6 +71,20 @@ def __init__(self, offline_store_name: str, data_source_name: str): | |
| ) | ||
|
|
||
|
|
||
| class FeastOnlineStoreInvalidName(Exception): | ||
| def __init__(self, online_store_class_name: str): | ||
| super().__init__( | ||
| f"Online Store Class '{online_store_class_name}' should end with the string `OnlineStore`.'" | ||
| ) | ||
|
|
||
|
|
||
| class FeastOnlineStoreConfigInvalidName(Exception): | ||
| def __init__(self, online_store_config_class_name: str): | ||
| super().__init__( | ||
| f"Online Store Config Class '{online_store_config_class_name}' should end with the string `OnlineStoreConfig`.'" | ||
| ) | ||
|
|
||
|
|
||
| class FeastOnlineStoreUnsupportedDataSource(Exception): | ||
| def __init__(self, online_store_name: str, data_source_name: str): | ||
| super().__init__( | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,10 +1,11 @@ | ||
| import importlib | ||
| from enum import Enum | ||
| from pathlib import Path | ||
| from typing import TypeVar, Generic, Any | ||
|
|
||
| import yaml | ||
| from pydantic import ( | ||
| BaseModel, | ||
| PositiveInt, | ||
| StrictInt, | ||
| StrictStr, | ||
| ValidationError, | ||
|
|
@@ -14,43 +15,35 @@ | |
| from pydantic.typing import Dict, Literal, Optional, Union | ||
|
|
||
| from feast.telemetry import log_exceptions | ||
| from feast import errors | ||
|
|
||
|
|
||
| # This dict exists so that: | ||
| # - existing values for the online store type in featurestore.yaml files continue to work in a backwards compatible way | ||
| # - first party and third party implementations can use the same class loading code path. | ||
| ONLINE_CONFIG_CLASS_FOR_TYPE = { | ||
| 'sqlite': 'feast.infra.online_stores.sqlite.SqliteOnlineStore', | ||
| 'datastore': 'feast.infra.online_stores.datastore.DatastoreOnlineStore' | ||
| } | ||
|
|
||
|
|
||
| class FeastBaseModel(BaseModel): | ||
| """ Feast Pydantic Configuration Class """ | ||
|
|
||
| class Config: | ||
| arbitrary_types_allowed = True | ||
| extra = "forbid" | ||
|
|
||
|
|
||
| class SqliteOnlineStoreConfig(FeastBaseModel): | ||
| """ Online store config for local (SQLite-based) store """ | ||
|
|
||
| type: Literal["sqlite"] = "sqlite" | ||
| """ Online store type selector""" | ||
| extra = "allow" | ||
|
achals marked this conversation as resolved.
Outdated
|
||
|
|
||
| path: StrictStr = "data/online.db" | ||
| """ (optional) Path to sqlite db """ | ||
|
|
||
| class FeastConfigBaseModel(BaseModel): | ||
| """ Feast Pydantic Configuration Class """ | ||
|
|
||
| class DatastoreOnlineStoreConfig(FeastBaseModel): | ||
| """ Online store config for GCP Datastore """ | ||
|
|
||
| type: Literal["datastore"] = "datastore" | ||
| """ Online store type selector""" | ||
|
|
||
| project_id: Optional[StrictStr] = None | ||
| """ (optional) GCP Project Id """ | ||
|
|
||
| namespace: Optional[StrictStr] = None | ||
| """ (optional) Datastore namespace """ | ||
| class Config: | ||
| arbitrary_types_allowed = True | ||
| extra = "forbid" | ||
|
|
||
| write_concurrency: Optional[PositiveInt] = 40 | ||
| """ (optional) Amount of threads to use when writing batches of feature rows into Datastore""" | ||
|
|
||
| write_batch_size: Optional[PositiveInt] = 50 | ||
| """ (optional) Amount of feature rows per batch being written into Datastore""" | ||
| OnlineT = TypeVar('OnlineT', bound=FeastConfigBaseModel) | ||
|
|
||
|
|
||
| class RedisType(str, Enum): | ||
|
|
@@ -72,9 +65,7 @@ class RedisOnlineStoreConfig(FeastBaseModel): | |
| format: host:port,parameter1,parameter2 eg. redis:6379,db=0 """ | ||
|
|
||
|
|
||
| OnlineStoreConfig = Union[ | ||
| DatastoreOnlineStoreConfig, SqliteOnlineStoreConfig, RedisOnlineStoreConfig | ||
| ] | ||
| OnlineStoreConfig = Union[RedisOnlineStoreConfig] | ||
|
|
||
|
|
||
| class FileOfflineStoreConfig(FeastBaseModel): | ||
|
|
@@ -125,7 +116,7 @@ class RepoConfig(FeastBaseModel): | |
| provider: StrictStr | ||
| """ str: local or gcp or redis """ | ||
|
|
||
| online_store: OnlineStoreConfig = SqliteOnlineStoreConfig() | ||
| online_store: Any | ||
|
woop marked this conversation as resolved.
|
||
| """ OnlineStoreConfig: Online store configuration (optional depending on provider) """ | ||
|
|
||
| offline_store: OfflineStoreConfig = FileOfflineStoreConfig() | ||
|
|
@@ -168,22 +159,17 @@ def _validate_online_store_config(cls, values): | |
|
|
||
| online_store_type = values["online_store"]["type"] | ||
|
|
||
| # Make sure the user hasn't provided the wrong type | ||
| assert online_store_type in ["datastore", "sqlite", "redis"] | ||
|
|
||
| # Validate the dict to ensure one of the union types match | ||
| try: | ||
| if online_store_type == "sqlite": | ||
| SqliteOnlineStoreConfig(**values["online_store"]) | ||
| elif online_store_type == "datastore": | ||
| DatastoreOnlineStoreConfig(**values["online_store"]) | ||
| elif online_store_type == "redis": | ||
| if online_store_type == "redis": | ||
| RedisOnlineStoreConfig(**values["online_store"]) | ||
| else: | ||
| raise ValueError(f"Invalid online store type {online_store_type}") | ||
| online_config_class = get_online_config_from_type(online_store_type) | ||
| online_config_class(**values["online_store"]) | ||
| except ValidationError as e: | ||
|
|
||
| raise ValidationError( | ||
| [ErrorWrapper(e, loc="online_store")], model=SqliteOnlineStoreConfig, | ||
| [ErrorWrapper(e, loc="online_store")], model=RepoConfig, | ||
| ) | ||
|
|
||
| return values | ||
|
|
@@ -246,6 +232,36 @@ def __repr__(self) -> str: | |
| ) | ||
|
|
||
|
|
||
| def get_online_config_from_type(online_store_type: str): | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think this should also be in
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I like leaving it here since it's more config-related. But happy to change if you feel strongly. |
||
| if online_store_type in ONLINE_CONFIG_CLASS_FOR_TYPE: | ||
| online_store_type = ONLINE_CONFIG_CLASS_FOR_TYPE[online_store_type] | ||
| module_name, class_name = online_store_type.rsplit(".", 1) | ||
|
|
||
| if not class_name.endswith('OnlineStore'): | ||
| raise errors.FeastOnlineStoreConfigInvalidName(class_name) | ||
| config_class_name = f"{class_name}Config" | ||
|
|
||
| # Try importing the module that contains the custom provider | ||
| try: | ||
| module = importlib.import_module(module_name) | ||
| except Exception as e: | ||
| # The original exception can be anything - either module not found, | ||
| # or any other kind of error happening during the module import time. | ||
| # So we should include the original error as well in the stack trace. | ||
| raise errors.FeastModuleImportError(module_name, module_type="OnlineStore") from e | ||
|
|
||
| # Try getting the provider class definition | ||
| try: | ||
| online_store_config_class = getattr(module, config_class_name) | ||
| except AttributeError: | ||
| # This can only be one type of error, when class_name attribute does not exist in the module | ||
| # So we don't have to include the original exception here | ||
| raise errors.FeastClassImportError( | ||
| module_name, config_class_name, class_type="OnlineStoreConfig" | ||
| ) from None | ||
| return online_store_config_class | ||
|
|
||
|
|
||
| def load_repo_config(repo_path: Path) -> RepoConfig: | ||
| config_path = repo_path / "feature_store.yaml" | ||
|
|
||
|
|
@@ -254,6 +270,11 @@ def load_repo_config(repo_path: Path) -> RepoConfig: | |
| try: | ||
| c = RepoConfig(**raw_config) | ||
| c.repo_path = repo_path | ||
| online_config_class = get_online_config_from_type(c.dict()['online_store']['type']) | ||
| c.online_store = online_config_class(**c.dict()['online_store']) | ||
| return c | ||
| except ValidationError as e: | ||
| raise FeastConfigError(e, config_path) | ||
|
|
||
|
|
||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
let's remove the default module_type here, instead explicitly pass the module.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
done in #1657