Skip to content

Commit

Permalink
feat: Added pydantic model support for Field, FVP, ODFV, Project Meta…
Browse files Browse the repository at this point in the history
…data and Feature Services (#6)

* feat: Added pydantic model support for Field, FeatureViewProjections, OnDemandFeatureViews, Feature Services and Project Metadata
---------

Co-authored-by: Bhargav Dodla <[email protected]>
  • Loading branch information
EXPEbdodla and Bhargav Dodla authored Jul 21, 2023
1 parent d99c7a2 commit 1273a12
Show file tree
Hide file tree
Showing 16 changed files with 912 additions and 158 deletions.
6 changes: 5 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -223,4 +223,8 @@ ui/.vercel
**/yarn-error.log*

# Go subprocess binaries (built during feast pip package building)
sdk/python/feast/binaries/
sdk/python/feast/binaries/
/sdk/python/feast/open_api/
/sdk/python/feast/test_open_api/

venv39/*
8 changes: 7 additions & 1 deletion protos/feast/core/FeatureView.proto
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ import "google/protobuf/duration.proto";
import "google/protobuf/timestamp.proto";
import "feast/core/DataSource.proto";
import "feast/core/Feature.proto";
import "feast/core/Entity.proto";

message FeatureView {
// User-specified specifications of this feature view.
Expand Down Expand Up @@ -75,6 +76,11 @@ message FeatureViewSpec {

// Whether these features should be served online or not
bool online = 8;

// User-specified specifications of this entity.
// Adding higher index to avoid conflicts in future
// if Feast adds more fields
repeated Entity original_entities = 30;
}

message FeatureViewMeta {
Expand All @@ -91,4 +97,4 @@ message FeatureViewMeta {
message MaterializationInterval {
google.protobuf.Timestamp start_time = 1;
google.protobuf.Timestamp end_time = 2;
}
}
5 changes: 5 additions & 0 deletions sdk/python/feast/errors.py
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,11 @@ def __init__(self, name, project=None):
super().__init__(f"On demand feature view {name} does not exist")


class ProjectMetadataNotFoundException(FeastObjectNotFoundException):
def __init__(self, project: str = None):
super().__init__(f"Project Metadata does not exist in project {project}")


class RequestDataNotFoundInEntityDfException(FeastObjectNotFoundException):
def __init__(self, feature_name, feature_view_name):
super().__init__(
Expand Down
97 changes: 39 additions & 58 deletions sdk/python/feast/expediagroup/pydantic_models/data_source_model.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,10 @@

from pydantic import BaseModel
from pydantic import Field as PydanticField
from typing_extensions import Annotated
from typing_extensions import Annotated, Self

from feast.data_source import RequestSource
from feast.field import Field
from feast.expediagroup.pydantic_models.field_model import FieldModel
from feast.infra.offline_stores.contrib.spark_offline_store.spark_source import (
SparkSource,
)
Expand Down Expand Up @@ -49,41 +49,31 @@ class RequestSourceModel(DataSourceModel):

name: str
model_type: Literal["RequestSourceModel"] = "RequestSourceModel"
schema_: List[Field] = PydanticField(None, alias="schema")
schema_: List[FieldModel]
description: Optional[str] = ""
tags: Optional[Dict[str, str]] = None
owner: Optional[str] = ""

class Config:
arbitrary_types_allowed = True
extra = "allow"

def to_data_source(self):
def to_data_source(self) -> RequestSource:
"""
Given a Pydantic RequestSourceModel, create and return a RequestSource.
Returns:
A RequestSource.
"""
params = {
"name": self.name,
"description": self.description,
"tags": self.tags if self.tags else None,
"owner": self.owner,
}
params["schema"] = [
Field(
name=sch.name,
dtype=sch.dtype,
description=sch.description,
tags=sch.tags,
)
for sch in self.schema_
]
return RequestSource(**params)
return RequestSource(
name=self.name,
schema=[sch.to_field() for sch in self.schema_],
description=self.description,
tags=self.tags,
owner=self.owner,
)

@classmethod
def from_data_source(cls, data_source):
def from_data_source(
cls,
data_source,
) -> Self: # type: ignore
"""
Converts a RequestSource object to its pydantic model representation.
Expand All @@ -92,7 +82,9 @@ def from_data_source(cls, data_source):
"""
return cls(
name=data_source.name,
schema=data_source.schema,
schema_=[
FieldModel.from_field(ds_schema) for ds_schema in data_source.schema
],
description=data_source.description,
tags=data_source.tags if data_source.tags else None,
owner=data_source.owner,
Expand All @@ -117,11 +109,7 @@ class SparkSourceModel(DataSourceModel):
owner: Optional[str] = ""
timestamp_field: Optional[str] = None

class Config:
arbitrary_types_allowed = True
extra = "allow"

def to_data_source(self):
def to_data_source(self) -> SparkSource:
"""
Given a Pydantic SparkSourceModel, create and return a SparkSource.
Expand All @@ -130,24 +118,23 @@ def to_data_source(self):
"""
return SparkSource(
name=self.name,
table=self.table if hasattr(self, "table") else "",
query=self.query if hasattr(self, "query") else "",
path=self.path if hasattr(self, "path") else "",
file_format=self.file_format if hasattr(self, "file_format") else "",
created_timestamp_column=self.created_timestamp_column
if hasattr(self, "created_timestamp_column")
else "",
field_mapping=self.field_mapping if self.field_mapping else None,
description=self.description or "",
tags=self.tags if self.tags else None,
owner=self.owner or "",
timestamp_field=self.timestamp_field
if hasattr(self, "timestamp_field")
else "",
table=self.table,
query=self.query,
path=self.path,
file_format=self.file_format,
created_timestamp_column=self.created_timestamp_column,
field_mapping=self.field_mapping,
description=self.description,
tags=self.tags,
owner=self.owner,
timestamp_field=self.timestamp_field,
)

@classmethod
def from_data_source(cls, data_source):
def from_data_source(
cls,
data_source,
) -> Self: # type: ignore
"""
Converts a SparkSource object to its pydantic model representation.
Expand All @@ -160,18 +147,12 @@ def from_data_source(cls, data_source):
query=data_source.query,
path=data_source.path,
file_format=data_source.file_format,
created_timestamp_column=data_source.created_timestamp_column
if data_source.created_timestamp_column
else "",
field_mapping=data_source.field_mapping
if data_source.field_mapping
else None,
description=data_source.description if data_source.description else "",
tags=data_source.tags if data_source.tags else None,
owner=data_source.owner if data_source.owner else "",
timestamp_field=data_source.timestamp_field
if data_source.timestamp_field
else "",
created_timestamp_column=data_source.created_timestamp_column,
field_mapping=data_source.field_mapping,
description=data_source.description,
tags=data_source.tags,
owner=data_source.owner,
timestamp_field=data_source.timestamp_field,
)


Expand Down
10 changes: 7 additions & 3 deletions sdk/python/feast/expediagroup/pydantic_models/entity_model.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
from typing import Callable, Dict, Optional

from pydantic import BaseModel
from typing_extensions import Self

from feast.entity import Entity
from feast.value_type import ValueType
Expand All @@ -36,7 +37,7 @@ class Config:
ValueType: lambda v: int(dumps(v.value, default=str))
}

def to_entity(self):
def to_entity(self) -> Entity:
"""
Given a Pydantic EntityModel, create and return an Entity.
Expand All @@ -51,12 +52,15 @@ def to_entity(self):
tags=self.tags if self.tags else None,
owner=self.owner,
)
entity.created_timestamp = (self.created_timestamp,)
entity.created_timestamp = self.created_timestamp
entity.last_updated_timestamp = self.last_updated_timestamp
return entity

@classmethod
def from_entity(cls, entity):
def from_entity(
cls,
entity,
) -> Self: # type: ignore
"""
Converts an entity object to its pydantic model representation.
Expand Down
81 changes: 81 additions & 0 deletions sdk/python/feast/expediagroup/pydantic_models/feature_service.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
import sys
from datetime import datetime
from typing import Dict, List, Optional, Union

from pydantic import BaseModel
from typing_extensions import Self

from feast.expediagroup.pydantic_models.feature_view_model import (
FeatureViewModel,
FeatureViewProjectionModel,
OnDemandFeatureViewModel,
)
from feast.feature_service import FeatureService


class FeatureServiceModel(BaseModel):
"""
Pydantic model for Feast FeatureService
"""

name: str
features: List[Union[FeatureViewModel, OnDemandFeatureViewModel]]
feature_view_projections: List[FeatureViewProjectionModel]
description: str
tags: Dict[str, str]
owner: str
created_timestamp: Optional[datetime]
last_updated_timestamp: Optional[datetime]
# TODO: logging_config option is not supported temporarily.
# we will add this fucntionality to FeatureServiceModel in future.
# logging_config: Optional[LoggingConfig] = None

def to_feature_service(self) -> FeatureService:
fs = FeatureService(
name=self.name,
features=[feature.to_feature_view() for feature in self.features],
description=self.description,
tags=self.tags,
owner=self.owner,
)

fs.feature_view_projections = [
feature_view_projection.to_feature_view_projection()
for feature_view_projection in self.feature_view_projections
]
fs.created_timestamp = self.created_timestamp
fs.last_updated_timestamp = self.last_updated_timestamp

return fs

@classmethod
def from_feature_service(
cls,
feature_service: FeatureService,
) -> Self: # type: ignore

features = []
for feature in feature_service._features:
class_ = getattr(
sys.modules[__name__],
type(feature).__name__ + "Model",
)
features.append(class_.from_feature_view(feature))

feature_view_projections = [
FeatureViewProjectionModel.from_feature_view_projection(
feature_view_projection
)
for feature_view_projection in feature_service.feature_view_projections
]

return cls(
name=feature_service.name,
features=features,
feature_view_projections=feature_view_projections,
description=feature_service.description,
tags=feature_service.tags,
owner=feature_service.owner,
created_timestamp=feature_service.created_timestamp,
last_updated_timestamp=feature_service.last_updated_timestamp,
)
Loading

0 comments on commit 1273a12

Please sign in to comment.