checker.py
 1  """
 2  FeatureChecker: two-tier feature flag evaluation.
 3  
 4  Evaluation priority (highest to lowest):
 5    1. Environment variable  SAM_FEATURE_<UPPER_KEY>=true|false
 6    2. Registry default      FeatureDefinition.default
 7  """
 8  
 9  from __future__ import annotations
10  
11  import logging
12  import os
13  from typing import TYPE_CHECKING, Optional
14  
15  if TYPE_CHECKING:
16      from .registry import FeatureRegistry
17  
18  logger = logging.getLogger(__name__)
19  
20  # Prefix for environment variable overrides (e.g., SAM_FEATURE_MY_FLAG=true)
21  _FLAG_PREFIX = "SAM_FEATURE_"
22  
23  
24  class FeatureChecker:
25      """
26      Evaluates feature flags according to the two-tier priority chain.
27  
28      Parameters
29      ----------
30      registry:
31          The FeatureRegistry holding all known FeatureDefinition objects.
32      """
33  
34      def __init__(self, registry: "FeatureRegistry") -> None:
35          self._registry = registry
36  
37      def is_known_flag(self, key: str) -> bool:
38          """Return True if key is registered in the registry."""
39          return self._registry.get(key) is not None
40  
41      def is_enabled(self, key: str) -> bool:
42          """
43          Return True if the feature identified by key is currently enabled.
44  
45          Unknown keys always return False with a warning log.
46          """
47          definition = self._registry.get(key)
48          if definition is None:
49              logger.warning("is_enabled() called for unknown feature key '%s'", key)
50              return False
51  
52          env_value = self._check_env_var(key)
53          if env_value is not None:
54              logger.debug("Feature '%s' resolved from env var: %s", key, env_value)
55              return env_value
56  
57          logger.debug(
58              "Feature '%s' resolved from registry default: %s",
59              key,
60              definition.default,
61          )
62          return definition.default
63  
64      def all_flags(self) -> dict[str, bool]:
65          """Return {key: is_enabled(key)} for every registered feature."""
66          return {defn.key: self.is_enabled(defn.key) for defn in self._registry.all()}
67  
68      @property
69      def registry(self) -> "FeatureRegistry":
70          """Return the underlying FeatureRegistry."""
71          return self._registry
72  
73      def has_env_override(self, key: str) -> bool:
74          """Return True if a SAM_FEATURE_<KEY> env var is set for key."""
75          return self._check_env_var(key) is not None
76  
77      def load_from_yaml(self, yaml_path) -> None:
78          """Load additional flag definitions from a YAML file into the registry."""
79          self._registry.load_from_yaml(yaml_path)
80  
81      @staticmethod
82      def _check_env_var(key: str) -> Optional[bool]:
83          """
84          Look up SAM_FEATURE_<UPPER_KEY> in the environment.
85  
86          Returns None when the variable is absent so callers can fall through.
87          Recognised truthy values: 1, true (case-insensitive).
88          Everything else is treated as false.
89          """
90          env_key = _FLAG_PREFIX + key.upper().replace("-", "_")
91          raw = os.environ.get(env_key)
92          if raw is None:
93              return None
94          return raw.strip().lower() in {"1", "true"}