"""Fast serialization utilities for Feature Server responses.
Matches the output format of MessageToDict with proto_json.patch() applied.
Values are serialized as native Python types (not wrapped dicts).
"""
import base64
import logging
from datetime import datetime, timezone
from typing import Any, Dict, Optional
from feast.protos.feast.serving.ServingService_pb2 import GetOnlineFeaturesResponse
from feast.protos.feast.types.Value_pb2 import Value
logger = logging.getLogger(__name__)
# FieldStatus enum mapping (protos/feast/serving/ServingService.proto)
_STATUS_NAMES: Dict[int, str] = {
0: "INVALID",
1: "PRESENT",
2: "NULL_VALUE",
3: "NOT_FOUND",
4: "OUTSIDE_MAX_AGE",
}
def convert_response_to_dict(response: GetOnlineFeaturesResponse) -> Dict[str, Any]:
"""Convert GetOnlineFeaturesResponse to a JSON-serializable dict.
Matches the structure produced by MessageToDict(proto, preserving_proto_field_name=True)
with proto_json.patch() applied, with one intentional difference:
- double_val fields are returned as Python float objects (json.dumps uses Python 3.1+
shortest round-trip form, ~15-17 sig digits) rather than 18 fixed significant digits
(float_precision=18). Values are numerically identical; only the JSON string length
may differ. This is safe for all ML feature types and avoids unnecessary precision
overhead.
"""
result: Dict[str, Any] = {
"results": [
{
"values": [_value_to_native(v) for v in feature_vector.values],
"statuses": [
_STATUS_NAMES.get(s, "INVALID") for s in feature_vector.statuses
],
**(
{
"event_timestamps": [
_timestamp_to_str(ts)
for ts in feature_vector.event_timestamps
]
}
if feature_vector.event_timestamps
else {}
),
}
for feature_vector in response.results
]
}
if response.HasField("metadata"):
result["metadata"] = _metadata_to_dict(response.metadata)
if response.status:
result["status"] = response.status
return result
def _value_to_native(v: Value) -> Optional[Any]:
"""Convert a Value proto to a JSON-serializable Python type.
bytes_val and bytes_list_val are base64-encoded (RFC 4648) so that
JSONResponse can serialize them without TypeError. This matches standard
protobuf JSON encoding for bytes fields and is safe for all HTTP clients.
"""
which = v.WhichOneof("val")
if which is None or which == "null_val":
return None
# bytes must be base64-encoded for JSON serialization
elif which == "bytes_val":
return base64.b64encode(v.bytes_val).decode("ascii")
# RepeatedValue â nested Values that must be recursively converted
elif which in ("list_val", "set_val"):
return [_value_to_native(nested) for nested in getattr(v, which).val]
# Map