Skip to content

Commit a0596d8

Browse files
zabarnZach Barnett
andauthored
feat: Implement get_any_feature_view in HttpRegistry for Feature Retriever (#339)
* feat: Implement get_any_feature_view in HttpRegistry for Ralphathon * fix: lint issues --------- Co-authored-by: Zach Barnett <zbarnett@expediagroup.com>
1 parent 6ae3f04 commit a0596d8

2 files changed

Lines changed: 251 additions & 2 deletions

File tree

sdk/python/feast/infra/registry/http.py

Lines changed: 28 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -857,10 +857,36 @@ def delete_project(
857857
):
858858
pass
859859

860-
def get_any_feature_view( # type: ignore
860+
def get_any_feature_view(
861861
self, name: str, project: str, allow_cache: bool = False
862862
) -> BaseFeatureView:
863-
raise NotImplementedError("Method not implemented")
863+
if allow_cache:
864+
self._check_if_registry_refreshed()
865+
return proto_registry_utils.get_any_feature_view(
866+
self.cached_registry_proto, name, project
867+
)
868+
try:
869+
return self.get_feature_view(name, project)
870+
except (FeatureViewNotFoundException, HTTPStatusError, httpx.HTTPError):
871+
pass
872+
try:
873+
return self.get_sorted_feature_view(name, project)
874+
except (SortedFeatureViewNotFoundException, HTTPStatusError, httpx.HTTPError):
875+
pass
876+
try:
877+
return self.get_on_demand_feature_view(name, project)
878+
except (FeatureViewNotFoundException, HTTPStatusError, httpx.HTTPError):
879+
pass
880+
try:
881+
return self.get_stream_feature_view(name, project)
882+
except (
883+
NotImplementedError,
884+
FeatureViewNotFoundException,
885+
HTTPStatusError,
886+
httpx.HTTPError,
887+
):
888+
pass
889+
raise FeatureViewNotFoundException(name, project)
864890

865891
def get_project( # type: ignore
866892
self,
Lines changed: 223 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,223 @@
1+
# Copyright 2021 The Feast Authors
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# https://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
15+
from unittest.mock import MagicMock, patch
16+
17+
import httpx
18+
import pytest
19+
from httpx import HTTPStatusError
20+
21+
from feast.errors import (
22+
FeatureViewNotFoundException,
23+
SortedFeatureViewNotFoundException,
24+
)
25+
from feast.feature_view import FeatureView
26+
from feast.infra.registry.http import HttpRegistry
27+
from feast.on_demand_feature_view import OnDemandFeatureView
28+
from feast.sorted_feature_view import SortedFeatureView
29+
30+
31+
@pytest.fixture
32+
def http_registry():
33+
"""Create an HttpRegistry with mocked HTTP client and background thread."""
34+
with (
35+
patch.object(HttpRegistry, "__init__", lambda self, *a, **kw: None),
36+
patch.object(HttpRegistry, "proto", return_value=MagicMock()),
37+
):
38+
registry = HttpRegistry.__new__(HttpRegistry)
39+
registry.base_url = "http://test-registry"
40+
registry.http_client = MagicMock()
41+
registry.project = "test_project"
42+
registry.cached_registry_proto = MagicMock()
43+
registry.cached_registry_proto_created = None
44+
registry.cached_registry_proto_ttl = MagicMock(
45+
total_seconds=MagicMock(return_value=0)
46+
)
47+
yield registry
48+
49+
50+
class TestGetAnyFeatureView:
51+
"""Tests for HttpRegistry.get_any_feature_view"""
52+
53+
def test_returns_feature_view_when_found(self, http_registry):
54+
"""Test that a FeatureView is returned when get_feature_view succeeds."""
55+
mock_fv = MagicMock(spec=FeatureView)
56+
with patch.object(http_registry, "get_feature_view", return_value=mock_fv):
57+
result = http_registry.get_any_feature_view(
58+
"my_fv", "test_project", allow_cache=False
59+
)
60+
assert result is mock_fv
61+
62+
def test_returns_sorted_feature_view_when_fv_not_found(self, http_registry):
63+
"""Test fallback to SortedFeatureView when FeatureView is not found."""
64+
mock_sfv = MagicMock(spec=SortedFeatureView)
65+
with (
66+
patch.object(
67+
http_registry,
68+
"get_feature_view",
69+
side_effect=FeatureViewNotFoundException("my_sfv", "test_project"),
70+
),
71+
patch.object(
72+
http_registry, "get_sorted_feature_view", return_value=mock_sfv
73+
),
74+
):
75+
result = http_registry.get_any_feature_view(
76+
"my_sfv", "test_project", allow_cache=False
77+
)
78+
assert result is mock_sfv
79+
80+
def test_returns_on_demand_feature_view_when_fv_and_sfv_not_found(
81+
self, http_registry
82+
):
83+
"""Test fallback to OnDemandFeatureView when FeatureView and SortedFeatureView are not found."""
84+
mock_odfv = MagicMock(spec=OnDemandFeatureView)
85+
with (
86+
patch.object(
87+
http_registry,
88+
"get_feature_view",
89+
side_effect=FeatureViewNotFoundException("my_odfv", "test_project"),
90+
),
91+
patch.object(
92+
http_registry,
93+
"get_sorted_feature_view",
94+
side_effect=SortedFeatureViewNotFoundException(
95+
"my_odfv", "test_project"
96+
),
97+
),
98+
patch.object(
99+
http_registry, "get_on_demand_feature_view", return_value=mock_odfv
100+
),
101+
):
102+
result = http_registry.get_any_feature_view(
103+
"my_odfv", "test_project", allow_cache=False
104+
)
105+
assert result is mock_odfv
106+
107+
def test_raises_not_found_when_no_view_exists(self, http_registry):
108+
"""Test that FeatureViewNotFoundException is raised when no feature view type matches."""
109+
with (
110+
patch.object(
111+
http_registry,
112+
"get_feature_view",
113+
side_effect=FeatureViewNotFoundException("missing", "test_project"),
114+
),
115+
patch.object(
116+
http_registry,
117+
"get_sorted_feature_view",
118+
side_effect=SortedFeatureViewNotFoundException(
119+
"missing", "test_project"
120+
),
121+
),
122+
patch.object(
123+
http_registry,
124+
"get_on_demand_feature_view",
125+
side_effect=httpx.HTTPError("not found"),
126+
),
127+
patch.object(
128+
http_registry,
129+
"get_stream_feature_view",
130+
side_effect=NotImplementedError("Method not implemented"),
131+
),
132+
):
133+
with pytest.raises(FeatureViewNotFoundException):
134+
http_registry.get_any_feature_view(
135+
"missing", "test_project", allow_cache=False
136+
)
137+
138+
def test_handles_http_status_error_on_feature_view(self, http_registry):
139+
"""Test that HTTPStatusError from get_feature_view is caught and falls through."""
140+
mock_request = MagicMock()
141+
mock_response = MagicMock()
142+
mock_response.status_code = 500
143+
mock_sfv = MagicMock(spec=SortedFeatureView)
144+
145+
with (
146+
patch.object(
147+
http_registry,
148+
"get_feature_view",
149+
side_effect=HTTPStatusError(
150+
"error", request=mock_request, response=mock_response
151+
),
152+
),
153+
patch.object(
154+
http_registry, "get_sorted_feature_view", return_value=mock_sfv
155+
),
156+
):
157+
result = http_registry.get_any_feature_view(
158+
"my_fv", "test_project", allow_cache=False
159+
)
160+
assert result is mock_sfv
161+
162+
def test_handles_http_error_on_sorted_feature_view(self, http_registry):
163+
"""Test that httpx.HTTPError from get_sorted_feature_view is caught and falls through."""
164+
mock_odfv = MagicMock(spec=OnDemandFeatureView)
165+
166+
with (
167+
patch.object(
168+
http_registry,
169+
"get_feature_view",
170+
side_effect=FeatureViewNotFoundException("my_fv", "test_project"),
171+
),
172+
patch.object(
173+
http_registry,
174+
"get_sorted_feature_view",
175+
side_effect=httpx.HTTPError("not found"),
176+
),
177+
patch.object(
178+
http_registry, "get_on_demand_feature_view", return_value=mock_odfv
179+
),
180+
):
181+
result = http_registry.get_any_feature_view(
182+
"my_fv", "test_project", allow_cache=False
183+
)
184+
assert result is mock_odfv
185+
186+
def test_cache_path_delegates_to_proto_utils(self, http_registry):
187+
"""Test that allow_cache=True delegates to proto_registry_utils."""
188+
mock_fv = MagicMock(spec=FeatureView)
189+
with (
190+
patch.object(http_registry, "_check_if_registry_refreshed"),
191+
patch(
192+
"feast.infra.registry.http.proto_registry_utils.get_any_feature_view",
193+
return_value=mock_fv,
194+
) as mock_proto_util,
195+
):
196+
result = http_registry.get_any_feature_view(
197+
"my_fv", "test_project", allow_cache=True
198+
)
199+
assert result is mock_fv
200+
mock_proto_util.assert_called_once_with(
201+
http_registry.cached_registry_proto, "my_fv", "test_project"
202+
)
203+
204+
def test_feature_view_found_does_not_try_other_types(self, http_registry):
205+
"""Test that when FeatureView is found, no other get methods are called."""
206+
mock_fv = MagicMock(spec=FeatureView)
207+
with (
208+
patch.object(
209+
http_registry, "get_feature_view", return_value=mock_fv
210+
) as mock_get_fv,
211+
patch.object(http_registry, "get_sorted_feature_view") as mock_get_sfv,
212+
patch.object(http_registry, "get_on_demand_feature_view") as mock_get_odfv,
213+
patch.object(http_registry, "get_stream_feature_view") as mock_get_stream,
214+
):
215+
result = http_registry.get_any_feature_view(
216+
"my_fv", "test_project", allow_cache=False
217+
)
218+
219+
assert result is mock_fv
220+
mock_get_fv.assert_called_once_with("my_fv", "test_project")
221+
mock_get_sfv.assert_not_called()
222+
mock_get_odfv.assert_not_called()
223+
mock_get_stream.assert_not_called()

0 commit comments

Comments
 (0)