Skip to content

Commit a5ddc98

Browse files
committed
feat: Extend enable/disable/set-state to all feature view types and REST APIs
Signed-off-by: RutujaPathade <73137503+RutujaPathade@users.noreply.github.com>
1 parent 5f1fa0d commit a5ddc98

3 files changed

Lines changed: 346 additions & 14 deletions

File tree

sdk/python/feast/api/registry/rest/feature_views.py

Lines changed: 154 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -349,6 +349,160 @@ def apply_feature_view(body: ApplyFeatureViewRequestBody):
349349
},
350350
)
351351

352+
@router.put("/feature_views/{name}/enable")
353+
def enable_feature_view(
354+
name: str,
355+
project: str = Query(...),
356+
):
357+
from feast.feature_view import FeatureView
358+
from feast.on_demand_feature_view import OnDemandFeatureView
359+
from feast.stream_feature_view import StreamFeatureView
360+
361+
req = RegistryServer_pb2.GetAnyFeatureViewRequest(
362+
name=name,
363+
project=project,
364+
)
365+
resp = grpc_call(grpc_handler.GetAnyFeatureView, req)
366+
any_fv = resp.any_feature_view
367+
368+
if any_fv.HasField("feature_view"):
369+
fv = FeatureView.from_proto(any_fv.feature_view)
370+
elif any_fv.HasField("on_demand_feature_view"):
371+
fv = OnDemandFeatureView.from_proto(any_fv.on_demand_feature_view)
372+
elif any_fv.HasField("stream_feature_view"):
373+
fv = StreamFeatureView.from_proto(any_fv.stream_feature_view)
374+
else:
375+
return JSONResponse(
376+
status_code=404,
377+
content={"error": f"Feature view '{name}' not found."},
378+
)
379+
380+
fv.enabled = True
381+
apply_req = RegistryServer_pb2.ApplyFeatureViewRequest(
382+
project=project, commit=True
383+
)
384+
if isinstance(fv, StreamFeatureView):
385+
apply_req.stream_feature_view.CopyFrom(fv.to_proto())
386+
elif isinstance(fv, OnDemandFeatureView):
387+
apply_req.on_demand_feature_view.CopyFrom(fv.to_proto())
388+
else:
389+
apply_req.feature_view.CopyFrom(fv.to_proto())
390+
grpc_call(grpc_handler.ApplyFeatureView, apply_req)
391+
392+
return {"name": name, "project": project, "enabled": True}
393+
394+
@router.put("/feature_views/{name}/disable")
395+
def disable_feature_view(
396+
name: str,
397+
project: str = Query(...),
398+
):
399+
from feast.feature_view import FeatureView
400+
from feast.on_demand_feature_view import OnDemandFeatureView
401+
from feast.stream_feature_view import StreamFeatureView
402+
403+
req = RegistryServer_pb2.GetAnyFeatureViewRequest(
404+
name=name,
405+
project=project,
406+
)
407+
resp = grpc_call(grpc_handler.GetAnyFeatureView, req)
408+
any_fv = resp.any_feature_view
409+
410+
if any_fv.HasField("feature_view"):
411+
fv = FeatureView.from_proto(any_fv.feature_view)
412+
elif any_fv.HasField("on_demand_feature_view"):
413+
fv = OnDemandFeatureView.from_proto(any_fv.on_demand_feature_view)
414+
elif any_fv.HasField("stream_feature_view"):
415+
fv = StreamFeatureView.from_proto(any_fv.stream_feature_view)
416+
else:
417+
return JSONResponse(
418+
status_code=404,
419+
content={"error": f"Feature view '{name}' not found."},
420+
)
421+
422+
fv.enabled = False
423+
apply_req = RegistryServer_pb2.ApplyFeatureViewRequest(
424+
project=project, commit=True
425+
)
426+
if isinstance(fv, StreamFeatureView):
427+
apply_req.stream_feature_view.CopyFrom(fv.to_proto())
428+
elif isinstance(fv, OnDemandFeatureView):
429+
apply_req.on_demand_feature_view.CopyFrom(fv.to_proto())
430+
else:
431+
apply_req.feature_view.CopyFrom(fv.to_proto())
432+
grpc_call(grpc_handler.ApplyFeatureView, apply_req)
433+
434+
return {"name": name, "project": project, "enabled": False}
435+
436+
@router.put("/feature_views/{name}/set-state")
437+
def set_feature_view_state(
438+
name: str,
439+
state: str = Query(...),
440+
project: str = Query(...),
441+
):
442+
from feast.feature_view import (
443+
_VALID_STATE_TRANSITIONS,
444+
FeatureView,
445+
FeatureViewState,
446+
)
447+
from feast.on_demand_feature_view import OnDemandFeatureView
448+
from feast.stream_feature_view import StreamFeatureView
449+
450+
try:
451+
new_state = FeatureViewState[state.upper()]
452+
except KeyError:
453+
return JSONResponse(
454+
status_code=400,
455+
content={
456+
"error": f"Invalid state '{state}'. "
457+
f"Valid states: CREATED, GENERATED, MATERIALIZING, AVAILABLE_ONLINE."
458+
},
459+
)
460+
461+
req = RegistryServer_pb2.GetAnyFeatureViewRequest(
462+
name=name,
463+
project=project,
464+
)
465+
resp = grpc_call(grpc_handler.GetAnyFeatureView, req)
466+
any_fv = resp.any_feature_view
467+
468+
if any_fv.HasField("feature_view"):
469+
fv = FeatureView.from_proto(any_fv.feature_view)
470+
elif any_fv.HasField("on_demand_feature_view"):
471+
fv = OnDemandFeatureView.from_proto(any_fv.on_demand_feature_view)
472+
elif any_fv.HasField("stream_feature_view"):
473+
fv = StreamFeatureView.from_proto(any_fv.stream_feature_view)
474+
else:
475+
return JSONResponse(
476+
status_code=404,
477+
content={"error": f"Feature view '{name}' not found."},
478+
)
479+
480+
if not fv.state.can_transition_to(new_state):
481+
current = fv.state.name
482+
allowed = _VALID_STATE_TRANSITIONS.get(fv.state, set())
483+
allowed_names = ", ".join(sorted(s.name for s in allowed)) or "none"
484+
return JSONResponse(
485+
status_code=400,
486+
content={
487+
"error": f"Invalid state transition: {current} -> {new_state.name}. "
488+
f"Allowed transitions from {current}: {allowed_names}."
489+
},
490+
)
491+
492+
fv.state = new_state
493+
apply_req = RegistryServer_pb2.ApplyFeatureViewRequest(
494+
project=project, commit=True
495+
)
496+
if isinstance(fv, StreamFeatureView):
497+
apply_req.stream_feature_view.CopyFrom(fv.to_proto())
498+
elif isinstance(fv, OnDemandFeatureView):
499+
apply_req.on_demand_feature_view.CopyFrom(fv.to_proto())
500+
else:
501+
apply_req.feature_view.CopyFrom(fv.to_proto())
502+
grpc_call(grpc_handler.ApplyFeatureView, apply_req)
503+
504+
return {"name": name, "project": project, "state": new_state.name}
505+
352506
@router.delete("/feature_views/{name}")
353507
def delete_feature_view(
354508
name: str,

sdk/python/feast/cli/on_demand_feature_views.py

Lines changed: 96 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,12 @@
1+
import sys
2+
13
import click
24
import yaml
35

46
from feast import utils
57
from feast.cli.cli_options import tagsOption
68
from feast.errors import FeastObjectNotFoundException
9+
from feast.feature_view import _VALID_STATE_TRANSITIONS, FeatureViewState
710
from feast.repo_operations import create_feature_store
811

912

@@ -30,13 +33,12 @@ def on_demand_feature_view_describe(ctx: click.Context, name: str):
3033
print(e)
3134
exit(1)
3235

33-
print(
34-
yaml.dump(
35-
yaml.safe_load(str(on_demand_feature_view)),
36-
default_flow_style=False,
37-
sort_keys=False,
38-
)
39-
)
36+
data = yaml.safe_load(str(on_demand_feature_view))
37+
if hasattr(on_demand_feature_view, "enabled"):
38+
data["enabled"] = on_demand_feature_view.enabled
39+
if hasattr(on_demand_feature_view, "state"):
40+
data["state"] = on_demand_feature_view.state.name
41+
print(yaml.dump(data, default_flow_style=False, sort_keys=False))
4042

4143

4244
@on_demand_feature_views_cmd.command(name="list")
@@ -55,3 +57,90 @@ def on_demand_feature_view_list(ctx: click.Context, tags: list[str]):
5557
from tabulate import tabulate
5658

5759
print(tabulate(table, headers=["NAME"], tablefmt="plain"))
60+
61+
62+
@on_demand_feature_views_cmd.command("enable")
63+
@click.argument("name", type=click.STRING)
64+
@click.pass_context
65+
def on_demand_feature_view_enable(ctx: click.Context, name: str):
66+
"""
67+
Enable an on demand feature view.
68+
"""
69+
store = create_feature_store(ctx)
70+
try:
71+
fv = store.get_on_demand_feature_view(name)
72+
except FeastObjectNotFoundException as e:
73+
print(e)
74+
sys.exit(1)
75+
76+
if fv.enabled:
77+
print(f"On demand feature view '{name}' is already enabled.")
78+
return
79+
80+
fv.enabled = True
81+
store.registry.apply_feature_view(fv, store.project)
82+
print(f"On demand feature view '{name}' has been enabled.")
83+
84+
85+
@on_demand_feature_views_cmd.command("disable")
86+
@click.argument("name", type=click.STRING)
87+
@click.pass_context
88+
def on_demand_feature_view_disable(ctx: click.Context, name: str):
89+
"""
90+
Disable an on demand feature view.
91+
"""
92+
store = create_feature_store(ctx)
93+
try:
94+
fv = store.get_on_demand_feature_view(name)
95+
except FeastObjectNotFoundException as e:
96+
print(e)
97+
sys.exit(1)
98+
99+
if not fv.enabled:
100+
print(f"On demand feature view '{name}' is already disabled.")
101+
return
102+
103+
fv.enabled = False
104+
store.registry.apply_feature_view(fv, store.project)
105+
print(f"On demand feature view '{name}' has been disabled.")
106+
107+
108+
@on_demand_feature_views_cmd.command("set-state")
109+
@click.argument("name", type=click.STRING)
110+
@click.argument(
111+
"state",
112+
type=click.Choice(
113+
["CREATED", "GENERATED", "MATERIALIZING", "AVAILABLE_ONLINE"],
114+
case_sensitive=False,
115+
),
116+
)
117+
@click.pass_context
118+
def on_demand_feature_view_set_state(ctx: click.Context, name: str, state: str):
119+
"""
120+
Set the lifecycle state of an on demand feature view.
121+
"""
122+
store = create_feature_store(ctx)
123+
try:
124+
fv = store.get_on_demand_feature_view(name)
125+
except FeastObjectNotFoundException as e:
126+
print(e)
127+
sys.exit(1)
128+
129+
new_state = FeatureViewState[state.upper()]
130+
if fv.state == new_state:
131+
print(f"On demand feature view '{name}' is already in state {new_state.name}.")
132+
return
133+
134+
if not fv.state.can_transition_to(new_state):
135+
current = fv.state.name
136+
allowed = _VALID_STATE_TRANSITIONS.get(fv.state, set())
137+
allowed_names = ", ".join(sorted(s.name for s in allowed)) or "none"
138+
print(
139+
f"Invalid state transition: {current} -> {new_state.name}. "
140+
f"Allowed transitions from {current}: {allowed_names}."
141+
)
142+
return
143+
144+
fv.state = new_state
145+
store.registry.apply_feature_view(fv, store.project)
146+
print(f"On demand feature view '{name}' state set to {new_state.name}.")

0 commit comments

Comments
 (0)