Skip to content

Commit d8d8cd1

Browse files
committed
feat(flags): Add feature flags enum
1 parent 2be9511 commit d8d8cd1

2 files changed

Lines changed: 85 additions & 0 deletions

File tree

src/lsst/cmservice/common/flags.py

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
import os
2+
from enum import IntFlag, auto
3+
from functools import reduce
4+
from typing import Any
5+
6+
from pydantic import Field, ValidationInfo, field_validator
7+
from pydantic_settings import BaseSettings, SettingsConfigDict
8+
9+
10+
class Features(IntFlag):
11+
"""A flag enum for setting specific application behaviors through feature
12+
flags.
13+
"""
14+
15+
API_V1 = auto()
16+
API_V2 = auto()
17+
DAEMON_CAMPAIGNS = auto()
18+
DAEMON_NODES = auto()
19+
DAEMON_V1 = auto()
20+
DAEMON_V2 = auto()
21+
WEBAPP_V1 = auto()
22+
23+
24+
class EnabledFeatures(BaseSettings):
25+
"""Pydantic Settings class for managing the enabled features of an
26+
application."""
27+
28+
model_config = SettingsConfigDict(
29+
env_prefix="FEATURE_",
30+
case_sensitive=False,
31+
extra="allow",
32+
)
33+
34+
enabled: Features = Field(
35+
description="A Flag Enum for enabled application features.", default=Features(0)
36+
)
37+
38+
@field_validator("enabled")
39+
@classmethod
40+
def determine_enabled_features(cls, data: Any, info: ValidationInfo) -> Any:
41+
"""Check all environment variables according to the `env_prefix` of the
42+
model config and set/unset feature flags for the matching feature.
43+
"""
44+
if (env_prefix := cls.model_config.get("env_prefix")) is None:
45+
return data
46+
enabled = set()
47+
disabled = set()
48+
for feature_env_var in filter(lambda k: k.startswith(env_prefix), os.environ.keys()):
49+
try:
50+
if os.getenv(feature_env_var, "false").strip().lower() in ("1", "t", "true", "yes", "on"):
51+
enabled.add(Features[feature_env_var.replace(env_prefix, "")])
52+
else:
53+
disabled.add(Features[feature_env_var.replace(env_prefix, "")])
54+
except KeyError:
55+
# no matching feature
56+
pass
57+
58+
data = reduce(lambda x, y: x | y, enabled, data)
59+
data = reduce(lambda x, y: x & ~y, disabled, data)
60+
return data

tests/common/test_flags.py

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
import pytest
2+
3+
from lsst.cmservice.common.flags import EnabledFeatures, Features
4+
5+
6+
def test_feature_flags(monkeypatch: pytest.MonkeyPatch) -> None:
7+
# Empty features have nothing set
8+
features = EnabledFeatures()
9+
assert Features.DAEMON_V1 not in features.enabled
10+
assert features.enabled.value == 0
11+
12+
# Setting features by env var should result in their being set in the
13+
# flags enum. Nonexistent features should not be an error.
14+
monkeypatch.setenv("FEATURE_DAEMON_V2", "1")
15+
monkeypatch.setenv("FEATURE_DAEMON_CAMPAIGNS", "on")
16+
monkeypatch.setenv("FEATURE_NO_SUCH_FEATURE", "true")
17+
features = EnabledFeatures()
18+
assert Features.DAEMON_V2 in features.enabled
19+
assert Features.DAEMON_CAMPAIGNS in features.enabled
20+
21+
# Disabling a feature by env var should result in their not being set in
22+
# the flags enum, overriding the default or initial value
23+
monkeypatch.setenv("FEATURE_WEBAPP_V1", "0")
24+
features = EnabledFeatures(enabled=Features.WEBAPP_V1)
25+
assert Features.WEBAPP_V1 not in features.enabled

0 commit comments

Comments
 (0)