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.
Files changed (69) hide show
  1. fastapi_modular-0.1.0.dist-info/METADATA +377 -0
  2. fastapi_modular-0.1.0.dist-info/RECORD +69 -0
  3. fastapi_modular-0.1.0.dist-info/WHEEL +4 -0
  4. fastapi_modular-0.1.0.dist-info/entry_points.txt +3 -0
  5. fastapi_modular-0.1.0.dist-info/licenses/LICENSE +21 -0
  6. pymodular/__init__.py +74 -0
  7. pymodular/cli/__init__.py +0 -0
  8. pymodular/cli/clean.py +39 -0
  9. pymodular/cli/configure_env.py +569 -0
  10. pymodular/cli/cong_cu.py +111 -0
  11. pymodular/cli/info.py +62 -0
  12. pymodular/cli/install.py +83 -0
  13. pymodular/cli/main.py +247 -0
  14. pymodular/cli/new_module.py +492 -0
  15. pymodular/cli/new_project.py +471 -0
  16. pymodular/cli/serve.py +59 -0
  17. pymodular/core/__init__.py +0 -0
  18. pymodular/core/clock.py +15 -0
  19. pymodular/core/compat.py +39 -0
  20. pymodular/core/config.py +495 -0
  21. pymodular/core/container.py +354 -0
  22. pymodular/core/context.py +78 -0
  23. pymodular/core/controller.py +208 -0
  24. pymodular/core/error_handlers.py +272 -0
  25. pymodular/core/exceptions.py +104 -0
  26. pymodular/core/guards.py +117 -0
  27. pymodular/core/lifespan.py +150 -0
  28. pymodular/core/logging.py +88 -0
  29. pymodular/core/metrics.py +190 -0
  30. pymodular/core/schemas.py +105 -0
  31. pymodular/core/websocket/__init__.py +31 -0
  32. pymodular/core/websocket/adapter.py +192 -0
  33. pymodular/core/websocket/gateway.py +735 -0
  34. pymodular/core/websocket/namespace.py +148 -0
  35. pymodular/core/websocket/protocol.py +157 -0
  36. pymodular/core/websocket/server.py +175 -0
  37. pymodular/core/websocket/socket.py +241 -0
  38. pymodular/discovery.py +180 -0
  39. pymodular/factory.py +126 -0
  40. pymodular/infrastructure/__init__.py +1 -0
  41. pymodular/infrastructure/database/__init__.py +8 -0
  42. pymodular/infrastructure/database/base.py +228 -0
  43. pymodular/infrastructure/database/circuit.py +207 -0
  44. pymodular/infrastructure/database/factory.py +88 -0
  45. pymodular/infrastructure/database/memory.py +112 -0
  46. pymodular/infrastructure/database/mongo.py +186 -0
  47. pymodular/infrastructure/database/repository.py +188 -0
  48. pymodular/infrastructure/database/sql.py +520 -0
  49. pymodular/infrastructure/kafka/__init__.py +26 -0
  50. pymodular/infrastructure/kafka/broker.py +231 -0
  51. pymodular/infrastructure/kafka/consumers.py +371 -0
  52. pymodular/infrastructure/kafka/metrics.py +17 -0
  53. pymodular/infrastructure/mqtt/__init__.py +35 -0
  54. pymodular/infrastructure/mqtt/client.py +292 -0
  55. pymodular/infrastructure/mqtt/consumers.py +219 -0
  56. pymodular/infrastructure/mqtt/metrics.py +17 -0
  57. pymodular/infrastructure/mqtt/patterns.py +116 -0
  58. pymodular/infrastructure/rabbitmq/__init__.py +33 -0
  59. pymodular/infrastructure/rabbitmq/broker.py +616 -0
  60. pymodular/infrastructure/rabbitmq/consumers.py +450 -0
  61. pymodular/infrastructure/rabbitmq/metrics.py +34 -0
  62. pymodular/infrastructure/rabbitmq/patterns.py +64 -0
  63. pymodular/infrastructure/redis/__init__.py +31 -0
  64. pymodular/infrastructure/redis/client.py +362 -0
  65. pymodular/infrastructure/redis/metrics.py +20 -0
  66. pymodular/infrastructure/redis/pubsub.py +262 -0
  67. pymodular/middleware/__init__.py +0 -0
  68. pymodular/middleware/request_context.py +164 -0
  69. 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