hexastack-fastapi 0.1.0__tar.gz → 0.3.0__tar.gz

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.
Files changed (34) hide show
  1. {hexastack_fastapi-0.1.0 → hexastack_fastapi-0.3.0}/PKG-INFO +18 -8
  2. {hexastack_fastapi-0.1.0 → hexastack_fastapi-0.3.0}/README.md +14 -7
  3. {hexastack_fastapi-0.1.0 → hexastack_fastapi-0.3.0}/pyproject.toml +26 -1
  4. {hexastack_fastapi-0.1.0 → hexastack_fastapi-0.3.0}/pyproject.toml.orig +31 -1
  5. {hexastack_fastapi-0.1.0 → hexastack_fastapi-0.3.0}/src/hexastack_fastapi/adapters/__init__.py +28 -2
  6. {hexastack_fastapi-0.1.0 → hexastack_fastapi-0.3.0}/src/hexastack_fastapi/adapters/dependencies.py +63 -0
  7. {hexastack_fastapi-0.1.0 → hexastack_fastapi-0.3.0}/src/hexastack_fastapi/adapters/health.py +20 -7
  8. hexastack_fastapi-0.3.0/src/hexastack_fastapi/adapters/metrics.py +44 -0
  9. hexastack_fastapi-0.3.0/src/hexastack_fastapi/adapters/ratelimit.py +208 -0
  10. {hexastack_fastapi-0.1.0 → hexastack_fastapi-0.3.0}/src/hexastack_fastapi/adapters/routing.py +119 -1
  11. hexastack_fastapi-0.3.0/src/hexastack_fastapi/adapters/sse.py +190 -0
  12. hexastack_fastapi-0.3.0/src/hexastack_fastapi/adapters/websockets.py +212 -0
  13. {hexastack_fastapi-0.1.0/src/hexastack_fastapi/infra → hexastack_fastapi-0.3.0/src/hexastack_fastapi/domain}/config.py +23 -21
  14. {hexastack_fastapi-0.1.0 → hexastack_fastapi-0.3.0}/src/hexastack_fastapi/infra/__init__.py +2 -0
  15. {hexastack_fastapi-0.1.0/src/hexastack_fastapi/adapters → hexastack_fastapi-0.3.0/src/hexastack_fastapi/infra}/app.py +41 -0
  16. {hexastack_fastapi-0.1.0 → hexastack_fastapi-0.3.0}/src/hexastack_fastapi/infra/autodiscovery.py +2 -0
  17. {hexastack_fastapi-0.1.0 → hexastack_fastapi-0.3.0}/src/hexastack_fastapi/infra/bootstrap.py +1 -1
  18. hexastack_fastapi-0.3.0/src/hexastack_fastapi/infra/config.py +31 -0
  19. {hexastack_fastapi-0.1.0 → hexastack_fastapi-0.3.0}/src/hexastack_fastapi/infra/decorators.py +14 -4
  20. {hexastack_fastapi-0.1.0 → hexastack_fastapi-0.3.0}/src/hexastack_fastapi/infra/middleware/__init__.py +4 -0
  21. hexastack_fastapi-0.3.0/src/hexastack_fastapi/infra/middleware/metrics.py +122 -0
  22. hexastack_fastapi-0.3.0/src/hexastack_fastapi/py.typed +0 -0
  23. {hexastack_fastapi-0.1.0 → hexastack_fastapi-0.3.0}/src/hexastack_fastapi/__init__.py +0 -0
  24. {hexastack_fastapi-0.1.0 → hexastack_fastapi-0.3.0}/src/hexastack_fastapi/adapters/db_session.py +0 -0
  25. {hexastack_fastapi-0.1.0 → hexastack_fastapi-0.3.0}/src/hexastack_fastapi/adapters/docs.py +0 -0
  26. {hexastack_fastapi-0.1.0 → hexastack_fastapi-0.3.0}/src/hexastack_fastapi/adapters/ui.py +0 -0
  27. /hexastack_fastapi-0.1.0/src/hexastack_fastapi/py.typed → /hexastack_fastapi-0.3.0/src/hexastack_fastapi/domain/__init__.py +0 -0
  28. {hexastack_fastapi-0.1.0 → hexastack_fastapi-0.3.0}/src/hexastack_fastapi/infra/exception_handlers.py +0 -0
  29. {hexastack_fastapi-0.1.0 → hexastack_fastapi-0.3.0}/src/hexastack_fastapi/infra/middleware/correlation.py +0 -0
  30. {hexastack_fastapi-0.1.0 → hexastack_fastapi-0.3.0}/src/hexastack_fastapi/infra/middleware/logging.py +0 -0
  31. {hexastack_fastapi-0.1.0 → hexastack_fastapi-0.3.0}/src/hexastack_fastapi/testing/__init__.py +0 -0
  32. {hexastack_fastapi-0.1.0 → hexastack_fastapi-0.3.0}/src/hexastack_fastapi/testing/cursor.py +0 -0
  33. {hexastack_fastapi-0.1.0 → hexastack_fastapi-0.3.0}/src/hexastack_fastapi/testing/recorder.py +0 -0
  34. {hexastack_fastapi-0.1.0 → hexastack_fastapi-0.3.0}/src/hexastack_fastapi/testing/server.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: hexastack-fastapi
