fastapi-modular 0.1.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.
- fastapi_modular-0.1.0.dist-info/METADATA +377 -0
- fastapi_modular-0.1.0.dist-info/RECORD +69 -0
- fastapi_modular-0.1.0.dist-info/WHEEL +4 -0
- fastapi_modular-0.1.0.dist-info/entry_points.txt +3 -0
- fastapi_modular-0.1.0.dist-info/licenses/LICENSE +21 -0
- pymodular/__init__.py +74 -0
- pymodular/cli/__init__.py +0 -0
- pymodular/cli/clean.py +39 -0
- pymodular/cli/configure_env.py +569 -0
- pymodular/cli/cong_cu.py +111 -0
- pymodular/cli/info.py +62 -0
- pymodular/cli/install.py +83 -0
- pymodular/cli/main.py +247 -0
- pymodular/cli/new_module.py +492 -0
- pymodular/cli/new_project.py +471 -0
- pymodular/cli/serve.py +59 -0
- pymodular/core/__init__.py +0 -0
- pymodular/core/clock.py +15 -0
- pymodular/core/compat.py +39 -0
- pymodular/core/config.py +495 -0
- pymodular/core/container.py +354 -0
- pymodular/core/context.py +78 -0
- pymodular/core/controller.py +208 -0
- pymodular/core/error_handlers.py +272 -0
- pymodular/core/exceptions.py +104 -0
- pymodular/core/guards.py +117 -0
- pymodular/core/lifespan.py +150 -0
- pymodular/core/logging.py +88 -0
- pymodular/core/metrics.py +190 -0
- pymodular/core/schemas.py +105 -0
- pymodular/core/websocket/__init__.py +31 -0
- pymodular/core/websocket/adapter.py +192 -0
- pymodular/core/websocket/gateway.py +735 -0
- pymodular/core/websocket/namespace.py +148 -0
- pymodular/core/websocket/protocol.py +157 -0
- pymodular/core/websocket/server.py +175 -0
- pymodular/core/websocket/socket.py +241 -0
- pymodular/discovery.py +180 -0
- pymodular/factory.py +126 -0
- pymodular/infrastructure/__init__.py +1 -0
- pymodular/infrastructure/database/__init__.py +8 -0
- pymodular/infrastructure/database/base.py +228 -0
- pymodular/infrastructure/database/circuit.py +207 -0
- pymodular/infrastructure/database/factory.py +88 -0
- pymodular/infrastructure/database/memory.py +112 -0
- pymodular/infrastructure/database/mongo.py +186 -0
- pymodular/infrastructure/database/repository.py +188 -0
- pymodular/infrastructure/database/sql.py +520 -0
- pymodular/infrastructure/kafka/__init__.py +26 -0
- pymodular/infrastructure/kafka/broker.py +231 -0
- pymodular/infrastructure/kafka/consumers.py +371 -0
- pymodular/infrastructure/kafka/metrics.py +17 -0
- pymodular/infrastructure/mqtt/__init__.py +35 -0
- pymodular/infrastructure/mqtt/client.py +292 -0
- pymodular/infrastructure/mqtt/consumers.py +219 -0
- pymodular/infrastructure/mqtt/metrics.py +17 -0
- pymodular/infrastructure/mqtt/patterns.py +116 -0
- pymodular/infrastructure/rabbitmq/__init__.py +33 -0
- pymodular/infrastructure/rabbitmq/broker.py +616 -0
- pymodular/infrastructure/rabbitmq/consumers.py +450 -0
- pymodular/infrastructure/rabbitmq/metrics.py +34 -0
- pymodular/infrastructure/rabbitmq/patterns.py +64 -0
- pymodular/infrastructure/redis/__init__.py +31 -0
- pymodular/infrastructure/redis/client.py +362 -0
- pymodular/infrastructure/redis/metrics.py +20 -0
- pymodular/infrastructure/redis/pubsub.py +262 -0
- pymodular/middleware/__init__.py +0 -0
- pymodular/middleware/request_context.py +164 -0
- pymodular/py.typed +0 -0
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
"""Middleware gán request-id và ghi access log.
|
|
2
|
+
|
|
3
|
+
Cố ý viết dưới dạng **ASGI middleware thuần**, không dùng
|
|
4
|
+
`starlette.middleware.base.BaseHTTPMiddleware`. Lý do không chỉ là hiệu năng:
|
|
5
|
+
|
|
6
|
+
`BaseHTTPMiddleware` chạy ứng dụng bên trong một task group riêng và trả về từ
|
|
7
|
+
`call_next` ngay khi response *bắt đầu* stream. Hệ quả là phần dọn dẹp của
|
|
8
|
+
dependency dạng `yield` — nơi session database COMMIT — có thể chạy **sau** khi
|
|
9
|
+
client đã nhận response. Client ghi xong, đọc lại ngay thì thấy dữ liệu cũ.
|
|
10
|
+
|
|
11
|
+
Với ASGI thuần, ta chỉ quan sát message đi qua; thứ tự "chạy xong handler ->
|
|
12
|
+
commit -> gửi response" của Starlette được giữ nguyên.
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
from __future__ import annotations
|
|
16
|
+
|
|
17
|
+
import time
|
|
18
|
+
|
|
19
|
+
from starlette.datastructures import Headers, MutableHeaders
|
|
20
|
+
from starlette.types import ASGIApp, Message, Receive, Scope, Send
|
|
21
|
+
|
|
22
|
+
from pymodular.core.context import (
|
|
23
|
+
new_request_id,
|
|
24
|
+
new_trace_id,
|
|
25
|
+
parse_traceparent,
|
|
26
|
+
reset_request_id,
|
|
27
|
+
reset_trace_id,
|
|
28
|
+
set_request_id,
|
|
29
|
+
set_trace_id,
|
|
30
|
+
)
|
|
31
|
+
from pymodular.core.logging import get_logger
|
|
32
|
+
from pymodular.core.metrics import http_duration, http_in_flight, http_requests
|
|
33
|
+
|
|
34
|
+
log = get_logger("http")
|
|
35
|
+
|
|
36
|
+
REQUEST_ID_HEADER = "x-request-id"
|
|
37
|
+
RESPONSE_TIME_HEADER = "x-response-time-ms"
|
|
38
|
+
TRACEPARENT_HEADER = "traceparent"
|
|
39
|
+
TRACE_ID_HEADER = "x-trace-id"
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
class RequestContextMiddleware:
|
|
43
|
+
"""Lấy request-id từ header nếu có (trace xuyên service), không thì sinh mới."""
|
|
44
|
+
|
|
45
|
+
def __init__(self, app: ASGIApp) -> None:
|
|
46
|
+
self.app = app
|
|
47
|
+
|
|
48
|
+
async def __call__(self, scope: Scope, receive: Receive, send: Send) -> None:
|
|
49
|
+
if scope["type"] != "http":
|
|
50
|
+
await self.app(scope, receive, send)
|
|
51
|
+
return
|
|
52
|
+
|
|
53
|
+
headers = Headers(scope=scope)
|
|
54
|
+
request_id = headers.get(REQUEST_ID_HEADER) or new_request_id()
|
|
55
|
+
|
|
56
|
+
# trace_id nối các dịch vụ lại với nhau: nếu bên gọi đã có traceparent
|
|
57
|
+
# thì dùng lại, chưa có thì sinh mới và truyền tiếp cho bên dưới.
|
|
58
|
+
trace_id = parse_traceparent(headers.get(TRACEPARENT_HEADER)) or new_trace_id()
|
|
59
|
+
|
|
60
|
+
request_token = set_request_id(request_id)
|
|
61
|
+
trace_token = set_trace_id(trace_id)
|
|
62
|
+
state = scope.setdefault("state", {})
|
|
63
|
+
state["request_id"] = request_id
|
|
64
|
+
state["trace_id"] = trace_id
|
|
65
|
+
|
|
66
|
+
async def send_with_ids(message: Message) -> None:
|
|
67
|
+
if message["type"] == "http.response.start":
|
|
68
|
+
response_headers = MutableHeaders(scope=message)
|
|
69
|
+
response_headers.append(REQUEST_ID_HEADER, request_id)
|
|
70
|
+
response_headers.append(TRACE_ID_HEADER, trace_id)
|
|
71
|
+
await send(message)
|
|
72
|
+
|
|
73
|
+
try:
|
|
74
|
+
await self.app(scope, receive, send_with_ids)
|
|
75
|
+
finally:
|
|
76
|
+
reset_trace_id(trace_token)
|
|
77
|
+
reset_request_id(request_token)
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
UNMATCHED_ROUTE = "unmatched"
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def route_template(scope: Scope) -> str:
|
|
84
|
+
"""Khuôn đường dẫn ĐẦY ĐỦ của route, ví dụ /api/users/{user_id}.
|
|
85
|
+
|
|
86
|
+
Bắt buộc dùng khuôn chứ không phải đường dẫn thật khi làm nhãn số đo:
|
|
87
|
+
/api/users/abc và /api/users/def là hai nhãn khác nhau, mỗi bản ghi tạo một
|
|
88
|
+
chuỗi số đo mới và làm nổ bộ nhớ Prometheus.
|
|
89
|
+
|
|
90
|
+
`route.path_format` chỉ là đường dẫn TRONG router con ("/users/{user_id}"),
|
|
91
|
+
mất tiền tố "/api" do include_router thêm vào. Ghép lại bằng cách lấy phần
|
|
92
|
+
đầu của đường dẫn thật: số đoạn dư ra chính là tiền tố.
|
|
93
|
+
"""
|
|
94
|
+
route = scope.get("route")
|
|
95
|
+
template = getattr(route, "path_format", None) or getattr(route, "path", None)
|
|
96
|
+
if not template:
|
|
97
|
+
return UNMATCHED_ROUTE
|
|
98
|
+
|
|
99
|
+
template_parts = [part for part in template.split("/") if part]
|
|
100
|
+
actual_parts = [part for part in scope.get("path", "").split("/") if part]
|
|
101
|
+
if len(actual_parts) < len(template_parts):
|
|
102
|
+
return template
|
|
103
|
+
|
|
104
|
+
prefix = actual_parts[: len(actual_parts) - len(template_parts)]
|
|
105
|
+
parts = [*prefix, *template_parts]
|
|
106
|
+
return "/" + "/".join(parts) if parts else "/"
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
class AccessLogMiddleware:
|
|
110
|
+
"""Ghi access log VÀ cập nhật số đo cho mỗi request."""
|
|
111
|
+
|
|
112
|
+
def __init__(self, app: ASGIApp) -> None:
|
|
113
|
+
self.app = app
|
|
114
|
+
|
|
115
|
+
async def __call__(self, scope: Scope, receive: Receive, send: Send) -> None:
|
|
116
|
+
if scope["type"] != "http":
|
|
117
|
+
await self.app(scope, receive, send)
|
|
118
|
+
return
|
|
119
|
+
|
|
120
|
+
started = time.perf_counter()
|
|
121
|
+
status_code = 500
|
|
122
|
+
http_in_flight.inc_gauge(1)
|
|
123
|
+
|
|
124
|
+
async def send_with_timing(message: Message) -> None:
|
|
125
|
+
nonlocal status_code
|
|
126
|
+
if message["type"] == "http.response.start":
|
|
127
|
+
status_code = message["status"]
|
|
128
|
+
duration_ms = round((time.perf_counter() - started) * 1000, 2)
|
|
129
|
+
MutableHeaders(scope=message).append(RESPONSE_TIME_HEADER, str(duration_ms))
|
|
130
|
+
await send(message)
|
|
131
|
+
|
|
132
|
+
client = scope.get("client")
|
|
133
|
+
try:
|
|
134
|
+
await self.app(scope, receive, send_with_timing)
|
|
135
|
+
except Exception:
|
|
136
|
+
log.exception(
|
|
137
|
+
"http.request_failed",
|
|
138
|
+
method=scope.get("method"),
|
|
139
|
+
path=scope.get("path"),
|
|
140
|
+
duration_ms=round((time.perf_counter() - started) * 1000, 2),
|
|
141
|
+
)
|
|
142
|
+
self._record(scope, started, 500)
|
|
143
|
+
raise
|
|
144
|
+
finally:
|
|
145
|
+
http_in_flight.inc_gauge(-1)
|
|
146
|
+
|
|
147
|
+
self._record(scope, started, status_code)
|
|
148
|
+
log.info(
|
|
149
|
+
"http.request",
|
|
150
|
+
method=scope.get("method"),
|
|
151
|
+
path=scope.get("path"),
|
|
152
|
+
status_code=status_code,
|
|
153
|
+
duration_ms=round((time.perf_counter() - started) * 1000, 2),
|
|
154
|
+
client=client[0] if client else None,
|
|
155
|
+
)
|
|
156
|
+
|
|
157
|
+
@staticmethod
|
|
158
|
+
def _record(scope: Scope, started: float, status_code: int) -> None:
|
|
159
|
+
method = scope.get("method", "?")
|
|
160
|
+
template = route_template(scope)
|
|
161
|
+
http_requests.inc(method=method, path=template, status=status_code)
|
|
162
|
+
http_duration.observe(
|
|
163
|
+
time.perf_counter() - started, method=method, path=template
|
|
164
|
+
)
|
pymodular/py.typed
ADDED
|
File without changes
|