Skip to content
Merged
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
2 changes: 2 additions & 0 deletions docs/roadmap.md
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,8 @@ The list below contains the functionality that contributors are planning to deve
* [x] [Python feature server](https://docs.feast.dev/reference/feature-servers/python-feature-server)
* [x] [Java feature server (alpha)](https://github.com/feast-dev/feast/blob/master/infra/charts/feast/README.md)
* [x] [Go feature server (alpha)](https://docs.feast.dev/reference/feature-servers/go-feature-server)
* [x] [Offline Feature Server (alpha)](https://docs.feast.dev/reference/feature-servers/offline-feature-server)
* [x] [Registry server (alpha)](https://github.com/feast-dev/feast/blob/master/docs/reference/feature-servers/registry-server.md)
* **Data Quality Management (See [RFC](https://docs.google.com/document/d/110F72d4NTv80p35wDSONxhhPBqWRwbZXG4f9mNEMd98/edit))**
* [x] Data profiling and validation (Great Expectations)
* **Feature Discovery and Governance**
Expand Down
1 change: 1 addition & 0 deletions infra/templates/README.md.jinja2
Original file line number Diff line number Diff line change
Expand Up @@ -152,6 +152,7 @@ pprint(feature_vector)
Please refer to the official documentation at [Documentation](https://docs.feast.dev/)
* [Quickstart](https://docs.feast.dev/getting-started/quickstart)
* [Tutorials](https://docs.feast.dev/tutorials/tutorials-overview)
* [Examples](https://github.com/feast-dev/feast/tree/master/examples)
* [Running Feast with Snowflake/GCP/AWS](https://docs.feast.dev/how-to-guides/feast-snowflake-gcp-aws)
* [Change Log](https://github.com/feast-dev/feast/blob/master/CHANGELOG.md)

Expand Down
170 changes: 170 additions & 0 deletions sdk/python/feast/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
from typing import Any, List, Optional

import click
import pandas as pd
import yaml
from bigtree import Node
from colorama import Fore, Style
Expand Down Expand Up @@ -537,6 +538,175 @@ def feature_view_list(ctx: click.Context, tags: list[str]):
print(tabulate(table, headers=["NAME", "ENTITIES", "TYPE"], tablefmt="plain"))


@cli.group(name="features")
def features_cmd():
"""
Access features
"""
pass


@features_cmd.command(name="list")
@click.option(
"--output",
type=click.Choice(["table", "json"], case_sensitive=False),
default="table",
show_default=True,
help="Output format",
)
@click.pass_context
def features_list(ctx: click.Context, output: str):
"""
List all features
"""
store = create_feature_store(ctx)
feature_views = [
*store.list_batch_feature_views(),
*store.list_on_demand_feature_views(),
*store.list_stream_feature_views(),
]
feature_list = []
for fv in feature_views:
for feature in fv.features:
feature_list.append([feature.name, fv.name, str(feature.dtype)])

if output == "json":
json_output = [
{"feature_name": fn, "feature_view": fv, "dtype": dt}
for fv, fn, dt in feature_list
]
click.echo(json.dumps(json_output, indent=4))
else:
from tabulate import tabulate

click.echo(
tabulate(
feature_list,
headers=["Feature", "Feature View", "Data Type"],
tablefmt="plain",
)
)


@features_cmd.command("describe")
@click.argument("feature_name", type=str)
@click.pass_context
def describe_feature(ctx: click.Context, feature_name: str):
"""
Describe a specific feature by name
"""
store = create_feature_store(ctx)
feature_views = [
*store.list_batch_feature_views(),
*store.list_on_demand_feature_views(),
*store.list_stream_feature_views(),
]

feature_details = []
for fv in feature_views:
for feature in fv.features:
if feature.name == feature_name:
feature_details.append(
{
"Feature Name": feature.name,
"Feature View": fv.name,
"Data Type": str(feature.dtype),
"Description": getattr(feature, "description", "N/A"),
"Online Store": getattr(fv, "online", "N/A"),
"Source": json.loads(str(getattr(fv, "batch_source", "N/A"))),
}
)
if not feature_details:
click.echo(f"Feature '{feature_name}' not found in any feature view.")
return

click.echo(json.dumps(feature_details, indent=4))


@cli.command("get-online-features")
@click.option(
"--entities",
"-e",
type=str,
multiple=True,
required=True,
help="Entity key-value pairs (e.g., driver_id=1001)",
)
@click.option(
"--features",
"-f",
multiple=True,
required=True,
help="Features to retrieve. (e.g.,feature-view:feature-name) ex: driver_hourly_stats:conv_rate",
)
@click.pass_context
def get_online_features(ctx: click.Context, entities: List[str], features: List[str]):
"""
Fetch online feature values for a given entity ID
"""
store = create_feature_store(ctx)
entity_dict: dict[str, List[str]] = {}
for entity in entities:
try:
key, value = entity.split("=")
if key not in entity_dict:
entity_dict[key] = []
entity_dict[key].append(value)
except ValueError:
click.echo(f"Invalid entity format: {entity}. Use key=value format.")
return
entity_rows = [
dict(zip(entity_dict.keys(), values)) for values in zip(*entity_dict.values())
]
feature_vector = store.get_online_features(
features=list(features),
entity_rows=entity_rows,
).to_dict()

click.echo(json.dumps(feature_vector, indent=4))


@cli.command(name="get-historical-features")
@click.option(
"--dataframe",
"-d",
type=str,
required=True,
help='JSON string containing entities and timestamps. Example: \'[{"event_timestamp": "2025-03-29T12:00:00", "driver_id": 1001}]\'',
)
@click.option(
"--features",
"-f",
multiple=True,
required=True,
help="Features to retrieve. feature-view:feature-name ex: driver_hourly_stats:conv_rate",
)
@click.pass_context
def get_historical_features(ctx: click.Context, dataframe: str, features: List[str]):
"""
Fetch historical feature values for a given entity ID
"""
store = create_feature_store(ctx)
try:
entity_list = json.loads(dataframe)
if not isinstance(entity_list, list):
raise ValueError("Entities must be a list of dictionaries.")

entity_df = pd.DataFrame(entity_list)
entity_df["event_timestamp"] = pd.to_datetime(entity_df["event_timestamp"])

except Exception as e:
click.echo(f"Error parsing entities JSON: {e}", err=True)
return

feature_vector = store.get_historical_features(
entity_df=entity_df,
features=list(features),
).to_df()

click.echo(feature_vector.to_json(orient="records", indent=4))


@cli.group(name="on-demand-feature-views")
def on_demand_feature_views_cmd():
"""
Expand Down