aury-boot 0.0.4__py3-none-any.whl → 0.0.7__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.
- aury/boot/__init__.py +2 -2
- aury/boot/_version.py +2 -2
- aury/boot/application/__init__.py +60 -36
- aury/boot/application/adapter/__init__.py +112 -0
- aury/boot/application/adapter/base.py +511 -0
- aury/boot/application/adapter/config.py +242 -0
- aury/boot/application/adapter/decorators.py +259 -0
- aury/boot/application/adapter/exceptions.py +202 -0
- aury/boot/application/adapter/http.py +325 -0
- aury/boot/application/app/__init__.py +12 -8
- aury/boot/application/app/base.py +12 -0
- aury/boot/application/app/components.py +137 -44
- aury/boot/application/app/middlewares.py +9 -4
- aury/boot/application/app/startup.py +249 -0
- aury/boot/application/config/__init__.py +36 -1
- aury/boot/application/config/multi_instance.py +216 -0
- aury/boot/application/config/settings.py +398 -149
- aury/boot/application/constants/components.py +6 -0
- aury/boot/application/errors/handlers.py +17 -3
- aury/boot/application/middleware/logging.py +21 -120
- aury/boot/application/rpc/__init__.py +2 -2
- aury/boot/commands/__init__.py +30 -10
- aury/boot/commands/app.py +131 -1
- aury/boot/commands/docs.py +104 -17
- aury/boot/commands/generate.py +22 -22
- aury/boot/commands/init.py +68 -17
- aury/boot/commands/server/app.py +2 -3
- aury/boot/commands/templates/project/AGENTS.md.tpl +221 -0
- aury/boot/commands/templates/project/README.md.tpl +2 -2
- aury/boot/commands/templates/project/aury_docs/00-overview.md.tpl +59 -0
- aury/boot/commands/templates/project/aury_docs/01-model.md.tpl +184 -0
- aury/boot/commands/templates/project/aury_docs/02-repository.md.tpl +206 -0
- aury/boot/commands/templates/project/aury_docs/03-service.md.tpl +398 -0
- aury/boot/commands/templates/project/aury_docs/04-schema.md.tpl +95 -0
- aury/boot/commands/templates/project/aury_docs/05-api.md.tpl +116 -0
- aury/boot/commands/templates/project/aury_docs/06-exception.md.tpl +118 -0
- aury/boot/commands/templates/project/aury_docs/07-cache.md.tpl +122 -0
- aury/boot/commands/templates/project/aury_docs/08-scheduler.md.tpl +32 -0
- aury/boot/commands/templates/project/aury_docs/09-tasks.md.tpl +38 -0
- aury/boot/commands/templates/project/aury_docs/10-storage.md.tpl +115 -0
- aury/boot/commands/templates/project/aury_docs/11-logging.md.tpl +131 -0
- aury/boot/commands/templates/project/aury_docs/12-admin.md.tpl +56 -0
- aury/boot/commands/templates/project/aury_docs/13-channel.md.tpl +104 -0
- aury/boot/commands/templates/project/aury_docs/14-mq.md.tpl +102 -0
- aury/boot/commands/templates/project/aury_docs/15-events.md.tpl +147 -0
- aury/boot/commands/templates/project/aury_docs/16-adapter.md.tpl +403 -0
- aury/boot/commands/templates/project/{CLI.md.tpl → aury_docs/99-cli.md.tpl} +19 -19
- aury/boot/commands/templates/project/config.py.tpl +10 -10
- aury/boot/commands/templates/project/env_templates/_header.tpl +10 -0
- aury/boot/commands/templates/project/env_templates/admin.tpl +49 -0
- aury/boot/commands/templates/project/env_templates/cache.tpl +14 -0
- aury/boot/commands/templates/project/env_templates/database.tpl +22 -0
- aury/boot/commands/templates/project/env_templates/log.tpl +18 -0
- aury/boot/commands/templates/project/env_templates/messaging.tpl +46 -0
- aury/boot/commands/templates/project/env_templates/rpc.tpl +28 -0
- aury/boot/commands/templates/project/env_templates/scheduler.tpl +18 -0
- aury/boot/commands/templates/project/env_templates/service.tpl +18 -0
- aury/boot/commands/templates/project/env_templates/storage.tpl +38 -0
- aury/boot/commands/templates/project/env_templates/third_party.tpl +43 -0
- aury/boot/commands/templates/project/modules/tasks.py.tpl +1 -1
- aury/boot/common/logging/__init__.py +26 -674
- aury/boot/common/logging/context.py +132 -0
- aury/boot/common/logging/decorators.py +118 -0
- aury/boot/common/logging/format.py +315 -0
- aury/boot/common/logging/setup.py +214 -0
- aury/boot/contrib/admin_console/auth.py +2 -3
- aury/boot/contrib/admin_console/install.py +1 -1
- aury/boot/domain/models/mixins.py +48 -1
- aury/boot/domain/pagination/__init__.py +94 -0
- aury/boot/domain/repository/impl.py +1 -1
- aury/boot/domain/repository/interface.py +1 -1
- aury/boot/domain/transaction/__init__.py +8 -9
- aury/boot/infrastructure/__init__.py +86 -29
- aury/boot/infrastructure/cache/backends.py +102 -18
- aury/boot/infrastructure/cache/base.py +12 -0
- aury/boot/infrastructure/cache/manager.py +153 -91
- aury/boot/infrastructure/channel/__init__.py +24 -0
- aury/boot/infrastructure/channel/backends/__init__.py +9 -0
- aury/boot/infrastructure/channel/backends/memory.py +83 -0
- aury/boot/infrastructure/channel/backends/redis.py +88 -0
- aury/boot/infrastructure/channel/base.py +92 -0
- aury/boot/infrastructure/channel/manager.py +203 -0
- aury/boot/infrastructure/clients/__init__.py +22 -0
- aury/boot/infrastructure/clients/rabbitmq/__init__.py +9 -0
- aury/boot/infrastructure/clients/rabbitmq/config.py +46 -0
- aury/boot/infrastructure/clients/rabbitmq/manager.py +288 -0
- aury/boot/infrastructure/clients/redis/__init__.py +28 -0
- aury/boot/infrastructure/clients/redis/config.py +51 -0
- aury/boot/infrastructure/clients/redis/manager.py +264 -0
- aury/boot/infrastructure/database/config.py +7 -16
- aury/boot/infrastructure/database/manager.py +16 -38
- aury/boot/infrastructure/events/__init__.py +18 -21
- aury/boot/infrastructure/events/backends/__init__.py +11 -0
- aury/boot/infrastructure/events/backends/memory.py +86 -0
- aury/boot/infrastructure/events/backends/rabbitmq.py +193 -0
- aury/boot/infrastructure/events/backends/redis.py +162 -0
- aury/boot/infrastructure/events/base.py +127 -0
- aury/boot/infrastructure/events/manager.py +224 -0
- aury/boot/infrastructure/mq/__init__.py +24 -0
- aury/boot/infrastructure/mq/backends/__init__.py +9 -0
- aury/boot/infrastructure/mq/backends/rabbitmq.py +179 -0
- aury/boot/infrastructure/mq/backends/redis.py +167 -0
- aury/boot/infrastructure/mq/base.py +143 -0
- aury/boot/infrastructure/mq/manager.py +239 -0
- aury/boot/infrastructure/scheduler/manager.py +7 -3
- aury/boot/infrastructure/storage/__init__.py +9 -9
- aury/boot/infrastructure/storage/base.py +17 -5
- aury/boot/infrastructure/storage/factory.py +0 -1
- aury/boot/infrastructure/tasks/__init__.py +2 -2
- aury/boot/infrastructure/tasks/config.py +5 -13
- aury/boot/infrastructure/tasks/manager.py +55 -33
- {aury_boot-0.0.4.dist-info → aury_boot-0.0.7.dist-info}/METADATA +20 -2
- aury_boot-0.0.7.dist-info/RECORD +197 -0
- aury/boot/commands/templates/project/DEVELOPMENT.md.tpl +0 -1397
- aury/boot/commands/templates/project/env.example.tpl +0 -213
- aury/boot/infrastructure/events/bus.py +0 -362
- aury/boot/infrastructure/events/config.py +0 -52
- aury/boot/infrastructure/events/consumer.py +0 -134
- aury/boot/infrastructure/events/models.py +0 -63
- aury_boot-0.0.4.dist-info/RECORD +0 -137
- {aury_boot-0.0.4.dist-info → aury_boot-0.0.7.dist-info}/WHEEL +0 -0
- {aury_boot-0.0.4.dist-info → aury_boot-0.0.7.dist-info}/entry_points.txt +0 -0
|
@@ -9,6 +9,7 @@ from abc import ABC, abstractmethod
|
|
|
9
9
|
from typing import TYPE_CHECKING
|
|
10
10
|
|
|
11
11
|
from fastapi import HTTPException, Request, status
|
|
12
|
+
from fastapi.exceptions import RequestValidationError
|
|
12
13
|
from fastapi.responses import JSONResponse
|
|
13
14
|
from pydantic import ValidationError
|
|
14
15
|
from sqlalchemy.exc import IntegrityError, SQLAlchemyError
|
|
@@ -121,9 +122,19 @@ class BaseErrorHandler(ErrorHandler):
|
|
|
121
122
|
|
|
122
123
|
errors = [detail.model_dump() for detail in exception.details] if exception.details else None
|
|
123
124
|
|
|
125
|
+
# 兼容 ErrorCode 枚举和字符串
|
|
126
|
+
code_value = exception.code.value if hasattr(exception.code, "value") else exception.code
|
|
127
|
+
|
|
128
|
+
# 尝试转换为 int,如果失败则使用 status_code
|
|
129
|
+
try:
|
|
130
|
+
code_int = int(code_value)
|
|
131
|
+
except (ValueError, TypeError):
|
|
132
|
+
# 非数字字符串(如 "TODO_ATTACHMENT_ERROR"),使用 HTTP 状态码作为 code
|
|
133
|
+
code_int = exception.status_code
|
|
134
|
+
|
|
124
135
|
response = ResponseBuilder.fail(
|
|
125
136
|
message=exception.message,
|
|
126
|
-
code=
|
|
137
|
+
code=code_int,
|
|
127
138
|
errors=errors,
|
|
128
139
|
)
|
|
129
140
|
|
|
@@ -160,11 +171,14 @@ class HTTPExceptionHandler(ErrorHandler):
|
|
|
160
171
|
|
|
161
172
|
|
|
162
173
|
class ValidationErrorHandler(ErrorHandler):
|
|
163
|
-
"""
|
|
174
|
+
"""验证异常处理器。
|
|
175
|
+
|
|
176
|
+
处理 Pydantic ValidationError 和 FastAPI RequestValidationError。
|
|
177
|
+
"""
|
|
164
178
|
|
|
165
179
|
def can_handle(self, exception: Exception) -> bool:
|
|
166
180
|
"""判断是否为验证异常。"""
|
|
167
|
-
return isinstance(exception, ValidationError)
|
|
181
|
+
return isinstance(exception, ValidationError | RequestValidationError)
|
|
168
182
|
|
|
169
183
|
async def handle(self, exception: Exception, request: Request) -> JSONResponse:
|
|
170
184
|
"""处理验证异常。"""
|
|
@@ -16,7 +16,8 @@ from starlette.middleware.base import BaseHTTPMiddleware
|
|
|
16
16
|
from starlette.requests import Request
|
|
17
17
|
from starlette.responses import Response
|
|
18
18
|
|
|
19
|
-
from aury.boot.
|
|
19
|
+
from aury.boot.application.errors import global_exception_handler
|
|
20
|
+
from aury.boot.common.logging import get_request_contexts, logger, set_trace_id
|
|
20
21
|
|
|
21
22
|
|
|
22
23
|
def log_request[T](func: Callable[..., T]) -> Callable[..., T]:
|
|
@@ -107,10 +108,7 @@ def _should_log_body(content_type: str | None) -> bool:
|
|
|
107
108
|
if not content_type:
|
|
108
109
|
return True
|
|
109
110
|
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
|
|
111
|
+
return all(skip_type not in content_type for skip_type in SKIP_BODY_CONTENT_TYPES)
|
|
114
112
|
|
|
115
113
|
|
|
116
114
|
class RequestLoggingMiddleware(BaseHTTPMiddleware):
|
|
@@ -189,6 +187,12 @@ class RequestLoggingMiddleware(BaseHTTPMiddleware):
|
|
|
189
187
|
)
|
|
190
188
|
logger.log(log_level.upper(), response_log)
|
|
191
189
|
|
|
190
|
+
# 记录请求上下文(user_id, tenant_id 等用户注册的字段)
|
|
191
|
+
request_contexts = get_request_contexts()
|
|
192
|
+
if request_contexts:
|
|
193
|
+
ctx_str = " | ".join(f"{k}: {v}" for k, v in request_contexts.items())
|
|
194
|
+
logger.info(f"[REQUEST_CONTEXT] Trace-ID: {trace_id} | {ctx_str}")
|
|
195
|
+
|
|
192
196
|
# 写入 access 日志(简洁格式)
|
|
193
197
|
logger.bind(access=True).info(
|
|
194
198
|
f"{request.method} {request.url.path} {status_code} {duration:.3f}s"
|
|
@@ -211,7 +215,18 @@ class RequestLoggingMiddleware(BaseHTTPMiddleware):
|
|
|
211
215
|
f"请求处理失败: {request.method} {request.url.path} | "
|
|
212
216
|
f"耗时: {duration:.3f}s | Trace-ID: {trace_id}"
|
|
213
217
|
)
|
|
214
|
-
|
|
218
|
+
|
|
219
|
+
# 记录请求上下文(即使异常也要记录,便于追踪问题)
|
|
220
|
+
request_contexts = get_request_contexts()
|
|
221
|
+
if request_contexts:
|
|
222
|
+
ctx_str = " | ".join(f"{k}: {v}" for k, v in request_contexts.items())
|
|
223
|
+
logger.info(f"[REQUEST_CONTEXT] Trace-ID: {trace_id} | {ctx_str}")
|
|
224
|
+
|
|
225
|
+
# 使用全局异常处理器生成响应,而不是直接抛出异常
|
|
226
|
+
# BaseHTTPMiddleware 中直接 raise 会绕过 FastAPI 的异常处理器
|
|
227
|
+
response = await global_exception_handler(request, exc)
|
|
228
|
+
response.headers["x-trace-id"] = trace_id
|
|
229
|
+
return response
|
|
215
230
|
|
|
216
231
|
|
|
217
232
|
class WebSocketLoggingMiddleware:
|
|
@@ -328,120 +343,6 @@ class WebSocketLoggingMiddleware:
|
|
|
328
343
|
raise
|
|
329
344
|
|
|
330
345
|
|
|
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
346
|
__all__ = [
|
|
446
347
|
"RequestLoggingMiddleware",
|
|
447
348
|
"WebSocketLoggingMiddleware",
|
|
@@ -51,13 +51,13 @@ __all__ = [
|
|
|
51
51
|
"BaseRPCClient",
|
|
52
52
|
"CompositeServiceDiscovery",
|
|
53
53
|
"ConfigServiceDiscovery",
|
|
54
|
-
"create_rpc_client",
|
|
55
54
|
"DNSServiceDiscovery",
|
|
56
|
-
"get_service_discovery",
|
|
57
55
|
"RPCClient",
|
|
58
56
|
"RPCError",
|
|
59
57
|
"RPCResponse",
|
|
60
58
|
"ServiceDiscovery",
|
|
59
|
+
"create_rpc_client",
|
|
60
|
+
"get_service_discovery",
|
|
61
61
|
"set_service_discovery",
|
|
62
62
|
]
|
|
63
63
|
|
aury/boot/commands/__init__.py
CHANGED
|
@@ -1,6 +1,21 @@
|
|
|
1
1
|
"""命令行工具模块。
|
|
2
2
|
|
|
3
|
-
统一入口:
|
|
3
|
+
统一入口: aury
|
|
4
|
+
|
|
5
|
+
CLI 继承接口:
|
|
6
|
+
子框架(如 aury-django、aury-cloud)可以通过以下接口继承基础命令:
|
|
7
|
+
|
|
8
|
+
- register_commands(app): 将所有命令注册到目标 app
|
|
9
|
+
- get_command_modules(): 获取所有命令模块,供进一步定制
|
|
10
|
+
|
|
11
|
+
示例:
|
|
12
|
+
```python
|
|
13
|
+
from typer import Typer
|
|
14
|
+
from aury.boot.commands import register_commands
|
|
15
|
+
|
|
16
|
+
app = Typer(name="aury-django")
|
|
17
|
+
register_commands(app) # 继承所有命令
|
|
18
|
+
```
|
|
4
19
|
"""
|
|
5
20
|
|
|
6
21
|
from __future__ import annotations
|
|
@@ -9,22 +24,27 @@ from __future__ import annotations
|
|
|
9
24
|
from .config import ProjectConfig, get_project_config, save_project_config
|
|
10
25
|
|
|
11
26
|
|
|
12
|
-
# 延迟导入 app
|
|
27
|
+
# 延迟导入 app、main、register_commands、get_command_modules,避免加载重型依赖
|
|
13
28
|
def __getattr__(name: str):
|
|
14
|
-
if name in ("app", "main"):
|
|
15
|
-
from .app import main
|
|
29
|
+
if name in ("app", "main", "register_commands", "get_command_modules"):
|
|
30
|
+
from .app import _get_app, get_command_modules, main, register_commands
|
|
16
31
|
if name == "main":
|
|
17
|
-
return
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
32
|
+
return main
|
|
33
|
+
if name == "app":
|
|
34
|
+
return _get_app()
|
|
35
|
+
if name == "register_commands":
|
|
36
|
+
return register_commands
|
|
37
|
+
if name == "get_command_modules":
|
|
38
|
+
return get_command_modules
|
|
21
39
|
raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
|
|
22
40
|
|
|
23
41
|
|
|
24
42
|
__all__ = [
|
|
25
|
-
"app",
|
|
26
|
-
"main",
|
|
27
43
|
"ProjectConfig",
|
|
44
|
+
"app",
|
|
45
|
+
"get_command_modules",
|
|
28
46
|
"get_project_config",
|
|
47
|
+
"main",
|
|
48
|
+
"register_commands",
|
|
29
49
|
"save_project_config",
|
|
30
50
|
]
|
aury/boot/commands/app.py
CHANGED
|
@@ -18,6 +18,22 @@
|
|
|
18
18
|
aury worker # 运行 Worker
|
|
19
19
|
aury migrate up # 执行数据库迁移
|
|
20
20
|
aury docs all --force # 更新所有文档
|
|
21
|
+
|
|
22
|
+
CLI 继承:
|
|
23
|
+
子框架(如 aury-django、aury-cloud)可以通过 `register_commands` 继承所有基础命令:
|
|
24
|
+
|
|
25
|
+
```python
|
|
26
|
+
from typer import Typer
|
|
27
|
+
from aury.boot.commands import register_commands
|
|
28
|
+
|
|
29
|
+
app = Typer(name="aury-django")
|
|
30
|
+
register_commands(app) # 继承所有 aury-boot 命令
|
|
31
|
+
|
|
32
|
+
# 添加 django 特有命令
|
|
33
|
+
@app.command()
|
|
34
|
+
def startapp(name: str):
|
|
35
|
+
...
|
|
36
|
+
```
|
|
21
37
|
"""
|
|
22
38
|
|
|
23
39
|
from __future__ import annotations
|
|
@@ -92,7 +108,119 @@ def main() -> None:
|
|
|
92
108
|
_get_app()()
|
|
93
109
|
|
|
94
110
|
|
|
95
|
-
|
|
111
|
+
def register_commands(
|
|
112
|
+
target_app: typer.Typer,
|
|
113
|
+
*,
|
|
114
|
+
include_init: bool = True,
|
|
115
|
+
include_add: bool = True,
|
|
116
|
+
include_generate: bool = True,
|
|
117
|
+
include_server: bool = True,
|
|
118
|
+
include_scheduler: bool = True,
|
|
119
|
+
include_worker: bool = True,
|
|
120
|
+
include_migrate: bool = True,
|
|
121
|
+
include_docker: bool = True,
|
|
122
|
+
include_docs: bool = True,
|
|
123
|
+
) -> None:
|
|
124
|
+
"""将 aury-boot 的所有命令注册到目标 Typer app。
|
|
125
|
+
|
|
126
|
+
用于子框架(如 aury-django、aury-cloud)继承基础命令。
|
|
127
|
+
|
|
128
|
+
Args:
|
|
129
|
+
target_app: 目标 Typer 应用
|
|
130
|
+
include_*: 是否包含对应的命令组
|
|
131
|
+
|
|
132
|
+
使用示例:
|
|
133
|
+
```python
|
|
134
|
+
from typer import Typer
|
|
135
|
+
from aury.boot.commands import register_commands
|
|
136
|
+
|
|
137
|
+
app = Typer(name="aury-django")
|
|
138
|
+
|
|
139
|
+
# 继承所有 aury-boot 命令
|
|
140
|
+
register_commands(app)
|
|
141
|
+
|
|
142
|
+
# 或选择性继承
|
|
143
|
+
register_commands(app, include_docker=False)
|
|
144
|
+
|
|
145
|
+
# 添加 django 特有命令
|
|
146
|
+
django_app = Typer(name="django")
|
|
147
|
+
|
|
148
|
+
@django_app.command()
|
|
149
|
+
def startapp(name: str):
|
|
150
|
+
'''Django startapp'''
|
|
151
|
+
...
|
|
152
|
+
|
|
153
|
+
app.add_typer(django_app, name="django")
|
|
154
|
+
```
|
|
155
|
+
"""
|
|
156
|
+
# 延迟导入子命令
|
|
157
|
+
if include_init:
|
|
158
|
+
from .init import init
|
|
159
|
+
target_app.command(name="init", help="🎯 初始化项目脚手架")(init)
|
|
160
|
+
|
|
161
|
+
if include_add:
|
|
162
|
+
from .add import app as add_app
|
|
163
|
+
target_app.add_typer(add_app, name="add", help="➕ 添加可选模块")
|
|
164
|
+
|
|
165
|
+
if include_generate:
|
|
166
|
+
from .generate import app as generate_app
|
|
167
|
+
target_app.add_typer(generate_app, name="generate", help="⚡ 代码生成器")
|
|
168
|
+
|
|
169
|
+
if include_server:
|
|
170
|
+
from .server import app as server_app
|
|
171
|
+
target_app.add_typer(server_app, name="server", help="🖥️ 服务器管理")
|
|
172
|
+
|
|
173
|
+
if include_scheduler:
|
|
174
|
+
from .scheduler import app as scheduler_app
|
|
175
|
+
target_app.add_typer(scheduler_app, name="scheduler", help="🕐 独立运行调度器")
|
|
176
|
+
|
|
177
|
+
if include_worker:
|
|
178
|
+
from .worker import app as worker_app
|
|
179
|
+
target_app.add_typer(worker_app, name="worker", help="⚙️ 运行任务队列 Worker")
|
|
180
|
+
|
|
181
|
+
if include_migrate:
|
|
182
|
+
from .migrate import app as migrate_app
|
|
183
|
+
target_app.add_typer(migrate_app, name="migrate", help="🗃️ 数据库迁移")
|
|
184
|
+
|
|
185
|
+
if include_docker:
|
|
186
|
+
from .docker import app as docker_app
|
|
187
|
+
target_app.add_typer(docker_app, name="docker", help="🐳 Docker 配置")
|
|
188
|
+
|
|
189
|
+
if include_docs:
|
|
190
|
+
from .docs import app as docs_app
|
|
191
|
+
target_app.add_typer(docs_app, name="docs", help="📚 生成/更新项目文档")
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
def get_command_modules() -> dict[str, type]:
|
|
195
|
+
"""获取所有命令模块,供子框架进一步定制。
|
|
196
|
+
|
|
197
|
+
Returns:
|
|
198
|
+
dict: 命令名 -> 模块对象
|
|
199
|
+
|
|
200
|
+
使用示例:
|
|
201
|
+
```python
|
|
202
|
+
from aury.boot.commands import get_command_modules
|
|
203
|
+
|
|
204
|
+
modules = get_command_modules()
|
|
205
|
+
# {'init': <module>, 'add': <module>, 'server': <module>, ...}
|
|
206
|
+
```
|
|
207
|
+
"""
|
|
208
|
+
from . import add, docker, docs, generate, init, migrate, scheduler, server, worker
|
|
209
|
+
|
|
210
|
+
return {
|
|
211
|
+
"init": init,
|
|
212
|
+
"add": add,
|
|
213
|
+
"generate": generate,
|
|
214
|
+
"server": server,
|
|
215
|
+
"scheduler": scheduler,
|
|
216
|
+
"worker": worker,
|
|
217
|
+
"migrate": migrate,
|
|
218
|
+
"docker": docker,
|
|
219
|
+
"docs": docs,
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
|
|
223
|
+
# 允许 `from .app import app`
|
|
96
224
|
def __getattr__(name: str):
|
|
97
225
|
if name == "app":
|
|
98
226
|
return _get_app()
|
|
@@ -101,5 +229,7 @@ def __getattr__(name: str):
|
|
|
101
229
|
|
|
102
230
|
__all__ = [
|
|
103
231
|
"app",
|
|
232
|
+
"get_command_modules",
|
|
104
233
|
"main",
|
|
234
|
+
"register_commands",
|
|
105
235
|
]
|
aury/boot/commands/docs.py
CHANGED
|
@@ -1,13 +1,15 @@
|
|
|
1
1
|
"""文档生成命令。
|
|
2
2
|
|
|
3
3
|
提供命令行工具用于在现有项目中生成/更新文档:
|
|
4
|
-
- aury docs
|
|
4
|
+
- aury docs agents 生成/更新 AGENTS.md(AI 编程助手上下文)
|
|
5
|
+
- aury docs dev 生成/更新 docs/ 目录(开发文档包)
|
|
5
6
|
- aury docs cli 生成/更新 CLI.md
|
|
6
7
|
- aury docs env 生成/更新 .env.example
|
|
7
8
|
- aury docs all 生成/更新所有文档
|
|
8
9
|
|
|
9
10
|
使用示例:
|
|
10
|
-
aury docs
|
|
11
|
+
aury docs agents # 生成 AI 编程助手上下文文档
|
|
12
|
+
aury docs dev # 生成 docs/ 开发文档包
|
|
11
13
|
aury docs cli # 生成 CLI 文档
|
|
12
14
|
aury docs env # 生成环境变量示例
|
|
13
15
|
aury docs all # 生成所有文档
|
|
@@ -79,10 +81,17 @@ def _detect_project_info(project_dir: Path) -> dict[str, str]:
|
|
|
79
81
|
|
|
80
82
|
|
|
81
83
|
def _render_template(template_name: str, context: dict[str, str]) -> str:
|
|
82
|
-
"""渲染模板。
|
|
84
|
+
"""渲染模板。
|
|
85
|
+
|
|
86
|
+
支持根目录模板和 aury_docs/ 子目录模板。
|
|
87
|
+
"""
|
|
88
|
+
# 先在根目录找
|
|
83
89
|
template_path = TEMPLATES_DIR / template_name
|
|
84
90
|
if not template_path.exists():
|
|
85
|
-
|
|
91
|
+
# 再在 aury_docs/ 子目录找
|
|
92
|
+
template_path = AURY_DOCS_TPL_DIR / template_name
|
|
93
|
+
if not template_path.exists():
|
|
94
|
+
raise FileNotFoundError(f"模板文件不存在: {template_name}")
|
|
86
95
|
|
|
87
96
|
content = template_path.read_text(encoding="utf-8")
|
|
88
97
|
return content.format(**context)
|
|
@@ -116,8 +125,8 @@ def _write_file(
|
|
|
116
125
|
return True
|
|
117
126
|
|
|
118
127
|
|
|
119
|
-
@app.command(name="
|
|
120
|
-
def
|
|
128
|
+
@app.command(name="agents")
|
|
129
|
+
def generate_agents_doc(
|
|
121
130
|
project_dir: Path = typer.Argument(
|
|
122
131
|
Path("."),
|
|
123
132
|
help="项目目录路径",
|
|
@@ -139,20 +148,84 @@ def generate_dev_doc(
|
|
|
139
148
|
help="预览模式,不实际写入文件",
|
|
140
149
|
),
|
|
141
150
|
) -> None:
|
|
142
|
-
"""生成/更新
|
|
151
|
+
"""生成/更新 AGENTS.md(AI 编程助手上下文文档)。"""
|
|
143
152
|
context = _detect_project_info(project_dir)
|
|
144
153
|
|
|
145
154
|
console.print(f"[cyan]📚 检测到项目: {context['project_name']}[/cyan]")
|
|
146
155
|
|
|
147
156
|
try:
|
|
148
|
-
content = _render_template("
|
|
149
|
-
output_path = project_dir / "
|
|
157
|
+
content = _render_template("AGENTS.md.tpl", context)
|
|
158
|
+
output_path = project_dir / "AGENTS.md"
|
|
150
159
|
_write_file(output_path, content, force=force, dry_run=dry_run)
|
|
151
160
|
except Exception as e:
|
|
152
161
|
console.print(f"[red]❌ 生成失败: {e}[/red]")
|
|
153
162
|
raise typer.Exit(1)
|
|
154
163
|
|
|
155
164
|
|
|
165
|
+
# aury_docs/ 模板目录
|
|
166
|
+
AURY_DOCS_TPL_DIR = TEMPLATES_DIR / "aury_docs"
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
def _get_aury_docs_templates() -> list[Path]:
|
|
170
|
+
"""动态扫描 aury_docs/ 模板目录。"""
|
|
171
|
+
if not AURY_DOCS_TPL_DIR.exists():
|
|
172
|
+
return []
|
|
173
|
+
return sorted(AURY_DOCS_TPL_DIR.glob("*.md.tpl"))
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
@app.command(name="dev")
|
|
177
|
+
def generate_dev_doc(
|
|
178
|
+
project_dir: Path = typer.Argument(
|
|
179
|
+
Path("."),
|
|
180
|
+
help="项目目录路径",
|
|
181
|
+
exists=True,
|
|
182
|
+
file_okay=False,
|
|
183
|
+
dir_okay=True,
|
|
184
|
+
resolve_path=True,
|
|
185
|
+
),
|
|
186
|
+
force: bool = typer.Option(
|
|
187
|
+
False,
|
|
188
|
+
"--force",
|
|
189
|
+
"-f",
|
|
190
|
+
help="强制覆盖已存在的文件",
|
|
191
|
+
),
|
|
192
|
+
dry_run: bool = typer.Option(
|
|
193
|
+
False,
|
|
194
|
+
"--dry-run",
|
|
195
|
+
"-n",
|
|
196
|
+
help="预览模式,不实际写入文件",
|
|
197
|
+
),
|
|
198
|
+
) -> None:
|
|
199
|
+
"""生成/更新 aury_docs/ 开发文档包。"""
|
|
200
|
+
context = _detect_project_info(project_dir)
|
|
201
|
+
|
|
202
|
+
console.print(f"[cyan]📚 检测到项目: {context['project_name']}[/cyan]")
|
|
203
|
+
console.print()
|
|
204
|
+
|
|
205
|
+
# 确保输出目录存在
|
|
206
|
+
aury_docs_dir = project_dir / "aury_docs"
|
|
207
|
+
if not dry_run:
|
|
208
|
+
aury_docs_dir.mkdir(parents=True, exist_ok=True)
|
|
209
|
+
|
|
210
|
+
success_count = 0
|
|
211
|
+
for tpl_path in _get_aury_docs_templates():
|
|
212
|
+
try:
|
|
213
|
+
output_name = tpl_path.stem # 去掉 .tpl 后缀,保留 .md
|
|
214
|
+
output_path = aury_docs_dir / output_name
|
|
215
|
+
content = tpl_path.read_text(encoding="utf-8")
|
|
216
|
+
content = content.format(**context)
|
|
217
|
+
if _write_file(output_path, content, force=force, dry_run=dry_run):
|
|
218
|
+
success_count += 1
|
|
219
|
+
except Exception as e:
|
|
220
|
+
console.print(f"[red]❌ 生成 {tpl_path.name} 失败: {e}[/red]")
|
|
221
|
+
|
|
222
|
+
console.print()
|
|
223
|
+
if dry_run:
|
|
224
|
+
console.print(f"[dim]🔍 预览模式完成,将生成 {success_count} 个文档到 aury_docs/ 目录[/dim]")
|
|
225
|
+
else:
|
|
226
|
+
console.print(f"[green]✨ 完成!成功生成 {success_count} 个文档到 aury_docs/ 目录[/green]")
|
|
227
|
+
|
|
228
|
+
|
|
156
229
|
@app.command(name="cli")
|
|
157
230
|
def generate_cli_doc(
|
|
158
231
|
project_dir: Path = typer.Argument(
|
|
@@ -176,14 +249,18 @@ def generate_cli_doc(
|
|
|
176
249
|
help="预览模式,不实际写入文件",
|
|
177
250
|
),
|
|
178
251
|
) -> None:
|
|
179
|
-
"""生成/更新
|
|
252
|
+
"""生成/更新 aury_docs/99-cli.md 命令行文档。"""
|
|
180
253
|
context = _detect_project_info(project_dir)
|
|
181
254
|
|
|
182
255
|
console.print(f"[cyan]📚 检测到项目: {context['project_name']}[/cyan]")
|
|
183
256
|
|
|
184
257
|
try:
|
|
185
|
-
|
|
186
|
-
|
|
258
|
+
tpl_path = AURY_DOCS_TPL_DIR / "99-cli.md.tpl"
|
|
259
|
+
content = tpl_path.read_text(encoding="utf-8")
|
|
260
|
+
content = content.format(**context)
|
|
261
|
+
output_path = project_dir / "aury_docs" / "99-cli.md"
|
|
262
|
+
if not dry_run:
|
|
263
|
+
output_path.parent.mkdir(parents=True, exist_ok=True)
|
|
187
264
|
_write_file(output_path, content, force=force, dry_run=dry_run)
|
|
188
265
|
except Exception as e:
|
|
189
266
|
console.print(f"[red]❌ 生成失败: {e}[/red]")
|
|
@@ -250,20 +327,30 @@ def generate_all_docs(
|
|
|
250
327
|
help="预览模式,不实际写入文件",
|
|
251
328
|
),
|
|
252
329
|
) -> None:
|
|
253
|
-
"""生成/更新所有文档(
|
|
330
|
+
"""生成/更新所有文档(AGENTS.md, docs/, CLI.md, .env.example)。"""
|
|
254
331
|
context = _detect_project_info(project_dir)
|
|
255
332
|
|
|
256
333
|
console.print(f"[cyan]📚 检测到项目: {context['project_name']}[/cyan]")
|
|
257
334
|
console.print()
|
|
258
335
|
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
("
|
|
336
|
+
# 根目录文档
|
|
337
|
+
root_docs: list[tuple[str, str, str]] = [
|
|
338
|
+
("AGENTS.md.tpl", "AGENTS.md", "AI 编程助手上下文"),
|
|
262
339
|
("env.example.tpl", ".env.example", "环境变量示例"),
|
|
263
340
|
]
|
|
264
341
|
|
|
342
|
+
# aury_docs/ 开发文档
|
|
343
|
+
aury_docs_templates = _get_aury_docs_templates()
|
|
344
|
+
dev_docs = [
|
|
345
|
+
(tpl.name, f"aury_docs/{tpl.stem}", f"开发文档: {tpl.stem}")
|
|
346
|
+
for tpl in aury_docs_templates
|
|
347
|
+
]
|
|
348
|
+
|
|
349
|
+
# 合并所有文档
|
|
350
|
+
all_docs = root_docs + dev_docs
|
|
351
|
+
|
|
265
352
|
success_count = 0
|
|
266
|
-
for template_name, output_name, description in
|
|
353
|
+
for template_name, output_name, description in all_docs:
|
|
267
354
|
try:
|
|
268
355
|
content = _render_template(template_name, context)
|
|
269
356
|
output_path = project_dir / output_name
|