3
- Version: 0.1.0
3
+ Version: 0.3.0
4
4
  Summary: FastAPI presentation adapter for Hexastack
5
5
  Author: Richard West
6
6
  Author-email: Richard West <dopplereffect.us@gmail.com>
@@ -10,12 +10,15 @@ Requires-Dist: hexastack-core
10
10
  Requires-Dist: hexastack-cqrs
11
11
  Requires-Dist: hexastack-auth ; extra == 'auth'
12
12
  Requires-Dist: zensical>=0.0.56 ; extra == 'docs'
13
+ Requires-Dist: limits>=3.14.0 ; extra == 'ratelimit'
14
+ Requires-Dist: slowapi>=0.1.9 ; extra == 'ratelimit'
13
15
  Requires-Dist: playwright>=1.49.0 ; extra == 'testing'
14
16
  Requires-Dist: schemathesis>=3.39.0 ; extra == 'testing'
15
17
  Requires-Dist: nicegui>=2.0.0 ; extra == 'ui'
16
18
  Requires-Python: >=3.13
17
19
  Provides-Extra: auth
18
20
  Provides-Extra: docs
21
+ Provides-Extra: ratelimit
19
22
  Provides-Extra: testing
20
23
  Provides-Extra: ui
21
24
  Description-Content-Type: text/markdown
@@ -37,39 +40,46 @@ Description-Content-Type: text/markdown
37
40
  ## 1. Overview & Capabilities
38
41
 
39
42
  - **Automatic CQRS Routing & Feature Gating**: Map HTTP endpoints directly to `CommandBusPort` or `QueryBusPort` using `@api_command` and `@api_query`, with native feature flag route guards (`@feature_flag_route` and `require_feature(...)`).
43
+ - **Server-Sent Events (SSE) Real-Time Streaming**: Native `ServerSentEvent` formatting and `EventSourceResponse` supporting chunked transfer encoding, keep-alive heartbeat pings, and direct streaming queries on `CqrsRouter.add_streaming_query`.
44
+ - **WebSockets Real-Time Channel Manager & CQRS Bridge**: `WebSocketConnectionManager` managing connection pools, room/channel groupings, and broadcasts, alongside `WebSocketCqrsBridge` mapping incoming socket JSON actions into CQRS Commands and Queries.
40
45
  - **Decoupled Database Session Middleware**: `DbSessionMiddleware` (sync) and `AsyncDbSessionMiddleware` (async) manage session-per-request lifecycles by consuming sessionmakers from DI without a hard dependency on `hexastack-db`.
