11from pathlib import Path
2- from typing import Optional , Union
32
43import yaml
5- from pydantic import BaseModel , StrictInt , StrictStr , ValidationError
4+ from pydantic import BaseModel , StrictInt , StrictStr , ValidationError , root_validator
5+ from pydantic .error_wrappers import ErrorWrapper
6+ from pydantic .typing import Dict , Literal , Optional , Union
67
78
89class FeastBaseModel (BaseModel ):
@@ -13,26 +14,27 @@ class Config:
1314 extra = "forbid"
1415
1516
16- class LocalOnlineStoreConfig (FeastBaseModel ):
17- """ Online store config for local (SQLite-based) online store """
17+ class SqliteOnlineStoreConfig (FeastBaseModel ):
18+ """ Online store config for local (SQLite-based) store """
1819
19- path : StrictStr
20- """ str: Path to sqlite db """
20+ type : Literal ["sqlite" ] = "sqlite"
21+ """ Online store type selector"""
22+
23+ path : StrictStr = "data/online.db"
24+ """ (optional) Path to sqlite db """
2125
2226
2327class DatastoreOnlineStoreConfig (FeastBaseModel ):
2428 """ Online store config for GCP Datastore """
2529
26- project_id : StrictStr
27- """ str: GCP Project Id """
30+ type : Literal [ "datastore" ] = "datastore"
31+ """ Online store type selector """
2832
33+ project_id : Optional [StrictStr ] = None
34+ """ (optional) GCP Project Id """
2935
30- class OnlineStoreConfig (FeastBaseModel ):
31- datastore : Optional [DatastoreOnlineStoreConfig ] = None
32- """ DatastoreOnlineStoreConfig: Optional Google Cloud Datastore config """
3336
34- local : Optional [LocalOnlineStoreConfig ] = None
35- """ LocalOnlineStoreConfig: Optional local online store config """
37+ OnlineStoreConfig = Union [DatastoreOnlineStoreConfig , SqliteOnlineStoreConfig ]
3638
3739
3840class RegistryConfig (FeastBaseModel ):
@@ -51,7 +53,7 @@ class RegistryConfig(FeastBaseModel):
5153class RepoConfig (FeastBaseModel ):
5254 """ Repo config. Typically loaded from `feature_store.yaml` """
5355
54- registry : Union [StrictStr , RegistryConfig ]
56+ registry : Union [StrictStr , RegistryConfig ] = "data/registry.db"
5557 """ str: Path to metadata store. Can be a local path, or remote object storage path, e.g. gcs://foo/bar """
5658
5759 project : StrictStr
@@ -63,7 +65,7 @@ class RepoConfig(FeastBaseModel):
6365 provider : StrictStr
6466 """ str: local or gcp """
6567
66- online_store : Optional [ OnlineStoreConfig ] = None
68+ online_store : OnlineStoreConfig = SqliteOnlineStoreConfig ()
6769 """ OnlineStoreConfig: Online store configuration (optional depending on provider) """
6870
6971 def get_registry_config (self ):
@@ -72,40 +74,54 @@ def get_registry_config(self):
7274 else :
7375 return self .registry
7476
75-
76- # This is the JSON Schema for config validation. We use this to have nice detailed error messages
77- # for config validation, something that bindr unfortunately doesn't provide out of the box.
78- #
79- # The schema should match the namedtuple structure above. It could technically even be inferred from
80- # the types above automatically; but for now we choose a more tedious but less magic path of
81- # providing the schema manually.
82-
83- config_schema = {
84- "type" : "object" ,
85- "properties" : {
86- "project" : {"type" : "string" },
87- "registry" : {"type" : "string" },
88- "provider" : {"type" : "string" },
89- "online_store" : {
90- "type" : "object" ,
91- "properties" : {
92- "local" : {
93- "type" : "object" ,
94- "properties" : {"path" : {"type" : "string" }},
95- "additionalProperties" : False ,
96- },
97- "datastore" : {
98- "type" : "object" ,
99- "properties" : {"project_id" : {"type" : "string" }},
100- "additionalProperties" : False ,
101- },
102- },
103- "additionalProperties" : False ,
104- },
105- },
106- "required" : ["project" ],
107- "additionalProperties" : False ,
108- }
77+ @root_validator (pre = True )
78+ def _validate_online_store_config (cls , values ):
79+ # This method will validate whether the online store configurations are set correctly. This explicit validation
80+ # is necessary because Pydantic Unions throw very verbose and cryptic exceptions. We also use this method to
81+ # impute the default online store type based on the selected provider. For the time being this method should be
82+ # considered tech debt until we can implement https://github.com/samuelcolvin/pydantic/issues/619 or a more
83+ # granular configuration system
84+
85+ # Skip if online store isn't set explicitly
86+ if "online_store" not in values :
87+ values ["online_store" ] = dict ()
88+
89+ # Skip if we arent creating the configuration from a dict
90+ if not isinstance (values ["online_store" ], Dict ):
91+ return values
92+
93+ # Make sure that the provider configuration is set. We need it to set the defaults
94+ assert "provider" in values
95+
96+ if "online_store" in values :
97+ # Set the default type
98+ if "type" not in values ["online_store" ]:
99+ if values ["provider" ] == "local" :
100+ values ["online_store" ]["type" ] = "sqlite"
101+ elif values ["provider" ] == "gcp" :
102+ values ["online_store" ]["type" ] = "datastore"
103+
104+ online_store_type = values ["online_store" ]["type" ]
105+
106+ # Make sure the user hasn't provided the wrong type
107+ assert online_store_type in ["datastore" , "sqlite" ]
108+
109+ # Validate the dict to ensure one of the union types match
110+ try :
111+ if online_store_type == "sqlite" :
112+ SqliteOnlineStoreConfig (** values ["online_store" ])
113+ elif values ["online_store" ]["type" ] == "datastore" :
114+ DatastoreOnlineStoreConfig (** values ["online_store" ])
115+ else :
116+ raise ValidationError (
117+ f"Invalid online store type { online_store_type } "
118+ )
119+ except ValidationError as e :
120+ raise ValidationError (
121+ [ErrorWrapper (e , loc = "online_store" )],
122+ model = SqliteOnlineStoreConfig ,
123+ )
124+ return values
109125
110126
111127class FeastConfigError (Exception ):
0 commit comments