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.
Files changed (138) hide show
  1. aury/boot/__init__.py +66 -0
  2. aury/boot/_version.py +2 -2
  3. aury/boot/application/__init__.py +120 -0
  4. aury/boot/application/app/__init__.py +39 -0
  5. aury/boot/application/app/base.py +511 -0
  6. aury/boot/application/app/components.py +434 -0
  7. aury/boot/application/app/middlewares.py +101 -0
  8. aury/boot/application/config/__init__.py +44 -0
  9. aury/boot/application/config/settings.py +663 -0
  10. aury/boot/application/constants/__init__.py +19 -0
  11. aury/boot/application/constants/components.py +50 -0
  12. aury/boot/application/constants/scheduler.py +28 -0
  13. aury/boot/application/constants/service.py +29 -0
  14. aury/boot/application/errors/__init__.py +55 -0
  15. aury/boot/application/errors/chain.py +80 -0
  16. aury/boot/application/errors/codes.py +67 -0
  17. aury/boot/application/errors/exceptions.py +238 -0
  18. aury/boot/application/errors/handlers.py +320 -0
  19. aury/boot/application/errors/response.py +120 -0
  20. aury/boot/application/interfaces/__init__.py +76 -0
  21. aury/boot/application/interfaces/egress.py +224 -0
  22. aury/boot/application/interfaces/ingress.py +98 -0
  23. aury/boot/application/middleware/__init__.py +22 -0
  24. aury/boot/application/middleware/logging.py +451 -0
  25. aury/boot/application/migrations/__init__.py +13 -0
  26. aury/boot/application/migrations/manager.py +685 -0
  27. aury/boot/application/migrations/setup.py +237 -0
  28. aury/boot/application/rpc/__init__.py +63 -0
  29. aury/boot/application/rpc/base.py +108 -0
  30. aury/boot/application/rpc/client.py +294 -0
  31. aury/boot/application/rpc/discovery.py +218 -0
  32. aury/boot/application/scheduler/__init__.py +13 -0
  33. aury/boot/application/scheduler/runner.py +123 -0
  34. aury/boot/application/server/__init__.py +296 -0
  35. aury/boot/commands/__init__.py +30 -0
  36. aury/boot/commands/add.py +76 -0
  37. aury/boot/commands/app.py +105 -0
  38. aury/boot/commands/config.py +177 -0
  39. aury/boot/commands/docker.py +367 -0
  40. aury/boot/commands/docs.py +284 -0
  41. aury/boot/commands/generate.py +1277 -0
  42. aury/boot/commands/init.py +890 -0
  43. aury/boot/commands/migrate/__init__.py +37 -0
  44. aury/boot/commands/migrate/app.py +54 -0
  45. aury/boot/commands/migrate/commands.py +303 -0
  46. aury/boot/commands/scheduler.py +124 -0
  47. aury/boot/commands/server/__init__.py +21 -0
  48. aury/boot/commands/server/app.py +541 -0
  49. aury/boot/commands/templates/generate/api.py.tpl +105 -0
  50. aury/boot/commands/templates/generate/model.py.tpl +17 -0
  51. aury/boot/commands/templates/generate/repository.py.tpl +19 -0
  52. aury/boot/commands/templates/generate/schema.py.tpl +29 -0
  53. aury/boot/commands/templates/generate/service.py.tpl +48 -0
  54. aury/boot/commands/templates/project/CLI.md.tpl +92 -0
  55. aury/boot/commands/templates/project/DEVELOPMENT.md.tpl +1397 -0
  56. aury/boot/commands/templates/project/README.md.tpl +111 -0
  57. aury/boot/commands/templates/project/admin_console_init.py.tpl +50 -0
  58. aury/boot/commands/templates/project/config.py.tpl +30 -0
  59. aury/boot/commands/templates/project/conftest.py.tpl +26 -0
  60. aury/boot/commands/templates/project/env.example.tpl +213 -0
  61. aury/boot/commands/templates/project/gitignore.tpl +128 -0
  62. aury/boot/commands/templates/project/main.py.tpl +41 -0
  63. aury/boot/commands/templates/project/modules/api.py.tpl +19 -0
  64. aury/boot/commands/templates/project/modules/exceptions.py.tpl +84 -0
  65. aury/boot/commands/templates/project/modules/schedules.py.tpl +18 -0
  66. aury/boot/commands/templates/project/modules/tasks.py.tpl +20 -0
  67. aury/boot/commands/worker.py +143 -0
  68. aury/boot/common/__init__.py +35 -0
  69. aury/boot/common/exceptions/__init__.py +114 -0
  70. aury/boot/common/i18n/__init__.py +16 -0
  71. aury/boot/common/i18n/translator.py +272 -0
  72. aury/boot/common/logging/__init__.py +716 -0
  73. aury/boot/contrib/__init__.py +10 -0
  74. aury/boot/contrib/admin_console/__init__.py +18 -0
  75. aury/boot/contrib/admin_console/auth.py +137 -0
  76. aury/boot/contrib/admin_console/discovery.py +69 -0
  77. aury/boot/contrib/admin_console/install.py +172 -0
  78. aury/boot/contrib/admin_console/utils.py +44 -0
  79. aury/boot/domain/__init__.py +79 -0
  80. aury/boot/domain/exceptions/__init__.py +132 -0
  81. aury/boot/domain/models/__init__.py +51 -0
  82. aury/boot/domain/models/base.py +69 -0
  83. aury/boot/domain/models/mixins.py +135 -0
  84. aury/boot/domain/models/models.py +96 -0
  85. aury/boot/domain/pagination/__init__.py +279 -0
  86. aury/boot/domain/repository/__init__.py +23 -0
  87. aury/boot/domain/repository/impl.py +423 -0
  88. aury/boot/domain/repository/interceptors.py +47 -0
  89. aury/boot/domain/repository/interface.py +106 -0
  90. aury/boot/domain/repository/query_builder.py +348 -0
  91. aury/boot/domain/service/__init__.py +11 -0
  92. aury/boot/domain/service/base.py +73 -0
  93. aury/boot/domain/transaction/__init__.py +404 -0
  94. aury/boot/infrastructure/__init__.py +104 -0
  95. aury/boot/infrastructure/cache/__init__.py +31 -0
  96. aury/boot/infrastructure/cache/backends.py +348 -0
  97. aury/boot/infrastructure/cache/base.py +68 -0
  98. aury/boot/infrastructure/cache/exceptions.py +37 -0
  99. aury/boot/infrastructure/cache/factory.py +94 -0
  100. aury/boot/infrastructure/cache/manager.py +274 -0
  101. aury/boot/infrastructure/database/__init__.py +39 -0
  102. aury/boot/infrastructure/database/config.py +71 -0
  103. aury/boot/infrastructure/database/exceptions.py +44 -0
  104. aury/boot/infrastructure/database/manager.py +317 -0
  105. aury/boot/infrastructure/database/query_tools/__init__.py +164 -0
  106. aury/boot/infrastructure/database/strategies/__init__.py +198 -0
  107. aury/boot/infrastructure/di/__init__.py +15 -0
  108. aury/boot/infrastructure/di/container.py +393 -0
  109. aury/boot/infrastructure/events/__init__.py +33 -0
  110. aury/boot/infrastructure/events/bus.py +362 -0
  111. aury/boot/infrastructure/events/config.py +52 -0
  112. aury/boot/infrastructure/events/consumer.py +134 -0
  113. aury/boot/infrastructure/events/middleware.py +51 -0
  114. aury/boot/infrastructure/events/models.py +63 -0
  115. aury/boot/infrastructure/monitoring/__init__.py +529 -0
  116. aury/boot/infrastructure/scheduler/__init__.py +19 -0
  117. aury/boot/infrastructure/scheduler/exceptions.py +37 -0
  118. aury/boot/infrastructure/scheduler/manager.py +478 -0
  119. aury/boot/infrastructure/storage/__init__.py +38 -0
  120. aury/boot/infrastructure/storage/base.py +164 -0
  121. aury/boot/infrastructure/storage/exceptions.py +37 -0
  122. aury/boot/infrastructure/storage/factory.py +88 -0
  123. aury/boot/infrastructure/tasks/__init__.py +24 -0
  124. aury/boot/infrastructure/tasks/config.py +45 -0
  125. aury/boot/infrastructure/tasks/constants.py +37 -0
  126. aury/boot/infrastructure/tasks/exceptions.py +37 -0
  127. aury/boot/infrastructure/tasks/manager.py +490 -0
  128. aury/boot/testing/__init__.py +24 -0
  129. aury/boot/testing/base.py +122 -0
  130. aury/boot/testing/client.py +163 -0
  131. aury/boot/testing/factory.py +154 -0
  132. aury/boot/toolkit/__init__.py +21 -0
  133. aury/boot/toolkit/http/__init__.py +367 -0
  134. {aury_boot-0.0.2.dist-info → aury_boot-0.0.3.dist-info}/METADATA +3 -2
  135. aury_boot-0.0.3.dist-info/RECORD +137 -0
  136. aury_boot-0.0.2.dist-info/RECORD +0 -5
  137. {aury_boot-0.0.2.dist-info → aury_boot-0.0.3.dist-info}/WHEEL +0 -0
  138. {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
+ ]