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.
Files changed (122) hide show
  1. aury/boot/__init__.py +2 -2
  2. aury/boot/_version.py +2 -2
  3. aury/boot/application/__init__.py +60 -36
  4. aury/boot/application/adapter/__init__.py +112 -0
  5. aury/boot/application/adapter/base.py +511 -0
  6. aury/boot/application/adapter/config.py +242 -0
  7. aury/boot/application/adapter/decorators.py +259 -0
  8. aury/boot/application/adapter/exceptions.py +202 -0
  9. aury/boot/application/adapter/http.py +325 -0
  10. aury/boot/application/app/__init__.py +12 -8
  11. aury/boot/application/app/base.py +12 -0
  12. aury/boot/application/app/components.py +137 -44
  13. aury/boot/application/app/middlewares.py +9 -4
  14. aury/boot/application/app/startup.py +249 -0
  15. aury/boot/application/config/__init__.py +36 -1
  16. aury/boot/application/config/multi_instance.py +216 -0
  17. aury/boot/application/config/settings.py +398 -149
  18. aury/boot/application/constants/components.py +6 -0
  19. aury/boot/application/errors/handlers.py +17 -3
  20. aury/boot/application/middleware/logging.py +21 -120
  21. aury/boot/application/rpc/__init__.py +2 -2
  22. aury/boot/commands/__init__.py +30 -10
  23. aury/boot/commands/app.py +131 -1
  24. aury/boot/commands/docs.py +104 -17
  25. aury/boot/commands/generate.py +22 -22
  26. aury/boot/commands/init.py +68 -17
  27. aury/boot/commands/server/app.py +2 -3
  28. aury/boot/commands/templates/project/AGENTS.md.tpl +221 -0
  29. aury/boot/commands/templates/project/README.md.tpl +2 -2
  30. aury/boot/commands/templates/project/aury_docs/00-overview.md.tpl +59 -0
  31. aury/boot/commands/templates/project/aury_docs/01-model.md.tpl +184 -0
  32. aury/boot/commands/templates/project/aury_docs/02-repository.md.tpl +206 -0
  33. aury/boot/commands/templates/project/aury_docs/03-service.md.tpl +398 -0
  34. aury/boot/commands/templates/project/aury_docs/04-schema.md.tpl +95 -0
  35. aury/boot/commands/templates/project/aury_docs/05-api.md.tpl +116 -0
  36. aury/boot/commands/templates/project/aury_docs/06-exception.md.tpl +118 -0
  37. aury/boot/commands/templates/project/aury_docs/07-cache.md.tpl +122 -0
  38. aury/boot/commands/templates/project/aury_docs/08-scheduler.md.tpl +32 -0
  39. aury/boot/commands/templates/project/aury_docs/09-tasks.md.tpl +38 -0
  40. aury/boot/commands/templates/project/aury_docs/10-storage.md.tpl +115 -0
  41. aury/boot/commands/templates/project/aury_docs/11-logging.md.tpl +131 -0
  42. aury/boot/commands/templates/project/aury_docs/12-admin.md.tpl +56 -0
  43. aury/boot/commands/templates/project/aury_docs/13-channel.md.tpl +104 -0
  44. aury/boot/commands/templates/project/aury_docs/14-mq.md.tpl +102 -0
  45. aury/boot/commands/templates/project/aury_docs/15-events.md.tpl +147 -0
  46. aury/boot/commands/templates/project/aury_docs/16-adapter.md.tpl +403 -0
  47. aury/boot/commands/templates/project/{CLI.md.tpl → aury_docs/99-cli.md.tpl} +19 -19
  48. aury/boot/commands/templates/project/config.py.tpl +10 -10
  49. aury/boot/commands/templates/project/env_templates/_header.tpl +10 -0
  50. aury/boot/commands/templates/project/env_templates/admin.tpl +49 -0
  51. aury/boot/commands/templates/project/env_templates/cache.tpl +14 -0
  52. aury/boot/commands/templates/project/env_templates/database.tpl +22 -0
  53. aury/boot/commands/templates/project/env_templates/log.tpl +18 -0
  54. aury/boot/commands/templates/project/env_templates/messaging.tpl +46 -0
  55. aury/boot/commands/templates/project/env_templates/rpc.tpl +28 -0
  56. aury/boot/commands/templates/project/env_templates/scheduler.tpl +18 -0
  57. aury/boot/commands/templates/project/env_templates/service.tpl +18 -0
  58. aury/boot/commands/templates/project/env_templates/storage.tpl +38 -0
  59. aury/boot/commands/templates/project/env_templates/third_party.tpl +43 -0
  60. aury/boot/commands/templates/project/modules/tasks.py.tpl +1 -1
  61. aury/boot/common/logging/__init__.py +26 -674
  62. aury/boot/common/logging/context.py +132 -0
  63. aury/boot/common/logging/decorators.py +118 -0
  64. aury/boot/common/logging/format.py +315 -0
  65. aury/boot/common/logging/setup.py +214 -0
  66. aury/boot/contrib/admin_console/auth.py +2 -3
  67. aury/boot/contrib/admin_console/install.py +1 -1
  68. aury/boot/domain/models/mixins.py +48 -1
  69. aury/boot/domain/pagination/__init__.py +94 -0
  70. aury/boot/domain/repository/impl.py +1 -1
  71. aury/boot/domain/repository/interface.py +1 -1
  72. aury/boot/domain/transaction/__init__.py +8 -9
  73. aury/boot/infrastructure/__init__.py +86 -29
  74. aury/boot/infrastructure/cache/backends.py +102 -18
  75. aury/boot/infrastructure/cache/base.py +12 -0
  76. aury/boot/infrastructure/cache/manager.py +153 -91
  77. aury/boot/infrastructure/channel/__init__.py +24 -0
  78. aury/boot/infrastructure/channel/backends/__init__.py +9 -0
  79. aury/boot/infrastructure/channel/backends/memory.py +83 -0
  80. aury/boot/infrastructure/channel/backends/redis.py +88 -0
  81. aury/boot/infrastructure/channel/base.py +92 -0
  82. aury/boot/infrastructure/channel/manager.py +203 -0
  83. aury/boot/infrastructure/clients/__init__.py +22 -0
  84. aury/boot/infrastructure/clients/rabbitmq/__init__.py +9 -0
  85. aury/boot/infrastructure/clients/rabbitmq/config.py +46 -0
  86. aury/boot/infrastructure/clients/rabbitmq/manager.py +288 -0
  87. aury/boot/infrastructure/clients/redis/__init__.py +28 -0
  88. aury/boot/infrastructure/clients/redis/config.py +51 -0
  89. aury/boot/infrastructure/clients/redis/manager.py +264 -0
  90. aury/boot/infrastructure/database/config.py +7 -16
  91. aury/boot/infrastructure/database/manager.py +16 -38
  92. aury/boot/infrastructure/events/__init__.py +18 -21
  93. aury/boot/infrastructure/events/backends/__init__.py +11 -0
  94. aury/boot/infrastructure/events/backends/memory.py +86 -0
  95. aury/boot/infrastructure/events/backends/rabbitmq.py +193 -0
  96. aury/boot/infrastructure/events/backends/redis.py +162 -0
  97. aury/boot/infrastructure/events/base.py +127 -0
  98. aury/boot/infrastructure/events/manager.py +224 -0
  99. aury/boot/infrastructure/mq/__init__.py +24 -0
  100. aury/boot/infrastructure/mq/backends/__init__.py +9 -0
  101. aury/boot/infrastructure/mq/backends/rabbitmq.py +179 -0
  102. aury/boot/infrastructure/mq/backends/redis.py +167 -0
  103. aury/boot/infrastructure/mq/base.py +143 -0
  104. aury/boot/infrastructure/mq/manager.py +239 -0
  105. aury/boot/infrastructure/scheduler/manager.py +7 -3
  106. aury/boot/infrastructure/storage/__init__.py +9 -9
  107. aury/boot/infrastructure/storage/base.py +17 -5
  108. aury/boot/infrastructure/storage/factory.py +0 -1
  109. aury/boot/infrastructure/tasks/__init__.py +2 -2
  110. aury/boot/infrastructure/tasks/config.py +5 -13
  111. aury/boot/infrastructure/tasks/manager.py +55 -33
  112. {aury_boot-0.0.4.dist-info → aury_boot-0.0.7.dist-info}/METADATA +20 -2
  113. aury_boot-0.0.7.dist-info/RECORD +197 -0
  114. aury/boot/commands/templates/project/DEVELOPMENT.md.tpl +0 -1397
  115. aury/boot/commands/templates/project/env.example.tpl +0 -213
  116. aury/boot/infrastructure/events/bus.py +0 -362
  117. aury/boot/infrastructure/events/config.py +0 -52
  118. aury/boot/infrastructure/events/consumer.py +0 -134
  119. aury/boot/infrastructure/events/models.py +0 -63
  120. aury_boot-0.0.4.dist-info/RECORD +0 -137
  121. {aury_boot-0.0.4.dist-info → aury_boot-0.0.7.dist-info}/WHEEL +0 -0
  122. {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 # noqa: F401
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: # noqa: D401
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 # noqa: F401
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 Base, GUID
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
- # 日志(已迁移到 common 层)
26
- # 从 common.logging 导入
45
+ # 依赖注入
46
+ from .di import Container, Lifetime, Scope, ServiceDescriptor
27
47
 
28
- # 调度器(可选依赖)
29
- try:
30
- from .scheduler import SchedulerManager
31
- except ImportError:
32
- SchedulerManager = None # type: ignore[assignment, misc]
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, conditional_actor
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
- conditional_actor = None # type: ignore[assignment, misc]
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
- "EventBus",
76
- "EventConsumer",
77
- "EventLoggingMiddleware",
78
- "EventMiddleware",
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
- "conditional_actor",
102
- # 日志(已迁移到 common 层,请从 common.logging 导入)
158
+ "UploadResult",
159
+ "conditional_task",
103
160
  ]
104
161