41
46
  - **Standardized Exception Handlers**: Automatically translates domain exceptions (`EntityNotFoundError`, `UniqueConstraintViolationError`, `HexastackError`) into appropriate HTTP status codes (404, 409, 500) and structured JSON error envelopes.
42
- - **Observability & Correlation Middleware**: Injects `X-Correlation-ID` headers and logs HTTP request lifecycles.
47
+ - **Observability & Correlation Middleware**: Injects `X-Correlation-ID` headers, logs HTTP request lifecycles, and captures HTTP RED metrics (`HttpMetricsMiddleware`) exposing a standard `/metrics` endpoint.
43
48
  - **Built-in Health Checks**: Configurable `/health` and `/ready` endpoints verifying container and subsystem readiness.
44
49
  - **NiceGUI Reactive UI & DevTools Dashboard**: Optional `[ui]` presentation adapter providing `ui_page`, `dispatch_command`, `dispatch_query`, and a zero-config interactive developer console (`mount_devtools_dashboard`) for inspecting CQRS buses, feature flags, and DI services.
45
50
 
51
+
46
52
  ---
47
53
 
48
54
  ## 2. Package Anatomy & Key Components
49
55
 
50
56
  ```
51
57
  hexastack_fastapi/
52
- ├── domain/ # HealthStatus, HTTP error envelope models
53
- ├── adapters/ # create_app, routing decorators, health endpoints, db_session middleware, dependencies, NiceGUI UI adapter
54
- └── infra/ # FastApiBootstrapper (order=30), exception handlers, correlation/logging middlewares
58
+ ├── domain/ # HealthStatus, HTTP error envelope models, HexastackFastApiConfig
59
+ ├── adapters/ # routing decorators, SSE streaming, WebSockets, health endpoints, db_session middleware, dependencies, NiceGUI UI adapter
60
+ └── infra/ # create_fastapi_app, FastApiBootstrapper (order=30), exception handlers, correlation/logging middlewares
55
61
  ```
56
62
 
63
+
57
64
  ### Key Exports
58
65
 
59
66
  | Category | Exports |
60
67
  |---|---|
61
- | **Application Factory** | `create_app`, `FastApiBootstrapper` (order=30) |
62
- | **Decorators** | `@api_command`, `@api_query`, `@feature_flag_route`, `RouteMetadata` |
63
- | **Dependencies** | `get_container`, `get_pipeline`, `get_feature_flags`, `require_feature` |
68
+ | **Application Factory** | `create_fastapi_app`, `FastApiBootstrapper` (order=30) |
69
+ | **Decorators** | `@api_command`, `@api_query`, `@feature_flag_route`, `@rate_limit`, `RouteMetadata` |
70
+ | **Dependencies** | `get_container`, `get_pipeline`, `get_feature_flags`, `require_feature`, `get_rate_limiter`, `require_rate_limit` |
64
71
  | **Documentation Mounting** | `mount_zensical_docs`, `DocumentationNotFoundError`, `ZensicalDocsConfig` |
65
72
  | **Exception Handlers** | `register_exception_handlers`, `domain_exception_handler` |
66
73
  | **Middlewares** | `DbSessionMiddleware`, `AsyncDbSessionMiddleware`, `add_db_session_middleware`, `CorrelationMiddleware`, `HttpLoggingMiddleware` |
74
+ | **Rate Limiting** | `SlowapiRateLimiterAdapter`, `get_remote_address`, `get_user_or_ip_key`, `RateLimitConfig` |
67
75
  | **Reactive UI (NiceGUI)** | `dispatch_command`, `dispatch_query`, `mount_devtools_dashboard`, `mount_ui_app`, `ui_page` |
76
+ | **Real-Time Streaming (SSE & WebSockets)** | `EventSourceResponse`, `ServerSentEvent`, `WebSocketConnectionManager`, `WebSocketCqrsBridge` |
68
77
  | **Routing** | `CqrsRouter`, `autodiscover_routes` |
