Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 19 additions & 0 deletions protos/feast/registry/RegistryServer.proto
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,9 @@ service RegistryServer{
// FeatureView RPCs
rpc ApplyFeatureView (ApplyFeatureViewRequest) returns (google.protobuf.Empty) {}
rpc DeleteFeatureView (DeleteFeatureViewRequest) returns (google.protobuf.Empty) {}
rpc EnableFeatureView (EnableFeatureViewRequest) returns (google.protobuf.Empty) {}
rpc DisableFeatureView (DisableFeatureViewRequest) returns (google.protobuf.Empty) {}
rpc SetFeatureViewState (SetFeatureViewStateRequest) returns (google.protobuf.Empty) {}
rpc GetAnyFeatureView (GetAnyFeatureViewRequest) returns (GetAnyFeatureViewResponse) {}
rpc ListAllFeatureViews (ListAllFeatureViewsRequest) returns (ListAllFeatureViewsResponse) {}

Expand Down Expand Up @@ -262,6 +265,22 @@ message DeleteFeatureViewRequest {
bool commit = 3;
}

message EnableFeatureViewRequest {
string name = 1;
string project = 2;
}

message DisableFeatureViewRequest {
string name = 1;
string project = 2;
}

message SetFeatureViewStateRequest {
string name = 1;
string project = 2;
feast.core.FeatureViewState state = 3;
}

message AnyFeatureView {
oneof any_feature_view {
feast.core.FeatureView feature_view = 1;
Expand Down
48 changes: 48 additions & 0 deletions sdk/python/feast/api/registry/rest/feature_views.py
Original file line number Diff line number Diff line change
Expand Up @@ -397,4 +397,52 @@ def delete_feature_view(

return {"name": name, "project": project, "status": "deleted"}

@router.post("/feature_views/{name}/enable")
def enable_feature_view(
name: str,
project: str = Query(...),
):
req = RegistryServer_pb2.EnableFeatureViewRequest(
name=name,
project=project,
)
grpc_call(grpc_handler.EnableFeatureView, req)
return {"name": name, "project": project, "status": "enabled"}

@router.post("/feature_views/{name}/disable")
def disable_feature_view(
name: str,
project: str = Query(...),
):
req = RegistryServer_pb2.DisableFeatureViewRequest(
name=name,
project=project,
)
grpc_call(grpc_handler.DisableFeatureView, req)
return {"name": name, "project": project, "status": "disabled"}

@router.post("/feature_views/{name}/state")
def set_feature_view_state(
name: str,
state: str = Query(...),
project: str = Query(...),
):
from feast.feature_view import FeatureViewState

try:
state_enum = FeatureViewState[state.upper()]
except KeyError:
raise HTTPException(
status_code=400,
detail=f"Invalid state '{state}'.",
)

req = RegistryServer_pb2.SetFeatureViewStateRequest(
name=name,
project=project,
state=state_enum.to_proto(),
)
grpc_call(grpc_handler.SetFeatureViewState, req)
return {"name": name, "project": project, "status": state.upper()}

return router
108 changes: 108 additions & 0 deletions sdk/python/feast/cli/on_demand_feature_views.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,13 @@
import sys

import click
import yaml

from feast import utils
from feast.cli.cli_options import tagsOption
from feast.errors import FeastObjectNotFoundException
from feast.feature_view import _VALID_STATE_TRANSITIONS, FeatureViewState
from feast.on_demand_feature_view import OnDemandFeatureView
from feast.repo_operations import create_feature_store


Expand Down Expand Up @@ -55,3 +59,107 @@ def on_demand_feature_view_list(ctx: click.Context, tags: list[str]):
from tabulate import tabulate

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


@on_demand_feature_views_cmd.command("enable")
@click.argument("name", type=click.STRING)
@click.pass_context
def on_demand_feature_view_enable(ctx: click.Context, name: str):
"""
[Experimental] Enable an on demand feature view for serving.
"""
store = create_feature_store(ctx)

try:
fv = store.registry.get_any_feature_view(name, store.project)
except FeastObjectNotFoundException as e:
print(e)
sys.exit(1)

if not isinstance(fv, OnDemandFeatureView):
print(f"Feature view '{name}' is not an on demand feature view.")
return

if fv.enabled:
print(f"On demand feature view '{name}' is already enabled.")
return

fv.enabled = True
store.registry.apply_feature_view(fv, store.project)
print(f"On demand feature view {name} has been enabled.")


@on_demand_feature_views_cmd.command("disable")
@click.argument("name", type=click.STRING)
@click.pass_context
def on_demand_feature_view_disable(ctx: click.Context, name: str):
"""
[Experimental] Disable an on demand feature view for serving.
"""
store = create_feature_store(ctx)

try:
fv = store.registry.get_any_feature_view(name, store.project)
except FeastObjectNotFoundException as e:
print(e)
sys.exit(1)

if not isinstance(fv, OnDemandFeatureView):
print(f"Feature view '{name}' is not an on demand feature view.")
return

if not fv.enabled:
print(f"On demand feature view '{name}' is already disabled.")
return

fv.enabled = False
store.registry.apply_feature_view(fv, store.project)
print(f"On demand feature view {name} has been disabled.")


@on_demand_feature_views_cmd.command("set-state")
@click.argument("name", type=click.STRING)
@click.argument(
"state",
type=click.Choice(
["CREATED", "GENERATED", "MATERIALIZING", "AVAILABLE_ONLINE"],
case_sensitive=False,
),
)
@click.pass_context
def on_demand_feature_view_set_state(ctx: click.Context, name: str, state: str):
"""
[Experimental] Set the lifecycle state of an on demand feature view.
"""
store = create_feature_store(ctx)

try:
fv = store.registry.get_any_feature_view(name, store.project)
except FeastObjectNotFoundException as e:
print(e)
sys.exit(1)

if not isinstance(fv, OnDemandFeatureView):
print(f"Feature view '{name}' is not an on demand feature view.")
return

new_state = FeatureViewState[state.upper()]
if fv.state == new_state:
print(
f"On demand feature view '{name}' is already in state '{new_state.name}'."
)
return

if not fv.state.can_transition_to(new_state):
current = fv.state.name
allowed = _VALID_STATE_TRANSITIONS.get(fv.state, set())
allowed_states = ", ".join(sorted(s.name for s in allowed)) or "none"
print(
f"Invalid state transition from '{current}' to '{new_state.name}'. "
f"Allowed transitions from '{current}' are: {allowed_states}."
)
return

fv.state = new_state
store.registry.apply_feature_view(fv, store.project)
print(f"On demand feature view {name} state has been set to {new_state.name}.")
104 changes: 104 additions & 0 deletions sdk/python/feast/cli/stream_feature_views.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,9 @@
from feast import utils
from feast.cli.cli_options import tagsOption
from feast.errors import FeastObjectNotFoundException
from feast.feature_view import _VALID_STATE_TRANSITIONS, FeatureViewState
from feast.repo_operations import create_feature_store
from feast.stream_feature_view import StreamFeatureView


@click.group(name="stream-feature-views")
Expand Down Expand Up @@ -55,3 +57,105 @@ def stream_feature_views_list(ctx: click.Context, tags: list[str]):
from tabulate import tabulate

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


@stream_feature_views_cmd.command("enable")
@click.argument("name", type=click.STRING)
@click.pass_context
def stream_feature_views_enable(ctx: click.Context, name: str):
"""
[Experimental] Enable a stream feature view for serving and materialization.
"""
store = create_feature_store(ctx)

try:
fv = store.registry.get_any_feature_view(name, store.project)
except FeastObjectNotFoundException as e:
print(e)
exit(1)

if not isinstance(fv, StreamFeatureView):
print(f"Feature view '{name}' is not a stream feature view.")
return

if fv.enabled:
print(f"Stream feature view '{name}' is already enabled.")
return

fv.enabled = True
store.registry.apply_feature_view(fv, store.project)
print(f"Stream feature view '{name}' has been enabled.")


@stream_feature_views_cmd.command("disable")
@click.argument("name", type=click.STRING)
@click.pass_context
def stream_feature_views_disable(ctx: click.Context, name: str):
"""
[Experimental] Disable a stream feature view for serving and materialization.
"""
store = create_feature_store(ctx)

try:
fv = store.registry.get_any_feature_view(name, store.project)
except FeastObjectNotFoundException as e:
print(e)
exit(1)

if not isinstance(fv, StreamFeatureView):
print(f"Feature view '{name}' is not a stream feature view.")
return

if not fv.enabled:
print(f"Stream feature view '{name}' is already disabled.")
return

fv.enabled = False
store.registry.apply_feature_view(fv, store.project)
print(f"Stream feature view '{name}' has been disabled.")


@stream_feature_views_cmd.command("set-state")
@click.argument("name", type=click.STRING)
@click.argument(
"state",
type=click.Choice(
["CREATED", "GENERATED", "MATERIALIZING", "AVAILABLE_ONLINE"],
case_sensitive=False,
),
)
@click.pass_context
def stream_feature_views_set_state(ctx: click.Context, name: str, state: str):
"""
[Experimental] Set the lifecycle state of a stream feature view.
"""
store = create_feature_store(ctx)

try:
fv = store.registry.get_any_feature_view(name, store.project)
except FeastObjectNotFoundException as e:
print(e)
exit(1)

if not isinstance(fv, StreamFeatureView):
print(f"Feature view '{name}' is not a stream feature view.")
return

new_state = FeatureViewState[state.upper()]
if fv.state == new_state:
print(f"Stream feature view '{name}' is already in state '{new_state.name}'.")
return

if not fv.state.can_transition_to(new_state):
current = fv.state.name
allowed = _VALID_STATE_TRANSITIONS.get(fv.state, set())
allowed_names = ", ".join(sorted(s.name for s in allowed)) or "none"
print(
f"Invalid state transition: {current} -> {new_state.name} (allowed: {allowed_names})"
f"Allowed transitions from {current}: {allowed_names}."
)
return

fv.state = new_state
store.registry.apply_feature_view(fv, store.project)
print(f"Stream feature view '{name}' state has been set to '{new_state.name}'.")
12 changes: 12 additions & 0 deletions sdk/python/feast/feature_store.py
Original file line number Diff line number Diff line change
Expand Up @@ -849,6 +849,10 @@ def enable_feature_view(self, name: str):
name: Name of feature view.
"""
fv = self.registry.get_any_feature_view(name, self.project)
if not isinstance(fv, (FeatureView, OnDemandFeatureView, StreamFeatureView)):
raise ValueError(
f"Feature view '{name}' does not support enabled/disabled."
)
fv.enabled = True # type: ignore[attr-defined]
self.registry.apply_feature_view(fv, self.project)

Expand All @@ -860,6 +864,10 @@ def disable_feature_view(self, name: str):
name: Name of feature view.
"""
fv = self.registry.get_any_feature_view(name, self.project)
if not isinstance(fv, (FeatureView, OnDemandFeatureView, StreamFeatureView)):
raise ValueError(
f"Feature view '{name}' does not support enabled/disabled."
)
fv.enabled = False # type: ignore[attr-defined]
self.registry.apply_feature_view(fv, self.project)

Expand All @@ -872,6 +880,10 @@ def set_feature_view_state(self, name: str, state: FeatureViewState):
state: Target state.
"""
fv = self.registry.get_any_feature_view(name, self.project)
if not isinstance(fv, (FeatureView, OnDemandFeatureView, StreamFeatureView)):
raise ValueError(
f"Feature view '{name}' does not support state management."
)
if not fv.state.can_transition_to(state): # type: ignore[attr-defined]
raise ValueError(
f"Invalid state transition: {fv.state.name} -> {state.name}." # type: ignore[attr-defined]
Expand Down
16 changes: 13 additions & 3 deletions sdk/python/feast/protos/feast/core/Aggregation_pb2.py

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading