Coverage for src/lexigram/admin/integrations/resilience.py: 78%
46 statements
« prev ^ index » next coverage.py v7.15.4, created at 2026-08-21 14:56 +0800
« prev ^ index » next coverage.py v7.15.4, created at 2026-08-21 14:56 +0800
1"""Resilience integration — wraps data-source calls with retry/circuit-breaker."""
3from __future__ import annotations
5from dataclasses import dataclass
6from typing import TYPE_CHECKING, Any
8if TYPE_CHECKING:
9 from lexigram.contracts.core.di import (
10 ContainerRegistrarProtocol,
11 ContainerResolverProtocol,
12 )
15@dataclass(frozen=True)
16class ResilientSpec:
17 """Specification for resilient data-source calls.
19 Attributes:
20 max_retries: Max retry attempts on transient failure.
21 circuit_breaker: Whether to enable circuit breaker.
22 timeout_seconds: Per-call timeout.
23 """
25 max_retries: int = 3
26 circuit_breaker: bool = True
27 timeout_seconds: float = 30.0
30class _NoOpResilience:
31 async def execute(self, func: Any, *args: Any, **kwargs: Any) -> Any:
32 return await func(*args, **kwargs)
35class ResilienceIntegration:
36 """Adapter that decorates data-source calls with retry and circuit-breaker.
38 Gracefully no-ops when ``lexigram-resilience`` is not installed or the
39 integration is disabled.
40 """
42 def __init__(self, config: Any) -> None:
43 self._config = config
44 self._pipeline: Any = None
45 self._enabled = False
47 def register(self, container: ContainerRegistrarProtocol) -> None:
48 from lexigram.admin.config import ResilienceIntegrationConfig
49 from lexigram.admin.integrations._optional import is_installed
51 cfg = self._config
52 if not isinstance(cfg, ResilienceIntegrationConfig):
53 cfg = ResilienceIntegrationConfig()
54 if not cfg.enabled:
55 self._pipeline = _NoOpResilience()
56 return
57 if not is_installed("lexigram.resilience"):
58 self._pipeline = _NoOpResilience()
59 return
60 self._enabled = True
62 async def boot(self, container: ContainerResolverProtocol) -> None:
63 if not self._enabled:
64 return
65 try:
66 from lexigram.contracts.infra.resilience import (
67 ResiliencePipelineFactoryProtocol,
68 )
70 self._pipeline = await container.resolve(ResiliencePipelineFactoryProtocol)
71 except Exception: # noqa: BLE001
72 self._pipeline = _NoOpResilience()
74 async def shutdown(self) -> None:
75 pass
77 async def health_check(self) -> dict[str, Any]:
78 return {
79 "status": "healthy"
80 if not isinstance(self._pipeline, _NoOpResilience)
81 else "noop"
82 }
84 async def execute(self, func: Any, *args: Any, **kwargs: Any) -> Any:
85 if hasattr(self._pipeline, "execute"):
86 return await self._pipeline.execute(func, *args, **kwargs)
87 return await func(*args, **kwargs)
90__all__ = ["ResilienceIntegration", "ResilientSpec"]