69
78
  | **Testing, E2E & Demo Narration** | `create_test_client`, `check_openapi_conformance`, `EphemeralServer`, `ephemeral_server`, `find_free_port`, `smart_click`, `DemoNarrator`, `VIRTUAL_CURSOR_SCRIPT` |
70
79
 
71
80
  ---
72
81
 
82
+
73
83
  ## 3. Monorepo & Sibling Relationships
74
84
 
75
85
  ```mermaid
@@ -15,39 +15,46 @@
15
15
  ## 1. Overview & Capabilities
16
16
 
17
17
  - **Automatic CQRS Routing & Feature Gating**: Map HTTP endpoints directly to `CommandBusPort` or `QueryBusPort` using `@api_command` and `@api_query`, with native feature flag route guards (`@feature_flag_route` and `require_feature(...)`).
18
+ - **Server-Sent Events (SSE) Real-Time Streaming**: Native `ServerSentEvent` formatting and `EventSourceResponse` supporting chunked transfer encoding, keep-alive heartbeat pings, and direct streaming queries on `CqrsRouter.add_streaming_query`.
19
+ - **WebSockets Real-Time Channel Manager & CQRS Bridge**: `WebSocketConnectionManager` managing connection pools, room/channel groupings, and broadcasts, alongside `WebSocketCqrsBridge` mapping incoming socket JSON actions into CQRS Commands and Queries.
18
20
  - **Decoupled Database Session Middleware**: `DbSessionMiddleware` (sync) and `AsyncDbSessionMiddleware` (async) manage session-per-request lifecycles by consuming sessionmakers from DI without a hard dependency on `hexastack-db`.
19
21
  - **Standardized Exception Handlers**: Automatically translates domain exceptions (`EntityNotFoundError`, `UniqueConstraintViolationError`, `HexastackError`) into appropriate HTTP status codes (404, 409, 500) and structured JSON error envelopes.
20
- - **Observability & Correlation Middleware**: Injects `X-Correlation-ID` headers and logs HTTP request lifecycles.
22
+ - **Observability & Correlation Middleware**: Injects `X-Correlation-ID` headers, logs HTTP request lifecycles, and captures HTTP RED metrics (`HttpMetricsMiddleware`) exposing a standard `/metrics` endpoint.
21
23
  - **Built-in Health Checks**: Configurable `/health` and `/ready` endpoints verifying container and subsystem readiness.
22
24
  - **NiceGUI Reactive UI & DevTools Dashboard**: Optional `[ui]` presentation adapter providing `ui_page`, `dispatch_command`, `dispatch_query`, and a zero-config interactive developer console (`mount_devtools_dashboard`) for inspecting CQRS buses, feature flags, and DI services.
23
25
 
26
+
24
27
  ---
25
28
 
26
29
  ## 2. Package Anatomy & Key Components
27
30
 
28
31
  ```
29
32
  hexastack_fastapi/
