Skip to content

Commit ec265f9

Browse files
author
Tsotne Tabidze
authored
Fix CLI entities command & add feature-views command (feast-dev#1471)
Signed-off-by: Tsotne Tabidze <[email protected]>
1 parent e087414 commit ec265f9

File tree

4 files changed

+105
-85
lines changed

4 files changed

+105
-85
lines changed

sdk/python/feast/cli.py

Lines changed: 58 additions & 80 deletions
Original file line numberDiff line numberDiff line change
@@ -15,16 +15,14 @@
1515
import logging
1616
from datetime import datetime
1717
from pathlib import Path
18-
from typing import Dict, List
18+
from typing import List
1919

2020
import click
2121
import pkg_resources
2222
import yaml
2323

24-
from feast.client import Client
25-
from feast.entity import Entity
24+
from feast.errors import FeastObjectNotFoundException
2625
from feast.feature_store import FeatureStore
27-
from feast.loaders.yaml import yaml_loader
2826
from feast.repo_config import load_repo_config
2927
from feast.repo_operations import (
3028
apply_total,
@@ -60,56 +58,27 @@ def version():
6058

6159

6260
@cli.group(name="entities")
63-
def entity():
61+
def entities_cmd():
6462
"""
65-
Create and manage entities
63+
Access entities
6664
"""
6765
pass
6866

6967

70-
@entity.command("apply")
71-
@click.option(
72-
"--filename",
73-
"-f",
74-
help="Path to an entity configuration file that will be applied",
75-
type=click.Path(exists=True),
76-
)
77-
@click.option(
78-
"--project",
79-
"-p",
80-
help="Project that entity belongs to",
81-
type=click.STRING,
82-
default="default",
83-
)
84-
def entity_create(filename, project):
85-
"""
86-
Create or update an entity
87-
"""
88-
89-
entities = [Entity.from_dict(entity_dict) for entity_dict in yaml_loader(filename)]
90-
feast_client = Client() # type: Client
91-
feast_client.apply(entities, project)
92-
93-
94-
@entity.command("describe")
68+
@entities_cmd.command("describe")
9569
@click.argument("name", type=click.STRING)
96-
@click.option(
97-
"--project",
98-
"-p",
99-
help="Project that entity belongs to",
100-
type=click.STRING,
101-
default="default",
102-
)
103-
def entity_describe(name: str, project: str):
70+
def entity_describe(name: str):
10471
"""
10572
Describe an entity
10673
"""
107-
feast_client = Client() # type: Client
108-
entity = feast_client.get_entity(name=name, project=project)
74+
cli_check_repo(Path.cwd())
75+
store = FeatureStore(repo_path=str(Path.cwd()))
10976

110-
if not entity:
111-
print(f'Entity with name "{name}" could not be found')
112-
return
77+
try:
78+
entity = store.get_entity(name)
79+
except FeastObjectNotFoundException as e:
80+
print(e)
81+
exit(1)
11382

11483
print(
11584
yaml.dump(
@@ -118,57 +87,66 @@ def entity_describe(name: str, project: str):
11887
)
11988

12089

121-
@entity.command(name="list")
122-
@click.option(
123-
"--project",
124-
"-p",
125-
help="Project that entity belongs to",
126-
type=click.STRING,
127-
default="",
128-
)
129-
@click.option(
130-
"--labels",
131-
"-l",
132-
help="Labels to filter for entities",
133-
type=click.STRING,
134-
default="",
135-
)
136-
def entity_list(project: str, labels: str):
90+
@entities_cmd.command(name="list")
91+
def entity_list():
13792
"""
13893
List all entities
13994
"""
140-
feast_client = Client() # type: Client
141-
142-
labels_dict = _get_labels_dict(labels)
143-
95+
cli_check_repo(Path.cwd())
96+
store = FeatureStore(repo_path=str(Path.cwd()))
14497
table = []
145-
for entity in feast_client.list_entities(project=project, labels=labels_dict):
98+
for entity in store.list_entities():
14699
table.append([entity.name, entity.description, entity.value_type])
147100

148101
from tabulate import tabulate
149102

150103
print(tabulate(table, headers=["NAME", "DESCRIPTION", "TYPE"], tablefmt="plain"))
151104

152105

153-
def _get_labels_dict(label_str: str) -> Dict[str, str]:
106+
@cli.group(name="feature-views")
107+
def feature_views_cmd():
108+
"""
109+
Access feature views
154110
"""
155-
Converts CLI input labels string to dictionary format if provided string is valid.
111+
pass
112+
113+
114+
@feature_views_cmd.command("describe")
115+
@click.argument("name", type=click.STRING)
116+
def feature_view_describe(name: str):
117+
"""
118+
Describe a feature view
119+
"""
120+
cli_check_repo(Path.cwd())
121+
store = FeatureStore(repo_path=str(Path.cwd()))
122+
123+
try:
124+
feature_view = store.get_feature_view(name)
125+
except FeastObjectNotFoundException as e:
126+
print(e)
127+
exit(1)
128+
129+
print(
130+
yaml.dump(
131+
yaml.safe_load(str(feature_view)), default_flow_style=False, sort_keys=False
132+
)
133+
)
156134

157-
Args:
158-
label_str: A comma-separated string of key-value pairs
159135

160-
Returns:
161-
Dict of key-value label pairs
136+
@feature_views_cmd.command(name="list")
137+
def feature_view_list():
138+
"""
139+
List all feature views
162140
"""
163-
labels_dict: Dict[str, str] = {}
164-
labels_kv = label_str.split(",")
165-
if label_str == "":
166-
return labels_dict
167-
if len(labels_kv) % 2 == 1:
168-
raise ValueError("Uneven key-value label pairs were entered")
169-
for k, v in zip(labels_kv[0::2], labels_kv[1::2]):
170-
labels_dict[k] = v
171-
return labels_dict
141+
cli_check_repo(Path.cwd())
142+
store = FeatureStore(repo_path=str(Path.cwd()))
143+
table = []
144+
for feature_view in store.list_feature_views():
145+
table.append([feature_view.name, feature_view.entities])
146+
147+
from tabulate import tabulate
148+
149+
print(tabulate(table, headers=["NAME", "ENTITIES"], tablefmt="plain"))
172150

173151

174152
@cli.command("apply")

sdk/python/feast/errors.py

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
class FeastObjectNotFoundException(Exception):
2+
pass
3+
4+
5+
class EntityNotFoundException(FeastObjectNotFoundException):
6+
def __init__(self, project, name):
7+
super().__init__(f"Entity {name} does not exist in project {project}")
8+
9+
10+
class FeatureViewNotFoundException(FeastObjectNotFoundException):
11+
def __init__(self, project, name):
12+
super().__init__(f"Feature view {name} does not exist in project {project}")
13+
14+
15+
class FeatureTableNotFoundException(FeastObjectNotFoundException):
16+
def __init__(self, project, name):
17+
super().__init__(f"Feature table {name} does not exist in project {project}")

sdk/python/feast/registry.py

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,11 @@
2323
from google.auth.exceptions import DefaultCredentialsError
2424

2525
from feast.entity import Entity
26+
from feast.errors import (
27+
EntityNotFoundException,
28+
FeatureTableNotFoundException,
29+
FeatureViewNotFoundException,
30+
)
2631
from feast.feature_table import FeatureTable
2732
from feast.feature_view import FeatureView
2833
from feast.protos.feast.core.Registry_pb2 import Registry as RegistryProto
@@ -121,7 +126,7 @@ def get_entity(self, name: str, project: str, allow_cache: bool = False) -> Enti
121126
for entity_proto in registry_proto.entities:
122127
if entity_proto.spec.name == name and entity_proto.spec.project == project:
123128
return Entity.from_proto(entity_proto)
124-
raise Exception(f"Entity {name} does not exist in project {project}")
129+
raise EntityNotFoundException(project, name)
125130

126131
def apply_feature_table(self, feature_table: FeatureTable, project: str):
127132
"""
@@ -238,7 +243,7 @@ def get_feature_table(self, name: str, project: str) -> FeatureTable:
238243
and feature_table_proto.spec.project == project
239244
):
240245
return FeatureTable.from_proto(feature_table_proto)
241-
raise Exception(f"Feature table {name} does not exist in project {project}")
246+
raise FeatureTableNotFoundException(project, name)
242247

243248
def get_feature_view(self, name: str, project: str) -> FeatureView:
244249
"""
@@ -259,7 +264,7 @@ def get_feature_view(self, name: str, project: str) -> FeatureView:
259264
and feature_view_proto.spec.project == project
260265
):
261266
return FeatureView.from_proto(feature_view_proto)
262-
raise Exception(f"Feature view {name} does not exist in project {project}")
267+
raise FeatureViewNotFoundException(project, name)
263268

264269
def delete_feature_table(self, name: str, project: str):
265270
"""
@@ -280,7 +285,7 @@ def updater(registry_proto: RegistryProto):
280285
):
281286
del registry_proto.feature_tables[idx]
282287
return registry_proto
283-
raise Exception(f"Feature table {name} does not exist in project {project}")
288+
raise FeatureTableNotFoundException(project, name)
284289

285290
self._registry_store.update_registry_proto(updater)
286291
return
@@ -304,7 +309,7 @@ def updater(registry_proto: RegistryProto):
304309
):
305310
del registry_proto.feature_views[idx]
306311
return registry_proto
307-
raise Exception(f"Feature view {name} does not exist in project {project}")
312+
raise FeatureViewNotFoundException(project, name)
308313

309314
self._registry_store.update_registry_proto(updater)
310315

sdk/python/tests/test_cli_local.py

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,26 @@ def test_workflow() -> None:
4040
result = runner.run(["apply"], cwd=repo_path)
4141
assert result.returncode == 0
4242

43+
# entity & feature view list commands should succeed
44+
result = runner.run(["entities", "list"], cwd=repo_path)
45+
assert result.returncode == 0
46+
result = runner.run(["feature-views", "list"], cwd=repo_path)
47+
assert result.returncode == 0
48+
49+
# entity & feature view describe commands should succeed when objects exist
50+
result = runner.run(["entities", "describe", "driver"], cwd=repo_path)
51+
assert result.returncode == 0
52+
result = runner.run(
53+
["feature-views", "describe", "driver_locations"], cwd=repo_path
54+
)
55+
assert result.returncode == 0
56+
57+
# entity & feature view describe commands should fail when objects don't exist
58+
result = runner.run(["entities", "describe", "foo"], cwd=repo_path)
59+
assert result.returncode == 1
60+
result = runner.run(["feature-views", "describe", "foo"], cwd=repo_path)
61+
assert result.returncode == 1
62+
4363
# Doing another apply should be a no op, and should not cause errors
4464
result = runner.run(["apply"], cwd=repo_path)
4565
assert result.returncode == 0

0 commit comments

Comments
 (0)