Skip to content

Commit 7b250e5

Browse files
authored
feat: Add cli list/describe for SavedDatasets, StreamFeatureViews, & … (#4487)
feat: Add cli list/describe for SavedDatasets, StreamFeatureViews, & ValidationReferences Signed-off-by: Tommy Hughes <tohughes@redhat.com>
1 parent 2118719 commit 7b250e5

File tree

3 files changed

+183
-0
lines changed

3 files changed

+183
-0
lines changed

sdk/python/feast/cli.py

+150
Original file line numberDiff line numberDiff line change
@@ -470,6 +470,156 @@ def on_demand_feature_view_list(ctx: click.Context, tags: list[str]):
470470
print(tabulate(table, headers=["NAME"], tablefmt="plain"))
471471

472472

473+
@cli.group(name="saved-datasets")
474+
def saved_datasets_cmd():
475+
"""
476+
[Experimental] Access saved datasets
477+
"""
478+
pass
479+
480+
481+
@saved_datasets_cmd.command("describe")
482+
@click.argument("name", type=click.STRING)
483+
@click.pass_context
484+
def saved_datasets_describe(ctx: click.Context, name: str):
485+
"""
486+
[Experimental] Describe a saved dataset
487+
"""
488+
store = create_feature_store(ctx)
489+
490+
try:
491+
saved_dataset = store.get_saved_dataset(name)
492+
except FeastObjectNotFoundException as e:
493+
print(e)
494+
exit(1)
495+
496+
print(
497+
yaml.dump(
498+
yaml.safe_load(str(saved_dataset)),
499+
default_flow_style=False,
500+
sort_keys=False,
501+
)
502+
)
503+
504+
505+
@saved_datasets_cmd.command(name="list")
506+
@tagsOption
507+
@click.pass_context
508+
def saved_datasets_list(ctx: click.Context, tags: list[str]):
509+
"""
510+
[Experimental] List all saved datasets
511+
"""
512+
store = create_feature_store(ctx)
513+
table = []
514+
tags_filter = utils.tags_list_to_dict(tags)
515+
for saved_dataset in store.list_saved_datasets(tags=tags_filter):
516+
table.append([saved_dataset.name])
517+
518+
from tabulate import tabulate
519+
520+
print(tabulate(table, headers=["NAME"], tablefmt="plain"))
521+
522+
523+
@cli.group(name="stream-feature-views")
524+
def stream_feature_views_cmd():
525+
"""
526+
[Experimental] Access stream feature views
527+
"""
528+
pass
529+
530+
531+
@stream_feature_views_cmd.command("describe")
532+
@click.argument("name", type=click.STRING)
533+
@click.pass_context
534+
def stream_feature_views_describe(ctx: click.Context, name: str):
535+
"""
536+
[Experimental] Describe a stream feature view
537+
"""
538+
store = create_feature_store(ctx)
539+
540+
try:
541+
stream_feature_view = store.get_stream_feature_view(name)
542+
except FeastObjectNotFoundException as e:
543+
print(e)
544+
exit(1)
545+
546+
print(
547+
yaml.dump(
548+
yaml.safe_load(str(stream_feature_view)),
549+
default_flow_style=False,
550+
sort_keys=False,
551+
)
552+
)
553+
554+
555+
@stream_feature_views_cmd.command(name="list")
556+
@tagsOption
557+
@click.pass_context
558+
def stream_feature_views_list(ctx: click.Context, tags: list[str]):
559+
"""
560+
[Experimental] List all stream feature views
561+
"""
562+
store = create_feature_store(ctx)
563+
table = []
564+
tags_filter = utils.tags_list_to_dict(tags)
565+
for stream_feature_view in store.list_stream_feature_views(tags=tags_filter):
566+
table.append([stream_feature_view.name])
567+
568+
from tabulate import tabulate
569+
570+
print(tabulate(table, headers=["NAME"], tablefmt="plain"))
571+
572+
573+
@cli.group(name="validation-references")
574+
def validation_references_cmd():
575+
"""
576+
[Experimental] Access validation references
577+
"""
578+
pass
579+
580+
581+
@validation_references_cmd.command("describe")
582+
@click.argument("name", type=click.STRING)
583+
@click.pass_context
584+
def validation_references_describe(ctx: click.Context, name: str):
585+
"""
586+
[Experimental] Describe a validation reference
587+
"""
588+
store = create_feature_store(ctx)
589+
590+
try:
591+
validation_reference = store.get_validation_reference(name)
592+
except FeastObjectNotFoundException as e:
593+
print(e)
594+
exit(1)
595+
596+
print(
597+
yaml.dump(
598+
yaml.safe_load(str(validation_reference)),
599+
default_flow_style=False,
600+
sort_keys=False,
601+
)
602+
)
603+
604+
605+
@validation_references_cmd.command(name="list")
606+
@tagsOption
607+
@click.pass_context
608+
def validation_references_list(ctx: click.Context, tags: list[str]):
609+
"""
610+
[Experimental] List all validation references
611+
"""
612+
store = create_feature_store(ctx)
613+
table = []
614+
tags_filter = utils.tags_list_to_dict(tags)
615+
for validation_reference in store.list_validation_references(tags=tags_filter):
616+
table.append([validation_reference.name])
617+
618+
from tabulate import tabulate
619+
620+
print(tabulate(table, headers=["NAME"], tablefmt="plain"))
621+
622+
473623
@cli.command("plan", cls=NoOptionDefaultFormat)
474624
@click.option(
475625
"--skip-source-validation",

sdk/python/tests/integration/offline_store/test_validation.py

+17
Original file line numberDiff line numberDiff line change
@@ -305,6 +305,23 @@ def test_e2e_validation_via_cli(environment, universal_data_sources):
305305
assert p.returncode == 0, p.stderr.decode()
306306
assert "Validation successful" in p.stdout.decode(), p.stderr.decode()
307307

308+
p = runner.run(
309+
["saved-datasets", "describe", saved_dataset.name], cwd=local_repo.repo_path
310+
)
311+
assert p.returncode == 0, p.stderr.decode()
312+
313+
p = runner.run(
314+
["validation-references", "describe", reference.name],
315+
cwd=local_repo.repo_path,
316+
)
317+
assert p.returncode == 0, p.stderr.decode()
318+
319+
p = runner.run(
320+
["feature-services", "describe", feature_service.name],
321+
cwd=local_repo.repo_path,
322+
)
323+
assert p.returncode == 0, p.stderr.decode()
324+
308325
# make sure second validation will use cached profile
309326
shutil.rmtree(saved_dataset.storage.file_options.uri)
310327

sdk/python/tests/integration/registration/test_universal_cli.py

+16
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,12 @@ def test_universal_cli():
6363
assertpy.assert_that(result.returncode).is_equal_to(0)
6464
result = runner.run(["permissions", "list"], cwd=repo_path)
6565
assertpy.assert_that(result.returncode).is_equal_to(0)
66+
result = runner.run(["validation-references", "list"], cwd=repo_path)
67+
assertpy.assert_that(result.returncode).is_equal_to(0)
68+
result = runner.run(["stream-feature-views", "list"], cwd=repo_path)
69+
assertpy.assert_that(result.returncode).is_equal_to(0)
70+
result = runner.run(["saved-datasets", "list"], cwd=repo_path)
71+
assertpy.assert_that(result.returncode).is_equal_to(0)
6672

6773
# entity & feature view describe commands should succeed when objects exist
6874
result = runner.run(["entities", "describe", "driver"], cwd=repo_path)
@@ -95,6 +101,16 @@ def test_universal_cli():
95101
assertpy.assert_that(result.returncode).is_equal_to(1)
96102
result = runner.run(["permissions", "describe", "foo"], cwd=repo_path)
97103
assertpy.assert_that(result.returncode).is_equal_to(1)
104+
result = runner.run(
105+
["validation-references", "describe", "foo"], cwd=repo_path
106+
)
107+
assertpy.assert_that(result.returncode).is_equal_to(1)
108+
result = runner.run(
109+
["stream-feature-views", "describe", "foo"], cwd=repo_path
110+
)
111+
assertpy.assert_that(result.returncode).is_equal_to(1)
112+
result = runner.run(["saved-datasets", "describe", "foo"], cwd=repo_path)
113+
assertpy.assert_that(result.returncode).is_equal_to(1)
98114

99115
# Doing another apply should be a no op, and should not cause errors
100116
result = runner.run(["apply"], cwd=repo_path)

0 commit comments

Comments
 (0)