30
- ├── domain/ # HealthStatus, HTTP error envelope models
31
- ├── adapters/ # create_app, routing decorators, health endpoints, db_session middleware, dependencies, NiceGUI UI adapter
32
- └── infra/ # FastApiBootstrapper (order=30), exception handlers, correlation/logging middlewares
33
+ ├── domain/ # HealthStatus, HTTP error envelope models, HexastackFastApiConfig
34
+ ├── adapters/ # routing decorators, SSE streaming, WebSockets, health endpoints, db_session middleware, dependencies, NiceGUI UI adapter
35
+ └── infra/ # create_fastapi_app, FastApiBootstrapper (order=30), exception handlers, correlation/logging middlewares
33
36
  ```
34
37
 
38
+
35
39
  ### Key Exports
36
40
 
37
41
  | Category | Exports |
38
42
  |---|---|
39
- | **Application Factory** | `create_app`, `FastApiBootstrapper` (order=30) |
40
- | **Decorators** | `@api_command`, `@api_query`, `@feature_flag_route`, `RouteMetadata` |
41
- | **Dependencies** | `get_container`, `get_pipeline`, `get_feature_flags`, `require_feature` |
43
+ | **Application Factory** | `create_fastapi_app`, `FastApiBootstrapper` (order=30) |
44
+ | **Decorators** | `@api_command`, `@api_query`, `@feature_flag_route`, `@rate_limit`, `RouteMetadata` |
45
+ | **Dependencies** | `get_container`, `get_pipeline`, `get_feature_flags`, `require_feature`, `get_rate_limiter`, `require_rate_limit` |
42
46
  | **Documentation Mounting** | `mount_zensical_docs`, `DocumentationNotFoundError`, `ZensicalDocsConfig` |
43
47
  | **Exception Handlers** | `register_exception_handlers`, `domain_exception_handler` |
44
48
  | **Middlewares** | `DbSessionMiddleware`, `AsyncDbSessionMiddleware`, `add_db_session_middleware`, `CorrelationMiddleware`, `HttpLoggingMiddleware` |
49
+ | **Rate Limiting** | `SlowapiRateLimiterAdapter`, `get_remote_address`, `get_user_or_ip_key`, `RateLimitConfig` |
45
50
  | **Reactive UI (NiceGUI)** | `dispatch_command`, `dispatch_query`, `mount_devtools_dashboard`, `mount_ui_app`, `ui_page` |
51
+ | **Real-Time Streaming (SSE & WebSockets)** | `EventSourceResponse`, `ServerSentEvent`, `WebSocketConnectionManager`, `WebSocketCqrsBridge` |
46
52
  | **Routing** | `CqrsRouter`, `autodiscover_routes` |
47
53
  | **Testing, E2E & Demo Narration** | `create_test_client`, `check_openapi_conformance`, `EphemeralServer`, `ephemeral_server`, `find_free_port`, `smart_click`, `DemoNarrator`, `VIRTUAL_CURSOR_SCRIPT` |
48
54
 
49
55
  ---
50
56
 
57
+
51
58
  ## 3. Monorepo & Sibling Relationships
52
59
 
53
60
  ```mermaid
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "hexastack-fastapi"
3
- version = "0.1.0"
3
+ version = "0.3.0"
4
4
  description = "FastAPI presentation adapter for Hexastack"
5
5
  readme = "README.md"
6
6
  license = "Apache-2.0"
@@ -18,6 +18,10 @@ email = "dopplereffect.us@gmail.com"
18
18
  [project.optional-dependencies]
19
19
  auth = ["hexastack-auth"]
20
20
  docs = ["zensical>=0.0.56"]
