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
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
"""日志配置和初始化。
|
|
2
|
+
|
|
3
|
+
提供 setup_logging 和 register_log_sink 功能。
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
import os
|
|
9
|
+
from typing import Any
|
|
10
|
+
|
|
11
|
+
from loguru import logger
|
|
12
|
+
|
|
13
|
+
from aury.boot.common.logging.context import (
|
|
14
|
+
ServiceContext,
|
|
15
|
+
_to_service_context,
|
|
16
|
+
get_service_context,
|
|
17
|
+
get_trace_id,
|
|
18
|
+
set_service_context,
|
|
19
|
+
)
|
|
20
|
+
from aury.boot.common.logging.format import create_console_sink, format_message
|
|
21
|
+
|
|
22
|
+
# 全局日志配置状态
|
|
23
|
+
_log_config: dict[str, Any] = {
|
|
24
|
+
"log_dir": "logs",
|
|
25
|
+
"rotation": "00:00",
|
|
26
|
+
"retention_days": 7,
|
|
27
|
+
"file_format": "",
|
|
28
|
+
"initialized": False,
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def register_log_sink(
|
|
33
|
+
name: str,
|
|
34
|
+
*,
|
|
35
|
+
filter_key: str | None = None,
|
|
36
|
+
level: str = "INFO",
|
|
37
|
+
sink_format: str | None = None,
|
|
38
|
+
) -> None:
|
|
39
|
+
"""注册自定义日志 sink。
|
|
40
|
+
|
|
41
|
+
使用 logger.bind() 标记的日志会写入对应文件。
|
|
42
|
+
|
|
43
|
+
Args:
|
|
44
|
+
name: 日志文件名前缀(如 "access" -> access_2024-01-01.log)
|
|
45
|
+
filter_key: 过滤键名,日志需要 logger.bind(key=True) 才会写入
|
|
46
|
+
level: 日志级别
|
|
47
|
+
sink_format: 自定义格式(默认使用简化格式)
|
|
48
|
+
|
|
49
|
+
使用示例:
|
|
50
|
+
# 注册 access 日志
|
|
51
|
+
register_log_sink("access", filter_key="access")
|
|
52
|
+
|
|
53
|
+
# 写入 access 日志
|
|
54
|
+
logger.bind(access=True).info("GET /api/users 200 0.05s")
|
|
55
|
+
"""
|
|
56
|
+
if not _log_config["initialized"]:
|
|
57
|
+
raise RuntimeError("请先调用 setup_logging() 初始化日志系统")
|
|
58
|
+
|
|
59
|
+
log_dir = _log_config["log_dir"]
|
|
60
|
+
rotation = _log_config["rotation"]
|
|
61
|
+
retention_days = _log_config["retention_days"]
|
|
62
|
+
|
|
63
|
+
default_format = (
|
|
64
|
+
"{time:YYYY-MM-DD HH:mm:ss} | "
|
|
65
|
+
"{extra[trace_id]} | "
|
|
66
|
+
"{message}"
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
# 创建 filter
|
|
70
|
+
if filter_key:
|
|
71
|
+
def sink_filter(record, key=filter_key):
|
|
72
|
+
return record["extra"].get(key, False)
|
|
73
|
+
else:
|
|
74
|
+
sink_filter = None
|
|
75
|
+
|
|
76
|
+
logger.add(
|
|
77
|
+
os.path.join(log_dir, f"{name}_{{time:YYYY-MM-DD}}.log"),
|
|
78
|
+
rotation=rotation,
|
|
79
|
+
retention=f"{retention_days} days",
|
|
80
|
+
level=level,
|
|
81
|
+
format=sink_format or default_format,
|
|
82
|
+
encoding="utf-8",
|
|
83
|
+
enqueue=True,
|
|
84
|
+
delay=True,
|
|
85
|
+
filter=sink_filter,
|
|
86
|
+
)
|
|
87
|
+
|
|
88
|
+
logger.debug(f"注册日志 sink: {name} (filter_key={filter_key})")
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def setup_logging(
|
|
92
|
+
log_level: str = "INFO",
|
|
93
|
+
log_dir: str | None = None,
|
|
94
|
+
service_type: ServiceContext | str = ServiceContext.API,
|
|
95
|
+
enable_file_rotation: bool = True,
|
|
96
|
+
rotation_time: str = "00:00",
|
|
97
|
+
retention_days: int = 7,
|
|
98
|
+
rotation_size: str = "50 MB",
|
|
99
|
+
enable_console: bool = True,
|
|
100
|
+
) -> None:
|
|
101
|
+
"""设置日志配置。
|
|
102
|
+
|
|
103
|
+
日志文件按服务类型分离:
|
|
104
|
+
- {service_type}_info_{date}.log - INFO/WARNING/DEBUG 日志
|
|
105
|
+
- {service_type}_error_{date}.log - ERROR/CRITICAL 日志
|
|
106
|
+
|
|
107
|
+
轮转策略:
|
|
108
|
+
- 文件名包含日期,每天自动创建新文件
|
|
109
|
+
- 单文件超过大小限制时,会轮转产生 .1, .2 等后缀
|
|
110
|
+
|
|
111
|
+
可通过 register_log_sink() 注册额外的日志文件(如 access.log)。
|
|
112
|
+
|
|
113
|
+
Args:
|
|
114
|
+
log_level: 日志级别(DEBUG/INFO/WARNING/ERROR/CRITICAL)
|
|
115
|
+
log_dir: 日志目录(默认:./logs)
|
|
116
|
+
service_type: 服务类型(app/scheduler/worker)
|
|
117
|
+
enable_file_rotation: 是否启用日志轮转
|
|
118
|
+
rotation_time: 每日轮转时间(默认:00:00)
|
|
119
|
+
retention_days: 日志保留天数(默认:7 天)
|
|
120
|
+
rotation_size: 单文件大小上限(默认:50 MB)
|
|
121
|
+
enable_console: 是否输出到控制台
|
|
122
|
+
"""
|
|
123
|
+
log_level = log_level.upper()
|
|
124
|
+
log_dir = log_dir or "logs"
|
|
125
|
+
os.makedirs(log_dir, exist_ok=True)
|
|
126
|
+
|
|
127
|
+
# 滚动策略:基于大小轮转(文件名已包含日期,每天自动新文件)
|
|
128
|
+
rotation = rotation_size if enable_file_rotation else None
|
|
129
|
+
|
|
130
|
+
# 标准化服务类型
|
|
131
|
+
service_type_enum = _to_service_context(service_type)
|
|
132
|
+
|
|
133
|
+
# 清理旧的 sink,避免重复日志(idempotent)
|
|
134
|
+
logger.remove()
|
|
135
|
+
|
|
136
|
+
# 保存全局配置(供 register_log_sink 使用)
|
|
137
|
+
_log_config.update({
|
|
138
|
+
"log_dir": log_dir,
|
|
139
|
+
"rotation": rotation,
|
|
140
|
+
"retention_days": retention_days,
|
|
141
|
+
"initialized": True,
|
|
142
|
+
})
|
|
143
|
+
|
|
144
|
+
# 设置默认服务上下文
|
|
145
|
+
set_service_context(service_type_enum)
|
|
146
|
+
|
|
147
|
+
# 配置 patcher,确保每条日志都有 service 和 trace_id
|
|
148
|
+
logger.configure(patcher=lambda record: (
|
|
149
|
+
record["extra"].update({
|
|
150
|
+
"trace_id": get_trace_id(),
|
|
151
|
+
# 记录字符串值,便于过滤器比较
|
|
152
|
+
"service": get_service_context().value,
|
|
153
|
+
})
|
|
154
|
+
))
|
|
155
|
+
|
|
156
|
+
# 控制台输出(使用 Java 风格堆栈)
|
|
157
|
+
if enable_console:
|
|
158
|
+
logger.add(
|
|
159
|
+
create_console_sink(),
|
|
160
|
+
format="{message}", # 简单格式,避免解析 <module> 等函数名
|
|
161
|
+
level=log_level,
|
|
162
|
+
colorize=False, # 颜色在 sink 内处理
|
|
163
|
+
)
|
|
164
|
+
|
|
165
|
+
# 为 app 和 scheduler 分别创建日志文件(通过 ContextVar 区分)
|
|
166
|
+
# API 模式下会同时运行嵌入式 scheduler,需要两个文件
|
|
167
|
+
contexts_to_create: list[str] = [service_type_enum.value]
|
|
168
|
+
# API 模式下也需要 scheduler 日志文件
|
|
169
|
+
if service_type_enum is ServiceContext.API:
|
|
170
|
+
contexts_to_create.append(ServiceContext.SCHEDULER.value)
|
|
171
|
+
|
|
172
|
+
for ctx in contexts_to_create:
|
|
173
|
+
# INFO 级别文件(使用 Java 风格堆栈)
|
|
174
|
+
info_file = os.path.join(
|
|
175
|
+
log_dir,
|
|
176
|
+
f"{ctx}_info_{{time:YYYY-MM-DD}}.log" if enable_file_rotation else f"{ctx}_info.log"
|
|
177
|
+
)
|
|
178
|
+
logger.add(
|
|
179
|
+
info_file,
|
|
180
|
+
format=lambda record: format_message(record),
|
|
181
|
+
rotation=rotation,
|
|
182
|
+
retention=f"{retention_days} days",
|
|
183
|
+
level=log_level, # >= INFO 都写入(包含 WARNING/ERROR/CRITICAL)
|
|
184
|
+
encoding="utf-8",
|
|
185
|
+
enqueue=True,
|
|
186
|
+
filter=lambda record, c=ctx: (
|
|
187
|
+
record["extra"].get("service") == c
|
|
188
|
+
and not record["extra"].get("access", False)
|
|
189
|
+
),
|
|
190
|
+
)
|
|
191
|
+
|
|
192
|
+
# ERROR 级别文件(使用 Java 风格堆栈)
|
|
193
|
+
error_file = os.path.join(
|
|
194
|
+
log_dir,
|
|
195
|
+
f"{ctx}_error_{{time:YYYY-MM-DD}}.log" if enable_file_rotation else f"{ctx}_error.log"
|
|
196
|
+
)
|
|
197
|
+
logger.add(
|
|
198
|
+
error_file,
|
|
199
|
+
format=lambda record: format_message(record),
|
|
200
|
+
rotation=rotation,
|
|
201
|
+
retention=f"{retention_days} days",
|
|
202
|
+
level="ERROR",
|
|
203
|
+
encoding="utf-8",
|
|
204
|
+
enqueue=True,
|
|
205
|
+
filter=lambda record, c=ctx: record["extra"].get("service") == c,
|
|
206
|
+
)
|
|
207
|
+
|
|
208
|
+
logger.info(f"日志系统初始化完成 | 服务: {service_type} | 级别: {log_level} | 目录: {log_dir}")
|
|
209
|
+
|
|
210
|
+
|
|
211
|
+
__all__ = [
|
|
212
|
+
"register_log_sink",
|
|
213
|
+
"setup_logging",
|
|
214
|
+
]
|
|
@@ -5,7 +5,6 @@ from typing import Any
|
|
|
5
5
|
|
|
6
6
|
from starlette.requests import Request
|
|
7
7
|
|
|
8
|
-
|
|
9
8
|
try: # pragma: no cover
|
|
10
9
|
from sqladmin.authentication import AuthenticationBackend as _SQLAdminAuthenticationBackend
|
|
11
10
|
except Exception: # pragma: no cover
|
|
@@ -14,7 +13,7 @@ except Exception: # pragma: no cover
|
|
|
14
13
|
|
|
15
14
|
def _require_sqladmin():
|
|
16
15
|
try:
|
|
17
|
-
from sqladmin.authentication import AuthenticationBackend
|
|
16
|
+
from sqladmin.authentication import AuthenticationBackend
|
|
18
17
|
except Exception as exc: # pragma: no cover
|
|
19
18
|
raise ImportError(
|
|
20
19
|
"未安装 sqladmin。请先安装: uv add \"aury-boot[admin]\" 或 uv add sqladmin"
|
|
@@ -118,7 +117,7 @@ def wrap_authenticate(
|
|
|
118
117
|
from sqladmin.authentication import AuthenticationBackend
|
|
119
118
|
|
|
120
119
|
class _Wrapped(AuthenticationBackend):
|
|
121
|
-
async def login(self, request: Request) -> bool:
|
|
120
|
+
async def login(self, request: Request) -> bool:
|
|
122
121
|
# 默认不提供登录页逻辑;用户可自己实现更复杂版本
|
|
123
122
|
return False
|
|
124
123
|
|
|
@@ -14,7 +14,7 @@ from .utils import import_from_string
|
|
|
14
14
|
|
|
15
15
|
def _require_sqladmin():
|
|
16
16
|
try:
|
|
17
|
-
from sqladmin import Admin
|
|
17
|
+
from sqladmin import Admin
|
|
18
18
|
except Exception as exc: # pragma: no cover
|
|
19
19
|
raise ImportError(
|
|
20
20
|
"未安装 sqladmin。请先安装: uv add \"aury-boot[admin]\" 或 uv add sqladmin"
|
|
@@ -119,7 +119,54 @@ class AuditableStateMixin:
|
|
|
119
119
|
|
|
120
120
|
|
|
121
121
|
class VersionMixin:
|
|
122
|
-
"""乐观锁版本控制 Mixin
|
|
122
|
+
"""乐观锁版本控制 Mixin。
|
|
123
|
+
|
|
124
|
+
用于防止并发更新冲突。每次更新时会检查 version 是否一致,
|
|
125
|
+
如果不一致则抛出 VersionConflictError。
|
|
126
|
+
|
|
127
|
+
工作原理:
|
|
128
|
+
1. 读取记录时获取当前 version
|
|
129
|
+
2. 更新时检查 version 是否与读取时一致
|
|
130
|
+
3. 如果一致,version + 1 并完成更新
|
|
131
|
+
4. 如果不一致,抛出 VersionConflictError
|
|
132
|
+
|
|
133
|
+
使用场景:
|
|
134
|
+
- 库存管理(防止超卖)
|
|
135
|
+
- 订单状态更新(防止重复支付)
|
|
136
|
+
- 文档编辑(防止覆盖他人修改)
|
|
137
|
+
|
|
138
|
+
示例:
|
|
139
|
+
class Product(IDMixin, VersionMixin, Base):
|
|
140
|
+
__tablename__ = "products"
|
|
141
|
+
name: Mapped[str]
|
|
142
|
+
stock: Mapped[int]
|
|
143
|
+
|
|
144
|
+
# 更新时自动检查版本
|
|
145
|
+
product = await repo.get(1) # version=1
|
|
146
|
+
product.stock -= 1
|
|
147
|
+
await repo.update(product) # version 自动变为 2
|
|
148
|
+
|
|
149
|
+
# 并发更新时抛出异常
|
|
150
|
+
# 线程 A: product.version = 1, 准备更新
|
|
151
|
+
# 线程 B: 已经更新,version = 2
|
|
152
|
+
# 线程 A: await repo.update(product) -> VersionConflictError
|
|
153
|
+
|
|
154
|
+
异常处理:
|
|
155
|
+
from aury.boot.domain.exceptions import VersionConflictError
|
|
156
|
+
|
|
157
|
+
try:
|
|
158
|
+
await repo.update(product)
|
|
159
|
+
except VersionConflictError as e:
|
|
160
|
+
# 重新加载数据并重试
|
|
161
|
+
product = await repo.get(product.id)
|
|
162
|
+
# ... 重新应用业务逻辑 ...
|
|
163
|
+
await repo.update(product)
|
|
164
|
+
|
|
165
|
+
注意:
|
|
166
|
+
- 乐观锁适用于读多写少的场景
|
|
167
|
+
- 高并发写入场景建议使用悲观锁 (QueryBuilder.for_update())
|
|
168
|
+
- BaseRepository.update() 已自动支持乐观锁检查
|
|
169
|
+
"""
|
|
123
170
|
|
|
124
171
|
version: Mapped[int] = mapped_column(
|
|
125
172
|
Integer,
|
|
@@ -75,10 +75,104 @@ class SortParams(BaseModel):
|
|
|
75
75
|
"""排序参数。
|
|
76
76
|
|
|
77
77
|
定义排序的字段和方向。
|
|
78
|
+
|
|
79
|
+
支持两种语法:
|
|
80
|
+
- 简洁语法: "-created_at" (前缀 - 表示降序)
|
|
81
|
+
- 完整语法: "created_at:desc"
|
|
82
|
+
|
|
83
|
+
示例:
|
|
84
|
+
# 从字符串解析
|
|
85
|
+
sort_params = SortParams.from_string("-created_at,priority")
|
|
86
|
+
sort_params = SortParams.from_string("created_at:desc,priority:asc")
|
|
87
|
+
|
|
88
|
+
# 带白名单验证
|
|
89
|
+
sort_params = SortParams.from_string(
|
|
90
|
+
"-created_at",
|
|
91
|
+
allowed_fields={"id", "created_at", "priority"}
|
|
92
|
+
)
|
|
93
|
+
|
|
94
|
+
# 程序化构建
|
|
95
|
+
sort_params = SortParams(sorts=[("-created_at",), ("priority", "asc")])
|
|
78
96
|
"""
|
|
79
97
|
|
|
80
98
|
sorts: list[tuple[str, str]] = Field(default_factory=list, description="排序字段列表")
|
|
81
99
|
|
|
100
|
+
@classmethod
|
|
101
|
+
def from_string(
|
|
102
|
+
cls,
|
|
103
|
+
sort_str: str | None,
|
|
104
|
+
*,
|
|
105
|
+
allowed_fields: set[str] | None = None,
|
|
106
|
+
default_direction: str = "desc",
|
|
107
|
+
) -> SortParams:
|
|
108
|
+
"""从字符串解析排序参数。
|
|
109
|
+
|
|
110
|
+
支持两种语法:
|
|
111
|
+
- 简洁语法: "-created_at" (前缀 - 表示降序)
|
|
112
|
+
- 完整语法: "created_at:desc"
|
|
113
|
+
|
|
114
|
+
Args:
|
|
115
|
+
sort_str: 排序字符串,如 "-created_at,priority" 或 "created_at:desc,priority:asc"
|
|
116
|
+
allowed_fields: 允许的字段白名单(可选,用于安全校验)
|
|
117
|
+
default_direction: 默认排序方向(当不指定方向时使用)
|
|
118
|
+
|
|
119
|
+
Returns:
|
|
120
|
+
SortParams: 排序参数对象
|
|
121
|
+
|
|
122
|
+
Raises:
|
|
123
|
+
ValueError: 字段不在白名单中 或 方向无效
|
|
124
|
+
|
|
125
|
+
示例:
|
|
126
|
+
>>> SortParams.from_string("-created_at,priority")
|
|
127
|
+
SortParams(sorts=[('created_at', 'desc'), ('priority', 'desc')])
|
|
128
|
+
|
|
129
|
+
>>> SortParams.from_string("created_at:desc,priority:asc")
|
|
130
|
+
SortParams(sorts=[('created_at', 'desc'), ('priority', 'asc')])
|
|
131
|
+
|
|
132
|
+
>>> SortParams.from_string("-id", allowed_fields={"id", "name"})
|
|
133
|
+
SortParams(sorts=[('id', 'desc')])
|
|
134
|
+
|
|
135
|
+
>>> SortParams.from_string("-invalid", allowed_fields={"id", "name"})
|
|
136
|
+
ValueError: 不允许的排序字段: invalid,允许的字段: id, name
|
|
137
|
+
"""
|
|
138
|
+
if not sort_str:
|
|
139
|
+
return cls(sorts=[])
|
|
140
|
+
|
|
141
|
+
sorts = []
|
|
142
|
+
for part in sort_str.split(","):
|
|
143
|
+
part = part.strip()
|
|
144
|
+
if not part:
|
|
145
|
+
continue
|
|
146
|
+
|
|
147
|
+
# 解析字段和方向
|
|
148
|
+
if part.startswith("-"):
|
|
149
|
+
# 简洁语法: -created_at
|
|
150
|
+
field = part[1:]
|
|
151
|
+
direction = "desc"
|
|
152
|
+
elif ":" in part:
|
|
153
|
+
# 完整语法: created_at:desc
|
|
154
|
+
field, direction = part.split(":", 1)
|
|
155
|
+
else:
|
|
156
|
+
# 无方向指示,使用默认
|
|
157
|
+
field = part
|
|
158
|
+
direction = default_direction
|
|
159
|
+
|
|
160
|
+
field = field.strip()
|
|
161
|
+
direction = direction.strip().lower()
|
|
162
|
+
|
|
163
|
+
# 字段白名单校验
|
|
164
|
+
if allowed_fields and field not in allowed_fields:
|
|
165
|
+
allowed_list = ", ".join(sorted(allowed_fields))
|
|
166
|
+
raise ValueError(f"不允许的排序字段: {field},允许的字段: {allowed_list}")
|
|
167
|
+
|
|
168
|
+
# 方向校验
|
|
169
|
+
if direction not in ("asc", "desc"):
|
|
170
|
+
raise ValueError(f"排序方向必须是 'asc' 或 'desc',得到: {direction}")
|
|
171
|
+
|
|
172
|
+
sorts.append((field, direction))
|
|
173
|
+
|
|
174
|
+
return cls(sorts=sorts)
|
|
175
|
+
|
|
82
176
|
def add_sort(self, field: str, direction: str = "asc") -> None:
|
|
83
177
|
"""添加排序字段。
|
|
84
178
|
|
|
@@ -17,7 +17,6 @@ from sqlalchemy.ext.asyncio import AsyncSession
|
|
|
17
17
|
from aury.boot.common.logging import logger
|
|
18
18
|
from aury.boot.domain.exceptions import VersionConflictError
|
|
19
19
|
from aury.boot.domain.models import GUID, Base
|
|
20
|
-
from aury.boot.domain.transaction import _transaction_depth
|
|
21
20
|
from aury.boot.domain.pagination import (
|
|
22
21
|
PaginationParams,
|
|
23
22
|
PaginationResult,
|
|
@@ -25,6 +24,7 @@ from aury.boot.domain.pagination import (
|
|
|
25
24
|
)
|
|
26
25
|
from aury.boot.domain.repository.interface import IRepository
|
|
27
26
|
from aury.boot.domain.repository.query_builder import QueryBuilder
|
|
27
|
+
from aury.boot.domain.transaction import _transaction_depth
|
|
28
28
|
|
|
29
29
|
if TYPE_CHECKING:
|
|
30
30
|
from typing import Self
|
|
@@ -8,7 +8,7 @@ from __future__ import annotations
|
|
|
8
8
|
from abc import ABC, abstractmethod
|
|
9
9
|
from typing import TYPE_CHECKING, Any
|
|
10
10
|
|
|
11
|
-
from aury.boot.domain.models import
|
|
11
|
+
from aury.boot.domain.models import GUID, Base
|
|
12
12
|
from aury.boot.domain.pagination import PaginationParams, PaginationResult, SortParams
|
|
13
13
|
|
|
14
14
|
if TYPE_CHECKING:
|
|
@@ -9,9 +9,9 @@
|
|
|
9
9
|
|
|
10
10
|
from __future__ import annotations
|
|
11
11
|
|
|
12
|
-
import contextvars
|
|
13
12
|
from collections.abc import AsyncGenerator, Awaitable, Callable
|
|
14
13
|
from contextlib import asynccontextmanager
|
|
14
|
+
import contextvars
|
|
15
15
|
from functools import wraps
|
|
16
16
|
from inspect import signature
|
|
17
17
|
from typing import Any
|
|
@@ -24,9 +24,9 @@ from aury.boot.domain.exceptions import TransactionRequiredError
|
|
|
24
24
|
# 用于跟踪嵌套事务的上下文变量
|
|
25
25
|
_transaction_depth: contextvars.ContextVar[int] = contextvars.ContextVar("transaction_depth", default=0)
|
|
26
26
|
|
|
27
|
-
# 用于存储 on_commit
|
|
28
|
-
_on_commit_callbacks: contextvars.ContextVar[list[Callable[[], Any] | Callable[[], Awaitable[Any]]]] = (
|
|
29
|
-
contextvars.ContextVar("on_commit_callbacks", default=
|
|
27
|
+
# 用于存储 on_commit 回调的上下文变量(不设置 default,避免可变对象问题)
|
|
28
|
+
_on_commit_callbacks: contextvars.ContextVar[list[Callable[[], Any] | Callable[[], Awaitable[Any]]] | None] = (
|
|
29
|
+
contextvars.ContextVar("on_commit_callbacks", default=None)
|
|
30
30
|
)
|
|
31
31
|
|
|
32
32
|
|
|
@@ -51,8 +51,7 @@ def on_commit(callback: Callable[[], Any] | Callable[[], Awaitable[Any]]) -> Non
|
|
|
51
51
|
- 嵌套事务中注册的回调,只在最外层事务提交后执行
|
|
52
52
|
"""
|
|
53
53
|
callbacks = _on_commit_callbacks.get()
|
|
54
|
-
|
|
55
|
-
if not callbacks:
|
|
54
|
+
if callbacks is None:
|
|
56
55
|
callbacks = []
|
|
57
56
|
_on_commit_callbacks.set(callbacks)
|
|
58
57
|
callbacks.append(callback)
|
|
@@ -65,7 +64,7 @@ async def _execute_on_commit_callbacks() -> None:
|
|
|
65
64
|
return
|
|
66
65
|
|
|
67
66
|
# 清空回调列表
|
|
68
|
-
_on_commit_callbacks.set(
|
|
67
|
+
_on_commit_callbacks.set(None)
|
|
69
68
|
|
|
70
69
|
for callback in callbacks:
|
|
71
70
|
try:
|
|
@@ -106,7 +105,7 @@ async def transactional_context(session: AsyncSession, auto_commit: bool = True)
|
|
|
106
105
|
|
|
107
106
|
# 最外层事务初始化回调列表
|
|
108
107
|
if is_outermost:
|
|
109
|
-
_on_commit_callbacks.set(
|
|
108
|
+
_on_commit_callbacks.set(None)
|
|
110
109
|
|
|
111
110
|
try:
|
|
112
111
|
yield session
|
|
@@ -121,7 +120,7 @@ async def transactional_context(session: AsyncSession, auto_commit: bool = True)
|
|
|
121
120
|
if is_outermost:
|
|
122
121
|
await session.rollback()
|
|
123
122
|
# 回滚时清空回调列表(不执行)
|
|
124
|
-
_on_commit_callbacks.set(
|
|
123
|
+
_on_commit_callbacks.set(None)
|
|
125
124
|
logger.error(f"事务回滚: {exc}")
|
|
126
125
|
raise
|
|
127
126
|
finally:
|
|
@@ -6,7 +6,11 @@
|
|
|
6
6
|
- 存储管理
|
|
7
7
|
- 调度器
|
|
8
8
|
- 任务队列
|
|
9
|
-
-
|
|
9
|
+
- 消息队列
|
|
10
|
+
- 通道 (SSE/PubSub)
|
|
11
|
+
- 事件总线
|
|
12
|
+
- Redis 客户端
|
|
13
|
+
- RabbitMQ 客户端
|
|
10
14
|
"""
|
|
11
15
|
|
|
12
16
|
# 数据库
|
|
@@ -20,16 +24,49 @@ from .cache import (
|
|
|
20
24
|
MemoryCache,
|
|
21
25
|
RedisCache,
|
|
22
26
|
)
|
|
27
|
+
|
|
28
|
+
# 通道 (SSE/PubSub)
|
|
29
|
+
from .channel import (
|
|
30
|
+
ChannelBackend,
|
|
31
|
+
ChannelManager,
|
|
32
|
+
ChannelMessage,
|
|
33
|
+
IChannel,
|
|
34
|
+
MemoryChannel,
|
|
35
|
+
RedisChannel,
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
# RabbitMQ 客户端
|
|
39
|
+
from .clients.rabbitmq import RabbitMQClient, RabbitMQConfig
|
|
40
|
+
|
|
41
|
+
# Redis 客户端
|
|
42
|
+
from .clients.redis import RedisClient, RedisConfig
|
|
23
43
|
from .database import DatabaseManager
|
|
24
44
|
|
|
25
|
-
#
|
|
26
|
-
|
|
45
|
+
# 依赖注入
|
|
46
|
+
from .di import Container, Lifetime, Scope, ServiceDescriptor
|
|
27
47
|
|
|
28
|
-
#
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
48
|
+
# 事件总线
|
|
49
|
+
from .events import (
|
|
50
|
+
Event,
|
|
51
|
+
EventBackend,
|
|
52
|
+
EventBusManager,
|
|
53
|
+
EventHandler,
|
|
54
|
+
EventType,
|
|
55
|
+
IEventBus,
|
|
56
|
+
MemoryEventBus,
|
|
57
|
+
RabbitMQEventBus,
|
|
58
|
+
RedisEventBus,
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
# 消息队列
|
|
62
|
+
from .mq import (
|
|
63
|
+
IMQ,
|
|
64
|
+
MQBackend,
|
|
65
|
+
MQManager,
|
|
66
|
+
MQMessage,
|
|
67
|
+
RabbitMQ,
|
|
68
|
+
RedisMQ,
|
|
69
|
+
)
|
|
33
70
|
|
|
34
71
|
# 存储(基于 aury-sdk-storage)
|
|
35
72
|
from .storage import (
|
|
@@ -44,45 +81,67 @@ from .storage import (
|
|
|
44
81
|
UploadResult,
|
|
45
82
|
)
|
|
46
83
|
|
|
84
|
+
# 调度器(可选依赖)
|
|
85
|
+
try:
|
|
86
|
+
from .scheduler import SchedulerManager
|
|
87
|
+
except ImportError:
|
|
88
|
+
SchedulerManager = None # type: ignore[assignment, misc]
|
|
89
|
+
|
|
47
90
|
# 任务队列(可选依赖)
|
|
48
91
|
try:
|
|
49
|
-
from .tasks import TaskManager, TaskProxy,
|
|
92
|
+
from .tasks import TaskManager, TaskProxy, conditional_task
|
|
50
93
|
except ImportError:
|
|
51
94
|
TaskManager = None # type: ignore[assignment, misc]
|
|
52
95
|
TaskProxy = None # type: ignore[assignment, misc]
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
# 事件总线
|
|
56
|
-
# 依赖注入
|
|
57
|
-
from .di import Container, Lifetime, Scope, ServiceDescriptor
|
|
58
|
-
from .events import (
|
|
59
|
-
EventBus,
|
|
60
|
-
EventConsumer,
|
|
61
|
-
EventLoggingMiddleware,
|
|
62
|
-
EventMiddleware,
|
|
63
|
-
)
|
|
96
|
+
conditional_task = None # type: ignore[assignment, misc]
|
|
64
97
|
|
|
65
98
|
__all__ = [
|
|
99
|
+
# 消息队列
|
|
100
|
+
"IMQ",
|
|
101
|
+
# 缓存
|
|
66
102
|
"CacheBackend",
|
|
67
103
|
"CacheFactory",
|
|
68
|
-
# 缓存
|
|
69
104
|
"CacheManager",
|
|
105
|
+
# 通道
|
|
106
|
+
"ChannelBackend",
|
|
107
|
+
"ChannelManager",
|
|
108
|
+
"ChannelMessage",
|
|
70
109
|
# 依赖注入
|
|
71
110
|
"Container",
|
|
72
111
|
# 数据库
|
|
73
112
|
"DatabaseManager",
|
|
74
113
|
# 事件总线
|
|
75
|
-
"
|
|
76
|
-
"
|
|
77
|
-
"
|
|
78
|
-
"
|
|
114
|
+
"Event",
|
|
115
|
+
"EventBackend",
|
|
116
|
+
"EventBusManager",
|
|
117
|
+
"EventHandler",
|
|
118
|
+
"EventType",
|
|
79
119
|
"ICache",
|
|
120
|
+
"IChannel",
|
|
121
|
+
"IEventBus",
|
|
122
|
+
# 存储
|
|
80
123
|
"IStorage",
|
|
81
124
|
"Lifetime",
|
|
82
125
|
"LocalStorage",
|
|
126
|
+
"MQBackend",
|
|
127
|
+
"MQManager",
|
|
128
|
+
"MQMessage",
|
|
83
129
|
"MemcachedCache",
|
|
84
130
|
"MemoryCache",
|
|
131
|
+
"MemoryChannel",
|
|
132
|
+
"MemoryEventBus",
|
|
133
|
+
"RabbitMQ",
|
|
134
|
+
# RabbitMQ 客户端
|
|
135
|
+
"RabbitMQClient",
|
|
136
|
+
"RabbitMQConfig",
|
|
137
|
+
"RabbitMQEventBus",
|
|
85
138
|
"RedisCache",
|
|
139
|
+
"RedisChannel",
|
|
140
|
+
# Redis 客户端
|
|
141
|
+
"RedisClient",
|
|
142
|
+
"RedisConfig",
|
|
143
|
+
"RedisEventBus",
|
|
144
|
+
"RedisMQ",
|
|
86
145
|
"S3Storage",
|
|
87
146
|
# 调度器
|
|
88
147
|
"SchedulerManager",
|
|
@@ -92,13 +151,11 @@ __all__ = [
|
|
|
92
151
|
"StorageConfig",
|
|
93
152
|
"StorageFactory",
|
|
94
153
|
"StorageFile",
|
|
95
|
-
# 存储
|
|
96
154
|
"StorageManager",
|
|
97
|
-
"UploadResult",
|
|
98
155
|
# 任务队列
|
|
99
156
|
"TaskManager",
|
|
100
157
|
"TaskProxy",
|
|
101
|
-
"
|
|
102
|
-
|
|
158
|
+
"UploadResult",
|
|
159
|
+
"conditional_task",
|
|
103
160
|
]
|
|
104
161
|
|