Skip to content

Commit e98d65a

Browse files
authored
Add flags file to include experimental flags and test/usage flags (feast-dev#1864)
* Add flags file to include experimental flags and test/usage flags Signed-off-by: Danny Chiao <danny@tecton.ai> * Address comments and enable subfeature flags by default Signed-off-by: Danny Chiao <danny@tecton.ai> * Rewriting to use yaml as source of truth for feature flags Signed-off-by: Danny Chiao <danny@tecton.ai> * Fixing defaults Signed-off-by: Danny Chiao <danny@tecton.ai> * Revert usage change Signed-off-by: Danny Chiao <danny@tecton.ai> * Revert usage change 2 Signed-off-by: Danny Chiao <danny@tecton.ai> * Revert usage change 3 Signed-off-by: Danny Chiao <danny@tecton.ai> * Fix description of flags member Signed-off-by: Danny Chiao <danny@tecton.ai> * Not sorting yaml order when serializing Signed-off-by: Danny Chiao <danny@tecton.ai> * Comments Signed-off-by: Danny Chiao <danny@tecton.ai>
1 parent c6bcb46 commit e98d65a

File tree

8 files changed

+212
-6
lines changed

8 files changed

+212
-6
lines changed

Makefile

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,7 @@ benchmark-python:
5656
FEAST_USAGE=False IS_TEST=True pytest --integration --benchmark sdk/python/tests
5757

5858
test-python:
59-
FEAST_USAGE=False pytest -n 8 sdk/python/tests
59+
FEAST_USAGE=False IS_TEST=True pytest -n 8 sdk/python/tests
6060

6161
test-python-integration:
6262
FEAST_USAGE=False IS_TEST=True pytest -n 8 --integration sdk/python/tests

sdk/python/feast/cli.py

Lines changed: 108 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@
2121
import pkg_resources
2222
import yaml
2323

24-
from feast import utils
24+
from feast import flags, flags_helper, utils
2525
from feast.errors import FeastObjectNotFoundException, FeastProviderLoginError
2626
from feast.feature_store import FeatureStore
2727
from feast.repo_config import load_repo_config
@@ -423,5 +423,112 @@ def serve_command(ctx: click.Context, port: int):
423423
store.serve(port)
424424

425425

426+
@cli.group(name="alpha")
427+
def alpha_cmd():
428+
"""
429+
Access alpha features
430+
"""
431+
pass
432+
433+
434+
@alpha_cmd.command("list")
435+
@click.pass_context
436+
def list_alpha_features(ctx: click.Context):
437+
"""
438+
Lists all alpha features
439+
"""
440+
repo = ctx.obj["CHDIR"]
441+
cli_check_repo(repo)
442+
repo_path = str(repo)
443+
store = FeatureStore(repo_path=repo_path)
444+
445+
flags_to_show = flags.FLAG_NAMES.copy()
446+
flags_to_show.remove(flags.FLAG_ALPHA_FEATURES_NAME)
447+
print("Alpha features:")
448+
for flag in flags_to_show:
449+
enabled_string = (
450+
"enabled"
451+
if flags_helper.feature_flag_enabled(store.config, flag)
452+
else "disabled"
453+
)
454+
print(f"{flag}: {enabled_string}")
455+
456+
457+
@alpha_cmd.command("enable-all")
458+
@click.pass_context
459+
def enable_alpha_features(ctx: click.Context):
460+
"""
461+
Enables all alpha features
462+
"""
463+
repo = ctx.obj["CHDIR"]
464+
cli_check_repo(repo)
465+
repo_path = str(repo)
466+
store = FeatureStore(repo_path=repo_path)
467+
468+
if store.config.flags is None:
469+
store.config.flags = {}
470+
for flag_name in flags.FLAG_NAMES:
471+
store.config.flags[flag_name] = True
472+
store.config.write_to_path(Path(repo_path))
473+
474+
475+
@alpha_cmd.command("enable")
476+
@click.argument("name", type=click.STRING)
477+
@click.pass_context
478+
def enable_alpha_feature(ctx: click.Context, name: str):
479+
"""
480+
Enables an alpha feature
481+
"""
482+
if name not in flags.FLAG_NAMES:
483+
raise ValueError(f"Flag name, {name}, not valid.")
484+
485+
repo = ctx.obj["CHDIR"]
486+
cli_check_repo(repo)
487+
repo_path = str(repo)
488+
store = FeatureStore(repo_path=repo_path)
489+
490+
if store.config.flags is None:
491+
store.config.flags = {}
492+
store.config.flags[flags.FLAG_ALPHA_FEATURES_NAME] = True
493+
store.config.flags[name] = True
494+
store.config.write_to_path(Path(repo_path))
495+
496+
497+
@alpha_cmd.command("disable")
498+
@click.argument("name", type=click.STRING)
499+
@click.pass_context
500+
def disable_alpha_feature(ctx: click.Context, name: str):
501+
"""
502+
Disables an alpha feature
503+
"""
504+
if name not in flags.FLAG_NAMES:
505+
raise ValueError(f"Flag name, {name}, not valid.")
506+
507+
repo = ctx.obj["CHDIR"]
508+
cli_check_repo(repo)
509+
repo_path = str(repo)
510+
store = FeatureStore(repo_path=repo_path)
511+
512+
if store.config.flags is None or name not in store.config.flags:
513+
return
514+
store.config.flags[name] = False
515+
store.config.write_to_path(Path(repo_path))
516+
517+
518+
@alpha_cmd.command("disable-all")
519+
@click.pass_context
520+
def disable_alpha_features(ctx: click.Context):
521+
"""
522+
Disables all alpha features
523+
"""
524+
repo = ctx.obj["CHDIR"]
525+
cli_check_repo(repo)
526+
repo_path = str(repo)
527+
store = FeatureStore(repo_path=repo_path)
528+
529+
store.config.flags = None
530+
store.config.write_to_path(Path(repo_path))
531+
532+
426533
if __name__ == "__main__":
427534
cli()

sdk/python/feast/errors.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -258,3 +258,11 @@ def __init__(self, feature_view_name: str):
258258
super().__init__(
259259
f"The feature view name: {feature_view_name} refers to both an on-demand feature view and a feature view"
260260
)
261+
262+
263+
class ExperimentalFeatureNotEnabled(Exception):
264+
def __init__(self, feature_flag_name: str):
265+
super().__init__(
266+
f"You are attempting to use an experimental feature that is not enabled. Please run "
267+
f"`feast alpha enable {feature_flag_name}` "
268+
)

sdk/python/feast/feature_store.py

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,11 +22,12 @@
2222
from colorama import Fore, Style
2323
from tqdm import tqdm
2424

25-
from feast import feature_server, utils
25+
from feast import feature_server, flags, flags_helper, utils
2626
from feast.data_source import RequestDataSource
2727
from feast.entity import Entity
2828
from feast.errors import (
2929
EntityNotFoundException,
30+
ExperimentalFeatureNotEnabled,
3031
FeatureNameCollisionError,
3132
FeatureViewNotFoundException,
3233
RequestDataNotFoundInEntityDfException,
@@ -380,6 +381,12 @@ def apply(
380381

381382
views_to_update = [ob for ob in objects if isinstance(ob, FeatureView)]
382383
odfvs_to_update = [ob for ob in objects if isinstance(ob, OnDemandFeatureView)]
384+
if (
385+
not flags_helper.enable_on_demand_feature_views(self.config)
386+
and len(odfvs_to_update) > 0
387+
):
388+
raise ExperimentalFeatureNotEnabled(flags.FLAG_ON_DEMAND_TRANSFORM_NAME)
389+
383390
_validate_feature_views(views_to_update)
384391
entities_to_update = [ob for ob in objects if isinstance(ob, Entity)]
385392
services_to_update = [ob for ob in objects if isinstance(ob, FeatureService)]
@@ -986,6 +993,9 @@ def _augment_response_with_on_demand_transforms(
986993
@log_exceptions_and_usage
987994
def serve(self, port: int) -> None:
988995
"""Start the feature consumption server locally on a given port."""
996+
if not flags_helper.enable_python_feature_server(self.config):
997+
raise ExperimentalFeatureNotEnabled(flags.FLAG_PYTHON_FEATURE_SERVER_NAME)
998+
989999
feature_server.start_server(self, port)
9901000

9911001

sdk/python/feast/flags.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
FLAG_ALPHA_FEATURES_NAME = "alpha_features"
2+
FLAG_ON_DEMAND_TRANSFORM_NAME = "on_demand_transforms"
3+
FLAG_PYTHON_FEATURE_SERVER_NAME = "python_feature_server"
4+
ENV_FLAG_IS_TEST = "IS_TEST"
5+
6+
FLAG_NAMES = {
7+
FLAG_ALPHA_FEATURES_NAME,
8+
FLAG_ON_DEMAND_TRANSFORM_NAME,
9+
FLAG_PYTHON_FEATURE_SERVER_NAME,
10+
}

sdk/python/feast/flags_helper.py

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
import os
2+
3+
from feast import flags
4+
from feast.repo_config import RepoConfig
5+
6+
7+
def _env_flag_enabled(name: str) -> bool:
8+
return os.getenv(name, default="False") == "True"
9+
10+
11+
def feature_flag_enabled(repo_config: RepoConfig, flag_name: str) -> bool:
12+
if is_test():
13+
return True
14+
return (
15+
_alpha_feature_flag_enabled(repo_config)
16+
and repo_config.flags is not None
17+
and flag_name in repo_config.flags
18+
and repo_config.flags[flag_name]
19+
)
20+
21+
22+
def _alpha_feature_flag_enabled(repo_config: RepoConfig) -> bool:
23+
return (
24+
repo_config.flags is not None
25+
and flags.FLAG_ALPHA_FEATURES_NAME in repo_config.flags
26+
and repo_config.flags[flags.FLAG_ALPHA_FEATURES_NAME]
27+
)
28+
29+
30+
def is_test() -> bool:
31+
return _env_flag_enabled(flags.ENV_FLAG_IS_TEST)
32+
33+
34+
def enable_on_demand_feature_views(repo_config: RepoConfig) -> bool:
35+
return feature_flag_enabled(repo_config, flags.FLAG_ON_DEMAND_TRANSFORM_NAME)
36+
37+
38+
def enable_python_feature_server(repo_config: RepoConfig) -> bool:
39+
return feature_flag_enabled(repo_config, flags.FLAG_PYTHON_FEATURE_SERVER_NAME)

sdk/python/feast/infra/offline_stores/bigquery.py

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
import os
21
import uuid
32
from datetime import date, datetime, timedelta
43
from typing import Dict, List, Optional, Union
@@ -10,6 +9,7 @@
109
from pydantic.typing import Literal
1110
from tenacity import Retrying, retry_if_exception_type, stop_after_delay, wait_fixed
1211

12+
from feast import flags_helper
1313
from feast.data_source import DataSource
1414
from feast.errors import (
1515
BigQueryJobCancelled,
@@ -270,8 +270,7 @@ def block_until_done(
270270
"""
271271

272272
# For test environments, retry more aggressively
273-
is_test = os.getenv("IS_TEST", default="False") == "True"
274-
if is_test:
273+
if flags_helper.is_test():
275274
retry_cadence = 0.1
276275

277276
def _wait_until_done(bq_job):

sdk/python/feast/repo_config.py

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
from pydantic.error_wrappers import ErrorWrapper
1414
from pydantic.typing import Dict, Optional, Union
1515

16+
from feast import flags
1617
from feast.errors import (
1718
FeastFeatureServerTypeInvalidError,
1819
FeastFeatureServerTypeSetError,
@@ -98,6 +99,9 @@ class RepoConfig(FeastBaseModel):
9899
feature_server: Optional[Any]
99100
""" FeatureServerConfig: Feature server configuration (optional depending on provider) """
100101

102+
flags: Any
103+
""" Flags: Feature flags for experimental features (optional) """
104+
101105
repo_path: Optional[Path] = None
102106

103107
def __init__(self, **data: Any):
@@ -255,6 +259,35 @@ def _validate_project_name(cls, v):
255259
)
256260
return v
257261

262+
@validator("flags")
263+
def _validate_flags(cls, v):
264+
if not isinstance(v, Dict):
265+
return
266+
267+
for flag_name, val in v.items():
268+
if flag_name not in flags.FLAG_NAMES:
269+
raise ValueError(f"Flag name, {flag_name}, not valid.")
270+
if type(val) is not bool:
271+
raise ValueError(f"Flag value, {val}, not valid.")
272+
273+
return v
274+
275+
def write_to_path(self, repo_path: Path):
276+
config_path = repo_path / "feature_store.yaml"
277+
with open(config_path, mode="w") as f:
278+
yaml.dump(
279+
yaml.safe_load(
280+
self.json(
281+
exclude={"repo_path"},
282+
exclude_none=True,
283+
exclude_unset=True,
284+
exclude_defaults=True,
285+
)
286+
),
287+
f,
288+
sort_keys=False,
289+
)
290+
258291

259292
class FeastConfigError(Exception):
260293
def __init__(self, error_message, config_path):

0 commit comments

Comments
 (0)