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.
- {hexastack_fastapi-0.3.0 → hexastack_fastapi-0.4.0}/PKG-INFO +3 -5
- {hexastack_fastapi-0.3.0 → hexastack_fastapi-0.4.0}/pyproject.toml +14 -4
- {hexastack_fastapi-0.3.0 → hexastack_fastapi-0.4.0}/pyproject.toml.orig +14 -7
- {hexastack_fastapi-0.3.0 → hexastack_fastapi-0.4.0}/src/hexastack_fastapi/adapters/__init__.py +0 -12
- {hexastack_fastapi-0.3.0 → hexastack_fastapi-0.4.0}/src/hexastack_fastapi/adapters/sse.py +1 -3
- hexastack_fastapi-0.3.0/src/hexastack_fastapi/adapters/ui.py +0 -502
- {hexastack_fastapi-0.3.0 → hexastack_fastapi-0.4.0}/README.md +0 -0
- {hexastack_fastapi-0.3.0 → hexastack_fastapi-0.4.0}/src/hexastack_fastapi/__init__.py +0 -0
- {hexastack_fastapi-0.3.0 → hexastack_fastapi-0.4.0}/src/hexastack_fastapi/adapters/db_session.py +0 -0
- {hexastack_fastapi-0.3.0 → hexastack_fastapi-0.4.0}/src/hexastack_fastapi/adapters/dependencies.py +0 -0
- {hexastack_fastapi-0.3.0 → hexastack_fastapi-0.4.0}/src/hexastack_fastapi/adapters/docs.py +0 -0
- {hexastack_fastapi-0.3.0 → hexastack_fastapi-0.4.0}/src/hexastack_fastapi/adapters/health.py +0 -0
- {hexastack_fastapi-0.3.0 → hexastack_fastapi-0.4.0}/src/hexastack_fastapi/adapters/metrics.py +0 -0
- {hexastack_fastapi-0.3.0 → hexastack_fastapi-0.4.0}/src/hexastack_fastapi/adapters/ratelimit.py +0 -0
- {hexastack_fastapi-0.3.0 → hexastack_fastapi-0.4.0}/src/hexastack_fastapi/adapters/routing.py +0 -0
- {hexastack_fastapi-0.3.0 → hexastack_fastapi-0.4.0}/src/hexastack_fastapi/adapters/websockets.py +0 -0
- {hexastack_fastapi-0.3.0 → hexastack_fastapi-0.4.0}/src/hexastack_fastapi/domain/__init__.py +0 -0
- {hexastack_fastapi-0.3.0 → hexastack_fastapi-0.4.0}/src/hexastack_fastapi/domain/config.py +0 -0
- {hexastack_fastapi-0.3.0 → hexastack_fastapi-0.4.0}/src/hexastack_fastapi/infra/__init__.py +0 -0
- {hexastack_fastapi-0.3.0 → hexastack_fastapi-0.4.0}/src/hexastack_fastapi/infra/app.py +0 -0
- {hexastack_fastapi-0.3.0 → hexastack_fastapi-0.4.0}/src/hexastack_fastapi/infra/autodiscovery.py +0 -0
- {hexastack_fastapi-0.3.0 → hexastack_fastapi-0.4.0}/src/hexastack_fastapi/infra/bootstrap.py +0 -0
- {hexastack_fastapi-0.3.0 → hexastack_fastapi-0.4.0}/src/hexastack_fastapi/infra/config.py +0 -0
- {hexastack_fastapi-0.3.0 → hexastack_fastapi-0.4.0}/src/hexastack_fastapi/infra/decorators.py +0 -0
- {hexastack_fastapi-0.3.0 → hexastack_fastapi-0.4.0}/src/hexastack_fastapi/infra/exception_handlers.py +0 -0
- {hexastack_fastapi-0.3.0 → hexastack_fastapi-0.4.0}/src/hexastack_fastapi/infra/middleware/__init__.py +0 -0
- {hexastack_fastapi-0.3.0 → hexastack_fastapi-0.4.0}/src/hexastack_fastapi/infra/middleware/correlation.py +0 -0
- {hexastack_fastapi-0.3.0 → hexastack_fastapi-0.4.0}/src/hexastack_fastapi/infra/middleware/logging.py +0 -0
- {hexastack_fastapi-0.3.0 → hexastack_fastapi-0.4.0}/src/hexastack_fastapi/infra/middleware/metrics.py +0 -0
- {hexastack_fastapi-0.3.0 → hexastack_fastapi-0.4.0}/src/hexastack_fastapi/py.typed +0 -0
- {hexastack_fastapi-0.3.0 → hexastack_fastapi-0.4.0}/src/hexastack_fastapi/testing/__init__.py +0 -0
- {hexastack_fastapi-0.3.0 → hexastack_fastapi-0.4.0}/src/hexastack_fastapi/testing/cursor.py +0 -0
- {hexastack_fastapi-0.3.0 → hexastack_fastapi-0.4.0}/src/hexastack_fastapi/testing/recorder.py +0 -0
- {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
|
+
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>=
|
|
14
|
-
Requires-Dist: slowapi>=0.1.
|
|
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
|

|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "hexastack-fastapi"
|
|
3
|
-
version = "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>=
|
|
23
|
-
"slowapi>=0.1.
|
|
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
|
+
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>=
|
|
26
|
-
"slowapi>=0.1.
|
|
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]]
|
{hexastack_fastapi-0.3.0 → hexastack_fastapi-0.4.0}/src/hexastack_fastapi/adapters/__init__.py
RENAMED
|
@@ -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
|
-
|
|
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)
|
|
File without changes
|
|
File without changes
|
{hexastack_fastapi-0.3.0 → hexastack_fastapi-0.4.0}/src/hexastack_fastapi/adapters/db_session.py
RENAMED
|
File without changes
|
{hexastack_fastapi-0.3.0 → hexastack_fastapi-0.4.0}/src/hexastack_fastapi/adapters/dependencies.py
RENAMED
|
File without changes
|
|
File without changes
|
{hexastack_fastapi-0.3.0 → hexastack_fastapi-0.4.0}/src/hexastack_fastapi/adapters/health.py
RENAMED
|
File without changes
|
{hexastack_fastapi-0.3.0 → hexastack_fastapi-0.4.0}/src/hexastack_fastapi/adapters/metrics.py
RENAMED
|
File without changes
|
{hexastack_fastapi-0.3.0 → hexastack_fastapi-0.4.0}/src/hexastack_fastapi/adapters/ratelimit.py
RENAMED
|
File without changes
|
{hexastack_fastapi-0.3.0 → hexastack_fastapi-0.4.0}/src/hexastack_fastapi/adapters/routing.py
RENAMED
|
File without changes
|
{hexastack_fastapi-0.3.0 → hexastack_fastapi-0.4.0}/src/hexastack_fastapi/adapters/websockets.py
RENAMED
|
File without changes
|
{hexastack_fastapi-0.3.0 → hexastack_fastapi-0.4.0}/src/hexastack_fastapi/domain/__init__.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{hexastack_fastapi-0.3.0 → hexastack_fastapi-0.4.0}/src/hexastack_fastapi/infra/autodiscovery.py
RENAMED
|
File without changes
|
{hexastack_fastapi-0.3.0 → hexastack_fastapi-0.4.0}/src/hexastack_fastapi/infra/bootstrap.py
RENAMED
|
File without changes
|
|
File without changes
|
{hexastack_fastapi-0.3.0 → hexastack_fastapi-0.4.0}/src/hexastack_fastapi/infra/decorators.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{hexastack_fastapi-0.3.0 → hexastack_fastapi-0.4.0}/src/hexastack_fastapi/testing/__init__.py
RENAMED
|
File without changes
|
|
File without changes
|
{hexastack_fastapi-0.3.0 → hexastack_fastapi-0.4.0}/src/hexastack_fastapi/testing/recorder.py
RENAMED
|
File without changes
|
|
File without changes
|