21
+ ratelimit = [
22
+ "limits>=3.14.0",
23
+ "slowapi>=0.1.9",
24
+ ]
21
25
  testing = [
22
26
  "playwright>=1.49.0",
23
27
  "schemathesis>=3.39.0",
@@ -42,3 +46,24 @@ workspace = true
42
46
 
43
47
  [tool.importlinter]
44
48
  root_packages = ["hexastack_fastapi"]
49
+
50
+ [[tool.importlinter.contracts]]
51
+ name = "Hexagonal architecture layer hierarchy"
52
+ type = "layers"
53
+ containers = ["hexastack_fastapi"]
54
+ layers = [
55
+ "infra",
56
+ "adapters",
57
+ ]
58
+
59
+ [[tool.importlinter.contracts]]
60
+ name = "Forbidden imports for adapters"
61
+ type = "forbidden"
62
+ source_modules = ["hexastack_fastapi.adapters"]
63
+ forbidden_modules = ["hexastack_fastapi.testing"]
64
+
65
+ [[tool.importlinter.contracts]]
66
+ name = "Forbidden imports for infra"
67
+ type = "forbidden"
68
+ source_modules = ["hexastack_fastapi.infra"]
69
+ forbidden_modules = ["hexastack_fastapi.testing"]
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "hexastack-fastapi"
3
- version = "0.1.0"
3
+ version = "0.3.0"
4
4
  description = "FastAPI presentation adapter for Hexastack"
5
5
  readme = "README.md"
6
6
  license = "Apache-2.0"
@@ -21,6 +21,10 @@ auth = [
21
21
  docs = [
22
22
  "zensical>=0.0.56",
23
23
  ]
24
+ ratelimit = [
25
+ "limits>=3.14.0",
26
+ "slowapi>=0.1.9",
27
+ ]
24
28
  testing = [
25
29
  "playwright>=1.49.0",
26
30
  "schemathesis>=3.39.0",
@@ -29,6 +33,7 @@ ui = [
29
33
  "nicegui>=2.0.0",
30
34
  ]
31
35
 
36
+
32
37
  [project.entry-points."hexastack.bootstrappers"]
33
38
  fastapi = "hexastack_fastapi.infra.bootstrap:FastApiBootstrapper"
34
39
 
@@ -43,3 +48,28 @@ hexastack-cqrs = { workspace = true }
43
48
 
44
49
  [tool.importlinter]
45
50
  root_packages = ["hexastack_fastapi"]
51
+
52
+ [[tool.importlinter.contracts]]
53
+ name = "Hexagonal architecture layer hierarchy"
54
+ type = "layers"
55
+ containers = ["hexastack_fastapi"]
56
+ layers = [
57
+ "infra",
58
+ "adapters",
59
+ ]
60
+
61
+ [[tool.importlinter.contracts]]
62
+ name = "Forbidden imports for adapters"
63
+ type = "forbidden"
64
+ source_modules = ["hexastack_fastapi.adapters"]
65
+ forbidden_modules = [
66
+ "hexastack_fastapi.testing",
67
+ ]
68
+
69
+ [[tool.importlinter.contracts]]
70
+ name = "Forbidden imports for infra"
71
+ type = "forbidden"
72
+ source_modules = ["hexastack_fastapi.infra"]
73
+ forbidden_modules = [
74
+ "hexastack_fastapi.testing",
75
+ ]
@@ -1,18 +1,30 @@
1
- from hexastack_fastapi.adapters.app import create_fastapi_app
2
1
  from hexastack_fastapi.adapters.dependencies import (
3
2
  check_openapi_conformance,
4
3
  create_test_client,
5
4
  get_container,
6
5
  get_feature_flags,
7
6
  get_pipeline,
7
+ get_rate_limiter,
8
8
  require_feature,
9
+ require_rate_limit,
9
10
  )
10
11
  from hexastack_fastapi.adapters.docs import (
11
12
  DocumentationNotFoundError,
12
13
  mount_zensical_docs,
13
14
  )
14
15
  from hexastack_fastapi.adapters.health import create_health_router
16
+ from hexastack_fastapi.adapters.metrics import create_metrics_router
17
+ from hexastack_fastapi.adapters.ratelimit import (
18
+ SlowapiRateLimiterAdapter,
19
+ get_remote_address,
20
+ get_user_or_ip_key,
21
+ rate_limit,
22
+ )
15
23
  from hexastack_fastapi.adapters.routing import CqrsRouter
24
+ from hexastack_fastapi.adapters.sse import (
25
+ EventSourceResponse,
26
+ ServerSentEvent,
27
+ )
16
28
  from hexastack_fastapi.adapters.ui import (
17
29
  dispatch_command,
18
30
  dispatch_query,
@@ -20,22 +32,36 @@ from hexastack_fastapi.adapters.ui import (
20
32
  mount_ui_app,
21
33
  ui_page,
22
34
  )
35
+ from hexastack_fastapi.adapters.websockets import (
36
+ WebSocketConnectionManager,
37
+ WebSocketCqrsBridge,
38
+ )
23
39
 
24
40
  __all__ = [
25
41
  "check_openapi_conformance",
26
42
  "CqrsRouter",
27
- "create_fastapi_app",
28
43
  "create_health_router",
44
+ "create_metrics_router",
29
45
  "create_test_client",
30
46
  "dispatch_command",
31
47
  "dispatch_query",
32
48
  "DocumentationNotFoundError",
49
+ "EventSourceResponse",
33
50
  "get_container",
34
51
  "get_feature_flags",
35
52
  "get_pipeline",
53
+ "get_rate_limiter",
54
+ "get_remote_address",
55
+ "get_user_or_ip_key",
36
56
  "mount_devtools_dashboard",
37
57
  "mount_ui_app",
38
58
  "mount_zensical_docs",
59
+ "rate_limit",
39
60
  "require_feature",
61
+ "require_rate_limit",
62
+ "ServerSentEvent",
63
+ "SlowapiRateLimiterAdapter",
40
64
  "ui_page",
65
+ "WebSocketConnectionManager",
66
+ "WebSocketCqrsBridge",
41
67
  ]
@@ -19,7 +19,9 @@ __all__ = [
19
19
  "get_container",
20
20
  "get_feature_flags",
21
21
  "get_pipeline",
22
+ "get_rate_limiter",
22
23
  "require_feature",
24
+ "require_rate_limit",
23
25
  ]
24
26
 
25
27
 
@@ -188,3 +190,64 @@ def require_feature(
188
190
  )
189
191
 
190
192
  return _dependency
193
+
194
+
195
+ def get_rate_limiter(request: Request) -> Any | None:
196
+ """FastAPI dependency resolving the registered RateLimiterPort instance if available.
197
+
198
+ Args:
199
+ request: Incoming FastAPI Request object.
200
+
201
+ Returns:
202
+ RateLimiterPort instance from app.state or DI container, or None.
203
+ """
204
+ limiter = getattr(request.app.state, "rate_limiter", None)
205
+ if limiter is not None:
206
+ return limiter
207
+
208
+ container = getattr(request.app.state, "container", None)
209
+ if isinstance(container, Container):
210
+ from hexastack_core.ports.ratelimit import RateLimiterPort
211
+
212
+ if RateLimiterPort in container:
213
+ return container.resolve(RateLimiterPort)
214
+ return None
215
+
216
+
217
+ def require_rate_limit(
218
+ limit: str,
219
+ *,
220
+ key_func: Any | None = None,
221
+ detail: str | None = None,
222
+ ) -> Any:
223
+ """Create a FastAPI dependency that enforces rate limiting on an endpoint.
224
+
225
+ Notes/Architectural Intent:
226
+ Checks rate limit against registered RateLimiterPort. If quota is exceeded,
227
+ raises an HTTPException with status 429 and Retry-After header.
228
+
229
+ Args:
230
+ limit: Rate limit specification string (e.g. '10/minute').
231
+ key_func: Optional callable to extract bucket key from request.
232
+ detail: Optional error message string.
233
+
234
+ Returns:
235
+ A FastAPI dependency callable.
236
+ """
237
+ from hexastack_fastapi.adapters.ratelimit import get_user_or_ip_key
238
+
239
+ key_extractor = key_func or get_user_or_ip_key
240
+
241
+ async def _dependency(request: Request) -> None:
242
+ limiter = get_rate_limiter(request)
243
+ if limiter is not None:
244
+ key = key_extractor(request)
245
+ if not limiter.hit(key, limit):
246
+ reset_sec = limiter.get_reset_window(key, limit)
247
+ raise HTTPException(
248
+ status_code=status.HTTP_429_TOO_MANY_REQUESTS,
249
+ detail=detail or f"Rate limit exceeded: {limit}.",
250
+ headers={"Retry-After": str(reset_sec)},
251
+ )
252
+
253
+ return _dependency
@@ -6,7 +6,6 @@ from fastapi.responses import JSONResponse
6
6
  from rodi import Container
7
7
 
8
8
  from hexastack_core.utils.context import get_correlation_id
9
- from hexastack_fastapi.infra.config import HealthConfig
10
9
 
11
10
  __all__ = [
12
11
  "create_health_router",
@@ -15,17 +14,22 @@ __all__ = [
15
14
 
16
15
  def create_health_router(
17
16
  container: Container | None = None,
18
- config: HealthConfig | None = None,
17
+ config: Any | None = None,
18
+ health_path: str = "/health",
19
+ ready_path: str = "/ready",
19
20
  ) -> APIRouter:
20
21
  """Create APIRouter with standard liveness and readiness probe endpoints.
21
22
 
22
23
  Notes/Architectural Intent:
23
24
  Exposes lightweight /health (liveness) and /ready (readiness) endpoints for orchestration
24
- platforms like Kubernetes and container engines.
25
+ platforms like Kubernetes and container engines. Decoupled from infra configuration schemas
26
+ by accepting primitives or duck-typed config objects.
25
27
 
26
28
  Args:
27
29
  container: Optional rodi Container instance to check dependency health.
28
- config: Optional HealthConfig specifying custom URL paths.
30
+ config: Optional configuration object specifying custom URL paths (e.g. HealthConfig).
31
+ health_path: Default liveness probe path (used if config not provided).
32
+ ready_path: Default readiness probe path (used if config not provided).
29
33
 
30
34
  Returns:
31
35
  APIRouter mounted with health endpoints.
@@ -33,10 +37,19 @@ def create_health_router(
33
37
  Raises:
34
38
  None.
35
39
  """
36
- cfg = config or HealthConfig()
40
+ resolved_health_path = (
41
+ getattr(config, "health_path", None) or health_path
42
+ if config is not None
43
+ else health_path
44
+ )
45
+ resolved_ready_path = (
46
+ getattr(config, "ready_path", None) or ready_path
47
+ if config is not None
48
+ else ready_path
49
+ )
37
50
  router = APIRouter(tags=["Health"])
38
51
 
39
- @router.get(cfg.health_path, summary="Liveness Probe")
52
+ @router.get(resolved_health_path, summary="Liveness Probe")
40
53
  async def liveness() -> dict[str, Any]:
41
54
  return {
42
55
  "status": "ok",
@@ -44,7 +57,7 @@ def create_health_router(
44
57
  "correlation_id": get_correlation_id(),
45
58
  }
46
59
 
47
- @router.get(cfg.ready_path, summary="Readiness Probe")
60
+ @router.get(resolved_ready_path, summary="Readiness Probe")
48
61
  async def readiness() -> JSONResponse:
49
62
  checks: dict[str, str] = {
50
63
  "container": "ok" if container is not None else "unconfigured",
@@ -0,0 +1,44 @@
1
+ """Prometheus Metrics Endpoint Router for FastAPI."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from fastapi import APIRouter, Response
6
+ from rodi import Container
7
+
8
+ from hexastack_core.ports.metrics import MetricsPort
9
+
10
+
11
+ def create_metrics_router(container: Container | None = None) -> APIRouter:
12
+ """Create a FastAPI APIRouter mounting the /metrics Prometheus scraper endpoint.
13
+
14
+ Args:
15
+ container: Active rodi Container to resolve MetricsPort.
16
+
17
+ Returns:
18
+ APIRouter exposing GET /metrics.
19
+ """
20
+ router = APIRouter(tags=["Observability"])
21
+
22
+ @router.get("/metrics", include_in_schema=False)
23
+ async def get_metrics() -> Response:
24
+ if container is not None and MetricsPort in container:
25
+ metrics_port = container.resolve(MetricsPort)
26
+ body = metrics_port.generate_metrics_text()
27
+ else:
28
+ try:
29
+ from prometheus_client import generate_latest
30
+
31
+ body = generate_latest()
32
+ except ImportError:
33
+ body = b"# Prometheus metrics not configured"
34
+
35
+ return Response(
36
+ content=body, media_type="text/plain; version=0.0.4; charset=utf-8"
37
+ )
38
+
39
+ return router
40
+
41
+
42
+ __all__ = [
43
+ "create_metrics_router",
44
+ ]