aury-boot 0.0.2__py3-none-any.whl → 0.0.3__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 (138) hide show
  1. aury/boot/__init__.py +66 -0
  2. aury/boot/_version.py +2 -2
  3. aury/boot/application/__init__.py +120 -0
  4. aury/boot/application/app/__init__.py +39 -0
  5. aury/boot/application/app/base.py +511 -0
  6. aury/boot/application/app/components.py +434 -0
  7. aury/boot/application/app/middlewares.py +101 -0
  8. aury/boot/application/config/__init__.py +44 -0
  9. aury/boot/application/config/settings.py +663 -0
  10. aury/boot/application/constants/__init__.py +19 -0
  11. aury/boot/application/constants/components.py +50 -0
  12. aury/boot/application/constants/scheduler.py +28 -0
  13. aury/boot/application/constants/service.py +29 -0
  14. aury/boot/application/errors/__init__.py +55 -0
  15. aury/boot/application/errors/chain.py +80 -0
  16. aury/boot/application/errors/codes.py +67 -0
  17. aury/boot/application/errors/exceptions.py +238 -0
  18. aury/boot/application/errors/handlers.py +320 -0
  19. aury/boot/application/errors/response.py +120 -0
  20. aury/boot/application/interfaces/__init__.py +76 -0
  21. aury/boot/application/interfaces/egress.py +224 -0
  22. aury/boot/application/interfaces/ingress.py +98 -0
  23. aury/boot/application/middleware/__init__.py +22 -0
  24. aury/boot/application/middleware/logging.py +451 -0
  25. aury/boot/application/migrations/__init__.py +13 -0
  26. aury/boot/application/migrations/manager.py +685 -0
  27. aury/boot/application/migrations/setup.py +237 -0
  28. aury/boot/application/rpc/__init__.py +63 -0
  29. aury/boot/application/rpc/base.py +108 -0
  30. aury/boot/application/rpc/client.py +294 -0
  31. aury/boot/application/rpc/discovery.py +218 -0
  32. aury/boot/application/scheduler/__init__.py +13 -0
  33. aury/boot/application/scheduler/runner.py +123 -0
  34. aury/boot/application/server/__init__.py +296 -0
  35. aury/boot/commands/__init__.py +30 -0
  36. aury/boot/commands/add.py +76 -0
  37. aury/boot/commands/app.py +105 -0
  38. aury/boot/commands/config.py +177 -0
  39. aury/boot/commands/docker.py +367 -0
  40. aury/boot/commands/docs.py +284 -0
  41. aury/boot/commands/generate.py +1277 -0
  42. aury/boot/commands/init.py +890 -0
  43. aury/boot/commands/migrate/__init__.py +37 -0
  44. aury/boot/commands/migrate/app.py +54 -0
  45. aury/boot/commands/migrate/commands.py +303 -0
  46. aury/boot/commands/scheduler.py +124 -0
  47. aury/boot/commands/server/__init__.py +21 -0
  48. aury/boot/commands/server/app.py +541 -0
  49. aury/boot/commands/templates/generate/api.py.tpl +105 -0
  50. aury/boot/commands/templates/generate/model.py.tpl +17 -0
  51. aury/boot/commands/templates/generate/repository.py.tpl +19 -0
  52. aury/boot/commands/templates/generate/schema.py.tpl +29 -0
  53. aury/boot/commands/templates/generate/service.py.tpl +48 -0
  54. aury/boot/commands/templates/project/CLI.md.tpl +92 -0
  55. aury/boot/commands/templates/project/DEVELOPMENT.md.tpl +1397 -0
  56. aury/boot/commands/templates/project/README.md.tpl +111 -0
  57. aury/boot/commands/templates/project/admin_console_init.py.tpl +50 -0
  58. aury/boot/commands/templates/project/config.py.tpl +30 -0
  59. aury/boot/commands/templates/project/conftest.py.tpl +26 -0
  60. aury/boot/commands/templates/project/env.example.tpl +213 -0
  61. aury/boot/commands/templates/project/gitignore.tpl +128 -0
  62. aury/boot/commands/templates/project/main.py.tpl +41 -0
  63. aury/boot/commands/templates/project/modules/api.py.tpl +19 -0
  64. aury/boot/commands/templates/project/modules/exceptions.py.tpl +84 -0
  65. aury/boot/commands/templates/project/modules/schedules.py.tpl +18 -0
  66. aury/boot/commands/templates/project/modules/tasks.py.tpl +20 -0
  67. aury/boot/commands/worker.py +143 -0
  68. aury/boot/common/__init__.py +35 -0
  69. aury/boot/common/exceptions/__init__.py +114 -0
  70. aury/boot/common/i18n/__init__.py +16 -0
  71. aury/boot/common/i18n/translator.py +272 -0
  72. aury/boot/common/logging/__init__.py +716 -0
  73. aury/boot/contrib/__init__.py +10 -0
  74. aury/boot/contrib/admin_console/__init__.py +18 -0
  75. aury/boot/contrib/admin_console/auth.py +137 -0
  76. aury/boot/contrib/admin_console/discovery.py +69 -0
  77. aury/boot/contrib/admin_console/install.py +172 -0
  78. aury/boot/contrib/admin_console/utils.py +44 -0
  79. aury/boot/domain/__init__.py +79 -0
  80. aury/boot/domain/exceptions/__init__.py +132 -0
  81. aury/boot/domain/models/__init__.py +51 -0
  82. aury/boot/domain/models/base.py +69 -0
  83. aury/boot/domain/models/mixins.py +135 -0
  84. aury/boot/domain/models/models.py +96 -0
  85. aury/boot/domain/pagination/__init__.py +279 -0
  86. aury/boot/domain/repository/__init__.py +23 -0
  87. aury/boot/domain/repository/impl.py +423 -0
  88. aury/boot/domain/repository/interceptors.py +47 -0
  89. aury/boot/domain/repository/interface.py +106 -0
  90. aury/boot/domain/repository/query_builder.py +348 -0
  91. aury/boot/domain/service/__init__.py +11 -0
  92. aury/boot/domain/service/base.py +73 -0
  93. aury/boot/domain/transaction/__init__.py +404 -0
  94. aury/boot/infrastructure/__init__.py +104 -0
  95. aury/boot/infrastructure/cache/__init__.py +31 -0
  96. aury/boot/infrastructure/cache/backends.py +348 -0
  97. aury/boot/infrastructure/cache/base.py +68 -0
  98. aury/boot/infrastructure/cache/exceptions.py +37 -0
  99. aury/boot/infrastructure/cache/factory.py +94 -0
  100. aury/boot/infrastructure/cache/manager.py +274 -0
  101. aury/boot/infrastructure/database/__init__.py +39 -0
  102. aury/boot/infrastructure/database/config.py +71 -0
  103. aury/boot/infrastructure/database/exceptions.py +44 -0
  104. aury/boot/infrastructure/database/manager.py +317 -0
  105. aury/boot/infrastructure/database/query_tools/__init__.py +164 -0
  106. aury/boot/infrastructure/database/strategies/__init__.py +198 -0
  107. aury/boot/infrastructure/di/__init__.py +15 -0
  108. aury/boot/infrastructure/di/container.py +393 -0
  109. aury/boot/infrastructure/events/__init__.py +33 -0
  110. aury/boot/infrastructure/events/bus.py +362 -0
  111. aury/boot/infrastructure/events/config.py +52 -0
  112. aury/boot/infrastructure/events/consumer.py +134 -0
  113. aury/boot/infrastructure/events/middleware.py +51 -0
  114. aury/boot/infrastructure/events/models.py +63 -0
  115. aury/boot/infrastructure/monitoring/__init__.py +529 -0
  116. aury/boot/infrastructure/scheduler/__init__.py +19 -0
  117. aury/boot/infrastructure/scheduler/exceptions.py +37 -0
  118. aury/boot/infrastructure/scheduler/manager.py +478 -0
  119. aury/boot/infrastructure/storage/__init__.py +38 -0
  120. aury/boot/infrastructure/storage/base.py +164 -0
  121. aury/boot/infrastructure/storage/exceptions.py +37 -0
  122. aury/boot/infrastructure/storage/factory.py +88 -0
  123. aury/boot/infrastructure/tasks/__init__.py +24 -0
  124. aury/boot/infrastructure/tasks/config.py +45 -0
  125. aury/boot/infrastructure/tasks/constants.py +37 -0
  126. aury/boot/infrastructure/tasks/exceptions.py +37 -0
  127. aury/boot/infrastructure/tasks/manager.py +490 -0
  128. aury/boot/testing/__init__.py +24 -0
  129. aury/boot/testing/base.py +122 -0
  130. aury/boot/testing/client.py +163 -0
  131. aury/boot/testing/factory.py +154 -0
  132. aury/boot/toolkit/__init__.py +21 -0
  133. aury/boot/toolkit/http/__init__.py +367 -0
  134. {aury_boot-0.0.2.dist-info → aury_boot-0.0.3.dist-info}/METADATA +3 -2
  135. aury_boot-0.0.3.dist-info/RECORD +137 -0
  136. aury_boot-0.0.2.dist-info/RECORD +0 -5
  137. {aury_boot-0.0.2.dist-info → aury_boot-0.0.3.dist-info}/WHEEL +0 -0
  138. {aury_boot-0.0.2.dist-info → aury_boot-0.0.3.dist-info}/entry_points.txt +0 -0
