Skip to content

Commit

Permalink
Fix CLI entities command & add feature-views command (#1471)
Browse files Browse the repository at this point in the history
Signed-off-by: Tsotne Tabidze <tsotne@tecton.ai>
  • Loading branch information
Tsotne Tabidze authored Apr 15, 2021
1 parent e087414 commit ec265f9
Show file tree
Hide file tree
Showing 4 changed files with 105 additions and 85 deletions.
138 changes: 58 additions & 80 deletions sdk/python/feast/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,16 +15,14 @@
import logging
from datetime import datetime
from pathlib import Path
from typing import Dict, List
from typing import List

import click
import pkg_resources
import yaml

from feast.client import Client
from feast.entity import Entity
from feast.errors import FeastObjectNotFoundException
from feast.feature_store import FeatureStore
from feast.loaders.yaml import yaml_loader
from feast.repo_config import load_repo_config
from feast.repo_operations import (
apply_total,
Expand Down Expand Up @@ -60,56 +58,27 @@ def version():


@cli.group(name="entities")
def entity():
def entities_cmd():
"""
Create and manage entities
Access entities
"""
pass


@entity.command("apply")
@click.option(
"--filename",
"-f",
help="Path to an entity configuration file that will be applied",
type=click.Path(exists=True),
)
@click.option(
"--project",
"-p",
help="Project that entity belongs to",
type=click.STRING,
default="default",
)
def entity_create(filename, project):
"""
Create or update an entity
"""

entities = [Entity.from_dict(entity_dict) for entity_dict in yaml_loader(filename)]
feast_client = Client() # type: Client
feast_client.apply(entities, project)


@entity.command("describe")
@entities_cmd.command("describe")
@click.argument("name", type=click.STRING)
@click.option(
"--project",
"-p",
help="Project that entity belongs to",
type=click.STRING,
default="default",
)
def entity_describe(name: str, project: str):
def entity_describe(name: str):
"""
Describe an entity
"""
feast_client = Client() # type: Client
entity = feast_client.get_entity(name=name, project=project)
cli_check_repo(Path.cwd())
store = FeatureStore(repo_path=str(Path.cwd()))

if not entity:
print(f'Entity with name "{name}" could not be found')
return
try:
entity = store.get_entity(name)
except FeastObjectNotFoundException as e:
print(e)
exit(1)

print(
yaml.dump(
Expand All @@ -118,57 +87,66 @@ def entity_describe(name: str, project: str):
)


@entity.command(name="list")
@click.option(
"--project",
"-p",
help="Project that entity belongs to",
type=click.STRING,
default="",
)
@click.option(
"--labels",
"-l",
help="Labels to filter for entities",
type=click.STRING,
default="",
)
def entity_list(project: str, labels: str):
@entities_cmd.command(name="list")
def entity_list():
"""
List all entities
"""
feast_client = Client() # type: Client

labels_dict = _get_labels_dict(labels)

cli_check_repo(Path.cwd())
store = FeatureStore(repo_path=str(Path.cwd()))
table = []
for entity in feast_client.list_entities(project=project, labels=labels_dict):
for entity in store.list_entities():
table.append([entity.name, entity.description, entity.value_type])

from tabulate import tabulate

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


def _get_labels_dict(label_str: str) -> Dict[str, str]:
@cli.group(name="feature-views")
def feature_views_cmd():
"""
Access feature views
"""
Converts CLI input labels string to dictionary format if provided string is valid.
pass


@feature_views_cmd.command("describe")
@click.argument("name", type=click.STRING)
def feature_view_describe(name: str):
"""
Describe a feature view
"""
cli_check_repo(Path.cwd())
store = FeatureStore(repo_path=str(Path.cwd()))

try:
feature_view = store.get_feature_view(name)
except FeastObjectNotFoundException as e:
print(e)
exit(1)

print(
yaml.dump(
yaml.safe_load(str(feature_view)), default_flow_style=False, sort_keys=False
)
)

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

Returns:
Dict of key-value label pairs
@feature_views_cmd.command(name="list")
def feature_view_list():
"""
List all feature views
"""
labels_dict: Dict[str, str] = {}
labels_kv = label_str.split(",")
if label_str == "":
return labels_dict
if len(labels_kv) % 2 == 1:
raise ValueError("Uneven key-value label pairs were entered")
for k, v in zip(labels_kv[0::2], labels_kv[1::2]):
labels_dict[k] = v
return labels_dict
cli_check_repo(Path.cwd())
store = FeatureStore(repo_path=str(Path.cwd()))
table = []
for feature_view in store.list_feature_views():
table.append([feature_view.name, feature_view.entities])

from tabulate import tabulate

print(tabulate(table, headers=["NAME", "ENTITIES"], tablefmt="plain"))


@cli.command("apply")
Expand Down
17 changes: 17 additions & 0 deletions sdk/python/feast/errors.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
class FeastObjectNotFoundException(Exception):
pass


class EntityNotFoundException(FeastObjectNotFoundException):
def __init__(self, project, name):
super().__init__(f"Entity {name} does not exist in project {project}")


class FeatureViewNotFoundException(FeastObjectNotFoundException):
def __init__(self, project, name):
super().__init__(f"Feature view {name} does not exist in project {project}")


class FeatureTableNotFoundException(FeastObjectNotFoundException):
def __init__(self, project, name):
super().__init__(f"Feature table {name} does not exist in project {project}")
15 changes: 10 additions & 5 deletions sdk/python/feast/registry.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,11 @@
from google.auth.exceptions import DefaultCredentialsError

from feast.entity import Entity
from feast.errors import (
EntityNotFoundException,
FeatureTableNotFoundException,
FeatureViewNotFoundException,
)
from feast.feature_table import FeatureTable
from feast.feature_view import FeatureView
from feast.protos.feast.core.Registry_pb2 import Registry as RegistryProto
Expand Down Expand Up @@ -121,7 +126,7 @@ def get_entity(self, name: str, project: str, allow_cache: bool = False) -> Enti
for entity_proto in registry_proto.entities:
if entity_proto.spec.name == name and entity_proto.spec.project == project:
return Entity.from_proto(entity_proto)
raise Exception(f"Entity {name} does not exist in project {project}")
raise EntityNotFoundException(project, name)

def apply_feature_table(self, feature_table: FeatureTable, project: str):
"""
Expand Down Expand Up @@ -238,7 +243,7 @@ def get_feature_table(self, name: str, project: str) -> FeatureTable:
and feature_table_proto.spec.project == project
):
return FeatureTable.from_proto(feature_table_proto)
raise Exception(f"Feature table {name} does not exist in project {project}")
raise FeatureTableNotFoundException(project, name)

def get_feature_view(self, name: str, project: str) -> FeatureView:
"""
Expand All @@ -259,7 +264,7 @@ def get_feature_view(self, name: str, project: str) -> FeatureView:
and feature_view_proto.spec.project == project
):
return FeatureView.from_proto(feature_view_proto)
raise Exception(f"Feature view {name} does not exist in project {project}")
raise FeatureViewNotFoundException(project, name)

def delete_feature_table(self, name: str, project: str):
"""
Expand All @@ -280,7 +285,7 @@ def updater(registry_proto: RegistryProto):
):
del registry_proto.feature_tables[idx]
return registry_proto
raise Exception(f"Feature table {name} does not exist in project {project}")
raise FeatureTableNotFoundException(project, name)

self._registry_store.update_registry_proto(updater)
return
Expand All @@ -304,7 +309,7 @@ def updater(registry_proto: RegistryProto):
):
del registry_proto.feature_views[idx]
return registry_proto
raise Exception(f"Feature view {name} does not exist in project {project}")
raise FeatureViewNotFoundException(project, name)

self._registry_store.update_registry_proto(updater)

Expand Down
20 changes: 20 additions & 0 deletions sdk/python/tests/test_cli_local.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,26 @@ def test_workflow() -> None:
result = runner.run(["apply"], cwd=repo_path)
assert result.returncode == 0

# entity & feature view list commands should succeed
result = runner.run(["entities", "list"], cwd=repo_path)
assert result.returncode == 0
result = runner.run(["feature-views", "list"], cwd=repo_path)
assert result.returncode == 0

# entity & feature view describe commands should succeed when objects exist
result = runner.run(["entities", "describe", "driver"], cwd=repo_path)
assert result.returncode == 0
result = runner.run(
["feature-views", "describe", "driver_locations"], cwd=repo_path
)
assert result.returncode == 0

# entity & feature view describe commands should fail when objects don't exist
result = runner.run(["entities", "describe", "foo"], cwd=repo_path)
assert result.returncode == 1
result = runner.run(["feature-views", "describe", "foo"], cwd=repo_path)
assert result.returncode == 1

# Doing another apply should be a no op, and should not cause errors
result = runner.run(["apply"], cwd=repo_path)
assert result.returncode == 0
Expand Down

0 comments on commit ec265f9

Please sign in to comment.