hexastack-fastapi 0.0.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of hexastack-fastapi might be problematic. Click here for more details.

@@ -0,0 +1,6 @@
1
+ from hexastack_fastapi import adapters, infra
2
+
3
+ __all__ = [
4
+ "adapters",
5
+ "infra",
6
+ ]
@@ -0,0 +1,41 @@
1
+ from hexastack_fastapi.adapters.app import create_fastapi_app
2
+ from hexastack_fastapi.adapters.dependencies import (
3
+ check_openapi_conformance,
4
+ create_test_client,
5
+ get_container,
6
+ get_feature_flags,
7
+ get_pipeline,
8
+ require_feature,
9
+ )
10
+ from hexastack_fastapi.adapters.docs import (
11
+ DocumentationNotFoundError,
12
+ mount_zensical_docs,
13
+ )
14
+ from hexastack_fastapi.adapters.health import create_health_router
15
+ from hexastack_fastapi.adapters.routing import CqrsRouter
16
+ from hexastack_fastapi.adapters.ui import (
17
+ dispatch_command,
18
+ dispatch_query,
19
+ mount_devtools_dashboard,
20
+ mount_ui_app,
21
+ ui_page,
22
+ )
23
+
24
+ __all__ = [
25
+ "check_openapi_conformance",
26
+ "CqrsRouter",
27
+ "create_fastapi_app",
28
+ "create_health_router",
29
+ "create_test_client",
30
+ "dispatch_command",
31
+ "dispatch_query",
32
+ "DocumentationNotFoundError",
33
+ "get_container",
34
+ "get_feature_flags",
35
+ "get_pipeline",
36
+ "mount_devtools_dashboard",
37
+ "mount_ui_app",
38
+ "mount_zensical_docs",
39
+ "require_feature",
40
+ "ui_page",
41
+ ]
@@ -0,0 +1,118 @@
1
+ from contextlib import asynccontextmanager
2
+
3
+ from fastapi import FastAPI
4
+ from fastapi.middleware.cors import CORSMiddleware
5
+ from rodi import Container
6
+
7
+ from hexastack_core.infra.registries.exception import ExceptionRegistry
8
+ from hexastack_core.ports.logging import LoggingPort
9
+ from hexastack_cqrs.infra.pipeline import ExecutionPipeline
10
+ from hexastack_fastapi.adapters.docs import mount_zensical_docs
11
+ from hexastack_fastapi.adapters.health import create_health_router
12
+ from hexastack_fastapi.infra.config import HexastackFastApiConfig
13
+ from hexastack_fastapi.infra.exception_handlers import (
14
+ register_exception_handlers,
15
+ )
16
+ from hexastack_fastapi.infra.middleware.correlation import (
17
+ CorrelationHttpMiddleware,
18
+ )
19
+ from hexastack_fastapi.infra.middleware.logging import (
20
+ RequestLoggingHttpMiddleware,
21
+ )
22
+
23
+ __all__ = [
24
+ "create_fastapi_app",
25
+ ]
26
+
27
+
28
+ def create_fastapi_app(
29
+ config: HexastackFastApiConfig | None = None,
30
+ container: Container | None = None,
31
+ pipeline: ExecutionPipeline | None = None,
32
+ exception_registry: ExceptionRegistry | None = None,
33
+ ) -> FastAPI:
34
+ """Factory creating and configuring a FastAPI application integrated with Hexastack.
35
+
36
+ Notes/Architectural Intent:
37
+ Assembles FastAPI instance with OpenAPI documentation metadata, CORS middleware,
38
+ Correlation ID ASGI middleware, access logging, health check probes, Zensical docs mounting,
39
+ unified exception mapping, and DI container lifecycle management.
40
+
41
+ Args:
42
+ config: Optional HexastackFastApiConfig instance.
43
+ container: Optional rodi Container instance.
44
+ pipeline: Optional ExecutionPipeline instance.
45
+ exception_registry: Optional ExceptionRegistry for domain error mapping.
46
+
47
+ Returns:
48
+ Configured FastAPI application instance.
49
+
50
+ Raises:
51
+ None.
52
+ """
53
+ cfg = config or HexastackFastApiConfig()
54
+
55
+ @asynccontextmanager
56
+ async def lifespan(app: FastAPI):
57
+ yield
58
+ # Teardown background resources
59
+ if container is not None and LoggingPort in container:
60
+ logger = container.resolve(LoggingPort)
61
+ close_fn = getattr(logger, "close", None)
62
+ if callable(close_fn):
63
+ close_fn()
64
+
65
+ app = FastAPI(
66
+ title=cfg.title,
67
+ version=cfg.version,
68
+ description=cfg.description,
69
+ docs_url=cfg.docs_url,
70
+ redoc_url=cfg.redoc_url,
71
+ openapi_url=cfg.openapi_url,
72
+ lifespan=lifespan,
73
+ )
74
+
75
+ # Attach container and pipeline to application state
76
+ if container is not None:
77
+ app.state.container = container
78
+ if pipeline is not None:
79
+ app.state.pipeline = pipeline
80
+
81
+ # Attach CORS middleware if enabled
82
+ if cfg.cors.enable:
83
+ app.add_middleware(
84
+ CORSMiddleware,
85
+ allow_origins=cfg.cors.allow_origins,
86
+ allow_credentials=cfg.cors.allow_credentials,
87
+ allow_methods=cfg.cors.allow_methods,
88
+ allow_headers=cfg.cors.allow_headers,
89
+ )
90
+
91
+ # Attach request logging middleware if enabled
92
+ if cfg.logging.enable:
93
+ app.add_middleware(
94
+ RequestLoggingHttpMiddleware,
95
+ config=cfg,
96
+ container=container,
97
+ )
98
+
99
+ # Attach correlation middleware
100
+ app.add_middleware(CorrelationHttpMiddleware, config=cfg)
101
+
102
+ # Register unified exception handlers
103
+ register_exception_handlers(app, exception_registry=exception_registry)
104
+
105
+ # Mount health and readiness endpoints if enabled
106
+ if cfg.health.enable:
107
+ health_router = create_health_router(container=container, config=cfg.health)
108
+ app.include_router(health_router)
109
+
110
+ # Mount Zensical documentation site if enabled
111
+ if cfg.zensical.enable:
112
+ mount_zensical_docs(
113
+ app=app,
114
+ path=cfg.zensical.path,
115
+ site_dir=cfg.zensical.site_dir,
116
+ )
117
+
118
+ return app
@@ -0,0 +1,160 @@
1
+ """SQLAlchemy session-per-request middleware for FastAPI.
2
+
3
+ Decoupling strategy:
4
+ This module depends only on SQLAlchemy (already a transitive dep via hexastack-db)
5
+ and Starlette (already a direct dep via FastAPI). It never imports from hexastack-db.
6
+ Session factories are resolved from the DI container by SQLAlchemy type at runtime,
7
+ so the two packages remain fully independent — the DI container is the only coupling.
8
+
9
+ Usage:
10
+ # In your app setup (after bootstrapping both DatabaseBootstrapper + FastApiBootstrapper):
11
+ from hexastack_fastapi.adapters.db_session import (
12
+ DbSessionMiddleware,
13
+ AsyncDbSessionMiddleware,
14
+ )
15
+
16
+ # Sync engine:
17
+ app.add_middleware(DbSessionMiddleware, session_factory=session_factory)
18
+
19
+ # Async engine:
20
+ app.add_middleware(AsyncDbSessionMiddleware, session_factory=async_session_factory)
21
+
22
+ # In a handler or dependency:
23
+ def my_endpoint(request: Request):
24
+ session = request.state.db_session # Session | AsyncSession
25
+ """
26
+
27
+ from __future__ import annotations
28
+
29
+ from collections.abc import Callable
30
+ from typing import Any
31
+
32
+ from starlette.middleware.base import BaseHTTPMiddleware
33
+ from starlette.requests import Request
34
+ from starlette.responses import Response
35
+ from starlette.types import ASGIApp
36
+
37
+
38
+ class DbSessionMiddleware(BaseHTTPMiddleware):
39
+ """Synchronous SQLAlchemy session-per-request Starlette middleware.
40
+
41
+ Notes/Architectural Intent:
42
+ Creates a new Session for every HTTP request and exposes it as
43
+ request.state.db_session. The session is always closed in the
44
+ finally block regardless of response outcome. No commit is issued
45
+ automatically — handlers own transaction boundaries via UnitOfWork.
46
+ """
47
+
48
+ def __init__(
49
+ self,
50
+ app: ASGIApp,
51
+ session_factory: Callable[[], Any],
52
+ ) -> None:
53
+ """Initialise with a synchronous sessionmaker callable.
54
+
55
+ Args:
56
+ app: The ASGI application to wrap.
57
+ session_factory: A callable returning a SQLAlchemy Session
58
+ (e.g. the sessionmaker instance returned by create_session_factory).
59
+ """
60
+ super().__init__(app)
61
+ self._session_factory = session_factory
62
+
63
+ async def dispatch(
64
+ self, request: Request, call_next: Callable[..., Any]
65
+ ) -> Response:
66
+ """Attach a fresh session to each request; close it after response.
67
+
68
+ Args:
69
+ request: Incoming Starlette Request.
70
+ call_next: Next middleware / route handler in the chain.
71
+
72
+ Returns:
73
+ The HTTP Response from the downstream handler.
74
+ """
75
+ session = self._session_factory()
76
+ request.state.db_session = session
77
+ try:
78
+ response = await call_next(request)
79
+ finally:
80
+ session.close()
81
+ return response
82
+
83
+
84
+ class AsyncDbSessionMiddleware(BaseHTTPMiddleware):
85
+ """Asynchronous SQLAlchemy session-per-request Starlette middleware.
86
+
87
+ Notes/Architectural Intent:
88
+ Creates a new AsyncSession for every HTTP request via an async_sessionmaker,
89
+ exposing it as request.state.db_session. Handlers own commit/rollback via
90
+ AsyncSqlAlchemyUnitOfWork — this middleware only manages lifecycle (open/close).
91
+ """
92
+
93
+ def __init__(
94
+ self,
95
+ app: ASGIApp,
96
+ session_factory: Callable[[], Any],
97
+ ) -> None:
98
+ """Initialise with an async_sessionmaker callable.
99
+
100
+ Args:
101
+ app: The ASGI application to wrap.
102
+ session_factory: A callable returning an AsyncSession
103
+ (e.g. the async_sessionmaker returned by create_async_session_factory).
104
+ """
105
+ super().__init__(app)
106
+ self._session_factory = session_factory
107
+
108
+ async def dispatch(
109
+ self, request: Request, call_next: Callable[..., Any]
110
+ ) -> Response:
111
+ """Attach a fresh AsyncSession to each request; close it after response.
112
+
113
+ Args:
114
+ request: Incoming Starlette Request.
115
+ call_next: Next middleware / route handler in the chain.
116
+
117
+ Returns:
118
+ The HTTP Response from the downstream handler.
119
+ """
120
+ session = self._session_factory()
121
+ request.state.db_session = session
122
+ try:
123
+ response = await call_next(request)
124
+ finally:
125
+ await session.close()
126
+ return response
127
+
128
+
129
+ __all__ = [
130
+ "add_db_session_middleware",
131
+ "AsyncDbSessionMiddleware",
132
+ "DbSessionMiddleware",
133
+ ]
134
+
135
+
136
+ def add_db_session_middleware(
137
+ app: Any,
138
+ session_factory: Callable[[], Any],
139
+ *,
140
+ async_mode: bool = False,
141
+ ) -> None:
142
+ """Convenience helper to attach the correct session middleware to a FastAPI app.
143
+
144
+ Notes/Architectural Intent:
145
+ Selects DbSessionMiddleware or AsyncDbSessionMiddleware based on async_mode.
146
+ Intended to be called from the FastAPI bootstrapper or app factory when
147
+ a session_factory is present in the DI container.
148
+
149
+ Args:
150
+ app: FastAPI / Starlette application instance.
151
+ session_factory: Sessionmaker callable (sync or async).
152
+ async_mode: If True, use AsyncDbSessionMiddleware.
153
+
154
+ Returns:
155
+ None.
156
+ """
157
+ if async_mode:
158
+ app.add_middleware(AsyncDbSessionMiddleware, session_factory=session_factory)
159
+ else:
160
+ app.add_middleware(DbSessionMiddleware, session_factory=session_factory)
@@ -0,0 +1,190 @@
1
+ from typing import Any
2
+
3
+ from fastapi import HTTPException, Request, status
4
+ from rodi import Container
5
+
6
+ from hexastack_core.adapters.feature_flags.config import ConfigFeatureFlagAdapter
7
+ from hexastack_core.adapters.feature_flags.in_memory import InMemoryFeatureFlagAdapter
8
+ from hexastack_core.domain.exceptions import (
9
+ DependencyResolutionError,
10
+ MissingDependencyError,
11
+ )
12
+ from hexastack_core.domain.feature_flags import EvaluationContext
13
+ from hexastack_core.ports.feature_flags import FeatureFlagPort
14
+ from hexastack_cqrs.infra.pipeline import ExecutionPipeline
15
+
16
+ __all__ = [
17
+ "check_openapi_conformance",
18
+ "create_test_client",
19
+ "get_container",
20
+ "get_feature_flags",
21
+ "get_pipeline",
22
+ "require_feature",
23
+ ]
24
+
25
+
26
+ def check_openapi_conformance(
27
+ app: Any,
28
+ *,
29
+ schema_url: str = "/openapi.json",
30
+ validate_schema: bool = True,
31
+ ) -> None:
32
+ """Run Schemathesis contract conformance and schema validation checks against a FastAPI app.
33
+
34
+ Notes/Architectural Intent:
35
+ Automatically tests that all exposed FastAPI endpoints conform to the
36
+ derived OpenAPI schema, validate properly, and have complete route definitions.
37
+
38
+ Args:
39
+ app: The FastAPI application instance.
40
+ schema_url: URL path where the OpenAPI JSON is served (defaults to '/openapi.json').
41
+ validate_schema: Whether to validate the OpenAPI specification structure itself.
42
+
43
+ Raises:
44
+ MissingDependencyError: If `schemathesis` is not installed.
45
+ AssertionError: If any API contract violation or unhandled error is detected.
46
+ """
47
+ try:
48
+ import schemathesis
49
+ except ImportError as e:
50
+ raise MissingDependencyError(
51
+ "schemathesis is required for check_openapi_conformance. "
52
+ "Install with 'pip install schemathesis' or 'pip install hexastack[testing]'."
53
+ ) from e
54
+
55
+ schema = schemathesis.openapi.from_asgi(schema_url, app)
56
+
57
+ if validate_schema:
58
+ schema.validate()
59
+
60
+
61
+ def create_test_client(
62
+ app: Any,
63
+ *,
64
+ flags: dict[str, Any] | None = None,
65
+ **test_client_kwargs: Any,
66
+ ) -> Any:
67
+ """Create a FastAPI TestClient configured with an in-memory feature flag adapter.
68
+
69
+ Args:
70
+ app: The FastAPI application instance.
71
+ flags: Optional initial flag states.
72
+ **test_client_kwargs: Extra keyword arguments forwarded to TestClient.
73
+
74
+ Returns:
75
+ Configured starlette / fastapi TestClient.
76
+ """
77
+ from fastapi.testclient import TestClient
78
+
79
+ if flags is not None:
80
+ container = getattr(app.state, "container", None)
81
+ flags_adapter = InMemoryFeatureFlagAdapter(flags=flags)
82
+ if container is not None:
83
+ container.add_instance(flags_adapter, declared_class=FeatureFlagPort)
84
+
85
+ return TestClient(app, **test_client_kwargs)
86
+
87
+
88
+ def get_container(request: Request) -> Container:
89
+ """FastAPI dependency provider resolving the active rodi Container instance.
90
+
91
+ Notes/Architectural Intent:
92
+ Enables endpoint routes to inject and resolve registered domain ports and services.
93
+
94
+ Args:
95
+ request: Incoming FastAPI Request object.
96
+
97
+ Returns:
98
+ The rodi Container attached to the application state.
99
+
100
+ Raises:
101
+ DependencyResolutionError: If Container has not been attached to app.state.
102
+ """
103
+ container = getattr(request.app.state, "container", None)
104
+ if container is None or not isinstance(container, Container):
105
+ raise DependencyResolutionError(
106
+ "rodi.Container is not configured on request.app.state.container."
107
+ )
108
+ return container
109
+
110
+
111
+ def get_feature_flags(request: Request) -> FeatureFlagPort:
112
+ """FastAPI dependency provider resolving the active FeatureFlagPort instance.
113
+
114
+ Notes/Architectural Intent:
115
+ Retrieves the registered FeatureFlagPort from the application container or
116
+ falls back to ConfigFeatureFlagAdapter.
117
+
118
+ Args:
119
+ request: Incoming FastAPI Request object.
120
+
121
+ Returns:
122
+ The FeatureFlagPort instance.
123
+ """
124
+ container = getattr(request.app.state, "container", None)
125
+ if isinstance(container, Container) and FeatureFlagPort in container:
126
+ return container.resolve(FeatureFlagPort)
127
+ return ConfigFeatureFlagAdapter()
128
+
129
+
130
+ def get_pipeline(request: Request) -> ExecutionPipeline:
131
+ """FastAPI dependency provider resolving the active ExecutionPipeline instance.
132
+
133
+ Notes/Architectural Intent:
134
+ Enables route handlers to execute CQRS commands and queries through the unified pipeline.
135
+
136
+ Args:
137
+ request: Incoming FastAPI Request object.
138
+
139
+ Returns:
140
+ The ExecutionPipeline instance attached to application state or resolved from DI.
141
+
142
+ Raises:
143
+ DependencyResolutionError: If ExecutionPipeline cannot be resolved from application state or container.
144
+ """
145
+ pipeline = getattr(request.app.state, "pipeline", None)
146
+ if isinstance(pipeline, ExecutionPipeline):
147
+ return pipeline
148
+
149
+ container = getattr(request.app.state, "container", None)
150
+ if isinstance(container, Container) and ExecutionPipeline in container:
151
+ return container.resolve(ExecutionPipeline)
152
+
153
+ raise DependencyResolutionError(
154
+ "ExecutionPipeline is not available on request.app.state.pipeline or container."
155
+ )
156
+
157
+
158
+ def require_feature(
159
+ flag_key: str,
160
+ *,
161
+ default: bool = False,
162
+ status_code: int = status.HTTP_404_NOT_FOUND,
163
+ detail: str | None = None,
164
+ ) -> Any:
165
+ """Create a FastAPI dependency that enforces a feature flag is enabled.
166
+
167
+ Notes/Architectural Intent:
168
+ Evaluates the feature flag dynamically against ambient UserContext / request state.
169
+ If disabled, raises an HTTPException (default 404 Not Found) to cleanly hide or gate routes.
170
+
171
+ Args:
172
+ flag_key: Unique identifier of the feature flag to check.
173
+ default: Fallback boolean value if flag is not explicitly configured.
174
+ status_code: HTTP status code to return when disabled (defaults to 404).
175
+ detail: Optional error detail message.
176
+
177
+ Returns:
178
+ A FastAPI dependency callable.
179
+ """
180
+
181
+ async def _dependency(request: Request) -> None:
182
+ flags = get_feature_flags(request)
183
+ eval_ctx = EvaluationContext.from_current_context()
184
+ if not flags.is_enabled(flag_key, default=default, context=eval_ctx):
185
+ raise HTTPException(
186
+ status_code=status_code,
187
+ detail=detail or f"Feature '{flag_key}' is disabled.",
188
+ )
189
+
190
+ return _dependency
@@ -0,0 +1,80 @@
1
+ """Zensical and static documentation mounting adapter for FastAPI.
2
+
3
+ Notes/Architectural Intent:
4
+ Provides a declarative helper to serve pre-built Zensical or static HTML documentation
5
+ directly alongside FastAPI API routes:
6
+ 1. Supports configurable mount paths (e.g. `/guide`, `/docs-site`, `/handbook`) to avoid
7
+ conflicts with FastAPI's native OpenAPI Swagger UI (`/docs`) and ReDoc (`/redoc`).
8
+ 2. Allows custom fallback and index.html serving with static directory validation.
9
+ """
10
+
11
+ from __future__ import annotations
12
+
13
+ import os
14
+ from pathlib import Path
15
+
16
+ from fastapi import FastAPI
17
+ from fastapi.staticfiles import StaticFiles
18
+
19
+ from hexastack_core.domain.exceptions import HexastackError
20
+
21
+
22
+ class DocumentationNotFoundError(HexastackError):
23
+ """Raised when configured documentation directory does not exist."""
24
+
25
+
26
+ def mount_zensical_docs(
27
+ app: FastAPI,
28
+ path: str = "/guide",
29
+ site_dir: str | Path = "site",
30
+ name: str = "zensical-docs",
31
+ html: bool = True,
32
+ ) -> None:
33
+ """Mount a pre-built Zensical static documentation site onto a FastAPI application.
34
+
35
+ Notes/Architectural Intent:
36
+ FastAPI defaults OpenAPI Swagger UI to `/docs` and ReDoc to `/redoc`.
37
+ Mounting Zensical under a configurable path (defaulting to `/guide`) enables
38
+ teams to serve complete developer guides, architecture diagrams, and tutorials
39
+ from the exact same microservice process without routing collisions.
40
+
41
+ Args:
42
+ app: Target FastAPI application.
43
+ path: URL prefix where documentation will be served (e.g. "/guide" or "/manual").
44
+ site_dir: Path to directory containing built HTML files (typically "site" from `zensical build`).
45
+ name: Internal ASGI mount name.
46
+ html: Enable HTML mode (auto-resolving directory paths to index.html).
47
+
48
+ Raises:
49
+ DocumentationNotFoundError: If site_dir does not exist and is required.
50
+ """
51
+ resolved_dir = Path(site_dir).resolve()
52
+
53
+ if not resolved_dir.exists():
54
+ if os.environ.get("ENVIRONMENT", "").lower() in ("prod", "production"):
55
+ raise DocumentationNotFoundError(
56
+ f"Zensical documentation directory '{resolved_dir}' not found. "
57
+ f"Run 'zensical build' before launching the production server."
58
+ )
59
+ # In development/test mode, create the directory if missing to allow graceful startup
60
+ resolved_dir.mkdir(parents=True, exist_ok=True)
61
+ index_file = resolved_dir / "index.html"
62
+ if not index_file.exists():
63
+ index_file.write_text(
64
+ "<!DOCTYPE html><html><body><h1>Documentation building...</h1>"
65
+ "<p>Run <code>zensical build</code> to compile docs.</p></body></html>",
66
+ encoding="utf-8",
67
+ )
68
+
69
+ normalized_path = "/" + path.strip("/")
70
+ app.mount(
71
+ normalized_path,
72
+ StaticFiles(directory=str(resolved_dir), html=html),
73
+ name=name,
74
+ )
75
+
76
+
77
+ __all__ = [
78
+ "DocumentationNotFoundError",
79
+ "mount_zensical_docs",
80
+ ]
@@ -0,0 +1,62 @@
1
+ from datetime import UTC, datetime
2
+ from typing import Any
3
+
4
+ from fastapi import APIRouter
5
+ from fastapi.responses import JSONResponse
6
+ from rodi import Container
7
+
8
+ from hexastack_core.utils.context import get_correlation_id
9
+ from hexastack_fastapi.infra.config import HealthConfig
10
+
11
+ __all__ = [
12
+ "create_health_router",
13
+ ]
14
+
15
+
16
+ def create_health_router(
17
+ container: Container | None = None,
18
+ config: HealthConfig | None = None,
19
+ ) -> APIRouter:
20
+ """Create APIRouter with standard liveness and readiness probe endpoints.
21
+
22
+ Notes/Architectural Intent:
23
+ Exposes lightweight /health (liveness) and /ready (readiness) endpoints for orchestration
24
+ platforms like Kubernetes and container engines.
25
+
26
+ Args:
27
+ container: Optional rodi Container instance to check dependency health.
28
+ config: Optional HealthConfig specifying custom URL paths.
29
+
30
+ Returns:
31
+ APIRouter mounted with health endpoints.
32
+
33
+ Raises:
34
+ None.
35
+ """
36
+ cfg = config or HealthConfig()
37
+ router = APIRouter(tags=["Health"])
38
+
39
+ @router.get(cfg.health_path, summary="Liveness Probe")
40
+ async def liveness() -> dict[str, Any]:
41
+ return {
42
+ "status": "ok",
43
+ "timestamp": datetime.now(tz=UTC).isoformat(),
44
+ "correlation_id": get_correlation_id(),
45
+ }
46
+
47
+ @router.get(cfg.ready_path, summary="Readiness Probe")
48
+ async def readiness() -> JSONResponse:
49
+ checks: dict[str, str] = {
50
+ "container": "ok" if container is not None else "unconfigured",
51
+ }
52
+ status_ok = all(v == "ok" for v in checks.values())
53
+ payload: dict[str, Any] = {
54
+ "status": "ready" if status_ok else "unhealthy",
55
+ "timestamp": datetime.now(tz=UTC).isoformat(),
56
+ "correlation_id": get_correlation_id(),
57
+ "checks": checks,
58
+ }
59
+ status_code = 200 if status_ok else 503
60
+ return JSONResponse(status_code=status_code, content=payload)
61
+
62
+ return router