@@ -0,0 +1,451 @@
1
+ """HTTP 请求日志中间件。
2
+
3
+ 提供 HTTP 相关的日志功能,包括:
4
+ - 请求日志中间件(支持链路追踪)
5
+ - 请求日志装饰器
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ from collections.abc import Callable
11
+ from functools import wraps
12
+ import time
13
+ import uuid
14
+
15
+ from starlette.middleware.base import BaseHTTPMiddleware
16
+ from starlette.requests import Request
17
+ from starlette.responses import Response
18
+
19
+ from aury.boot.common.logging import get_trace_id, logger, set_trace_id
20
+
21
+
22
+ def log_request[T](func: Callable[..., T]) -> Callable[..., T]:
23
+ """请求日志装饰器。
24
+
25
+ 记录请求的详细信息。
26
+
27
+ 使用示例:
28
+ @router.get("/users")
29
+ @log_request
30
+ async def get_users(request: Request):
31
+ return {"users": []}
32
+ """
33
+ @wraps(func)
34
+ async def wrapper(*args, **kwargs) -> T:
35
+ request: Request | None = None
36
+
37
+ # 查找Request对象
38
+ for arg in args:
39
+ if isinstance(arg, Request):
40
+ request = arg
41
+ break
42
+
43
+ if not request:
44
+ request = kwargs.get("request")
45
+
46
+ # 记录请求信息
47
+ if request:
48
+ logger.info(
49
+ f"请求: {request.method} {request.url.path} | "
50
+ f"客户端: {request.client.host if request.client else 'unknown'} | "
51
+ f"查询参数: {dict(request.query_params)}"
52
+ )
53
+
54
+ try:
55
+ # 执行函数
56
+ start_time = time.time()
57
+ response = await func(*args, **kwargs)
58
+ duration = time.time() - start_time
59
+
60
+ # 记录响应信息
61
+ if request:
62
+ logger.info(
63
+ f"响应: {request.method} {request.url.path} | "
64
+ f"耗时: {duration:.3f}s"
65
+ )
66
+
67
+ return response
68
+ except Exception as exc:
69
+ # 记录错误
70
+ if request:
71
+ logger.error(
72
+ f"错误: {request.method} {request.url.path} | "
73
+ f"异常: {type(exc).__name__}: {exc}"
74
+ )
75
+ raise
76
+
77
+ return wrapper
78
+
79
+
80
+ # 请求/响应体最大记录长度
81
+ MAX_BODY_LOG_SIZE = 2000
82
+ # 不记录 body 的 Content-Type
83
+ SKIP_BODY_CONTENT_TYPES = (
84
+ "multipart/form-data",
85
+ "application/octet-stream",
86
+ "image/",
87
+ "audio/",
88
+ "video/",
89
+ )
90
+
91
+
92
+ def _truncate_body(body: bytes | None, max_size: int = MAX_BODY_LOG_SIZE) -> str | None:
93
+ """截取请求/响应体用于日志记录。"""
94
+ if not body:
95
+ return None
96
+ try:
97
+ text = body.decode("utf-8")
98
+ if len(text) > max_size:
99
+ return text[:max_size] + f"...(截取,总长{len(text)})"
100
+ return text
101
+ except UnicodeDecodeError:
102
+ return f"<二进制数据 {len(body)} bytes>"
103
+
104
+
105
+ def _should_log_body(content_type: str | None) -> bool:
106
+ """判断是否应该记录 body。"""
107
+ if not content_type:
108
+ return True
109
+ content_type = content_type.lower()
110
+ for skip_type in SKIP_BODY_CONTENT_TYPES:
111
+ if skip_type in content_type:
112
+ return False
113
+ return True
114
+
115
+
116
+ class RequestLoggingMiddleware(BaseHTTPMiddleware):
117
+ """请求日志中间件(支持链路追踪)。
118
+
119
+ 自动记录所有HTTP请求的详细信息,包括:
120
+ - 请求方法、路径、查询参数、请求体
121
+ - 客户端IP、User-Agent
122
+ - 响应状态码、耗时、响应体
123
+ - 链路追踪 ID(X-Trace-ID / X-Request-ID)
124
+
125
+ 注意:文件上传、二进制数据等不会记录 body 内容。
126
+
127
+ 使用示例:
128
+ from aury.boot.application.middleware.logging import RequestLoggingMiddleware
129
+
130
+ app.add_middleware(RequestLoggingMiddleware)
131
+ """
132
+
133
+ async def dispatch(self, request: Request, call_next) -> Response:
134
+ """处理请求并记录日志。"""
135
+ start_time = time.time()
136
+
137
+ # 从请求头获取或生成链路追踪 ID
138
+ trace_id = (
139
+ request.headers.get("x-trace-id") or
140
+ request.headers.get("x-request-id") or
141
+ str(uuid.uuid4())
142
+ )
143
+ set_trace_id(trace_id)
144
+
145
+ # 获取客户端信息
146
+ client_host = request.client.host if request.client else "unknown"
147
+ content_type = request.headers.get("content-type", "")
148
+
149
+ # 读取请求体
150
+ request_body: bytes | None = None
151
+ request_body_log: str | None = None
152
+ if request.method in ("POST", "PUT", "PATCH", "DELETE"):
153
+ try:
154
+ request_body = await request.body()
155
+ if _should_log_body(content_type):
156
+ request_body_log = _truncate_body(request_body)
157
+ else:
158
+ request_body_log = f"<{content_type}, {len(request_body)} bytes>"
159
+ except Exception:
160
+ pass
161
+
162
+ # 构建请求日志
163
+ request_log = f"→ {request.method} {request.url.path}"
164
+ if request.query_params:
165
+ request_log += f" | 参数: {dict(request.query_params)}"
166
+ if request_body_log:
167
+ request_log += f" | Body: {request_body_log}"
168
+ request_log += f" | 客户端: {client_host} | Trace-ID: {trace_id}"
169
+
170
+ logger.info(request_log)
171
+
172
+ # 执行请求
173
+ try:
174
+ response = await call_next(request)
175
+ duration = time.time() - start_time
176
+
177
+ # 在响应头中添加追踪 ID
178
+ response.headers["x-trace-id"] = trace_id
179
+
180
+ # 记录响应信息
181
+ status_code = response.status_code
182
+ log_level = "error" if status_code >= 500 else "warning" if status_code >= 400 else "info"
183
+
184
+ response_log = (
185
+ f"← {request.method} {request.url.path} | "
186
+ f"状态: {status_code} | "
187
+ f"耗时: {duration:.3f}s | "
188
+ f"Trace-ID: {trace_id}"
189
+ )
190
+ logger.log(log_level.upper(), response_log)
191
+
192
+ # 写入 access 日志(简洁格式)
193
+ logger.bind(access=True).info(
194
+ f"{request.method} {request.url.path} {status_code} {duration:.3f}s"
195
+ )
196
+
197
+ # 慢请求警告
198
+ if duration > 1.0:
199
+ logger.warning(
200
+ f"慢请求: {request.method} {request.url.path} | "
201
+ f"耗时: {duration:.3f}s (超过1秒) | "
202
+ f"Trace-ID: {trace_id}"
203
+ )
204
+
205
+ return response
206
+
207
+ except Exception as exc:
208
+ duration = time.time() - start_time
209
+ # diagnose=True 会自动记录局部变量(request_body_log, client_host, trace_id 等)
210
+ logger.exception(
211
+ f"请求处理失败: {request.method} {request.url.path} | "
212
+ f"耗时: {duration:.3f}s | Trace-ID: {trace_id}"
213
+ )
214
+ raise
215
+
216
+
217
+ class WebSocketLoggingMiddleware:
218
+ """WebSocket 日志中间件。
219
+
220
+ 记录 WebSocket 连接生命周期和消息收发(可选)。
221
+
222
+ 使用示例:
223
+ from aury.boot.application.middleware.logging import WebSocketLoggingMiddleware
224
+
225
+ app.add_middleware(WebSocketLoggingMiddleware, log_messages=True)
226
+ """
227
+
228
+ def __init__(
229
+ self,
230
+ app,
231
+ *,
232
+ log_messages: bool = False,
233
+ max_message_length: int = 500,
234
+ ) -> None:
235
+ """初始化 WebSocket 日志中间件。
236
+
237
+ Args:
238
+ app: ASGI 应用
239
+ log_messages: 是否记录消息内容(默认 False,注意性能和敏感数据)
240
+ max_message_length: 消息内容最大记录长度
241
+ """
242
+ self.app = app
243
+ self.log_messages = log_messages
244
+ self.max_message_length = max_message_length
245
+
246
+ async def __call__(self, scope, receive, send) -> None:
247
+ if scope["type"] != "websocket":
248
+ await self.app(scope, receive, send)
249
+ return
250
+
251
+ # 获取或生成 trace_id
252
+ headers = dict(scope.get("headers", []))
253
+ trace_id = (
254
+ headers.get(b"x-trace-id", b"").decode() or
255
+ headers.get(b"x-request-id", b"").decode() or
256
+ str(uuid.uuid4())
257
+ )
258
+ set_trace_id(trace_id)
259
+
260
+ path = scope.get("path", "/")
261
+ client = scope.get("client")
262
+ client_host = f"{client[0]}:{client[1]}" if client else "unknown"
263
+
264
+ start_time = time.time()
265
+ message_count = {"sent": 0, "received": 0}
266
+
267
+ async def logging_receive():
268
+ message = await receive()
269
+ msg_type = message.get("type", "")
270
+
271
+ if msg_type == "websocket.connect":
272
+ logger.info(
273
+ f"WS → 连接建立: {path} | "
274
+ f"客户端: {client_host} | Trace-ID: {trace_id}"
275
+ )
276
+ elif msg_type == "websocket.disconnect":
277
+ duration = time.time() - start_time
278
+ logger.info(
279
+ f"WS ← 连接关闭: {path} | "
280
+ f"时长: {duration:.1f}s | "
281
+ f"收/发: {message_count['received']}/{message_count['sent']} | "
282
+ f"Trace-ID: {trace_id}"
283
+ )
284
+ elif msg_type == "websocket.receive":
285
+ message_count["received"] += 1
286
+ if self.log_messages:
287
+ text = message.get("text") or message.get("bytes", b"").decode("utf-8", errors="replace")
288
+ if len(text) > self.max_message_length:
289
+ text = text[:self.max_message_length] + "..."
290
+ logger.debug(f"WS → 收到: {path} | {text}")
291
+
292
+ return message
293
+
294
+ async def logging_send(message):
295
+ msg_type = message.get("type", "")
296
+
297
+ if msg_type == "websocket.send":
298
+ message_count["sent"] += 1
299
+ if self.log_messages:
300
+ text = message.get("text") or message.get("bytes", b"").decode("utf-8", errors="replace")
301
+ if len(text) > self.max_message_length:
302
+ text = text[:self.max_message_length] + "..."
303
+ logger.debug(f"WS ← 发送: {path} | {text}")
304
+ elif msg_type == "websocket.close":
305
+ code = message.get("code", 1000)
306
+ reason = message.get("reason", "")
307
+ duration = time.time() - start_time
308
+ log_level = "warning" if code != 1000 else "info"
309
+ logger.log(
310
+ log_level.upper(),
311
+ f"WS ← 服务关闭: {path} | "
312
+ f"Code: {code} | 原因: {reason or '正常'} | "
313
+ f"时长: {duration:.1f}s | Trace-ID: {trace_id}"
314
+ )
315
+
316
+ await send(message)
317
+
318
+ try:
319
+ await self.app(scope, logging_receive, logging_send)
320
+ except Exception as exc:
321
+ duration = time.time() - start_time
322
+ logger.exception(
323
+ f"WS ✖ 异常: {path} | "
324
+ f"时长: {duration:.1f}s | "
325
+ f"收/发: {message_count['received']}/{message_count['sent']} | "
326
+ f"Trace-ID: {trace_id}"
327
+ )
328
+ raise
329
+
330
+
331
+ class WebSocketLoggingMiddleware:
332
+ """WebSocket 日志中间件。
333
+
334
+ 记录 WebSocket 连接生命周期和消息收发(可选)。
335
+
336
+ 使用示例:
337
+ from aury.boot.application.middleware.logging import WebSocketLoggingMiddleware
338
+
339
+ app.add_middleware(WebSocketLoggingMiddleware, log_messages=True)
340
+ """
341
+
342
+ def __init__(
343
+ self,
344
+ app,
345
+ *,
346
+ log_messages: bool = False,
347
+ max_message_length: int = 500,
348
+ ) -> None:
349
+ """初始化 WebSocket 日志中间件。
350
+
351
+ Args:
352
+ app: ASGI 应用
353
+ log_messages: 是否记录消息内容(默认 False,注意性能和敏感数据)
354
+ max_message_length: 消息内容最大记录长度
355
+ """
356
+ self.app = app
357
+ self.log_messages = log_messages
358
+ self.max_message_length = max_message_length
359
+
360
+ async def __call__(self, scope, receive, send) -> None:
361
+ if scope["type"] != "websocket":
362
+ await self.app(scope, receive, send)
363
+ return
364
+
365
+ # 获取或生成 trace_id
366
+ headers = dict(scope.get("headers", []))
367
+ trace_id = (
368
+ headers.get(b"x-trace-id", b"").decode() or
369
+ headers.get(b"x-request-id", b"").decode() or
370
+ str(uuid.uuid4())
371
+ )
372
+ set_trace_id(trace_id)
373
+
374
+ path = scope.get("path", "/")
375
+ client = scope.get("client")
376
+ client_host = f"{client[0]}:{client[1]}" if client else "unknown"
377
+
378
+ start_time = time.time()
379
+ message_count = {"sent": 0, "received": 0}
380
+
381
+ async def logging_receive():
382
+ message = await receive()
383
+ msg_type = message.get("type", "")
384
+
385
+ if msg_type == "websocket.connect":
386
+ logger.info(
387
+ f"WS → 连接: {path} | "
388
+ f"客户端: {client_host} | Trace-ID: {trace_id}"
389
+ )
390
+ elif msg_type == "websocket.disconnect":
391
+ duration = time.time() - start_time
392
+ logger.info(
393
+ f"WS ← 断开: {path} | "
394
+ f"时长: {duration:.1f}s | "
395
+ f"收/发: {message_count['received']}/{message_count['sent']} | "
396
+ f"Trace-ID: {trace_id}"
397
+ )
398
+ elif msg_type == "websocket.receive":
399
+ message_count["received"] += 1
400
+ if self.log_messages:
401
+ text = message.get("text") or message.get("bytes", b"").decode("utf-8", errors="replace")
402
+ if len(text) > self.max_message_length:
403
+ text = text[:self.max_message_length] + "..."
404
+ logger.debug(f"WS → 收: {path} | {text}")
405
+
406
+ return message
407
+
408
+ async def logging_send(message):
409
+ msg_type = message.get("type", "")
410
+
411
+ if msg_type == "websocket.send":
412
+ message_count["sent"] += 1
413
+ if self.log_messages:
414
+ text = message.get("text") or message.get("bytes", b"").decode("utf-8", errors="replace")
415
+ if len(text) > self.max_message_length:
416
+ text = text[:self.max_message_length] + "..."
417
+ logger.debug(f"WS ← 发: {path} | {text}")
418
+ elif msg_type == "websocket.close":
419
+ code = message.get("code", 1000)
420
+ reason = message.get("reason", "")
421
+ duration = time.time() - start_time
422
+ log_level = "warning" if code != 1000 else "info"
423
+ logger.log(
424
+ log_level.upper(),
425
+ f"WS × 关闭: {path} | "
426
+ f"Code: {code}{' | 原因: ' + reason if reason else ''} | "
427
+ f"时长: {duration:.1f}s | Trace-ID: {trace_id}"
428
+ )
429
+
430
+ await send(message)
431
+
432
+ try:
433
+ await self.app(scope, logging_receive, logging_send)
434
+ except Exception as exc:
435
+ duration = time.time() - start_time
436
+ logger.exception(
437
+ f"WS ✖ 异常: {path} | "
438
+ f"时长: {duration:.1f}s | "
439
+ f"收/发: {message_count['received']}/{message_count['sent']} | "
440
+ f"Trace-ID: {trace_id}"
441
+ )
442
+ raise
443
+
444
+
445
+ __all__ = [
446
+ "RequestLoggingMiddleware",
447
+ "WebSocketLoggingMiddleware",
448
+ "log_request",
449
+ ]
450
+
451
+
@@ -0,0 +1,13 @@
1
+ """数据库迁移管理模块。
2
+
3
+ 提供类似 Django 的迁移管理接口。
4
+ """
5
+
6
+ from .manager import MigrationManager, load_all_models
7
+
8
+ __all__ = [
9
+ "MigrationManager",
10
+ "load_all_models",
11
+ ]
12
+
13
+