hexastack-fastapi 0.3.0__tar.gz → 0.4.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.3.0 → hexastack_fastapi-0.4.0}/PKG-INFO +3 -5
  2. {hexastack_fastapi-0.3.0 → hexastack_fastapi-0.4.0}/pyproject.toml +14 -4
  3. {hexastack_fastapi-0.3.0 → hexastack_fastapi-0.4.0}/pyproject.toml.orig +14 -7
  4. {hexastack_fastapi-0.3.0 → hexastack_fastapi-0.4.0}/src/hexastack_fastapi/adapters/__init__.py +0 -12
  5. {hexastack_fastapi-0.3.0 → hexastack_fastapi-0.4.0}/src/hexastack_fastapi/adapters/sse.py +1 -3
  6. hexastack_fastapi-0.3.0/src/hexastack_fastapi/adapters/ui.py +0 -502
  7. {hexastack_fastapi-0.3.0 → hexastack_fastapi-0.4.0}/README.md +0 -0
  8. {hexastack_fastapi-0.3.0 → hexastack_fastapi-0.4.0}/src/hexastack_fastapi/__init__.py +0 -0
  9. {hexastack_fastapi-0.3.0 → hexastack_fastapi-0.4.0}/src/hexastack_fastapi/adapters/db_session.py +0 -0
  10. {hexastack_fastapi-0.3.0 → hexastack_fastapi-0.4.0}/src/hexastack_fastapi/adapters/dependencies.py +0 -0
  11. {hexastack_fastapi-0.3.0 → hexastack_fastapi-0.4.0}/src/hexastack_fastapi/adapters/docs.py +0 -0
  12. {hexastack_fastapi-0.3.0 → hexastack_fastapi-0.4.0}/src/hexastack_fastapi/adapters/health.py +0 -0
  13. {hexastack_fastapi-0.3.0 → hexastack_fastapi-0.4.0}/src/hexastack_fastapi/adapters/metrics.py +0 -0
  14. {hexastack_fastapi-0.3.0 → hexastack_fastapi-0.4.0}/src/hexastack_fastapi/adapters/ratelimit.py +0 -0
  15. {hexastack_fastapi-0.3.0 → hexastack_fastapi-0.4.0}/src/hexastack_fastapi/adapters/routing.py +0 -0
  16. {hexastack_fastapi-0.3.0 → hexastack_fastapi-0.4.0}/src/hexastack_fastapi/adapters/websockets.py +0 -0
  17. {hexastack_fastapi-0.3.0 → hexastack_fastapi-0.4.0}/src/hexastack_fastapi/domain/__init__.py +0 -0
  18. {hexastack_fastapi-0.3.0 → hexastack_fastapi-0.4.0}/src/hexastack_fastapi/domain/config.py +0 -0
  19. {hexastack_fastapi-0.3.0 → hexastack_fastapi-0.4.0}/src/hexastack_fastapi/infra/__init__.py +0 -0
  20. {hexastack_fastapi-0.3.0 → hexastack_fastapi-0.4.0}/src/hexastack_fastapi/infra/app.py +0 -0
  21. {hexastack_fastapi-0.3.0 → hexastack_fastapi-0.4.0}/src/hexastack_fastapi/infra/autodiscovery.py +0 -0
  22. {hexastack_fastapi-0.3.0 → hexastack_fastapi-0.4.0}/src/hexastack_fastapi/infra/bootstrap.py +0 -0
  23. {hexastack_fastapi-0.3.0 → hexastack_fastapi-0.4.0}/src/hexastack_fastapi/infra/config.py +0 -0
  24. {hexastack_fastapi-0.3.0 → hexastack_fastapi-0.4.0}/src/hexastack_fastapi/infra/decorators.py +0 -0
  25. {hexastack_fastapi-0.3.0 → hexastack_fastapi-0.4.0}/src/hexastack_fastapi/infra/exception_handlers.py +0 -0
  26. {hexastack_fastapi-0.3.0 → hexastack_fastapi-0.4.0}/src/hexastack_fastapi/infra/middleware/__init__.py +0 -0
  27. {hexastack_fastapi-0.3.0 → hexastack_fastapi-0.4.0}/src/hexastack_fastapi/infra/middleware/correlation.py +0 -0
  28. {hexastack_fastapi-0.3.0 → hexastack_fastapi-0.4.0}/src/hexastack_fastapi/infra/middleware/logging.py +0 -0
  29. {hexastack_fastapi-0.3.0 → hexastack_fastapi-0.4.0}/src/hexastack_fastapi/infra/middleware/metrics.py +0 -0
  30. {hexastack_fastapi-0.3.0 → hexastack_fastapi-0.4.0}/src/hexastack_fastapi/py.typed +0 -0
  31. {hexastack_fastapi-0.3.0 → hexastack_fastapi-0.4.0}/src/hexastack_fastapi/testing/__init__.py +0 -0
  32. {hexastack_fastapi-0.3.0 → hexastack_fastapi-0.4.0}/src/hexastack_fastapi/testing/cursor.py +0 -0
  33. {hexastack_fastapi-0.3.0 → hexastack_fastapi-0.4.0}/src/hexastack_fastapi/testing/recorder.py +0 -0
  34. {hexastack_fastapi-0.3.0 → hexastack_fastapi-0.4.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.3.0
3
+ Version: 0.4.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,17 +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
+ Requires-Dist: limits>=5.8.0 ; extra == 'ratelimit'
14
+ Requires-Dist: slowapi>=0.1.10 ; extra == 'ratelimit'
15
15
  Requires-Dist: playwright>=1.49.0 ; extra == 'testing'
16
16
  Requires-Dist: schemathesis>=3.39.0 ; extra == 'testing'
17
- Requires-Dist: nicegui>=2.0.0 ; extra == 'ui'
18
17
  Requires-Python: >=3.13
19
18
  Provides-Extra: auth
20
19
  Provides-Extra: docs
21
20
  Provides-Extra: ratelimit
22
21
  Provides-Extra: testing
23
- Provides-Extra: ui
24
22
  Description-Content-Type: text/markdown
25
23
 
26
24
  ![hexastack-fastapi](../../docs/assets/static/logos/packages/hexastack_fastapi.png)
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "hexastack-fastapi"
3
- version = "0.3.0"
3
+ version = "0.4.0"
4
4
  description = "FastAPI presentation adapter for Hexastack"
5
5
  readme = "README.md"
6
6
  license = "Apache-2.0"
@@ -19,14 +19,13 @@ email = "dopplereffect.us@gmail.com"
19
19
  auth = ["hexastack-auth"]
20
20
  docs = ["zensical>=0.0.56"]
21
21
  ratelimit = [
22
- "limits>=3.14.0",
23
- "slowapi>=0.1.9",
22
+ "limits>=5.8.0",
23
+ "slowapi>=0.1.10",
24
24
  ]
25
25
  testing = [
26
26
  "playwright>=1.49.0",
27
27
  "schemathesis>=3.39.0",
28
28
  ]
29
- ui = ["nicegui>=2.0.0"]
30
29
 
31
30
  [project.entry-points."hexastack.bootstrappers"]
32
31
  fastapi = "hexastack_fastapi.infra.bootstrap:FastApiBootstrapper"
@@ -54,6 +53,17 @@ containers = ["hexastack_fastapi"]
54
53
  layers = [
55
54
  "infra",
56
55
  "adapters",
56
+ "domain",
57
+ ]
58
+
59
+ [[tool.importlinter.contracts]]
60
+ name = "Forbidden imports for domain"
61
+ type = "forbidden"
62
+ source_modules = ["hexastack_fastapi.domain"]
63
+ forbidden_modules = [
64
+ "hexastack_fastapi.adapters",
65
+ "hexastack_fastapi.infra",
66
+ "hexastack_fastapi.testing",
57
67
  ]
58
68
 
59
69
  [[tool.importlinter.contracts]]
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "hexastack-fastapi"
3
- version = "0.3.0"
3
+ version = "0.4.0"
4
4
  description = "FastAPI presentation adapter for Hexastack"
5
5
  readme = "README.md"
6
6
  license = "Apache-2.0"
@@ -22,17 +22,13 @@ docs = [
22
22
  "zensical>=0.0.56",
23
23
  ]
24
24
  ratelimit = [
25
- "limits>=3.14.0",
26
- "slowapi>=0.1.9",
25
+ "limits>=5.8.0",
26
+ "slowapi>=0.1.10",
27
27
  ]
28
28
  testing = [
29
29
  "playwright>=1.49.0",
30
30
  "schemathesis>=3.39.0",
31
31
  ]
32
- ui = [
33
- "nicegui>=2.0.0",
34
- ]
35
-
36
32
 
37
33
  [project.entry-points."hexastack.bootstrappers"]
38
34
  fastapi = "hexastack_fastapi.infra.bootstrap:FastApiBootstrapper"
@@ -56,6 +52,17 @@ containers = ["hexastack_fastapi"]
56
52
  layers = [
57
53
  "infra",
58
54
  "adapters",
55
+ "domain",
56
+ ]
57
+
58
+ [[tool.importlinter.contracts]]
59
+ name = "Forbidden imports for domain"
60
+ type = "forbidden"
61
+ source_modules = ["hexastack_fastapi.domain"]
62
+ forbidden_modules = [
63
+ "hexastack_fastapi.adapters",
64
+ "hexastack_fastapi.infra",
65
+ "hexastack_fastapi.testing",
59
66
  ]
60
67
 
61
68
  [[tool.importlinter.contracts]]
@@ -25,13 +25,6 @@ from hexastack_fastapi.adapters.sse import (
25
25
  EventSourceResponse,
26
26
  ServerSentEvent,
27
27
  )
28
- from hexastack_fastapi.adapters.ui import (
29
- dispatch_command,
30
- dispatch_query,
31
- mount_devtools_dashboard,
32
- mount_ui_app,
33
- ui_page,
34
- )
35
28
  from hexastack_fastapi.adapters.websockets import (
36
29
  WebSocketConnectionManager,
37
30
  WebSocketCqrsBridge,
@@ -43,8 +36,6 @@ __all__ = [
43
36
  "create_health_router",
44
37
  "create_metrics_router",
45
38
  "create_test_client",
46
- "dispatch_command",
47
- "dispatch_query",
48
39
  "DocumentationNotFoundError",
49
40
  "EventSourceResponse",
50
41
  "get_container",
@@ -53,15 +44,12 @@ __all__ = [
53
44
  "get_rate_limiter",
54
45
  "get_remote_address",
55
46
  "get_user_or_ip_key",
56
- "mount_devtools_dashboard",
57
- "mount_ui_app",
58
47
  "mount_zensical_docs",
59
48
  "rate_limit",
60
49
  "require_feature",
61
50
  "require_rate_limit",
62
51
  "ServerSentEvent",
63
52
  "SlowapiRateLimiterAdapter",
64
- "ui_page",
65
53
  "WebSocketConnectionManager",
66
54
  "WebSocketCqrsBridge",
67
55
  ]
@@ -3,7 +3,6 @@
3
3
  from __future__ import annotations
4
4
 
5
5
  import asyncio
6
- import contextlib
7
6
  from collections.abc import AsyncIterable, AsyncIterator
8
7
  from dataclasses import dataclass
9
8
  from typing import Any
@@ -150,8 +149,7 @@ class EventSourceResponse(Response):
150
149
  finally:
151
150
  if not producer_task.done():
152
151
  producer_task.cancel()
153
- with contextlib.suppress(asyncio.CancelledError, Exception):
154
- _res = await producer_task
152
+ _ = await asyncio.gather(producer_task, return_exceptions=True)
155
153
 
156
154
  async def __call__(self, scope: Scope, receive: Receive, send: Send) -> None:
157
155
  """ASGI response streaming callable."""
@@ -1,502 +0,0 @@
1
- """NiceGUI UI presentation adapter and CQRS reactive bindings for Hexastack.
2
-
3
- Notes/Architectural Intent:
4
- Provides reactive UI integration built on NiceGUI and FastAPI:
5
- 1. Primitives: `ui_page`, `dispatch_command`, `dispatch_query`, and reactive hooks
6
- enabling developers to build custom reactive web applications directly on CQRS.
7
- 2. Default DevTools Dashboard: `mount_devtools_dashboard` providing an out-of-the-box
8
- interactive inspector for CQRS buses, feature flags, DI container, and metadata.
9
- """
10
-
11
- from __future__ import annotations
12
-
13
- import inspect
14
- from collections.abc import Callable
15
- from typing import TYPE_CHECKING, Any
16
-
17
- from fastapi import FastAPI
18
- from rodi import Container
19
-
20
- from hexastack_core.domain.exceptions import MissingDependencyError
21
- from hexastack_cqrs.infra.pipeline import ExecutionPipeline
22
-
23
- if TYPE_CHECKING:
24
- from hexastack_core.domain import Command, Query
25
-
26
- __all__ = [
27
- "dispatch_command",
28
- "dispatch_query",
29
- "mount_devtools_dashboard",
30
- "mount_ui_app",
31
- "ui_page",
32
- ]
33
-
34
-
35
- def _check_nicegui_installed() -> None:
36
- """Verify NiceGUI is available in runtime environment."""
37
- try:
38
- import nicegui # noqa: F401
39
- except ImportError as e:
40
- raise MissingDependencyError(
41
- "NiceGUI is required for hexastack-fastapi UI support. "
42
- "Install with 'pip install hexastack-fastapi[ui]' or 'pip install nicegui'."
43
- ) from e
44
-
45
-
46
- def ui_page(
47
- path: str,
48
- *,
49
- title: str | None = None,
50
- viewport: str | None = None,
51
- favicon: str | None = None,
52
- dark: bool | None = None,
53
- response_timeout: float = 3.0,
54
- ) -> Callable[[Callable[..., Any]], Callable[..., Any]]:
55
- """Declarative decorator registering a NiceGUI reactive UI page.
56
-
57
- Notes/Architectural Intent:
58
- Wraps `nicegui.ui.page` while cleanly decoupling application modules from
59
- direct NiceGUI hard dependency at module import time.
60
-
61
- Args:
62
- path: URL path for the page (e.g. '/dashboard', '/users').
63
- title: Optional browser tab title.
64
- viewport: Optional viewport meta tag string.
65
- favicon: Optional favicon path or URL.
66
- dark: Optional dark mode setting (True/False/None).
67
- response_timeout: Maximum seconds to wait for client connection response.
68
-
69
- Returns:
70
- Decorated page function.
71
- """
72
- _check_nicegui_installed()
73
- from nicegui import ui
74
-
75
- return ui.page(
76
- path,
77
- title=title,
78
- viewport=viewport,
79
- favicon=favicon,
80
- dark=dark,
81
- response_timeout=response_timeout,
82
- )
83
-
84
-
85
- async def dispatch_command(pipeline: ExecutionPipeline, command: Command) -> Any:
86
- """Dispatch a CQRS command from a NiceGUI interactive event handler.
87
-
88
- Notes/Architectural Intent:
89
- Executes a command through the standard ExecutionPipeline,
90
- supporting synchronous or asynchronous bus handlers.
91
-
92
- Args:
93
- pipeline: ExecutionPipeline instance.
94
- command: Command instance to execute.
95
-
96
- Returns:
97
- Result returned by command execution.
98
- """
99
- result = pipeline.execute(command)
100
- if inspect.isawaitable(result):
101
- return await result
102
- return result
103
-
104
-
105
- async def dispatch_query(pipeline: ExecutionPipeline, query: Query) -> Any:
106
- """Dispatch a CQRS query from a NiceGUI interactive event handler or data loader.
107
-
108
- Notes/Architectural Intent:
109
- Executes a query through the standard ExecutionPipeline,
110
- supporting synchronous or asynchronous bus handlers.
111
-
112
- Args:
113
- pipeline: ExecutionPipeline instance.
114
- query: Query instance to execute.
115
-
116
- Returns:
117
- Result returned by query execution.
118
- """
119
- result = pipeline.execute(query)
120
- if inspect.isawaitable(result):
121
- return await result
122
- return result
123
-
124
-
125
- def mount_ui_app(
126
- app: FastAPI,
127
- *,
128
- title: str = "Hexastack UI",
129
- viewport: str = "width=device-width, initial-scale=1",
130
- favicon: str | None = None,
131
- dark: bool | None = None,
132
- ) -> None:
133
- """Mount NiceGUI reactive engine onto an existing FastAPI application instance.
134
-
135
- Notes/Architectural Intent:
136
- Integrates NiceGUI's Socket.IO and static assets into the FastAPI lifecycle.
137
-
138
- Args:
139
- app: Target FastAPI application.
140
- title: Application default page title.
141
- viewport: Viewport meta tag.
142
- favicon: Optional favicon.
143
- dark: Dark mode preference.
144
-
145
- Returns:
146
- None.
147
- """
148
- _check_nicegui_installed()
149
- from nicegui import ui
150
-
151
- ui.run_with(
152
- app,
153
- title=title,
154
- viewport=viewport,
155
- favicon=favicon,
156
- dark=dark,
157
- )
158
-
159
-
160
- def _render_cqrs_messages(container: Container) -> None:
161
- """Render table of registered CQRS message contracts."""
162
- from nicegui import ui
163
-
164
- from hexastack_cqrs.infra.registries.command import CommandRegistry
165
- from hexastack_cqrs.infra.registries.query import QueryRegistry
166
-
167
- ui.label("Registered CQRS Commands & Queries").classes("text-lg font-semibold mb-4")
168
-
169
- cmd_reg = (
170
- container.resolve(CommandRegistry) if CommandRegistry in container else None
171
- )
172
- qry_reg = container.resolve(QueryRegistry) if QueryRegistry in container else None
173
-
174
- commands_list = (
175
- [
176
- {"type": "Command", "name": getattr(k, "__name__", str(k))}
177
- for k in cmd_reg.all.values()
178
- ]
179
- if cmd_reg
180
- else []
181
- )
182
- queries_list = (
183
- [
184
- {"type": "Query", "name": getattr(k, "__name__", str(k))}
185
- for k in qry_reg.all.values()
186
- ]
187
- if qry_reg
188
- else []
189
- )
190
- cqrs_rows = commands_list + queries_list
191
-
192
- if cqrs_rows:
193
- ui.table(
194
- columns=[
195
- {
196
- "name": "type",
197
- "label": "Message Type",
198
- "field": "type",
199
- "sortable": True,
200
- },
201
- {
202
- "name": "name",
203
- "label": "Class Name",
204
- "field": "name",
205
- "sortable": True,
206
- },
207
- ],
208
- rows=cqrs_rows,
209
- row_key="name",
210
- ).classes("w-full mb-6")
211
- else:
212
- ui.label("No CQRS messages registered in container.").classes(
213
- "text-slate-500 italic mb-6"
214
- )
215
-
216
-
217
- def _render_middleware_chain(middlewares: list[Any]) -> None:
218
- """Render visual chain of active command bus middlewares."""
219
- from nicegui import ui
220
-
221
- ui.label("Command Bus Middleware Pipeline").classes("text-lg font-semibold mb-2")
222
- if middlewares:
223
- with (
224
- ui.card().classes("w-full bg-slate-50 dark:bg-slate-900 border p-4 mb-6"),
225
- ui.row().classes("items-center flex-wrap gap-2"),
226
- ):
227
- for idx, mw in enumerate(middlewares, 1):
228
- mw_name = type(mw).__name__
229
- ui.chip(
230
- f"{idx}. {mw_name}",
231
- icon="security" if "Auth" in mw_name else "filter_alt",
232
- color="blue-9",
233
- text_color="white",
234
- ).classes("font-medium")
235
- if idx < len(middlewares):
236
- ui.icon("arrow_forward", size="sm").classes(
237
- "text-slate-600 dark:text-slate-300"
238
- )
239
- ui.icon("arrow_forward", size="sm").classes(
240
- "text-slate-600 dark:text-slate-300"
241
- )
242
- ui.chip(
243
- "Handler Execution",
244
- icon="play_circle",
245
- color="green-9",
246
- text_color="white",
247
- ).classes("font-bold")
248
- else:
249
- ui.label("No active middlewares attached to CommandBus.").classes(
250
- "text-slate-500 italic mb-6"
251
- )
252
-
253
-
254
- def _render_live_runner(
255
- container: Container,
256
- pipeline: ExecutionPipeline | None,
257
- middlewares: list[Any],
258
- ) -> None:
259
- """Render interactive test runner card for ping command dispatch."""
260
- from nicegui import ui
261
-
262
- ui.label("Interactive Pipeline Execution").classes("text-lg font-semibold mb-2")
263
- with ui.card().classes("w-full bg-slate-50 dark:bg-slate-900 border p-4"):
264
- ui.label("Dispatch PingDemoCommand across the middleware pipeline:").classes(
265
- "text-sm text-slate-600 dark:text-slate-400 mb-3"
266
- )
267
- with ui.row().classes("items-center gap-3 w-full"):
268
- ping_input = ui.input(
269
- label="Message Payload", value="Hello from Hexastack DevTools!"
270
- ).classes("flex-grow")
271
- log_output = ui.log(max_lines=10).classes(
272
- "w-full h-32 bg-slate-950 text-emerald-400 p-2 font-mono text-xs rounded border"
273
- )
274
-
275
- async def _run_ping() -> None:
276
- log_output.push(
277
- f"➡️ [DISPATCH] PingDemoCommand(message='{ping_input.value}')"
278
- )
279
- for mw in middlewares:
280
- log_output.push(
281
- f" ↳ [MIDDLEWARE] Intercepting through {type(mw).__name__}..."
282
- )
283
-
284
- try:
285
- active_pipeline = pipeline
286
- if active_pipeline is None:
287
- from hexastack_cqrs.infra.pipeline import ExecutionPipeline
288
-
289
- if ExecutionPipeline in container:
290
- active_pipeline = container.resolve(ExecutionPipeline)
291
-
292
- if active_pipeline is not None:
293
- # Dynamically find registered command by name matching 'ping'
294
- cmd_cls = None
295
- from hexastack_cqrs.infra.registries.command import (
296
- CommandRegistry,
297
- )
298
-
299
- if CommandRegistry in container:
300
- creg = container.resolve(CommandRegistry)
301
- for name, cls in creg.all.items():
302
- if "ping" in name.lower():
303
- cmd_cls = cls
304
- break
305
-
306
- if cmd_cls is not None:
307
- cmd_instance = cmd_cls.model_validate(
308
- {"message": ping_input.value}
309
- )
310
- res = active_pipeline.execute(cmd_instance)
311
- log_output.push(f"✅ [SUCCESS] Result: {res}")
312
- else:
313
- # Fallback to execute_by_name directly
314
- res = active_pipeline.execute_by_name(
315
- "PingDemoCommand", {"message": ping_input.value}
316
- )
317
- log_output.push(f"✅ [SUCCESS] Result: {res}")
318
- else:
319
- log_output.push(
320
- "⚠️ [WARNING] ExecutionPipeline instance not directly bound."
321
- )
322
- except Exception as exc:
323
- log_output.push(f"❌ [ERROR] Execution failed: {exc}")
324
-
325
- ui.button(
326
- "Dispatch Ping Command", on_click=_run_ping, icon="send", color="blue-9"
327
- ).classes("text-white font-medium shadow-sm")
328
-
329
-
330
- def _render_cqrs_tab(
331
- container: Container, pipeline: ExecutionPipeline | None = None
332
- ) -> None:
333
- """Render the CQRS tab panel showing registered messages, middleware pipeline, and live runner."""
334
- from hexastack_cqrs.ports.buses import CommandBusPort
335
-
336
- _render_cqrs_messages(container)
337
- cmd_bus = container.resolve(CommandBusPort) if CommandBusPort in container else None
338
- middlewares = getattr(cmd_bus, "_middleware", []) if cmd_bus else []
339
- _render_middleware_chain(middlewares)
340
- _render_live_runner(container, pipeline, middlewares)
341
-
342
-
343
- def _render_flags_tab(container: Container) -> None:
344
- """Render the feature flags tab panel showing active flags."""
345
- from nicegui import ui
346
-
347
- from hexastack_core.ports.feature_flags import FeatureFlagPort
348
-
349
- ui.label("Active Feature Flags").classes("text-lg font-semibold mb-4")
350
- flags_adapter = (
351
- container.resolve(FeatureFlagPort) if FeatureFlagPort in container else None
352
- )
353
-
354
- get_all_fn = getattr(flags_adapter, "get_all_flags", None)
355
- if callable(get_all_fn):
356
- flags_data: dict[str, Any] = get_all_fn()
357
- if flags_data:
358
- flag_rows = [{"flag": k, "enabled": str(v)} for k, v in flags_data.items()]
359
- ui.table(
360
- columns=[
361
- {
362
- "name": "flag",
363
- "label": "Flag Key",
364
- "field": "flag",
365
- "sortable": True,
366
- },
367
- {
368
- "name": "enabled",
369
- "label": "Status",
370
- "field": "enabled",
371
- "sortable": True,
372
- },
373
- ],
374
- rows=flag_rows,
375
- row_key="flag",
376
- pagination={"rowsPerPage": 10},
377
- ).classes("w-full")
378
- else:
379
- ui.label("No feature flags currently configured in provider.").classes(
380
- "text-slate-600 dark:text-slate-400 italic"
381
- )
382
- else:
383
- ui.label(
384
- "Feature flag provider not available or does not support listing."
385
- ).classes("text-slate-600 dark:text-slate-400 italic")
386
-
387
-
388
- def _render_container_tab(container: Container) -> None:
389
- """Render the DI container tab panel showing registered services."""
390
- from nicegui import ui
391
-
392
- ui.label("Dependency Injection Services").classes("text-lg font-semibold mb-4")
393
-
394
- # rodi.Container stores service registrations in `_map`
395
- service_map = getattr(container, "_map", {})
396
- services: list[dict[str, str]] = []
397
-
398
- for cls, resolver in service_map.items():
399
- service_name = getattr(cls, "__qualname__", getattr(cls, "__name__", str(cls)))
400
- module_name = getattr(cls, "__module__", "")
401
- resolver_desc = str(resolver).strip("<>")
402
-
403
- services.append(
404
- {
405
- "service": service_name,
406
- "module": module_name,
407
- "resolver": resolver_desc,
408
- }
409
- )
410
-
411
- # Sort services alphabetically by service name
412
- services.sort(key=lambda s: s["service"])
413
-
414
- if services:
415
- ui.table(
416
- columns=[
417
- {
418
- "name": "service",
419
- "label": "Registered Port / Service",
420
- "field": "service",
421
- "sortable": True,
422
- },
423
- {
424
- "name": "module",
425
- "label": "Module",
426
- "field": "module",
427
- "sortable": True,
428
- },
429
- {
430
- "name": "resolver",
431
- "label": "Binding / Lifetime",
432
- "field": "resolver",
433
- "sortable": True,
434
- },
435
- ],
436
- rows=services,
437
- row_key="service",
438
- pagination={"rowsPerPage": 10},
439
- ).classes("w-full")
440
- else:
441
- ui.label("No direct services found in container introspection.").classes(
442
- "text-slate-600 dark:text-slate-400 italic"
443
- )
444
-
445
-
446
- def mount_devtools_dashboard(
447
- app: FastAPI,
448
- container: Container,
449
- pipeline: ExecutionPipeline | None = None,
450
- *,
451
- path: str = "/_devtools",
452
- title: str = "Hexastack DevTools",
453
- ) -> None:
454
- """Mount the default Hexastack interactive DevTools dashboard on FastAPI.
455
-
456
- Notes/Architectural Intent:
457
- Renders a rich developer console inspecting CQRS buses, feature flags,
458
- and DI container services built entirely using the NiceGUI primitives.
459
-
460
- Args:
461
- app: Target FastAPI application.
462
- container: Application rodi.Container instance.
463
- pipeline: Optional ExecutionPipeline instance.
464
- path: URL path where the dashboard is mounted (default '/_devtools').
465
- title: Dashboard page title.
466
-
467
- Returns:
468
- None.
469
- """
470
- _check_nicegui_installed()
471
- from nicegui import ui
472
-
473
- mount_ui_app(app, title=title)
474
-
475
- @ui.page(path, title=title)
476
- def devtools_page():
477
- ui.colors(primary="#1E40AF", secondary="#065F46", accent="#5B21B6")
478
-
479
- with ui.header().classes(
480
- "items-center justify-between bg-slate-900 text-white px-6 py-3"
481
- ):
482
- with ui.row().classes("items-center gap-3"):
483
- ui.icon("layers", size="md").classes("text-blue-400")
484
- ui.label(title).classes("text-xl font-bold tracking-tight")
485
- ui.badge("v0.1.0", color="blue-10").classes(
486
- "text-xs font-semibold text-white"
487
- )
488
-
489
- with ui.tabs().classes("w-full bg-slate-100 dark:bg-slate-800") as tabs:
490
- tab_cqrs = ui.tab("CQRS Registry", icon="bolt")
491
- tab_flags = ui.tab("Feature Flags", icon="toggle_on")
492
- tab_container = ui.tab("DI Container", icon="hub")
493
-
494
- with ui.tab_panels(tabs, value=tab_cqrs).classes("w-full p-6"):
495
- with ui.tab_panel(tab_cqrs):
496
- _render_cqrs_tab(container, pipeline=pipeline)
497
-
498
- with ui.tab_panel(tab_flags):
499
- _render_flags_tab(container)
500
-
501
- with ui.tab_panel(tab_container):
502
- _render_container_tab(container)