Skip to content

Commit d80b733

Browse files
committed
add tags filter capability to 'feature-views list'
Signed-off-by: Tommy Hughes <[email protected]>
1 parent c08e6f9 commit d80b733

File tree

4 files changed

+82
-11
lines changed

4 files changed

+82
-11
lines changed

sdk/python/feast/cli.py

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -370,16 +370,21 @@ def feature_view_describe(ctx: click.Context, name: str):
370370

371371

372372
@feature_views_cmd.command(name="list")
373+
@click.option(
374+
"--tags",
375+
help="Filter by tags (e.g. 'key:value, key:value, ...')",
376+
multiple=True,
377+
)
373378
@click.pass_context
374-
def feature_view_list(ctx: click.Context):
379+
def feature_view_list(ctx: click.Context, tags: str):
375380
"""
376381
List all feature views
377382
"""
378383
store = create_feature_store(ctx)
379384
table = []
380385
for feature_view in [
381-
*store.list_feature_views(),
382-
*store.list_on_demand_feature_views(),
386+
*store.list_feature_views(tags=tags),
387+
*store.list_on_demand_feature_views(tags=tags),
383388
]:
384389
entities = set()
385390
if isinstance(feature_view, FeatureView):

sdk/python/feast/feature_store.py

Lines changed: 21 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -247,23 +247,28 @@ def list_feature_services(self) -> List[FeatureService]:
247247
"""
248248
return self._registry.list_feature_services(self.project)
249249

250-
def list_feature_views(self, allow_cache: bool = False) -> List[FeatureView]:
250+
def list_feature_views(
251+
self, allow_cache: bool = False, tags: str = ""
252+
) -> List[FeatureView]:
251253
"""
252254
Retrieves the list of feature views from the registry.
253255
254256
Args:
257+
tags: Tags to filter by.
255258
allow_cache: Whether to allow returning entities from a cached registry.
256259
257260
Returns:
258261
A list of feature views.
259262
"""
260-
return self._list_feature_views(allow_cache)
263+
return self._list_feature_views(allow_cache=allow_cache, tags=tags)
261264

262265
def _list_feature_views(
263266
self,
264267
allow_cache: bool = False,
265268
hide_dummy_entity: bool = True,
269+
tags: str = "",
266270
) -> List[FeatureView]:
271+
tagDict = utils.tags_str_to_dict(tags)
267272
feature_views = []
268273
for fv in self._registry.list_feature_views(
269274
self.project, allow_cache=allow_cache
@@ -275,7 +280,8 @@ def _list_feature_views(
275280
):
276281
fv.entities = []
277282
fv.entity_columns = []
278-
feature_views.append(fv)
283+
if all(fv.tags.get(key, None) == val for key, val in tagDict.items()):
284+
feature_views.append(fv)
279285
return feature_views
280286

281287
def _list_stream_feature_views(
@@ -294,17 +300,26 @@ def _list_stream_feature_views(
294300
return stream_feature_views
295301

296302
def list_on_demand_feature_views(
297-
self, allow_cache: bool = False
303+
self, allow_cache: bool = False, tags: str = ""
298304
) -> List[OnDemandFeatureView]:
299305
"""
300306
Retrieves the list of on demand feature views from the registry.
301307
308+
Args:
309+
tags: Tags to filter by.
310+
allow_cache: Whether to allow returning entities from a cached registry.
311+
302312
Returns:
303313
A list of on demand feature views.
304314
"""
305-
return self._registry.list_on_demand_feature_views(
315+
tagDict = utils.tags_str_to_dict(tags)
316+
on_demand_feature_views = []
317+
for odfv in self._registry.list_on_demand_feature_views(
306318
self.project, allow_cache=allow_cache
307-
)
319+
):
320+
if all(odfv.tags.get(key, None) == val for key, val in tagDict.items()):
321+
on_demand_feature_views.append(odfv)
322+
return on_demand_feature_views
308323

309324
def list_stream_feature_views(
310325
self, allow_cache: bool = False

sdk/python/feast/utils.py

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
from collections import defaultdict
44
from datetime import datetime
55
from pathlib import Path
6-
from typing import Dict, List, Optional, Tuple, Union
6+
from typing import Dict, List, Optional, Tuple, Union, cast
77

88
import pandas as pd
99
import pyarrow
@@ -256,3 +256,13 @@ def _convert_arrow_to_proto(
256256
created_timestamps = [None] * table.num_rows
257257

258258
return list(zip(entity_keys, features, event_timestamps, created_timestamps))
259+
260+
261+
def tags_str_to_dict(tags: str) -> Dict[str, str]:
262+
tagPairs = (
263+
str(tags).strip().strip("()").replace('"', "").replace("'", "").split(",")
264+
)
265+
tagDict = dict(
266+
cast(tuple[str, str], tag.split(":", 1)) for tag in tagPairs if ":" in tag
267+
)
268+
return {key.strip(): value.strip() for key, value in tagDict.items()}

sdk/python/tests/unit/local_feast_tests/test_local_feature_store.py

Lines changed: 42 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -89,7 +89,7 @@ def test_apply_feature_view(test_feature_store):
8989
Field(name="entity_id", dtype=Int64),
9090
],
9191
entities=[entity],
92-
tags={"team": "matchmaking"},
92+
tags={"team": "matchmaking", "tag": "two"},
9393
source=batch_source,
9494
ttl=timedelta(minutes=5),
9595
)
@@ -114,6 +114,47 @@ def test_apply_feature_view(test_feature_store):
114114
and feature_views[0].entities[0] == "fs1_my_entity_1"
115115
)
116116

117+
feature_views = test_feature_store.list_feature_views(tags="('team:matchmaking',)")
118+
119+
# List Feature Views
120+
assert (
121+
len(feature_views) == 2
122+
and feature_views[0].name == "my_feature_view_1"
123+
and feature_views[0].features[0].name == "fs1_my_feature_1"
124+
and feature_views[0].features[0].dtype == Int64
125+
and feature_views[0].features[1].name == "fs1_my_feature_2"
126+
and feature_views[0].features[1].dtype == String
127+
and feature_views[0].features[2].name == "fs1_my_feature_3"
128+
and feature_views[0].features[2].dtype == Array(String)
129+
and feature_views[0].features[3].name == "fs1_my_feature_4"
130+
and feature_views[0].features[3].dtype == Array(Bytes)
131+
and feature_views[0].entities[0] == "fs1_my_entity_1"
132+
)
133+
134+
feature_views = test_feature_store.list_feature_views(
135+
tags="(' team :matchmaking, tag: two ',)"
136+
)
137+
138+
# List Feature Views
139+
assert (
140+
len(feature_views) == 1
141+
and feature_views[0].name == "batch_feature_view"
142+
and feature_views[0].features[0].name == "fs1_my_feature_1"
143+
and feature_views[0].features[0].dtype == Int64
144+
and feature_views[0].features[1].name == "fs1_my_feature_2"
145+
and feature_views[0].features[1].dtype == String
146+
and feature_views[0].features[2].name == "fs1_my_feature_3"
147+
and feature_views[0].features[2].dtype == Array(String)
148+
and feature_views[0].features[3].name == "fs1_my_feature_4"
149+
and feature_views[0].features[3].dtype == Array(Bytes)
150+
and feature_views[0].entities[0] == "fs1_my_entity_1"
151+
)
152+
153+
feature_views = test_feature_store.list_feature_views(tags="('missing:tag',)")
154+
155+
# List Feature Views
156+
assert len(feature_views) == 0
157+
117158
test_feature_store.teardown()
118159

119160

0 commit comments

Comments
 (0)