Visitar URL original
Merge branch 'master' into feat/lance-table-format · feast-dev/feast@8d5e3a5 · GitHub
Skip to content

Commit 8d5e3a5

Browse files
authored
Merge branch 'master' into feat/lance-table-format
2 parents ecee7b9 + bc5aeef commit 8d5e3a5

6 files changed

Lines changed: 107 additions & 1 deletion

File tree

‎sdk/python/feast/feature_server.py‎

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -174,8 +174,14 @@ class FeatureVectorResponse(BaseModel):
174174
event_timestamps: List[str] = []
175175

176176

177+
class FeatureViewMetadataResponse(BaseModel):
178+
name: str = ""
179+
version: int = 0
180+
181+
177182
class OnlineFeaturesMetadataResponse(BaseModel):
178183
feature_names: List[str] = []
184+
feature_view_metadata: List[FeatureViewMetadataResponse] = []
179185

180186
@field_validator("feature_names", mode="before")
181187
@classmethod

‎sdk/python/feast/feature_server_utils.py‎

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -157,4 +157,14 @@ def _metadata_to_dict(metadata) -> Dict[str, Any]:
157157
result: Dict[str, Any] = {}
158158
if metadata.HasField("feature_names"):
159159
result["feature_names"] = list(metadata.feature_names.val)
160+
if metadata.feature_view_metadata:
161+
fv_metadata = []
162+
for fvm in metadata.feature_view_metadata:
163+
entry: Dict[str, Any] = {}
164+
if fvm.name:
165+
entry["name"] = fvm.name
166+
if fvm.version:
167+
entry["version"] = fvm.version
168+
fv_metadata.append(entry)
169+
result["feature_view_metadata"] = fv_metadata
160170
return result

‎sdk/python/feast/infra/online_stores/remote.py‎

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -222,8 +222,13 @@ def _build_online_response_from_json(
222222
proto = GetOnlineFeaturesResponse()
223223

224224
metadata = GetOnlineFeaturesResponseMetadata()
225-
feature_names = resp_json.get("metadata", {}).get("feature_names", [])
225+
resp_metadata = resp_json.get("metadata", {})
226+
feature_names = resp_metadata.get("feature_names", [])
226227
metadata.feature_names.val.extend(feature_names)
228+
for fvm in resp_metadata.get("feature_view_metadata", []):
229+
fv_metadata = metadata.feature_view_metadata.add()
230+
fv_metadata.name = fvm.get("name", "")
231+
fv_metadata.version = fvm.get("version", 0)
227232
proto.metadata.CopyFrom(metadata)
228233

229234
for result in resp_json.get("results", []):

‎sdk/python/tests/unit/infra/online_store/test_remote_online_store.py‎

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -564,6 +564,30 @@ def test_build_online_response_status_mapping(self, remote_store):
564564
assert proto.results[2].statuses[0] == FieldStatus.NULL_VALUE
565565
assert proto.results[3].statuses[0] == FieldStatus.OUTSIDE_MAX_AGE
566566

567+
def test_build_online_response_feature_view_metadata(self, remote_store):
568+
"""feature_view_metadata returned by the server must be carried over
569+
to the reconstructed proto, so versioned reads work end-to-end."""
570+
resp_json = {
571+
"metadata": {
572+
"feature_names": ["user_id", "score"],
573+
"feature_view_metadata": [
574+
{"name": "user_stats", "version": 2},
575+
{"name": "user_profile"},
576+
],
577+
},
578+
"results": [
579+
{"values": [101], "statuses": ["PRESENT"]},
580+
{"values": [0.95], "statuses": ["PRESENT"]},
581+
],
582+
}
583+
584+
result = remote_store._build_online_response_from_json(resp_json)
585+
fv_metadata = result.proto.metadata.feature_view_metadata
586+
assert [(m.name, m.version) for m in fv_metadata] == [
587+
("user_stats", 2),
588+
("user_profile", 0),
589+
]
590+
567591
# ── Error handling ────────────────────────────────────────────────
568592

569593
@patch("feast.infra.online_stores.remote.get_remote_online_features")

‎sdk/python/tests/unit/test_feature_server.py‎

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -764,3 +764,21 @@ def test_gunicorn_post_worker_init_starts_resource_and_freshness_monitoring():
764764

765765
mock_fm.init_worker_monitoring.assert_called_once()
766766
mock_fm.init_worker_freshness_monitoring.assert_called_once_with(mock_store)
767+
768+
769+
def test_get_online_features_returns_feature_view_version_metadata(test_client):
770+
"""``include_feature_view_version_metadata=True`` must surface the
771+
``feature_view_metadata`` block over REST, like it does on the proto."""
772+
push_response = test_client.post("/push", json=push_body())
773+
assert push_response.status_code == 200
774+
775+
request_payload = get_online_features_body()
776+
request_payload["include_feature_view_version_metadata"] = True
777+
response = test_client.post("/get-online-features", json=request_payload)
778+
assert response.status_code == 200
779+
780+
metadata = response.json()["metadata"]
781+
assert "feature_view_metadata" in metadata, metadata
782+
assert [m["name"] for m in metadata["feature_view_metadata"]] == [
783+
"pushed_driver_locations"
784+
]

‎sdk/python/tests/unit/test_feature_server_utils.py‎

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -692,3 +692,46 @@ def test_all_status_codes_mapped(self):
692692

693693
def test_unknown_status_returns_invalid(self):
694694
assert _STATUS_NAMES.get(999, "INVALID") == "INVALID"
695+
696+
697+
class TestFeatureViewVersionMetadata:
698+
"""``feature_view_metadata`` is part of the response metadata and must
699+
survive ``convert_response_to_dict`` exactly as MessageToDict emits it."""
700+
701+
@pytest.fixture(autouse=True)
702+
def setup_proto_json_patch(self):
703+
proto_json.patch()
704+
705+
def test_feature_view_metadata_matches_message_to_dict(self):
706+
response = GetOnlineFeaturesResponse()
707+
response.metadata.feature_names.val.extend(["trips_today"])
708+
fv_meta = response.metadata.feature_view_metadata.add()
709+
fv_meta.name = "driver_stats"
710+
fv_meta.version = 2
711+
fv = response.results.add()
712+
fv.values.append(Value(int64_val=7))
713+
fv.statuses.append(FieldStatus.PRESENT)
714+
715+
fast_result = convert_response_to_dict(response)
716+
standard_result = MessageToDict(response, preserving_proto_field_name=True)
717+
718+
assert "feature_view_metadata" in standard_result["metadata"]
719+
assert fast_result["metadata"] == standard_result["metadata"]
720+
721+
def test_feature_view_metadata_version_zero_matches_message_to_dict(self):
722+
response = GetOnlineFeaturesResponse()
723+
response.metadata.feature_names.val.extend(["trips_today"])
724+
response.metadata.feature_view_metadata.add(name="driver_stats", version=0)
725+
726+
fast_result = convert_response_to_dict(response)
727+
standard_result = MessageToDict(response, preserving_proto_field_name=True)
728+
729+
assert fast_result["metadata"] == standard_result["metadata"]
730+
731+
def test_feature_view_metadata_omitted_when_not_requested(self):
732+
response = GetOnlineFeaturesResponse()
733+
response.metadata.feature_names.val.extend(["trips_today"])
734+
735+
fast_result = convert_response_to_dict(response)
736+
737+
assert "feature_view_metadata" not in fast_result["metadata"]

0 commit comments

Comments
 (0)