Coverage for src/lexigram/features/di/provider.py: 95%
60 statements
« prev ^ index » next coverage.py v7.15.4, created at 2026-08-26 02:04 +0800
« prev ^ index » next coverage.py v7.15.4, created at 2026-08-26 02:04 +0800
1"""DI provider for the feature-flag subsystem.
3:class:`FeatureFlagsProvider` registers a :class:`~lexigram.features.manager.FlagManager`
4singleton in the container so application services can resolve it via DI
5rather than constructing it manually.
7The default configuration uses a
8:class:`~lexigram.features.backends.local.LocalProvider` for
9the simple boolean contract and a
10:class:`~lexigram.features.backends.local.LocalProvider` backed by
11``config.initial_flags`` for the rich evaluation API.
12"""
14from __future__ import annotations
16from typing import TYPE_CHECKING, Any, Self
18from lexigram.contracts.core import ProviderPriority
19from lexigram.contracts.core.health import HealthCheckResult, HealthStatus
20from lexigram.contracts.feature_flags import FlagProviderProtocol
21from lexigram.di.provider import Provider
22from lexigram.features.backends.local import LocalProvider
23from lexigram.features.config import FeatureFlagsConfig
24from lexigram.features.manager import FlagManager
25from lexigram.features.types import Flag, FlagType
26from lexigram.logging import get_logger
28if TYPE_CHECKING:
29 from lexigram.contracts.core.di import (
30 BootContainerProtocol,
31 ContainerRegistrarProtocol,
32 )
35logger = get_logger(__name__)
38class FeatureFlagsProvider(Provider):
39 """Provider that registers feature-flag infrastructure in the container.
41 Registers:
43 * ``FlagProviderProtocol`` (simple boolean contract) as a singleton backed by
44 :class:`~lexigram.features.backends.local.LocalProvider`.
45 * ``FlagManager`` as a singleton wrapping a
46 :class:`~lexigram.features.backends.local.LocalProvider` seeded from
47 :attr:`~lexigram.features.config.FeatureFlagsConfig.initial_flags`.
48 """
50 name = "features"
51 config_key: str | None = "features"
52 config_model: type | None = FeatureFlagsConfig
53 priority = ProviderPriority.INFRASTRUCTURE
55 def __init__(self, config: FeatureFlagsConfig | None = None) -> None:
56 """Create the feature-flags provider.
58 Args:
59 config: Optional feature-flag configuration. When omitted,
60 defaults are used (all flags disabled, cache TTL 60 s).
61 """
62 super().__init__()
63 self._config = config or FeatureFlagsConfig()
64 self._simple_provider: LocalProvider | None = None
65 self._manager: FlagManager | None = None
67 @classmethod
68 def from_config(cls, config: FeatureFlagsConfig, **context: Any) -> Self:
69 """Create provider from config object."""
70 return cls(config=config)
72 async def register(self, container: ContainerRegistrarProtocol) -> None:
73 """Register flag infrastructure in the container.
75 Registers ``FlagProviderProtocol`` (simple boolean API) and ``FlagManager``
76 (rich evaluation API) as singletons, seeded from the provider config.
77 """
78 container.singleton(FeatureFlagsConfig, self._config)
80 if not self._config.enabled:
81 logger.info("features_disabled", reason="FeatureFlagsConfig.enabled=False")
82 return
84 # Simple boolean provider for the FlagProviderProtocol contract.
85 simple = LocalProvider()
86 for flag_name, enabled in self._config.initial_flags.items():
87 simple.set_flag_sync(flag_name, enabled)
88 container.singleton(FlagProviderProtocol, simple)
89 self._simple_provider = simple
91 # Rich provider + manager for full evaluation API.
92 initial: dict[str, Flag] = {
93 flag_name: Flag(name=flag_name, type=FlagType.BOOLEAN, enabled=enabled)
94 for flag_name, enabled in self._config.initial_flags.items()
95 }
96 local = LocalProvider(initial)
97 manager = FlagManager(
98 local,
99 cache_ttl=self._config.cache_ttl,
100 default_enabled=self._config.default_enabled,
101 )
102 container.singleton(FlagManager, manager)
103 from lexigram.contracts.feature_flags.protocols import FlagManagerProtocol
105 container.singleton(FlagManagerProtocol, manager)
106 self._manager = manager
108 async def boot(self, container: BootContainerProtocol) -> None:
109 """Wire the event bus into the flag manager when one is available."""
110 if self._manager is None:
111 return
112 try:
113 from lexigram.contracts.events.protocols import EventBusProtocol
115 event_bus = await container.resolve(EventBusProtocol)
116 self._manager._event_bus = event_bus
117 except Exception as e: # noqa: BLE001
118 logger.debug(
119 "event_bus_unavailable",
120 error=str(e),
121 ) # EventBusProtocol is optional; feature flags work without it
123 async def health_check(self, timeout: float = 5.0) -> HealthCheckResult:
124 """Check health of feature flags.
126 Feature flags are in-memory and have no external dependencies,
127 so this always returns HEALTHY.
129 Args:
130 timeout: Not used - feature flags have no external dependencies.
132 Returns:
133 HealthCheckResult showing healthy status.
134 """
135 flag_count = (
136 len(self._config.initial_flags) if self._config.initial_flags else 0
137 )
138 return HealthCheckResult(
139 component="features",
140 status=HealthStatus.HEALTHY,
141 message=f"Feature flags operational with {flag_count} initial flags",
142 details={"initial_flags": flag_count},
143 )
145 async def shutdown(self) -> None:
146 """No resources to release for the feature-flags module."""
148 def get_simple_provider(self) -> LocalProvider | None:
149 """Return the registered simple provider after registration.
151 Returns:
152 The ``LocalProvider`` instance, or ``None`` before registration.
153 """
154 return self._simple_provider
156 def get_manager(self) -> FlagManager | None:
157 """Return the registered manager after registration.
159 Returns:
160 The ``FlagManager`` instance, or ``None`` before registration.
161 """
162 return self._manager
165__all__ = ["FeatureFlagsProvider"]