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,478 @@
|
|
|
1
|
+
"""任务调度器管理器 - 命名多实例支持。
|
|
2
|
+
|
|
3
|
+
提供:
|
|
4
|
+
- 统一的调度器管理
|
|
5
|
+
- 任务注册和启动
|
|
6
|
+
- 生命周期管理
|
|
7
|
+
- 自动设置日志上下文(调度器任务日志自动写入 scheduler_xxx.log)
|
|
8
|
+
- 支持多个命名实例
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
import asyncio
|
|
14
|
+
from collections.abc import Callable
|
|
15
|
+
from functools import wraps
|
|
16
|
+
from typing import TYPE_CHECKING, Any
|
|
17
|
+
|
|
18
|
+
from aury.boot.common.logging import logger, set_service_context
|
|
19
|
+
|
|
20
|
+
# 延迟导入 apscheduler(可选依赖)
|
|
21
|
+
try:
|
|
22
|
+
from apscheduler.schedulers.asyncio import AsyncIOScheduler
|
|
23
|
+
from apscheduler.triggers.cron import CronTrigger
|
|
24
|
+
from apscheduler.triggers.interval import IntervalTrigger
|
|
25
|
+
_APSCHEDULER_AVAILABLE = True
|
|
26
|
+
except ImportError:
|
|
27
|
+
_APSCHEDULER_AVAILABLE = False
|
|
28
|
+
# 创建占位符类型,避免类型检查错误
|
|
29
|
+
if TYPE_CHECKING:
|
|
30
|
+
from apscheduler.schedulers.asyncio import AsyncIOScheduler
|
|
31
|
+
from apscheduler.triggers.cron import CronTrigger
|
|
32
|
+
from apscheduler.triggers.interval import IntervalTrigger
|
|
33
|
+
else:
|
|
34
|
+
AsyncIOScheduler = None
|
|
35
|
+
CronTrigger = None
|
|
36
|
+
IntervalTrigger = None
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
class SchedulerManager:
|
|
40
|
+
"""调度器管理器(命名多实例)。
|
|
41
|
+
|
|
42
|
+
职责:
|
|
43
|
+
1. 管理调度器实例
|
|
44
|
+
2. 注册任务
|
|
45
|
+
3. 生命周期管理
|
|
46
|
+
4. 支持多个命名实例,如不同业务线的调度器
|
|
47
|
+
|
|
48
|
+
使用示例:
|
|
49
|
+
# 默认实例
|
|
50
|
+
scheduler = SchedulerManager.get_instance()
|
|
51
|
+
await scheduler.initialize()
|
|
52
|
+
|
|
53
|
+
# 命名实例
|
|
54
|
+
report_scheduler = SchedulerManager.get_instance("report")
|
|
55
|
+
cleanup_scheduler = SchedulerManager.get_instance("cleanup")
|
|
56
|
+
|
|
57
|
+
# 注册任务
|
|
58
|
+
scheduler.add_job(
|
|
59
|
+
func=my_task,
|
|
60
|
+
trigger="interval",
|
|
61
|
+
seconds=60
|
|
62
|
+
)
|
|
63
|
+
|
|
64
|
+
# 启动调度器
|
|
65
|
+
scheduler.start()
|
|
66
|
+
"""
|
|
67
|
+
|
|
68
|
+
_instances: dict[str, SchedulerManager] = {}
|
|
69
|
+
|
|
70
|
+
def __init__(self, name: str = "default") -> None:
|
|
71
|
+
"""初始化调度器管理器。
|
|
72
|
+
|
|
73
|
+
Args:
|
|
74
|
+
name: 实例名称
|
|
75
|
+
"""
|
|
76
|
+
self.name = name
|
|
77
|
+
self._scheduler: AsyncIOScheduler | None = None
|
|
78
|
+
self._initialized: bool = False
|
|
79
|
+
self._pending_jobs: list[dict[str, Any]] = [] # 待注册的任务(装饰器收集)
|
|
80
|
+
self._started: bool = False # 调度器是否已启动
|
|
81
|
+
|
|
82
|
+
@classmethod
|
|
83
|
+
def get_instance(cls, name: str = "default") -> SchedulerManager:
|
|
84
|
+
"""获取指定名称的实例。
|
|
85
|
+
|
|
86
|
+
首次获取时会同步初始化调度器实例,使装饰器可以在模块导入时使用。
|
|
87
|
+
|
|
88
|
+
Args:
|
|
89
|
+
name: 实例名称,默认为 "default"
|
|
90
|
+
|
|
91
|
+
Returns:
|
|
92
|
+
SchedulerManager: 调度器管理器实例
|
|
93
|
+
"""
|
|
94
|
+
if name not in cls._instances:
|
|
95
|
+
if not _APSCHEDULER_AVAILABLE:
|
|
96
|
+
raise ImportError(
|
|
97
|
+
"apscheduler 未安装。请安装可选依赖: pip install 'aury-boot[scheduler-apscheduler]'"
|
|
98
|
+
)
|
|
99
|
+
instance = cls(name)
|
|
100
|
+
instance._scheduler = AsyncIOScheduler()
|
|
101
|
+
instance._initialized = True
|
|
102
|
+
cls._instances[name] = instance
|
|
103
|
+
logger.debug(f"调度器实例已创建: {name}")
|
|
104
|
+
return cls._instances[name]
|
|
105
|
+
|
|
106
|
+
@classmethod
|
|
107
|
+
def reset_instance(cls, name: str | None = None) -> None:
|
|
108
|
+
"""重置实例(仅用于测试)。
|
|
109
|
+
|
|
110
|
+
Args:
|
|
111
|
+
name: 要重置的实例名称。如果为 None,则重置所有实例。
|
|
112
|
+
|
|
113
|
+
注意:调用此方法前应先调用 shutdown() 释放资源。
|
|
114
|
+
"""
|
|
115
|
+
if name is None:
|
|
116
|
+
cls._instances.clear()
|
|
117
|
+
elif name in cls._instances:
|
|
118
|
+
del cls._instances[name]
|
|
119
|
+
|
|
120
|
+
async def initialize(self) -> None:
|
|
121
|
+
"""初始化调度器(已废弃,保留以保持后向兼容)。
|
|
122
|
+
|
|
123
|
+
调度器现在在 get_instance() 时同步初始化。
|
|
124
|
+
"""
|
|
125
|
+
if not self._initialized:
|
|
126
|
+
# 如果还未初始化(理论上不会发生),进行初始化
|
|
127
|
+
if not _APSCHEDULER_AVAILABLE:
|
|
128
|
+
raise ImportError(
|
|
129
|
+
"apscheduler 未安装。请安装可选依赖: pip install 'aury-boot[scheduler-apscheduler]'"
|
|
130
|
+
)
|
|
131
|
+
self._scheduler = AsyncIOScheduler()
|
|
132
|
+
self._initialized = True
|
|
133
|
+
logger.debug("调度器已就绪")
|
|
134
|
+
|
|
135
|
+
@property
|
|
136
|
+
def scheduler(self) -> AsyncIOScheduler:
|
|
137
|
+
"""获取调度器实例。"""
|
|
138
|
+
if self._scheduler is None:
|
|
139
|
+
raise RuntimeError("调度器未初始化,请先调用 initialize()")
|
|
140
|
+
return self._scheduler
|
|
141
|
+
|
|
142
|
+
def add_job(
|
|
143
|
+
self,
|
|
144
|
+
func: Callable,
|
|
145
|
+
trigger: str = "interval",
|
|
146
|
+
*,
|
|
147
|
+
seconds: int | None = None,
|
|
148
|
+
minutes: int | None = None,
|
|
149
|
+
hours: int | None = None,
|
|
150
|
+
days: int | None = None,
|
|
151
|
+
cron: str | None = None,
|
|
152
|
+
id: str | None = None,
|
|
153
|
+
**kwargs: Any,
|
|
154
|
+
) -> None:
|
|
155
|
+
"""添加任务。
|
|
156
|
+
|
|
157
|
+
Args:
|
|
158
|
+
func: 任务函数
|
|
159
|
+
trigger: 触发器类型(interval/cron)
|
|
160
|
+
seconds: 间隔秒数
|
|
161
|
+
minutes: 间隔分钟数
|
|
162
|
+
hours: 间隔小时数
|
|
163
|
+
days: 间隔天数
|
|
164
|
+
cron: Cron表达式(如 "0 0 * * *")
|
|
165
|
+
id: 任务ID
|
|
166
|
+
**kwargs: 其他参数
|
|
167
|
+
"""
|
|
168
|
+
if not self._initialized:
|
|
169
|
+
raise RuntimeError("调度器未初始化")
|
|
170
|
+
|
|
171
|
+
# 使用函数式编程构建触发器
|
|
172
|
+
def build_interval_trigger() -> IntervalTrigger:
|
|
173
|
+
"""构建间隔触发器。"""
|
|
174
|
+
if seconds:
|
|
175
|
+
return IntervalTrigger(seconds=seconds)
|
|
176
|
+
if minutes:
|
|
177
|
+
return IntervalTrigger(minutes=minutes)
|
|
178
|
+
if hours:
|
|
179
|
+
return IntervalTrigger(hours=hours)
|
|
180
|
+
if days:
|
|
181
|
+
return IntervalTrigger(days=days)
|
|
182
|
+
raise ValueError("必须指定间隔时间")
|
|
183
|
+
|
|
184
|
+
def build_cron_trigger() -> CronTrigger:
|
|
185
|
+
"""构建Cron触发器。"""
|
|
186
|
+
if not cron:
|
|
187
|
+
raise ValueError("Cron触发器必须提供cron表达式")
|
|
188
|
+
return CronTrigger.from_crontab(cron)
|
|
189
|
+
|
|
190
|
+
trigger_builders: dict[str, Callable[[], Any]] = {
|
|
191
|
+
"interval": build_interval_trigger,
|
|
192
|
+
"cron": build_cron_trigger,
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
builder = trigger_builders.get(trigger)
|
|
196
|
+
if builder is None:
|
|
197
|
+
available = ", ".join(trigger_builders.keys())
|
|
198
|
+
raise ValueError(
|
|
199
|
+
f"不支持的触发器类型: {trigger}。可用类型: {available}"
|
|
200
|
+
)
|
|
201
|
+
|
|
202
|
+
trigger_obj = builder()
|
|
203
|
+
|
|
204
|
+
# 包装任务函数,自动设置日志上下文
|
|
205
|
+
wrapped_func = self._wrap_with_context(func)
|
|
206
|
+
|
|
207
|
+
# 添加任务
|
|
208
|
+
job_id = id or f"{func.__module__}.{func.__name__}"
|
|
209
|
+
self._scheduler.add_job(
|
|
210
|
+
func=wrapped_func,
|
|
211
|
+
trigger=trigger_obj,
|
|
212
|
+
id=job_id,
|
|
213
|
+
**kwargs,
|
|
214
|
+
)
|
|
215
|
+
|
|
216
|
+
logger.info(f"任务已注册: {job_id} | 触发器: {trigger}")
|
|
217
|
+
|
|
218
|
+
def _wrap_with_context(self, func: Callable) -> Callable:
|
|
219
|
+
"""包装任务函数,自动设置 scheduler 日志上下文。"""
|
|
220
|
+
@wraps(func)
|
|
221
|
+
async def async_wrapper(*args, **kwargs):
|
|
222
|
+
set_service_context("scheduler")
|
|
223
|
+
return await func(*args, **kwargs)
|
|
224
|
+
|
|
225
|
+
@wraps(func)
|
|
226
|
+
def sync_wrapper(*args, **kwargs):
|
|
227
|
+
set_service_context("scheduler")
|
|
228
|
+
return func(*args, **kwargs)
|
|
229
|
+
|
|
230
|
+
# 根据函数类型选择包装器
|
|
231
|
+
if asyncio.iscoroutinefunction(func):
|
|
232
|
+
return async_wrapper
|
|
233
|
+
return sync_wrapper
|
|
234
|
+
|
|
235
|
+
def remove_job(self, job_id: str) -> None:
|
|
236
|
+
"""移除任务。
|
|
237
|
+
|
|
238
|
+
Args:
|
|
239
|
+
job_id: 任务ID
|
|
240
|
+
"""
|
|
241
|
+
if self._scheduler:
|
|
242
|
+
self._scheduler.remove_job(job_id)
|
|
243
|
+
logger.info(f"任务已移除: {job_id}")
|
|
244
|
+
|
|
245
|
+
def get_jobs(self) -> list:
|
|
246
|
+
"""获取所有任务。"""
|
|
247
|
+
if self._scheduler:
|
|
248
|
+
return self._scheduler.get_jobs()
|
|
249
|
+
return []
|
|
250
|
+
|
|
251
|
+
def get_job(self, job_id: str) -> Any | None:
|
|
252
|
+
"""获取单个任务。
|
|
253
|
+
|
|
254
|
+
Args:
|
|
255
|
+
job_id: 任务ID
|
|
256
|
+
|
|
257
|
+
Returns:
|
|
258
|
+
任务对象,不存在则返回 None
|
|
259
|
+
"""
|
|
260
|
+
if self._scheduler:
|
|
261
|
+
return self._scheduler.get_job(job_id)
|
|
262
|
+
return None
|
|
263
|
+
|
|
264
|
+
def modify_job(
|
|
265
|
+
self,
|
|
266
|
+
job_id: str,
|
|
267
|
+
*,
|
|
268
|
+
func: Callable | None = None,
|
|
269
|
+
args: tuple | None = None,
|
|
270
|
+
kwargs: dict | None = None,
|
|
271
|
+
name: str | None = None,
|
|
272
|
+
**changes: Any,
|
|
273
|
+
) -> None:
|
|
274
|
+
"""修改任务属性。
|
|
275
|
+
|
|
276
|
+
Args:
|
|
277
|
+
job_id: 任务ID
|
|
278
|
+
func: 新的任务函数
|
|
279
|
+
args: 新的位置参数
|
|
280
|
+
kwargs: 新的关键字参数
|
|
281
|
+
name: 新的任务名称
|
|
282
|
+
**changes: 其他要修改的属性
|
|
283
|
+
"""
|
|
284
|
+
if not self._scheduler:
|
|
285
|
+
raise RuntimeError("调度器未初始化")
|
|
286
|
+
|
|
287
|
+
modify_kwargs: dict[str, Any] = {**changes}
|
|
288
|
+
if func is not None:
|
|
289
|
+
modify_kwargs["func"] = self._wrap_with_context(func)
|
|
290
|
+
if args is not None:
|
|
291
|
+
modify_kwargs["args"] = args
|
|
292
|
+
if kwargs is not None:
|
|
293
|
+
modify_kwargs["kwargs"] = kwargs
|
|
294
|
+
if name is not None:
|
|
295
|
+
modify_kwargs["name"] = name
|
|
296
|
+
|
|
297
|
+
self._scheduler.modify_job(job_id, **modify_kwargs)
|
|
298
|
+
logger.info(f"任务已修改: {job_id}")
|
|
299
|
+
|
|
300
|
+
def reschedule_job(
|
|
301
|
+
self,
|
|
302
|
+
job_id: str,
|
|
303
|
+
trigger: str = "interval",
|
|
304
|
+
*,
|
|
305
|
+
seconds: int | None = None,
|
|
306
|
+
minutes: int | None = None,
|
|
307
|
+
hours: int | None = None,
|
|
308
|
+
days: int | None = None,
|
|
309
|
+
cron: str | None = None,
|
|
310
|
+
) -> None:
|
|
311
|
+
"""重新调度任务(修改触发器)。
|
|
312
|
+
|
|
313
|
+
Args:
|
|
314
|
+
job_id: 任务ID
|
|
315
|
+
trigger: 触发器类型(interval/cron)
|
|
316
|
+
seconds: 间隔秒数
|
|
317
|
+
minutes: 间隔分钟数
|
|
318
|
+
hours: 间隔小时数
|
|
319
|
+
days: 间隔天数
|
|
320
|
+
cron: Cron表达式
|
|
321
|
+
"""
|
|
322
|
+
if not self._scheduler:
|
|
323
|
+
raise RuntimeError("调度器未初始化")
|
|
324
|
+
|
|
325
|
+
# 构建触发器
|
|
326
|
+
if trigger == "interval":
|
|
327
|
+
if seconds:
|
|
328
|
+
trigger_obj = IntervalTrigger(seconds=seconds)
|
|
329
|
+
elif minutes:
|
|
330
|
+
trigger_obj = IntervalTrigger(minutes=minutes)
|
|
331
|
+
elif hours:
|
|
332
|
+
trigger_obj = IntervalTrigger(hours=hours)
|
|
333
|
+
elif days:
|
|
334
|
+
trigger_obj = IntervalTrigger(days=days)
|
|
335
|
+
else:
|
|
336
|
+
raise ValueError("必须指定间隔时间")
|
|
337
|
+
elif trigger == "cron":
|
|
338
|
+
if not cron:
|
|
339
|
+
raise ValueError("Cron触发器必须提供cron表达式")
|
|
340
|
+
trigger_obj = CronTrigger.from_crontab(cron)
|
|
341
|
+
else:
|
|
342
|
+
raise ValueError(f"不支持的触发器类型: {trigger}")
|
|
343
|
+
|
|
344
|
+
self._scheduler.reschedule_job(job_id, trigger=trigger_obj)
|
|
345
|
+
logger.info(f"任务已重新调度: {job_id} | 触发器: {trigger}")
|
|
346
|
+
|
|
347
|
+
def pause_job(self, job_id: str) -> None:
|
|
348
|
+
"""暂停单个任务。
|
|
349
|
+
|
|
350
|
+
Args:
|
|
351
|
+
job_id: 任务ID
|
|
352
|
+
"""
|
|
353
|
+
if self._scheduler:
|
|
354
|
+
self._scheduler.pause_job(job_id)
|
|
355
|
+
logger.info(f"任务已暂停: {job_id}")
|
|
356
|
+
|
|
357
|
+
def resume_job(self, job_id: str) -> None:
|
|
358
|
+
"""恢复单个任务。
|
|
359
|
+
|
|
360
|
+
Args:
|
|
361
|
+
job_id: 任务ID
|
|
362
|
+
"""
|
|
363
|
+
if self._scheduler:
|
|
364
|
+
self._scheduler.resume_job(job_id)
|
|
365
|
+
logger.info(f"任务已恢复: {job_id}")
|
|
366
|
+
|
|
367
|
+
def start(self) -> None:
|
|
368
|
+
"""启动调度器。
|
|
369
|
+
|
|
370
|
+
启动时会注册所有通过装饰器收集的待处理任务。
|
|
371
|
+
"""
|
|
372
|
+
if not self._initialized:
|
|
373
|
+
raise RuntimeError("调度器未初始化")
|
|
374
|
+
|
|
375
|
+
if self._scheduler.running:
|
|
376
|
+
logger.warning("调度器已在运行")
|
|
377
|
+
return
|
|
378
|
+
|
|
379
|
+
# 注册所有待处理的任务
|
|
380
|
+
for job_config in self._pending_jobs:
|
|
381
|
+
self.add_job(**job_config)
|
|
382
|
+
self._pending_jobs.clear()
|
|
383
|
+
|
|
384
|
+
self._scheduler.start()
|
|
385
|
+
self._started = True
|
|
386
|
+
logger.info("调度器已启动")
|
|
387
|
+
|
|
388
|
+
def shutdown(self) -> None:
|
|
389
|
+
"""关闭调度器。"""
|
|
390
|
+
if self._scheduler and self._scheduler.running:
|
|
391
|
+
self._scheduler.shutdown()
|
|
392
|
+
logger.info("调度器已关闭")
|
|
393
|
+
|
|
394
|
+
def pause(self) -> None:
|
|
395
|
+
"""暂停调度器。"""
|
|
396
|
+
if self._scheduler:
|
|
397
|
+
self._scheduler.pause()
|
|
398
|
+
logger.info("调度器已暂停")
|
|
399
|
+
|
|
400
|
+
def resume(self) -> None:
|
|
401
|
+
"""恢复调度器。"""
|
|
402
|
+
if self._scheduler:
|
|
403
|
+
self._scheduler.resume()
|
|
404
|
+
logger.info("调度器已恢复")
|
|
405
|
+
|
|
406
|
+
def scheduled_job(
|
|
407
|
+
self,
|
|
408
|
+
trigger: str = "interval",
|
|
409
|
+
*,
|
|
410
|
+
seconds: int | None = None,
|
|
411
|
+
minutes: int | None = None,
|
|
412
|
+
hours: int | None = None,
|
|
413
|
+
days: int | None = None,
|
|
414
|
+
cron: str | None = None,
|
|
415
|
+
id: str | None = None,
|
|
416
|
+
**kwargs: Any,
|
|
417
|
+
) -> Callable[[Callable], Callable]:
|
|
418
|
+
"""任务注册装饰器。
|
|
419
|
+
|
|
420
|
+
使用示例:
|
|
421
|
+
scheduler = SchedulerManager.get_instance()
|
|
422
|
+
|
|
423
|
+
@scheduler.scheduled_job("interval", seconds=60)
|
|
424
|
+
async def my_task():
|
|
425
|
+
print("Task executed")
|
|
426
|
+
|
|
427
|
+
@scheduler.scheduled_job("cron", cron="0 0 * * *")
|
|
428
|
+
async def daily_task():
|
|
429
|
+
print("Daily task")
|
|
430
|
+
|
|
431
|
+
Args:
|
|
432
|
+
trigger: 触发器类型(interval/cron)
|
|
433
|
+
seconds: 间隔秒数
|
|
434
|
+
minutes: 间隔分钟数
|
|
435
|
+
hours: 间隔小时数
|
|
436
|
+
days: 间隔天数
|
|
437
|
+
cron: Cron表达式
|
|
438
|
+
id: 任务ID
|
|
439
|
+
**kwargs: 其他参数
|
|
440
|
+
|
|
441
|
+
Returns:
|
|
442
|
+
装饰器函数
|
|
443
|
+
"""
|
|
444
|
+
def decorator(func: Callable) -> Callable:
|
|
445
|
+
job_config = {
|
|
446
|
+
"func": func,
|
|
447
|
+
"trigger": trigger,
|
|
448
|
+
"seconds": seconds,
|
|
449
|
+
"minutes": minutes,
|
|
450
|
+
"hours": hours,
|
|
451
|
+
"days": days,
|
|
452
|
+
"cron": cron,
|
|
453
|
+
"id": id,
|
|
454
|
+
**kwargs,
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
if self._started:
|
|
458
|
+
# 调度器已启动,直接注册
|
|
459
|
+
self.add_job(**job_config)
|
|
460
|
+
else:
|
|
461
|
+
# 调度器未启动,加入待注册列表
|
|
462
|
+
self._pending_jobs.append(job_config)
|
|
463
|
+
job_id = id or f"{func.__module__}.{func.__name__}"
|
|
464
|
+
logger.debug(f"任务已加入待注册列表: {job_id}")
|
|
465
|
+
|
|
466
|
+
return func
|
|
467
|
+
return decorator
|
|
468
|
+
|
|
469
|
+
def __repr__(self) -> str:
|
|
470
|
+
"""字符串表示。"""
|
|
471
|
+
status = "running" if self._scheduler and self._scheduler.running else "stopped"
|
|
472
|
+
return f"<SchedulerManager status={status}>"
|
|
473
|
+
|
|
474
|
+
|
|
475
|
+
__all__ = [
|
|
476
|
+
"SchedulerManager",
|
|
477
|
+
]
|
|
478
|
+
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
"""对象存储系统模块(统一出口)。
|
|
2
|
+
|
|
3
|
+
本包基于 aury-sdk-storage 提供的实现,对外暴露统一接口与管理器。
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from .base import StorageManager
|
|
7
|
+
from .exceptions import StorageBackendError, StorageError, StorageNotFoundError
|
|
8
|
+
from .factory import StorageFactory
|
|
9
|
+
|
|
10
|
+
# 从 SDK 直接导出核心类型
|
|
11
|
+
from aury.sdk.storage.storage import (
|
|
12
|
+
IStorage,
|
|
13
|
+
LocalStorage,
|
|
14
|
+
S3Storage, # 可选依赖,未安装 aws extras 时为 None
|
|
15
|
+
StorageBackend,
|
|
16
|
+
StorageConfig,
|
|
17
|
+
StorageFile,
|
|
18
|
+
UploadResult,
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
__all__ = [
|
|
22
|
+
# SDK 类型
|
|
23
|
+
"IStorage",
|
|
24
|
+
"LocalStorage",
|
|
25
|
+
"S3Storage",
|
|
26
|
+
"StorageBackend",
|
|
27
|
+
"StorageConfig",
|
|
28
|
+
"StorageFile",
|
|
29
|
+
"UploadResult",
|
|
30
|
+
# 管理器与工厂
|
|
31
|
+
"StorageManager",
|
|
32
|
+
"StorageFactory",
|
|
33
|
+
# 异常
|
|
34
|
+
"StorageError",
|
|
35
|
+
"StorageBackendError",
|
|
36
|
+
"StorageNotFoundError",
|
|
37
|
+
]
|
|
38
|
+
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
"""对象存储系统 - 基于 aury-sdk-storage。
|
|
2
|
+
|
|
3
|
+
本模块提供 StorageManager 命名多实例管理,内部委托给 aury-sdk-storage 的实现。
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
from aury.sdk.storage.storage import (
|
|
9
|
+
IStorage,
|
|
10
|
+
LocalStorage,
|
|
11
|
+
S3Storage,
|
|
12
|
+
StorageBackend,
|
|
13
|
+
StorageConfig,
|
|
14
|
+
StorageFile,
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
from aury.boot.common.logging import logger
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class StorageManager:
|
|
21
|
+
"""存储管理器(命名多实例)。
|
|
22
|
+
|
|
23
|
+
- 仅负责装配具体后端,不读取环境变量
|
|
24
|
+
- 对上层暴露稳定的最小接口
|
|
25
|
+
- 支持多个命名实例,如 source/target 存储
|
|
26
|
+
|
|
27
|
+
使用示例:
|
|
28
|
+
# 默认实例
|
|
29
|
+
storage = StorageManager.get_instance()
|
|
30
|
+
await storage.init(config)
|
|
31
|
+
|
|
32
|
+
# 命名实例
|
|
33
|
+
source = StorageManager.get_instance("source")
|
|
34
|
+
target = StorageManager.get_instance("target")
|
|
35
|
+
"""
|
|
36
|
+
|
|
37
|
+
_instances: dict[str, StorageManager] = {}
|
|
38
|
+
|
|
39
|
+
def __init__(self, name: str = "default") -> None:
|
|
40
|
+
self.name = name
|
|
41
|
+
self._backend: IStorage | None = None
|
|
42
|
+
self._config: StorageConfig | None = None
|
|
43
|
+
|
|
44
|
+
@classmethod
|
|
45
|
+
def get_instance(cls, name: str = "default") -> StorageManager:
|
|
46
|
+
"""获取指定名称的实例。
|
|
47
|
+
|
|
48
|
+
Args:
|
|
49
|
+
name: 实例名称,默认为 "default"
|
|
50
|
+
|
|
51
|
+
Returns:
|
|
52
|
+
StorageManager: 存储管理器实例
|
|
53
|
+
"""
|
|
54
|
+
if name not in cls._instances:
|
|
55
|
+
cls._instances[name] = cls(name)
|
|
56
|
+
return cls._instances[name]
|
|
57
|
+
|
|
58
|
+
@classmethod
|
|
59
|
+
def reset_instance(cls, name: str | None = None) -> None:
|
|
60
|
+
"""重置实例(仅用于测试)。
|
|
61
|
+
|
|
62
|
+
Args:
|
|
63
|
+
name: 要重置的实例名称。如果为 None,则重置所有实例。
|
|
64
|
+
|
|
65
|
+
注意:调用此方法前应先调用 cleanup() 释放资源。
|
|
66
|
+
"""
|
|
67
|
+
if name is None:
|
|
68
|
+
cls._instances.clear()
|
|
69
|
+
elif name in cls._instances:
|
|
70
|
+
del cls._instances[name]
|
|
71
|
+
|
|
72
|
+
async def init(self, config: StorageConfig) -> None:
|
|
73
|
+
"""使用 SDK 的 StorageConfig 初始化存储后端。"""
|
|
74
|
+
self._config = config
|
|
75
|
+
if config.backend == StorageBackend.LOCAL:
|
|
76
|
+
self._backend = LocalStorage(base_path=config.base_path or "./storage")
|
|
77
|
+
else:
|
|
78
|
+
# S3/COS/OSS/MinIO 统一走 S3Storage
|
|
79
|
+
self._backend = S3Storage(config)
|
|
80
|
+
logger.info(f"存储管理器初始化完成: {config.backend.value}")
|
|
81
|
+
|
|
82
|
+
@property
|
|
83
|
+
def backend(self) -> IStorage:
|
|
84
|
+
if self._backend is None:
|
|
85
|
+
raise RuntimeError("存储管理器未初始化,请先调用 init()")
|
|
86
|
+
return self._backend
|
|
87
|
+
|
|
88
|
+
async def upload_file(
|
|
89
|
+
self,
|
|
90
|
+
file: StorageFile,
|
|
91
|
+
*,
|
|
92
|
+
bucket_name: str | None = None,
|
|
93
|
+
) -> str:
|
|
94
|
+
"""上传文件并返回 URL。"""
|
|
95
|
+
result = await self.backend.upload_file(file, bucket_name=bucket_name)
|
|
96
|
+
return result.url
|
|
97
|
+
|
|
98
|
+
async def upload_files(
|
|
99
|
+
self,
|
|
100
|
+
files: list[StorageFile],
|
|
101
|
+
*,
|
|
102
|
+
bucket_name: str | None = None,
|
|
103
|
+
) -> list[str]:
|
|
104
|
+
"""批量上传文件并返回 URL 列表。"""
|
|
105
|
+
results = await self.backend.upload_files(files, bucket_name=bucket_name)
|
|
106
|
+
return [r.url for r in results]
|
|
107
|
+
|
|
108
|
+
async def delete_file(
|
|
109
|
+
self,
|
|
110
|
+
object_name: str,
|
|
111
|
+
*,
|
|
112
|
+
bucket_name: str | None = None,
|
|
113
|
+
) -> None:
|
|
114
|
+
await self.backend.delete_file(object_name, bucket_name=bucket_name)
|
|
115
|
+
|
|
116
|
+
async def get_file_url(
|
|
117
|
+
self,
|
|
118
|
+
object_name: str,
|
|
119
|
+
*,
|
|
120
|
+
bucket_name: str | None = None,
|
|
121
|
+
expires_in: int | None = None,
|
|
122
|
+
) -> str:
|
|
123
|
+
return await self.backend.get_file_url(
|
|
124
|
+
object_name, bucket_name=bucket_name, expires_in=expires_in
|
|
125
|
+
)
|
|
126
|
+
|
|
127
|
+
async def file_exists(
|
|
128
|
+
self,
|
|
129
|
+
object_name: str,
|
|
130
|
+
*,
|
|
131
|
+
bucket_name: str | None = None,
|
|
132
|
+
) -> bool:
|
|
133
|
+
return await self.backend.file_exists(object_name, bucket_name=bucket_name)
|
|
134
|
+
|
|
135
|
+
async def download_file(
|
|
136
|
+
self,
|
|
137
|
+
object_name: str,
|
|
138
|
+
*,
|
|
139
|
+
bucket_name: str | None = None,
|
|
140
|
+
) -> bytes:
|
|
141
|
+
return await self.backend.download_file(object_name, bucket_name=bucket_name)
|
|
142
|
+
|
|
143
|
+
async def cleanup(self) -> None:
|
|
144
|
+
if self._backend:
|
|
145
|
+
# SDK 的 IStorage 可能没有 close() 方法
|
|
146
|
+
if hasattr(self._backend, "close"):
|
|
147
|
+
await self._backend.close()
|
|
148
|
+
self._backend = None
|
|
149
|
+
logger.info("存储管理器已清理")
|
|
150
|
+
|
|
151
|
+
def __repr__(self) -> str:
|
|
152
|
+
backend_type = self._config.backend.value if self._config else "未初始化"
|
|
153
|
+
return f"<StorageManager backend={backend_type}>"
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
__all__ = [
|
|
157
|
+
"IStorage",
|
|
158
|
+
"LocalStorage",
|
|
159
|
+
"S3Storage",
|
|
160
|
+
"StorageBackend",
|
|
161
|
+
"StorageConfig",
|
|
162
|
+
"StorageFile",
|
|
163
|
+
"StorageManager",
|
|
164
|
+
]
|