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.
- aury/boot/__init__.py +66 -0
- aury/boot/_version.py +2 -2
- aury/boot/application/__init__.py +120 -0
- aury/boot/application/app/__init__.py +39 -0
- aury/boot/application/app/base.py +511 -0
- aury/boot/application/app/components.py +434 -0
- aury/boot/application/app/middlewares.py +101 -0
- aury/boot/application/config/__init__.py +44 -0
- aury/boot/application/config/settings.py +663 -0
- aury/boot/application/constants/__init__.py +19 -0
- aury/boot/application/constants/components.py +50 -0
- aury/boot/application/constants/scheduler.py +28 -0
- aury/boot/application/constants/service.py +29 -0
- aury/boot/application/errors/__init__.py +55 -0
- aury/boot/application/errors/chain.py +80 -0
- aury/boot/application/errors/codes.py +67 -0
- aury/boot/application/errors/exceptions.py +238 -0
- aury/boot/application/errors/handlers.py +320 -0
- aury/boot/application/errors/response.py +120 -0
- aury/boot/application/interfaces/__init__.py +76 -0
- aury/boot/application/interfaces/egress.py +224 -0
- aury/boot/application/interfaces/ingress.py +98 -0
- aury/boot/application/middleware/__init__.py +22 -0
- aury/boot/application/middleware/logging.py +451 -0
- aury/boot/application/migrations/__init__.py +13 -0
- aury/boot/application/migrations/manager.py +685 -0
- aury/boot/application/migrations/setup.py +237 -0
- aury/boot/application/rpc/__init__.py +63 -0
- aury/boot/application/rpc/base.py +108 -0
- aury/boot/application/rpc/client.py +294 -0
- aury/boot/application/rpc/discovery.py +218 -0
- aury/boot/application/scheduler/__init__.py +13 -0
- aury/boot/application/scheduler/runner.py +123 -0
- aury/boot/application/server/__init__.py +296 -0
- aury/boot/commands/__init__.py +30 -0
- aury/boot/commands/add.py +76 -0
- aury/boot/commands/app.py +105 -0
- aury/boot/commands/config.py +177 -0
- aury/boot/commands/docker.py +367 -0
- aury/boot/commands/docs.py +284 -0
- aury/boot/commands/generate.py +1277 -0
- aury/boot/commands/init.py +890 -0
- aury/boot/commands/migrate/__init__.py +37 -0
- aury/boot/commands/migrate/app.py +54 -0
- aury/boot/commands/migrate/commands.py +303 -0
- aury/boot/commands/scheduler.py +124 -0
- aury/boot/commands/server/__init__.py +21 -0
- aury/boot/commands/server/app.py +541 -0
- aury/boot/commands/templates/generate/api.py.tpl +105 -0
- aury/boot/commands/templates/generate/model.py.tpl +17 -0
- aury/boot/commands/templates/generate/repository.py.tpl +19 -0
- aury/boot/commands/templates/generate/schema.py.tpl +29 -0
- aury/boot/commands/templates/generate/service.py.tpl +48 -0
- aury/boot/commands/templates/project/CLI.md.tpl +92 -0
- aury/boot/commands/templates/project/DEVELOPMENT.md.tpl +1397 -0
- aury/boot/commands/templates/project/README.md.tpl +111 -0
- aury/boot/commands/templates/project/admin_console_init.py.tpl +50 -0
- aury/boot/commands/templates/project/config.py.tpl +30 -0
- aury/boot/commands/templates/project/conftest.py.tpl +26 -0
- aury/boot/commands/templates/project/env.example.tpl +213 -0
- aury/boot/commands/templates/project/gitignore.tpl +128 -0
- aury/boot/commands/templates/project/main.py.tpl +41 -0
- aury/boot/commands/templates/project/modules/api.py.tpl +19 -0
- aury/boot/commands/templates/project/modules/exceptions.py.tpl +84 -0
- aury/boot/commands/templates/project/modules/schedules.py.tpl +18 -0
- aury/boot/commands/templates/project/modules/tasks.py.tpl +20 -0
- aury/boot/commands/worker.py +143 -0
- aury/boot/common/__init__.py +35 -0
- aury/boot/common/exceptions/__init__.py +114 -0
- aury/boot/common/i18n/__init__.py +16 -0
- aury/boot/common/i18n/translator.py +272 -0
- aury/boot/common/logging/__init__.py +716 -0
- aury/boot/contrib/__init__.py +10 -0
- aury/boot/contrib/admin_console/__init__.py +18 -0
- aury/boot/contrib/admin_console/auth.py +137 -0
- aury/boot/contrib/admin_console/discovery.py +69 -0
- aury/boot/contrib/admin_console/install.py +172 -0
- aury/boot/contrib/admin_console/utils.py +44 -0
- aury/boot/domain/__init__.py +79 -0
- aury/boot/domain/exceptions/__init__.py +132 -0
- aury/boot/domain/models/__init__.py +51 -0
- aury/boot/domain/models/base.py +69 -0
- aury/boot/domain/models/mixins.py +135 -0
- aury/boot/domain/models/models.py +96 -0
- aury/boot/domain/pagination/__init__.py +279 -0
- aury/boot/domain/repository/__init__.py +23 -0
- aury/boot/domain/repository/impl.py +423 -0
- aury/boot/domain/repository/interceptors.py +47 -0
- aury/boot/domain/repository/interface.py +106 -0
- aury/boot/domain/repository/query_builder.py +348 -0
- aury/boot/domain/service/__init__.py +11 -0
- aury/boot/domain/service/base.py +73 -0
- aury/boot/domain/transaction/__init__.py +404 -0
- aury/boot/infrastructure/__init__.py +104 -0
- aury/boot/infrastructure/cache/__init__.py +31 -0
- aury/boot/infrastructure/cache/backends.py +348 -0
- aury/boot/infrastructure/cache/base.py +68 -0
- aury/boot/infrastructure/cache/exceptions.py +37 -0
- aury/boot/infrastructure/cache/factory.py +94 -0
- aury/boot/infrastructure/cache/manager.py +274 -0
- aury/boot/infrastructure/database/__init__.py +39 -0
- aury/boot/infrastructure/database/config.py +71 -0
- aury/boot/infrastructure/database/exceptions.py +44 -0
- aury/boot/infrastructure/database/manager.py +317 -0
- aury/boot/infrastructure/database/query_tools/__init__.py +164 -0
- aury/boot/infrastructure/database/strategies/__init__.py +198 -0
- aury/boot/infrastructure/di/__init__.py +15 -0
- aury/boot/infrastructure/di/container.py +393 -0
- aury/boot/infrastructure/events/__init__.py +33 -0
- aury/boot/infrastructure/events/bus.py +362 -0
- aury/boot/infrastructure/events/config.py +52 -0
- aury/boot/infrastructure/events/consumer.py +134 -0
- aury/boot/infrastructure/events/middleware.py +51 -0
- aury/boot/infrastructure/events/models.py +63 -0
- aury/boot/infrastructure/monitoring/__init__.py +529 -0
- aury/boot/infrastructure/scheduler/__init__.py +19 -0
- aury/boot/infrastructure/scheduler/exceptions.py +37 -0
- aury/boot/infrastructure/scheduler/manager.py +478 -0
- aury/boot/infrastructure/storage/__init__.py +38 -0
- aury/boot/infrastructure/storage/base.py +164 -0
- aury/boot/infrastructure/storage/exceptions.py +37 -0
- aury/boot/infrastructure/storage/factory.py +88 -0
- aury/boot/infrastructure/tasks/__init__.py +24 -0
- aury/boot/infrastructure/tasks/config.py +45 -0
- aury/boot/infrastructure/tasks/constants.py +37 -0
- aury/boot/infrastructure/tasks/exceptions.py +37 -0
- aury/boot/infrastructure/tasks/manager.py +490 -0
- aury/boot/testing/__init__.py +24 -0
- aury/boot/testing/base.py +122 -0
- aury/boot/testing/client.py +163 -0
- aury/boot/testing/factory.py +154 -0
- aury/boot/toolkit/__init__.py +21 -0
- aury/boot/toolkit/http/__init__.py +367 -0
- {aury_boot-0.0.2.dist-info → aury_boot-0.0.3.dist-info}/METADATA +3 -2
- aury_boot-0.0.3.dist-info/RECORD +137 -0
- aury_boot-0.0.2.dist-info/RECORD +0 -5
- {aury_boot-0.0.2.dist-info → aury_boot-0.0.3.dist-info}/WHEEL +0 -0
- {aury_boot-0.0.2.dist-info → aury_boot-0.0.3.dist-info}/entry_points.txt +0 -0
|
@@ -0,0 +1,490 @@
|
|
|
1
|
+
"""异步任务管理器 - 统一的任务队列接口。
|
|
2
|
+
|
|
3
|
+
提供:
|
|
4
|
+
- 统一的任务队列管理
|
|
5
|
+
- 任务注册和执行
|
|
6
|
+
- 错误处理和重试
|
|
7
|
+
- 条件注册(避免 API 模式下重复注册)
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
from collections.abc import Callable
|
|
13
|
+
from typing import TYPE_CHECKING, Any
|
|
14
|
+
|
|
15
|
+
from aury.boot.common.logging import logger
|
|
16
|
+
from aury.boot.infrastructure.tasks.config import TaskConfig
|
|
17
|
+
from aury.boot.infrastructure.tasks.constants import TaskQueueName, TaskRunMode
|
|
18
|
+
|
|
19
|
+
# 延迟导入 dramatiq(可选依赖)
|
|
20
|
+
try:
|
|
21
|
+
import dramatiq
|
|
22
|
+
from dramatiq import Message
|
|
23
|
+
from dramatiq.middleware import AsyncIO, CurrentMessage, TimeLimit
|
|
24
|
+
_DRAMATIQ_AVAILABLE = True
|
|
25
|
+
except ImportError:
|
|
26
|
+
_DRAMATIQ_AVAILABLE = False
|
|
27
|
+
# 创建占位符类型,避免类型检查错误
|
|
28
|
+
if TYPE_CHECKING:
|
|
29
|
+
from dramatiq import Message
|
|
30
|
+
from dramatiq.middleware import AsyncIO, CurrentMessage, TimeLimit
|
|
31
|
+
else:
|
|
32
|
+
Message = None
|
|
33
|
+
AsyncIO = None
|
|
34
|
+
CurrentMessage = None
|
|
35
|
+
TimeLimit = None
|
|
36
|
+
|
|
37
|
+
# 延迟导入 kombu broker(可选依赖)
|
|
38
|
+
try:
|
|
39
|
+
from dramatiq_kombu_broker import KombuBroker
|
|
40
|
+
_KOMBU_BROKER_AVAILABLE = True
|
|
41
|
+
except ImportError:
|
|
42
|
+
_KOMBU_BROKER_AVAILABLE = False
|
|
43
|
+
if TYPE_CHECKING:
|
|
44
|
+
from dramatiq_kombu_broker import KombuBroker
|
|
45
|
+
else:
|
|
46
|
+
KombuBroker = None
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
class TaskProxy:
|
|
50
|
+
"""任务代理类,用于在 API 模式下发送消息而不注册 actor。
|
|
51
|
+
|
|
52
|
+
在 API 服务中,我们不希望注册 actor(因为 worker 已经注册了),
|
|
53
|
+
但仍然需要能够发送任务消息。TaskProxy 提供了这个功能。
|
|
54
|
+
|
|
55
|
+
使用示例:
|
|
56
|
+
@conditional_actor(queue_name="default")
|
|
57
|
+
async def send_email(to: str, subject: str):
|
|
58
|
+
# 在 worker 中会注册为 actor
|
|
59
|
+
# 在 API 中会返回 TaskProxy
|
|
60
|
+
pass
|
|
61
|
+
|
|
62
|
+
# API 模式下发送任务
|
|
63
|
+
send_email.send("user@example.com", "Hello")
|
|
64
|
+
"""
|
|
65
|
+
|
|
66
|
+
def __init__(
|
|
67
|
+
self,
|
|
68
|
+
func: Callable,
|
|
69
|
+
queue_name: str,
|
|
70
|
+
actor_name: str,
|
|
71
|
+
broker: Any, # RedisBroker | None,但使用 Any 避免类型检查错误
|
|
72
|
+
**actor_kwargs: Any,
|
|
73
|
+
) -> None:
|
|
74
|
+
"""初始化任务代理。
|
|
75
|
+
|
|
76
|
+
Args:
|
|
77
|
+
func: 任务函数
|
|
78
|
+
queue_name: 队列名称
|
|
79
|
+
actor_name: Actor 名称(完整模块路径)
|
|
80
|
+
broker: Broker 实例
|
|
81
|
+
**actor_kwargs: Actor 参数
|
|
82
|
+
"""
|
|
83
|
+
self.func = func
|
|
84
|
+
self.queue_name = queue_name
|
|
85
|
+
self.actor_name = actor_name
|
|
86
|
+
self.actor_kwargs = actor_kwargs
|
|
87
|
+
self._broker = broker
|
|
88
|
+
|
|
89
|
+
def send(self, *args: Any, **kwargs: Any) -> Message | None:
|
|
90
|
+
"""发送任务消息,直接通过 broker 发送,不注册 actor。
|
|
91
|
+
|
|
92
|
+
Args:
|
|
93
|
+
*args: 位置参数
|
|
94
|
+
**kwargs: 关键字参数
|
|
95
|
+
|
|
96
|
+
Returns:
|
|
97
|
+
Message | None: 发送的消息对象
|
|
98
|
+
"""
|
|
99
|
+
if not self._broker:
|
|
100
|
+
raise RuntimeError("Broker 未初始化,无法发送任务")
|
|
101
|
+
|
|
102
|
+
# 使用 dramatiq 的 Message 类创建消息
|
|
103
|
+
# actor_name 必须是 worker 中注册的完整函数路径
|
|
104
|
+
message = Message(
|
|
105
|
+
queue_name=self.queue_name,
|
|
106
|
+
actor_name=self.actor_name,
|
|
107
|
+
args=args,
|
|
108
|
+
kwargs=kwargs,
|
|
109
|
+
options={
|
|
110
|
+
"max_retries": self.actor_kwargs.get("max_retries", 0),
|
|
111
|
+
"time_limit": self.actor_kwargs.get("time_limit"),
|
|
112
|
+
},
|
|
113
|
+
)
|
|
114
|
+
|
|
115
|
+
result = self._broker.enqueue(message)
|
|
116
|
+
logger.debug(f"任务消息已发送: {self.actor_name}")
|
|
117
|
+
return result
|
|
118
|
+
|
|
119
|
+
def __call__(self, *args: Any, **kwargs: Any) -> Any:
|
|
120
|
+
"""直接调用函数(用于测试等场景)。
|
|
121
|
+
|
|
122
|
+
Args:
|
|
123
|
+
*args: 位置参数
|
|
124
|
+
**kwargs: 关键字参数
|
|
125
|
+
|
|
126
|
+
Returns:
|
|
127
|
+
Any: 函数返回值
|
|
128
|
+
"""
|
|
129
|
+
return self.func(*args, **kwargs)
|
|
130
|
+
|
|
131
|
+
def __repr__(self) -> str:
|
|
132
|
+
"""字符串表示。"""
|
|
133
|
+
return f"<TaskProxy actor_name={self.actor_name} queue={self.queue_name}>"
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
class TaskManager:
|
|
137
|
+
"""任务管理器(命名多实例)。
|
|
138
|
+
|
|
139
|
+
职责:
|
|
140
|
+
1. 管理任务队列broker
|
|
141
|
+
2. 注册任务
|
|
142
|
+
3. 任务执行和监控
|
|
143
|
+
4. 支持多个命名实例,如不同的消息队列
|
|
144
|
+
|
|
145
|
+
使用示例:
|
|
146
|
+
# 默认实例
|
|
147
|
+
task_manager = TaskManager.get_instance()
|
|
148
|
+
await task_manager.initialize()
|
|
149
|
+
|
|
150
|
+
# 命名实例
|
|
151
|
+
email_tasks = TaskManager.get_instance("email")
|
|
152
|
+
report_tasks = TaskManager.get_instance("report")
|
|
153
|
+
|
|
154
|
+
# 注册任务
|
|
155
|
+
@task_manager.task
|
|
156
|
+
async def send_email(to: str, subject: str):
|
|
157
|
+
# 发送邮件
|
|
158
|
+
pass
|
|
159
|
+
|
|
160
|
+
# 执行任务
|
|
161
|
+
send_email.send("user@example.com", "Hello")
|
|
162
|
+
"""
|
|
163
|
+
|
|
164
|
+
_instances: dict[str, TaskManager] = {}
|
|
165
|
+
|
|
166
|
+
def __init__(self, name: str = "default") -> None:
|
|
167
|
+
"""初始化任务管理器。
|
|
168
|
+
|
|
169
|
+
Args:
|
|
170
|
+
name: 实例名称
|
|
171
|
+
"""
|
|
172
|
+
self.name = name
|
|
173
|
+
self._broker: Any = None # KombuBroker | None
|
|
174
|
+
self._initialized: bool = False
|
|
175
|
+
self._task_config: TaskConfig | None = None
|
|
176
|
+
self._run_mode: TaskRunMode = TaskRunMode.WORKER # 默认 Worker 模式(调度者)
|
|
177
|
+
|
|
178
|
+
@classmethod
|
|
179
|
+
def get_instance(cls, name: str = "default") -> TaskManager:
|
|
180
|
+
"""获取指定名称的实例。
|
|
181
|
+
|
|
182
|
+
Args:
|
|
183
|
+
name: 实例名称,默认为 "default"
|
|
184
|
+
|
|
185
|
+
Returns:
|
|
186
|
+
TaskManager: 任务管理器实例
|
|
187
|
+
"""
|
|
188
|
+
if name not in cls._instances:
|
|
189
|
+
cls._instances[name] = cls(name)
|
|
190
|
+
return cls._instances[name]
|
|
191
|
+
|
|
192
|
+
@classmethod
|
|
193
|
+
def reset_instance(cls, name: str | None = None) -> None:
|
|
194
|
+
"""重置实例(仅用于测试)。
|
|
195
|
+
|
|
196
|
+
Args:
|
|
197
|
+
name: 要重置的实例名称。如果为 None,则重置所有实例。
|
|
198
|
+
|
|
199
|
+
注意:调用此方法前应先调用 cleanup() 释放资源。
|
|
200
|
+
"""
|
|
201
|
+
if name is None:
|
|
202
|
+
cls._instances.clear()
|
|
203
|
+
elif name in cls._instances:
|
|
204
|
+
del cls._instances[name]
|
|
205
|
+
|
|
206
|
+
async def initialize(
|
|
207
|
+
self,
|
|
208
|
+
task_config: TaskConfig | None = None,
|
|
209
|
+
run_mode: TaskRunMode | str | None = None,
|
|
210
|
+
broker_url: str | None = None,
|
|
211
|
+
*,
|
|
212
|
+
middleware: list | None = None,
|
|
213
|
+
) -> None:
|
|
214
|
+
"""初始化任务队列。
|
|
215
|
+
|
|
216
|
+
Args:
|
|
217
|
+
task_config: 任务配置(TaskConfig)
|
|
218
|
+
run_mode: 运行模式(TaskRunMode 或字符串,如 "api", "worker")
|
|
219
|
+
broker_url: Broker连接URL(可选,优先使用 config)
|
|
220
|
+
middleware: 中间件列表
|
|
221
|
+
"""
|
|
222
|
+
if self._initialized:
|
|
223
|
+
logger.warning("任务管理器已初始化,跳过")
|
|
224
|
+
return
|
|
225
|
+
|
|
226
|
+
self._task_config = task_config or TaskConfig()
|
|
227
|
+
|
|
228
|
+
# 处理 run_mode 参数
|
|
229
|
+
if run_mode is None:
|
|
230
|
+
self._run_mode = TaskRunMode.WORKER # 默认 Worker 模式(调度者)
|
|
231
|
+
elif isinstance(run_mode, str):
|
|
232
|
+
try:
|
|
233
|
+
self._run_mode = TaskRunMode(run_mode.lower())
|
|
234
|
+
except ValueError:
|
|
235
|
+
logger.warning(f"无效的运行模式: {run_mode},使用默认值: {TaskRunMode.WORKER.value}")
|
|
236
|
+
self._run_mode = TaskRunMode.WORKER
|
|
237
|
+
else:
|
|
238
|
+
self._run_mode = run_mode
|
|
239
|
+
|
|
240
|
+
# 获取 broker URL(优先级:参数 > 配置 > 环境变量)
|
|
241
|
+
url = broker_url or self._task_config.broker_url
|
|
242
|
+
if not url:
|
|
243
|
+
logger.warning("未配置任务队列URL,任务功能将被禁用")
|
|
244
|
+
return
|
|
245
|
+
|
|
246
|
+
if not _DRAMATIQ_AVAILABLE:
|
|
247
|
+
raise ImportError(
|
|
248
|
+
"dramatiq 未安装。请安装可选依赖: pip install 'aury-boot[queue-dramatiq]'"
|
|
249
|
+
)
|
|
250
|
+
|
|
251
|
+
if not _KOMBU_BROKER_AVAILABLE:
|
|
252
|
+
raise ImportError(
|
|
253
|
+
"dramatiq-kombu-broker 未安装。请安装可选依赖: pip install 'aury-boot[queue-dramatiq-kombu]'"
|
|
254
|
+
)
|
|
255
|
+
|
|
256
|
+
try:
|
|
257
|
+
# 使用函数式编程创建默认中间件(如果未提供)
|
|
258
|
+
def create_default_middleware() -> list:
|
|
259
|
+
"""创建默认中间件列表。"""
|
|
260
|
+
return [AsyncIO(), CurrentMessage(), TimeLimit()]
|
|
261
|
+
|
|
262
|
+
middleware_list = middleware if middleware is not None else create_default_middleware()
|
|
263
|
+
|
|
264
|
+
# 使用 KombuBroker,支持多种后端(Redis、RabbitMQ、SQS 等)
|
|
265
|
+
self._broker = KombuBroker(url=url, middleware=middleware_list)
|
|
266
|
+
dramatiq.set_broker(self._broker)
|
|
267
|
+
self._initialized = True
|
|
268
|
+
logger.info(f"任务管理器初始化完成(使用 Kombu Broker: {url})")
|
|
269
|
+
except Exception as exc:
|
|
270
|
+
logger.error(f"任务队列初始化失败: {exc}")
|
|
271
|
+
raise
|
|
272
|
+
|
|
273
|
+
def task(
|
|
274
|
+
self,
|
|
275
|
+
func: Callable | None = None,
|
|
276
|
+
*,
|
|
277
|
+
actor_name: str | None = None,
|
|
278
|
+
max_retries: int = 3,
|
|
279
|
+
time_limit: int | None = None,
|
|
280
|
+
queue_name: str = TaskQueueName.DEFAULT.value,
|
|
281
|
+
**kwargs: Any,
|
|
282
|
+
) -> Any:
|
|
283
|
+
"""任务装饰器(始终注册)。
|
|
284
|
+
|
|
285
|
+
Args:
|
|
286
|
+
func: 任务函数
|
|
287
|
+
actor_name: Actor名称
|
|
288
|
+
max_retries: 最大重试次数
|
|
289
|
+
time_limit: 时间限制(毫秒)
|
|
290
|
+
queue_name: 队列名称
|
|
291
|
+
**kwargs: 其他参数
|
|
292
|
+
|
|
293
|
+
使用示例:
|
|
294
|
+
@task_manager.task(max_retries=3)
|
|
295
|
+
async def send_email(to: str, subject: str):
|
|
296
|
+
# 发送邮件
|
|
297
|
+
pass
|
|
298
|
+
"""
|
|
299
|
+
if not _DRAMATIQ_AVAILABLE:
|
|
300
|
+
raise ImportError(
|
|
301
|
+
"dramatiq 未安装。请安装可选依赖: pip install 'aury-boot[queue-dramatiq]'"
|
|
302
|
+
)
|
|
303
|
+
|
|
304
|
+
def decorator(f: Callable) -> Callable:
|
|
305
|
+
actor = dramatiq.actor(
|
|
306
|
+
f,
|
|
307
|
+
actor_name=actor_name or f.__name__,
|
|
308
|
+
max_retries=max_retries,
|
|
309
|
+
time_limit=time_limit,
|
|
310
|
+
queue_name=queue_name,
|
|
311
|
+
**kwargs,
|
|
312
|
+
)
|
|
313
|
+
logger.debug(f"任务已注册: {actor_name or f.__name__}")
|
|
314
|
+
return actor
|
|
315
|
+
|
|
316
|
+
if func is None:
|
|
317
|
+
return decorator
|
|
318
|
+
return decorator(func)
|
|
319
|
+
|
|
320
|
+
def conditional_task(
|
|
321
|
+
self,
|
|
322
|
+
func: Callable | None = None,
|
|
323
|
+
*,
|
|
324
|
+
actor_name: str | None = None,
|
|
325
|
+
max_retries: int = 3,
|
|
326
|
+
time_limit: int | None = None,
|
|
327
|
+
queue_name: str = TaskQueueName.DEFAULT.value,
|
|
328
|
+
**kwargs: Any,
|
|
329
|
+
) -> Any:
|
|
330
|
+
"""条件注册的任务装饰器。
|
|
331
|
+
|
|
332
|
+
根据 SERVICE_TYPE 环境变量决定:
|
|
333
|
+
- worker 模式:正常注册为 actor
|
|
334
|
+
- api 模式:返回 TaskProxy,可以发送消息但不注册
|
|
335
|
+
|
|
336
|
+
Args:
|
|
337
|
+
func: 任务函数
|
|
338
|
+
actor_name: Actor名称
|
|
339
|
+
max_retries: 最大重试次数
|
|
340
|
+
time_limit: 时间限制(毫秒)
|
|
341
|
+
queue_name: 队列名称
|
|
342
|
+
**kwargs: 其他参数
|
|
343
|
+
|
|
344
|
+
使用示例:
|
|
345
|
+
@task_manager.conditional_task(max_retries=3)
|
|
346
|
+
async def send_email(to: str, subject: str):
|
|
347
|
+
# 在 worker 中会注册为 actor
|
|
348
|
+
# 在 API 中会返回 TaskProxy
|
|
349
|
+
pass
|
|
350
|
+
"""
|
|
351
|
+
if not _DRAMATIQ_AVAILABLE:
|
|
352
|
+
raise ImportError(
|
|
353
|
+
"dramatiq 未安装。请安装可选依赖: pip install 'aury-boot[queue-dramatiq]'"
|
|
354
|
+
)
|
|
355
|
+
|
|
356
|
+
def decorator(f: Callable) -> Any:
|
|
357
|
+
# 从配置获取运行模式,默认为 API
|
|
358
|
+
run_mode = self._run_mode
|
|
359
|
+
|
|
360
|
+
if run_mode == TaskRunMode.WORKER:
|
|
361
|
+
# Worker 模式下正常注册(执行者)
|
|
362
|
+
actor = dramatiq.actor(
|
|
363
|
+
f,
|
|
364
|
+
actor_name=actor_name or f.__name__,
|
|
365
|
+
max_retries=max_retries,
|
|
366
|
+
time_limit=time_limit,
|
|
367
|
+
queue_name=queue_name,
|
|
368
|
+
**kwargs,
|
|
369
|
+
)
|
|
370
|
+
logger.debug(f"任务已注册(worker 模式): {actor_name or f.__name__}")
|
|
371
|
+
return actor
|
|
372
|
+
else:
|
|
373
|
+
# Producer 模式下返回代理对象,不注册但可以发送消息
|
|
374
|
+
# 获取函数的完整模块路径作为 actor_name
|
|
375
|
+
module_name = f.__module__
|
|
376
|
+
func_name = f.__name__
|
|
377
|
+
full_actor_name = actor_name or f"{module_name}.{func_name}"
|
|
378
|
+
|
|
379
|
+
proxy = TaskProxy(
|
|
380
|
+
func=f,
|
|
381
|
+
queue_name=queue_name,
|
|
382
|
+
actor_name=full_actor_name,
|
|
383
|
+
broker=self._broker,
|
|
384
|
+
max_retries=max_retries,
|
|
385
|
+
time_limit=time_limit,
|
|
386
|
+
**kwargs,
|
|
387
|
+
)
|
|
388
|
+
logger.debug(f"任务代理已创建(producer 模式): {full_actor_name}")
|
|
389
|
+
return proxy
|
|
390
|
+
|
|
391
|
+
if func is None:
|
|
392
|
+
return decorator
|
|
393
|
+
return decorator(func)
|
|
394
|
+
|
|
395
|
+
@property
|
|
396
|
+
def broker(self) -> Any: # KombuBroker | None
|
|
397
|
+
"""获取broker实例。"""
|
|
398
|
+
return self._broker
|
|
399
|
+
|
|
400
|
+
def is_initialized(self) -> bool:
|
|
401
|
+
"""检查是否已初始化。"""
|
|
402
|
+
return self._initialized
|
|
403
|
+
|
|
404
|
+
async def cleanup(self) -> None:
|
|
405
|
+
"""清理资源。"""
|
|
406
|
+
if self._broker:
|
|
407
|
+
# Dramatiq会自动管理broker
|
|
408
|
+
pass
|
|
409
|
+
self._initialized = False
|
|
410
|
+
logger.info("任务管理器已清理")
|
|
411
|
+
|
|
412
|
+
def __repr__(self) -> str:
|
|
413
|
+
"""字符串表示。"""
|
|
414
|
+
status = "initialized" if self._initialized else "not initialized"
|
|
415
|
+
return f"<TaskManager status={status}>"
|
|
416
|
+
|
|
417
|
+
|
|
418
|
+
def conditional_actor(
|
|
419
|
+
queue_name: str = TaskQueueName.DEFAULT.value,
|
|
420
|
+
run_mode: TaskRunMode | str | None = None,
|
|
421
|
+
**kwargs: Any,
|
|
422
|
+
) -> Callable[[Callable], Any]:
|
|
423
|
+
"""条件注册的 actor 装饰器(独立函数版本)。
|
|
424
|
+
|
|
425
|
+
在 worker 模式下正常注册为 actor(执行者)
|
|
426
|
+
在 producer 模式下返回 TaskProxy,可以发送消息但不注册(生产者)
|
|
427
|
+
|
|
428
|
+
Args:
|
|
429
|
+
queue_name: 队列名称
|
|
430
|
+
run_mode: 运行模式(TaskRunMode 或字符串),默认为 WORKER
|
|
431
|
+
**kwargs: 其他参数
|
|
432
|
+
|
|
433
|
+
使用示例:
|
|
434
|
+
@conditional_actor(queue_name="default", max_retries=3)
|
|
435
|
+
async def send_email(to: str, subject: str):
|
|
436
|
+
# 在 worker 中会注册为 actor
|
|
437
|
+
# 在 producer 中会返回 TaskProxy
|
|
438
|
+
pass
|
|
439
|
+
|
|
440
|
+
# Producer 模式下发送任务
|
|
441
|
+
send_email.send("user@example.com", "Hello")
|
|
442
|
+
"""
|
|
443
|
+
if not _DRAMATIQ_AVAILABLE:
|
|
444
|
+
raise ImportError(
|
|
445
|
+
"dramatiq 未安装。请安装可选依赖: pip install 'aury-boot[queue-dramatiq]'"
|
|
446
|
+
)
|
|
447
|
+
|
|
448
|
+
def decorator(func: Callable) -> Any:
|
|
449
|
+
# 处理 run_mode 参数,默认为 WORKER
|
|
450
|
+
if run_mode is None:
|
|
451
|
+
mode = TaskRunMode.WORKER
|
|
452
|
+
elif isinstance(run_mode, str):
|
|
453
|
+
try:
|
|
454
|
+
mode = TaskRunMode(run_mode.lower())
|
|
455
|
+
except ValueError:
|
|
456
|
+
logger.warning(f"无效的运行模式: {run_mode},使用默认值: {TaskRunMode.WORKER.value}")
|
|
457
|
+
mode = TaskRunMode.WORKER
|
|
458
|
+
else:
|
|
459
|
+
mode = run_mode
|
|
460
|
+
|
|
461
|
+
if mode == TaskRunMode.WORKER:
|
|
462
|
+
# Worker 模式下正常注册(执行者)
|
|
463
|
+
return dramatiq.actor(queue_name=queue_name, **kwargs)(func)
|
|
464
|
+
else:
|
|
465
|
+
# Producer 模式下返回代理对象,不注册但可以发送消息
|
|
466
|
+
# 获取函数的完整模块路径作为 actor_name
|
|
467
|
+
module_name = func.__module__
|
|
468
|
+
func_name = func.__name__
|
|
469
|
+
actor_name = f"{module_name}.{func_name}"
|
|
470
|
+
|
|
471
|
+
# 获取全局 broker(如果已设置)
|
|
472
|
+
broker = dramatiq.get_broker() if hasattr(dramatiq, "get_broker") else None
|
|
473
|
+
|
|
474
|
+
return TaskProxy(
|
|
475
|
+
func=func,
|
|
476
|
+
queue_name=queue_name,
|
|
477
|
+
actor_name=actor_name,
|
|
478
|
+
broker=broker,
|
|
479
|
+
**kwargs,
|
|
480
|
+
)
|
|
481
|
+
|
|
482
|
+
return decorator
|
|
483
|
+
|
|
484
|
+
|
|
485
|
+
__all__ = [
|
|
486
|
+
"TaskManager",
|
|
487
|
+
"TaskProxy",
|
|
488
|
+
"conditional_actor",
|
|
489
|
+
]
|
|
490
|
+
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
"""测试框架模块。
|
|
2
|
+
|
|
3
|
+
提供便捷的测试工具,包括测试基类、测试客户端、数据工厂等。
|
|
4
|
+
|
|
5
|
+
注意:此模块需要 pytest 作为依赖,仅在开发环境可用。
|
|
6
|
+
生产环境不会安装此模块的依赖,导入时会静默失败。
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
# 检查 pytest 是否可用,如果不可用则静默失败(让外层捕获)
|
|
10
|
+
try:
|
|
11
|
+
import pytest
|
|
12
|
+
except ImportError:
|
|
13
|
+
# 在生产环境,pytest 不存在,让导入失败以便外层捕获
|
|
14
|
+
raise ImportError("testing 模块需要 pytest,仅在开发环境可用")
|
|
15
|
+
|
|
16
|
+
from .base import TestCase
|
|
17
|
+
from .client import TestClient
|
|
18
|
+
from .factory import Factory
|
|
19
|
+
|
|
20
|
+
__all__ = [
|
|
21
|
+
"Factory",
|
|
22
|
+
"TestCase",
|
|
23
|
+
"TestClient",
|
|
24
|
+
]
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
"""测试基类。
|
|
2
|
+
|
|
3
|
+
提供类似 Django TestCase 的功能,自动处理数据库事务回滚。
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
from abc import ABC
|
|
9
|
+
|
|
10
|
+
import pytest
|
|
11
|
+
from sqlalchemy.ext.asyncio import AsyncSession
|
|
12
|
+
|
|
13
|
+
from aury.boot.common.logging import logger
|
|
14
|
+
from aury.boot.infrastructure.database.manager import DatabaseManager
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class TestCase(ABC): # noqa: B024
|
|
18
|
+
"""测试基类。
|
|
19
|
+
|
|
20
|
+
提供类似 Django TestCase 的功能:
|
|
21
|
+
- 自动数据库事务回滚
|
|
22
|
+
- setUp/tearDown 钩子
|
|
23
|
+
- 测试客户端支持
|
|
24
|
+
- Fixtures 支持
|
|
25
|
+
|
|
26
|
+
使用示例:
|
|
27
|
+
class UserServiceTest(TestCase):
|
|
28
|
+
async def setUp(self):
|
|
29
|
+
self.db = await DatabaseManager.get_session()
|
|
30
|
+
self.user_repo = UserRepository(self.db)
|
|
31
|
+
|
|
32
|
+
async def test_create_user(self):
|
|
33
|
+
user = await self.user_repo.create({"name": "张三"})
|
|
34
|
+
assert user.name == "张三"
|
|
35
|
+
"""
|
|
36
|
+
|
|
37
|
+
def __init__(self) -> None:
|
|
38
|
+
"""初始化测试用例。"""
|
|
39
|
+
self._db_session: AsyncSession | None = None
|
|
40
|
+
self._db_manager: DatabaseManager | None = None
|
|
41
|
+
|
|
42
|
+
@property
|
|
43
|
+
def db(self) -> AsyncSession:
|
|
44
|
+
"""获取数据库会话。
|
|
45
|
+
|
|
46
|
+
Returns:
|
|
47
|
+
AsyncSession: 数据库会话
|
|
48
|
+
|
|
49
|
+
Raises:
|
|
50
|
+
RuntimeError: 如果会话未初始化
|
|
51
|
+
"""
|
|
52
|
+
if self._db_session is None:
|
|
53
|
+
raise RuntimeError("数据库会话未初始化,请在 setUp 中调用 await self.setup_db()")
|
|
54
|
+
return self._db_session
|
|
55
|
+
|
|
56
|
+
async def setup_db(self) -> AsyncSession:
|
|
57
|
+
"""设置数据库会话(在事务中)。
|
|
58
|
+
|
|
59
|
+
Returns:
|
|
60
|
+
AsyncSession: 数据库会话
|
|
61
|
+
"""
|
|
62
|
+
if self._db_manager is None:
|
|
63
|
+
self._db_manager = DatabaseManager.get_instance()
|
|
64
|
+
if not hasattr(self._db_manager, '_initialized') or not self._db_manager._initialized:
|
|
65
|
+
await self._db_manager.initialize()
|
|
66
|
+
|
|
67
|
+
# 使用 session_factory 创建会话
|
|
68
|
+
self._db_session = self._db_manager.session_factory()
|
|
69
|
+
# 开始事务(测试结束后会自动回滚)
|
|
70
|
+
await self._db_session.begin()
|
|
71
|
+
logger.debug("测试数据库会话已创建(事务模式)")
|
|
72
|
+
return self._db_session
|
|
73
|
+
|
|
74
|
+
async def setUp(self) -> None: # noqa: B027
|
|
75
|
+
"""测试前准备(子类可重写)。
|
|
76
|
+
|
|
77
|
+
在此方法中初始化测试所需的资源。
|
|
78
|
+
"""
|
|
79
|
+
pass
|
|
80
|
+
|
|
81
|
+
async def tearDown(self) -> None: # noqa: B027
|
|
82
|
+
"""测试后清理(子类可重写)。
|
|
83
|
+
|
|
84
|
+
在此方法中清理测试资源。
|
|
85
|
+
"""
|
|
86
|
+
pass
|
|
87
|
+
|
|
88
|
+
async def _cleanup(self) -> None:
|
|
89
|
+
"""内部清理方法,自动回滚事务。"""
|
|
90
|
+
if self._db_session:
|
|
91
|
+
try:
|
|
92
|
+
await self._db_session.rollback()
|
|
93
|
+
logger.debug("测试事务已回滚")
|
|
94
|
+
except Exception as e:
|
|
95
|
+
logger.error(f"回滚测试事务失败: {e}")
|
|
96
|
+
finally:
|
|
97
|
+
await self._db_session.close()
|
|
98
|
+
self._db_session = None
|
|
99
|
+
|
|
100
|
+
if self._db_manager:
|
|
101
|
+
await self._db_manager.cleanup()
|
|
102
|
+
self._db_manager = None
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
# Pytest fixtures 支持
|
|
106
|
+
@pytest.fixture()
|
|
107
|
+
async def test_case():
|
|
108
|
+
"""Pytest fixture,提供测试用例实例。"""
|
|
109
|
+
case = TestCase()
|
|
110
|
+
await case.setUp()
|
|
111
|
+
try:
|
|
112
|
+
yield case
|
|
113
|
+
finally:
|
|
114
|
+
await case.tearDown()
|
|
115
|
+
await case._cleanup()
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
@pytest.fixture()
|
|
119
|
+
async def db_session(test_case: TestCase):
|
|
120
|
+
"""Pytest fixture,提供数据库会话。"""
|
|
121
|
+
await test_case.setup_db()
|
|
122
|
+
return test_case.db
|