Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions protos/feast/core/PrecomputedFeatureVector.proto
Original file line number Diff line number Diff line change
Expand Up @@ -28,4 +28,5 @@ message PrecomputedFeatureVector {
message FeatureViewTimestamp {
string feature_view_name = 1;
google.protobuf.Timestamp event_timestamp = 2;
google.protobuf.Timestamp created_timestamp = 3;
}
1 change: 1 addition & 0 deletions protos/feast/serving/ServingService.proto
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,7 @@ message GetOnlineFeaturesResponse {
repeated feast.types.Value values = 1;
repeated FieldStatus statuses = 2;
repeated google.protobuf.Timestamp event_timestamps = 3;
repeated google.protobuf.Timestamp created_timestamps = 4;
}

bool status = 3;
Expand Down
17 changes: 15 additions & 2 deletions sdk/python/feast/feature_store.py
Original file line number Diff line number Diff line change
Expand Up @@ -3223,7 +3223,13 @@ def precompute_feature_service(
# Read features for each FV via base class online_read.
fv_data: Dict[
str,
List[Tuple[Optional[datetime], Optional[Dict[str, ValueProto]]]],
List[
Tuple[
Optional[datetime],
Optional[Dict[str, ValueProto]],
Optional[datetime],
]
],
] = {}
for fv_obj, proj in feature_views:
req_features = [f.name for f in proj.features]
Expand All @@ -3242,11 +3248,18 @@ def precompute_feature_service(

for _fv, proj in feature_views:
fv_name = proj.name_to_use()
fv_row_ts, feat_dict = fv_data[fv_name][entity_idx]
fv_row_ts, feat_dict, fv_created_ts = fv_data[fv_name][
entity_idx
]

if fv_row_ts:
fv_ts = Timestamp()
fv_ts.FromDatetime(utils.make_tzaware(fv_row_ts))
created_ts_proto = Timestamp()
if fv_created_ts:
created_ts_proto.FromDatetime(
utils.make_tzaware(fv_created_ts)
)
fv_timestamps.append(
FeatureViewTimestamp(
feature_view_name=fv_name,
Expand Down
2 changes: 1 addition & 1 deletion sdk/python/feast/infra/online_stores/online_store.py
Original file line number Diff line number Diff line change
Expand Up @@ -812,7 +812,7 @@ def read_precomputed_vectors(
return [None] * len(entity_keys)

result: List[Optional[bytes]] = []
for _ts, feature_dict in rows:
for _ts, feature_dict, _created_ts in rows:
if feature_dict and "vector" in feature_dict:
val = feature_dict["vector"]
if val.HasField("bytes_val"):
Expand Down
4 changes: 3 additions & 1 deletion sdk/python/feast/infra/online_stores/redis.py
Original file line number Diff line number Diff line change
Expand Up @@ -568,7 +568,9 @@ def online_read(
table: FeatureView,
entity_keys: List[EntityKeyProto],
requested_features: Optional[List[str]] = None,
) -> List[Tuple[Optional[datetime], Optional[Dict[str, ValueProto]]]]:
) -> List[
Tuple[Optional[datetime], Optional[Dict[str, ValueProto]], Optional[datetime]]
]:
online_store_config = config.online_store
assert isinstance(online_store_config, RedisOnlineStoreConfig)

Expand Down
15 changes: 11 additions & 4 deletions sdk/python/feast/infra/online_stores/snowflake.py
Original file line number Diff line number Diff line change
Expand Up @@ -160,7 +160,9 @@ def online_read(
table: FeatureView,
entity_keys: List[EntityKeyProto],
requested_features: Optional[List[str]] = None,
) -> List[Tuple[Optional[datetime], Optional[Dict[str, ValueProto]]]]:
) -> List[
Tuple[Optional[datetime], Optional[Dict[str, ValueProto]], Optional[datetime]]
]:
assert isinstance(config.online_store, SnowflakeOnlineStoreConfig)

result: List[Tuple[Optional[datetime], Optional[Dict[str, ValueProto]]]] = []
Expand Down Expand Up @@ -194,7 +196,7 @@ def online_read(
with GetSnowflakeConnection(config.online_store) as conn:
query = f"""
SELECT
"entity_key", "feature_name", "value", "event_ts"
"entity_key", "feature_name", "value", "event_ts", "created_ts"
FROM
{online_path}."[online-transient] {config.project}_{table.name}"
WHERE
Expand All @@ -210,11 +212,16 @@ def online_read(
val.ParseFromString(row["value"])
res[row["feature_name"]] = val
res_ts = row["event_ts"].to_pydatetime()
res_created_ts = (
row["created_ts"].to_pydatetime()
if "created_ts" in row and pd.notnull(row["created_ts"])
else None
)

if not res:
result.append((None, None))
result.append((None, None, None))
else:
result.append((res_ts, res))
result.append((res_ts, res, res_created_ts))
return result

def update(
Expand Down
39 changes: 27 additions & 12 deletions sdk/python/feast/infra/online_stores/sqlite.py
Original file line number Diff line number Diff line change
Expand Up @@ -376,11 +376,17 @@ def online_read(
table: FeatureView,
entity_keys: List[EntityKeyProto],
requested_features: Optional[List[str]] = None,
) -> List[Tuple[Optional[datetime], Optional[Dict[str, ValueProto]]]]:
) -> List[
Tuple[Optional[datetime], Optional[Dict[str, ValueProto]], Optional[datetime]]
]:
conn = self._get_conn(config)
cur = conn.cursor()

result: List[Tuple[Optional[datetime], Optional[Dict[str, ValueProto]]]] = []
result: List[
Tuple[
Optional[datetime], Optional[Dict[str, ValueProto]], Optional[datetime]
]
] = []

serialized_entity_keys = [
serialize_entity_key(
Expand All @@ -391,7 +397,7 @@ def online_read(
]
# Fetch all entities in one go
cur.execute(
f"SELECT entity_key, feature_name, value, event_ts "
f"SELECT entity_key, feature_name, value, event_ts, created_ts "
f"FROM {_quote_id(_table_id(config.project, table, config.registry.enable_online_feature_view_versioning))} "
f"WHERE entity_key IN ({','.join('?' * len(entity_keys))}) "
f"ORDER BY entity_key",
Expand All @@ -404,20 +410,29 @@ def online_read(
for entity_key_bin in serialized_entity_keys:
res = {}
res_ts = None
for _, feature_name, val_bin, ts in rows.get(entity_key_bin, []):
res_created_ts = None
for _, feature_name, val_bin, ts, created_ts in rows.get(
entity_key_bin, []
):
val = ValueProto()
val.ParseFromString(val_bin)
res[feature_name] = val
ts = cast(datetime, ts)
if ts.tzinfo is not None:
res_ts = ts.astimezone(timezone.utc)
else:
res_ts = ts.replace(tzinfo=timezone.utc)

if ts is not None:
ts = cast(datetime, ts)
if ts.tzinfo is not None:
res_ts = ts.astimezone(timezone.utc)
else:
res_ts = ts.replace(tzinfo=timezone.utc)
if created_ts is not None:
created_ts = cast(datetime, created_ts)
if created_ts.tzinfo is not None:
res_created_ts = created_ts.astimezone(timezone.utc)
else:
res_created_ts = created_ts.replace(tzinfo=timezone.utc)
if not res:
result.append((None, None))
result.append((None, None, None))
else:
result.append((res_ts, res))
result.append((res_ts, res, res_created_ts))
return result

def update(
Expand Down
25 changes: 21 additions & 4 deletions sdk/python/feast/online_response.py
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,11 @@ def __init__(

break

def to_dict(self, include_event_timestamps: bool = False) -> Dict[str, Any]:
def to_dict(
self,
include_event_timestamps: bool = False,
include_created_timestamps: bool = False,
) -> Dict[str, Any]:
"""
Converts GetOnlineFeaturesResponse features into a dictionary form.

Expand All @@ -84,18 +88,31 @@ def to_dict(self, include_event_timestamps: bool = False) -> Dict[str, Any]:
response[timestamp_ref] = [
ts.seconds for ts in feature_vector.event_timestamps
]

if include_created_timestamps:
created_timestamp_ref = feature_ref + "__created_timestamp"
response[created_timestamp_ref] = [
ts.seconds for ts in feature_vector.created_timestamps
]
return response

def to_df(self, include_event_timestamps: bool = False) -> pd.DataFrame:
def to_df(
self,
include_event_timestamps: bool = False,
include_created_timestamps: bool = False,
) -> pd.DataFrame:
"""
Converts GetOnlineFeaturesResponse features into Panda dataframe form.

Args:
include_event_timestamps: bool Optionally include feature timestamps in the dataframe
"""

return pd.DataFrame(self.to_dict(include_event_timestamps))
return pd.DataFrame(
self.to_dict(
include_event_timestamps,
include_created_timestamps=include_created_timestamps,
)
)

def to_arrow(self, include_event_timestamps: bool = False) -> pa.Table:
"""
Expand Down
16 changes: 13 additions & 3 deletions sdk/python/feast/protos/feast/core/Aggregation_pb2.py

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

20 changes: 20 additions & 0 deletions sdk/python/feast/protos/feast/core/Aggregation_pb2_grpc.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,24 @@
# Generated by the gRPC Python protocol compiler plugin. DO NOT EDIT!
"""Client and server classes corresponding to protobuf-defined services."""
import grpc
import warnings


GRPC_GENERATED_VERSION = '1.81.1'
GRPC_VERSION = grpc.__version__
_version_not_supported = False

try:
from grpc._utilities import first_version_is_lower
_version_not_supported = first_version_is_lower(GRPC_VERSION, GRPC_GENERATED_VERSION)
except ImportError:
_version_not_supported = True

if _version_not_supported:
raise RuntimeError(
f'The grpc package installed is at version {GRPC_VERSION},'
+ ' but the generated code in feast/core/Aggregation_pb2_grpc.py depends on'
+ f' grpcio>={GRPC_GENERATED_VERSION}.'
+ f' Please upgrade your grpc module to grpcio>={GRPC_GENERATED_VERSION}'
+ f' or downgrade your generated code using grpcio-tools<={GRPC_VERSION}.'
)
24 changes: 17 additions & 7 deletions sdk/python/feast/protos/feast/core/DataFormat_pb2.py

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

20 changes: 20 additions & 0 deletions sdk/python/feast/protos/feast/core/DataFormat_pb2_grpc.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,24 @@
# Generated by the gRPC Python protocol compiler plugin. DO NOT EDIT!
"""Client and server classes corresponding to protobuf-defined services."""
import grpc
import warnings


GRPC_GENERATED_VERSION = '1.81.1'
GRPC_VERSION = grpc.__version__
_version_not_supported = False

try:
from grpc._utilities import first_version_is_lower
_version_not_supported = first_version_is_lower(GRPC_VERSION, GRPC_GENERATED_VERSION)
except ImportError:
_version_not_supported = True

if _version_not_supported:
raise RuntimeError(
f'The grpc package installed is at version {GRPC_VERSION},'
+ ' but the generated code in feast/core/DataFormat_pb2_grpc.py depends on'
+ f' grpcio>={GRPC_GENERATED_VERSION}.'
+ f' Please upgrade your grpc module to grpcio>={GRPC_GENERATED_VERSION}'
+ f' or downgrade your generated code using grpcio-tools<={GRPC_VERSION}.'
)
22 changes: 16 additions & 6 deletions sdk/python/feast/protos/feast/core/DataSource_pb2.py

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading