aury-boot 0.0.14__py3-none-any.whl → 0.0.17__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/_version.py +2 -2
- aury/boot/application/app/components.py +50 -3
- aury/boot/application/app/startup.py +1 -1
- aury/boot/application/config/settings.py +31 -0
- aury/boot/application/middleware/logging.py +1 -13
- aury/boot/commands/templates/project/AGENTS.md.tpl +12 -0
- aury/boot/commands/templates/project/aury_docs/08-scheduler.md.tpl +150 -4
- aury/boot/commands/templates/project/aury_docs/11-logging.md.tpl +5 -41
- aury/boot/commands/templates/project/aury_docs/99-cli.md.tpl +1 -1
- aury/boot/commands/templates/project/modules/schedules.py.tpl +4 -1
- aury/boot/common/logging/__init__.py +0 -5
- aury/boot/common/logging/context.py +1 -52
- aury/boot/infrastructure/cache/manager.py +5 -1
- aury/boot/infrastructure/database/manager.py +8 -2
- aury/boot/infrastructure/scheduler/manager.py +125 -128
- {aury_boot-0.0.14.dist-info → aury_boot-0.0.17.dist-info}/METADATA +1 -1
- {aury_boot-0.0.14.dist-info → aury_boot-0.0.17.dist-info}/RECORD +19 -19
- {aury_boot-0.0.14.dist-info → aury_boot-0.0.17.dist-info}/WHEEL +0 -0
- {aury_boot-0.0.14.dist-info → aury_boot-0.0.17.dist-info}/entry_points.txt +0 -0
aury/boot/_version.py
CHANGED
|
@@ -28,7 +28,7 @@ version_tuple: VERSION_TUPLE
|
|
|
28
28
|
commit_id: COMMIT_ID
|
|
29
29
|
__commit_id__: COMMIT_ID
|
|
30
30
|
|
|
31
|
-
__version__ = version = '0.0.
|
|
32
|
-
__version_tuple__ = version_tuple = (0, 0,
|
|
31
|
+
__version__ = version = '0.0.17'
|
|
32
|
+
__version_tuple__ = version_tuple = (0, 0, 17)
|
|
33
33
|
|
|
34
34
|
__commit_id__ = commit_id = None
|
|
@@ -281,18 +281,65 @@ class SchedulerComponent(Component):
|
|
|
281
281
|
except Exception as e:
|
|
282
282
|
logger.warning(f"加载定时任务模块失败 ({module_name}): {e}")
|
|
283
283
|
|
|
284
|
+
def _build_scheduler_config(self, config: BaseConfig) -> dict:
|
|
285
|
+
"""根据配置构建 APScheduler 初始化参数。"""
|
|
286
|
+
scheduler_kwargs: dict = {}
|
|
287
|
+
scheduler_config = config.scheduler
|
|
288
|
+
|
|
289
|
+
# jobstores: 根据 URL 自动选择存储后端
|
|
290
|
+
if scheduler_config.jobstore_url:
|
|
291
|
+
url = scheduler_config.jobstore_url
|
|
292
|
+
if url.startswith("redis://"):
|
|
293
|
+
try:
|
|
294
|
+
from apscheduler.jobstores.redis import RedisJobStore
|
|
295
|
+
scheduler_kwargs["jobstores"] = {
|
|
296
|
+
"default": RedisJobStore.from_url(url)
|
|
297
|
+
}
|
|
298
|
+
logger.info(f"调度器使用 Redis 存储: {url.split('@')[-1]}")
|
|
299
|
+
except ImportError:
|
|
300
|
+
logger.warning("Redis jobstore 需要安装 redis: pip install redis")
|
|
301
|
+
else:
|
|
302
|
+
# SQLAlchemy 存储 (sqlite/postgresql/mysql)
|
|
303
|
+
try:
|
|
304
|
+
from apscheduler.jobstores.sqlalchemy import SQLAlchemyJobStore
|
|
305
|
+
scheduler_kwargs["jobstores"] = {
|
|
306
|
+
"default": SQLAlchemyJobStore(url=url)
|
|
307
|
+
}
|
|
308
|
+
logger.info("调度器使用 SQLAlchemy 存储")
|
|
309
|
+
except ImportError:
|
|
310
|
+
logger.warning("SQLAlchemy jobstore 需要安装 sqlalchemy")
|
|
311
|
+
|
|
312
|
+
# timezone
|
|
313
|
+
if scheduler_config.timezone:
|
|
314
|
+
scheduler_kwargs["timezone"] = scheduler_config.timezone
|
|
315
|
+
|
|
316
|
+
# job_defaults
|
|
317
|
+
scheduler_kwargs["job_defaults"] = {
|
|
318
|
+
"coalesce": scheduler_config.coalesce,
|
|
319
|
+
"max_instances": scheduler_config.max_instances,
|
|
320
|
+
"misfire_grace_time": scheduler_config.misfire_grace_time,
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
return scheduler_kwargs
|
|
324
|
+
|
|
284
325
|
async def setup(self, app: FoundationApp, config: BaseConfig) -> None:
|
|
285
326
|
"""启动调度器。
|
|
286
327
|
|
|
287
|
-
1.
|
|
288
|
-
2.
|
|
328
|
+
1. 根据配置初始化调度器(jobstore/timezone/job_defaults)
|
|
329
|
+
2. 自动发现并加载定时任务模块
|
|
330
|
+
3. 启动调度器(注册装饰器收集的任务)
|
|
289
331
|
"""
|
|
290
332
|
try:
|
|
333
|
+
# 构建配置
|
|
334
|
+
scheduler_kwargs = self._build_scheduler_config(config)
|
|
335
|
+
|
|
336
|
+
# 获取/创建调度器实例
|
|
337
|
+
scheduler = SchedulerManager.get_instance("default", **scheduler_kwargs)
|
|
338
|
+
|
|
291
339
|
# 自动发现并加载定时任务模块
|
|
292
340
|
self._autodiscover_schedules(app, config)
|
|
293
341
|
|
|
294
342
|
# 启动调度器
|
|
295
|
-
scheduler = SchedulerManager.get_instance()
|
|
296
343
|
scheduler.start()
|
|
297
344
|
except Exception as e:
|
|
298
345
|
logger.warning(f"调度器启动失败(非关键): {e}")
|
|
@@ -152,7 +152,7 @@ def collect_component_status() -> list[ComponentStatus]:
|
|
|
152
152
|
name="Cache" if name == "default" else f"Cache [{name}]",
|
|
153
153
|
status="ok",
|
|
154
154
|
backend=instance.backend_type,
|
|
155
|
-
url=instance._config.get("CACHE_URL")
|
|
155
|
+
url=(instance._config or {}).get("CACHE_URL"),
|
|
156
156
|
)
|
|
157
157
|
)
|
|
158
158
|
|
|
@@ -416,6 +416,10 @@ class SchedulerSettings(BaseModel):
|
|
|
416
416
|
- SCHEDULER__ENABLED=false: 只运行 API,不启动调度器
|
|
417
417
|
|
|
418
418
|
独立调度器通过 `aury scheduler` 命令运行,不需要此配置。
|
|
419
|
+
|
|
420
|
+
分布式调度:
|
|
421
|
+
- 配置 SCHEDULER__JOBSTORE_URL 使用 Redis/SQLAlchemy 存储
|
|
422
|
+
- 多节点部署时共享任务状态
|
|
419
423
|
"""
|
|
420
424
|
|
|
421
425
|
enabled: bool = Field(
|
|
@@ -426,6 +430,33 @@ class SchedulerSettings(BaseModel):
|
|
|
426
430
|
default_factory=list,
|
|
427
431
|
description="定时任务模块列表。为空时自动发现 schedules 模块"
|
|
428
432
|
)
|
|
433
|
+
# APScheduler 配置
|
|
434
|
+
jobstore_url: str | None = Field(
|
|
435
|
+
default=None,
|
|
436
|
+
description=(
|
|
437
|
+
"任务存储 URL。支持:\n"
|
|
438
|
+
"- redis://localhost:6379/0(Redis 存储)\n"
|
|
439
|
+
"- sqlite:///jobs.db(SQLite 存储)\n"
|
|
440
|
+
"- postgresql://user:pass@host/db(PostgreSQL 存储)\n"
|
|
441
|
+
"- 不配置则使用内存存储"
|
|
442
|
+
)
|
|
443
|
+
)
|
|
444
|
+
timezone: str | None = Field(
|
|
445
|
+
default=None,
|
|
446
|
+
description="调度器时区,如 Asia/Shanghai、UTC"
|
|
447
|
+
)
|
|
448
|
+
coalesce: bool = Field(
|
|
449
|
+
default=True,
|
|
450
|
+
description="是否合并错过的任务执行(多次错过只执行一次)"
|
|
451
|
+
)
|
|
452
|
+
max_instances: int = Field(
|
|
453
|
+
default=1,
|
|
454
|
+
description="同一任务的最大并发实例数"
|
|
455
|
+
)
|
|
456
|
+
misfire_grace_time: int = Field(
|
|
457
|
+
default=60,
|
|
458
|
+
description="任务错过容忍时间(秒),超过此时间则跳过"
|
|
459
|
+
)
|
|
429
460
|
|
|
430
461
|
|
|
431
462
|
class TaskSettings(BaseModel):
|
|
@@ -17,7 +17,7 @@ from starlette.requests import Request
|
|
|
17
17
|
from starlette.responses import Response
|
|
18
18
|
|
|
19
19
|
from aury.boot.application.errors import global_exception_handler
|
|
20
|
-
from aury.boot.common.logging import
|
|
20
|
+
from aury.boot.common.logging import logger, set_trace_id
|
|
21
21
|
|
|
22
22
|
|
|
23
23
|
def log_request[T](func: Callable[..., T]) -> Callable[..., T]:
|
|
@@ -187,12 +187,6 @@ class RequestLoggingMiddleware(BaseHTTPMiddleware):
|
|
|
187
187
|
)
|
|
188
188
|
logger.log(log_level.upper(), response_log)
|
|
189
189
|
|
|
190
|
-
# 记录请求上下文(user_id, tenant_id 等用户注册的字段)
|
|
191
|
-
request_contexts = get_request_contexts()
|
|
192
|
-
if request_contexts:
|
|
193
|
-
ctx_str = " | ".join(f"{k}: {v}" for k, v in request_contexts.items())
|
|
194
|
-
logger.info(f"[REQUEST_CONTEXT] Trace-ID: {trace_id} | {ctx_str}")
|
|
195
|
-
|
|
196
190
|
# 写入 access 日志(简洁格式)
|
|
197
191
|
logger.bind(access=True).info(
|
|
198
192
|
f"{request.method} {request.url.path} {status_code} {duration:.3f}s"
|
|
@@ -216,12 +210,6 @@ class RequestLoggingMiddleware(BaseHTTPMiddleware):
|
|
|
216
210
|
f"耗时: {duration:.3f}s | Trace-ID: {trace_id}"
|
|
217
211
|
)
|
|
218
212
|
|
|
219
|
-
# 记录请求上下文(即使异常也要记录,便于追踪问题)
|
|
220
|
-
request_contexts = get_request_contexts()
|
|
221
|
-
if request_contexts:
|
|
222
|
-
ctx_str = " | ".join(f"{k}: {v}" for k, v in request_contexts.items())
|
|
223
|
-
logger.info(f"[REQUEST_CONTEXT] Trace-ID: {trace_id} | {ctx_str}")
|
|
224
|
-
|
|
225
213
|
# 使用全局异常处理器生成响应,而不是直接抛出异常
|
|
226
214
|
# BaseHTTPMiddleware 中直接 raise 会绕过 FastAPI 的异常处理器
|
|
227
215
|
response = await global_exception_handler(request, exc)
|
|
@@ -122,6 +122,8 @@ mypy {package_name}/
|
|
|
122
122
|
|
|
123
123
|
## 代码规范
|
|
124
124
|
|
|
125
|
+
> 项目所有业务配置请通过应用 `settings`/配置对象获取,**不要**直接使用 `os.environ` 在业务代码中读环境变量。
|
|
126
|
+
|
|
125
127
|
### Model 规范
|
|
126
128
|
|
|
127
129
|
- **必须**继承框架预定义基类,**不要**直接继承 `Base`
|
|
@@ -129,6 +131,16 @@ mypy {package_name}/
|
|
|
129
131
|
- 软删除模型**必须**使用复合唯一约束(包含 `deleted_at`),不能单独使用 `unique=True`
|
|
130
132
|
- **不建议**使用数据库外键(`ForeignKey`),通过程序控制关系,便于分库分表和微服务拆分
|
|
131
133
|
|
|
134
|
+
**重要:软删除机制**
|
|
135
|
+
|
|
136
|
+
框架采用「默认 0」策略,而非 IS NULL:
|
|
137
|
+
- `deleted_at = 0`:未删除
|
|
138
|
+
- `deleted_at > 0`:已删除(Unix 时间戳)
|
|
139
|
+
|
|
140
|
+
查询未删除记录时,使用 `WHERE deleted_at = 0`,不是 `WHERE deleted_at IS NULL`。
|
|
141
|
+
|
|
142
|
+
BaseRepository 已自动处理软删除过滤,无需手动添加条件。
|
|
143
|
+
|
|
132
144
|
```python
|
|
133
145
|
# ✅ 正确
|
|
134
146
|
from aury.boot.domain.models import AuditableStateModel
|
|
@@ -1,32 +1,178 @@
|
|
|
1
1
|
# 定时任务(Scheduler)
|
|
2
2
|
|
|
3
|
+
基于 APScheduler,完全透传原生 API。
|
|
4
|
+
|
|
5
|
+
## 基本用法
|
|
6
|
+
|
|
3
7
|
**文件**: `{package_name}/schedules/__init__.py`
|
|
4
8
|
|
|
5
9
|
```python
|
|
6
10
|
"""定时任务模块。"""
|
|
7
11
|
|
|
12
|
+
from apscheduler.triggers.cron import CronTrigger
|
|
13
|
+
from apscheduler.triggers.interval import IntervalTrigger
|
|
14
|
+
|
|
8
15
|
from aury.boot.common.logging import logger
|
|
9
16
|
from aury.boot.infrastructure.scheduler import SchedulerManager
|
|
10
17
|
|
|
11
18
|
scheduler = SchedulerManager.get_instance()
|
|
12
19
|
|
|
13
20
|
|
|
14
|
-
@scheduler.scheduled_job(
|
|
21
|
+
@scheduler.scheduled_job(IntervalTrigger(seconds=60))
|
|
15
22
|
async def every_minute():
|
|
16
23
|
"""每 60 秒执行。"""
|
|
17
24
|
logger.info("定时任务执行中...")
|
|
18
25
|
|
|
19
26
|
|
|
20
|
-
@scheduler.scheduled_job(
|
|
27
|
+
@scheduler.scheduled_job(CronTrigger(hour=0, minute=0))
|
|
21
28
|
async def daily_task():
|
|
22
29
|
"""每天凌晨执行。"""
|
|
23
30
|
logger.info("每日任务执行中...")
|
|
24
31
|
|
|
25
32
|
|
|
26
|
-
@scheduler.scheduled_job(
|
|
33
|
+
@scheduler.scheduled_job(CronTrigger(day_of_week="mon", hour=9))
|
|
27
34
|
async def weekly_report():
|
|
28
35
|
"""每周一 9 点执行。"""
|
|
29
36
|
logger.info("周报任务执行中...")
|
|
30
37
|
```
|
|
31
38
|
|
|
32
|
-
启用方式:配置 `
|
|
39
|
+
启用方式:配置 `SCHEDULER__ENABLED=true`,框架自动加载 `{package_name}/schedules/` 模块。
|
|
40
|
+
|
|
41
|
+
## 配置项
|
|
42
|
+
|
|
43
|
+
```bash
|
|
44
|
+
# .env
|
|
45
|
+
SCHEDULER__ENABLED=true # 是否启用
|
|
46
|
+
SCHEDULER__TIMEZONE=Asia/Shanghai # 时区
|
|
47
|
+
SCHEDULER__COALESCE=true # 合并错过的任务
|
|
48
|
+
SCHEDULER__MAX_INSTANCES=1 # 同一任务最大并发数
|
|
49
|
+
SCHEDULER__MISFIRE_GRACE_TIME=60 # 错过容忍时间(秒)
|
|
50
|
+
SCHEDULER__JOBSTORE_URL=redis://localhost:6379/0 # 分布式存储(可选)
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
## 触发器类型
|
|
54
|
+
|
|
55
|
+
### CronTrigger - 定时触发
|
|
56
|
+
|
|
57
|
+
```python
|
|
58
|
+
from apscheduler.triggers.cron import CronTrigger
|
|
59
|
+
|
|
60
|
+
# 每天凌晨 2:30
|
|
61
|
+
CronTrigger(hour=2, minute=30)
|
|
62
|
+
|
|
63
|
+
# 每小时整点
|
|
64
|
+
CronTrigger(hour="*", minute=0)
|
|
65
|
+
|
|
66
|
+
# 工作日 9:00
|
|
67
|
+
CronTrigger(day_of_week="mon-fri", hour=9)
|
|
68
|
+
|
|
69
|
+
# 每月 1 号
|
|
70
|
+
CronTrigger(day=1, hour=0)
|
|
71
|
+
|
|
72
|
+
# 使用 crontab 表达式
|
|
73
|
+
CronTrigger.from_crontab("0 2 * * *") # 每天 2:00
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
### IntervalTrigger - 间隔触发
|
|
77
|
+
|
|
78
|
+
```python
|
|
79
|
+
from apscheduler.triggers.interval import IntervalTrigger
|
|
80
|
+
|
|
81
|
+
IntervalTrigger(seconds=30) # 每 30 秒
|
|
82
|
+
IntervalTrigger(minutes=5) # 每 5 分钟
|
|
83
|
+
IntervalTrigger(hours=1) # 每小时
|
|
84
|
+
IntervalTrigger(days=1) # 每天
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
### DateTrigger - 一次性触发
|
|
88
|
+
|
|
89
|
+
```python
|
|
90
|
+
from apscheduler.triggers.date import DateTrigger
|
|
91
|
+
from datetime import datetime, timedelta
|
|
92
|
+
|
|
93
|
+
# 10 秒后执行
|
|
94
|
+
DateTrigger(run_date=datetime.now() + timedelta(seconds=10))
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
## 多实例支持
|
|
98
|
+
|
|
99
|
+
支持不同业务线使用独立的调度器实例:
|
|
100
|
+
|
|
101
|
+
```python
|
|
102
|
+
# 默认实例
|
|
103
|
+
scheduler = SchedulerManager.get_instance()
|
|
104
|
+
|
|
105
|
+
# 命名实例
|
|
106
|
+
report_scheduler = SchedulerManager.get_instance("report")
|
|
107
|
+
cleanup_scheduler = SchedulerManager.get_instance("cleanup")
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
## 分布式调度
|
|
111
|
+
|
|
112
|
+
多节点部署时,配置相同的 `SCHEDULER__JOBSTORE_URL`,所有节点共享任务状态:
|
|
113
|
+
|
|
114
|
+
```bash
|
|
115
|
+
# 所有节点使用相同配置
|
|
116
|
+
SCHEDULER__JOBSTORE_URL=redis://redis:6379/0
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
APScheduler 自动协调防止重复执行。
|
|
120
|
+
|
|
121
|
+
### 代码方式配置(高级)
|
|
122
|
+
|
|
123
|
+
```python
|
|
124
|
+
from apscheduler.jobstores.redis import RedisJobStore
|
|
125
|
+
from apscheduler.executors.asyncio import AsyncIOExecutor
|
|
126
|
+
|
|
127
|
+
scheduler = SchedulerManager.get_instance(
|
|
128
|
+
"distributed",
|
|
129
|
+
jobstores={"default": RedisJobStore(host="localhost", port=6379)},
|
|
130
|
+
executors={"default": AsyncIOExecutor()},
|
|
131
|
+
job_defaults={"coalesce": True, "max_instances": 1},
|
|
132
|
+
timezone="Asia/Shanghai",
|
|
133
|
+
)
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
## 任务管理
|
|
137
|
+
|
|
138
|
+
```python
|
|
139
|
+
# 添加任务
|
|
140
|
+
scheduler.add_job(my_task, CronTrigger(hour=2), id="my_task")
|
|
141
|
+
|
|
142
|
+
# 获取任务
|
|
143
|
+
job = scheduler.get_job("my_task")
|
|
144
|
+
jobs = scheduler.get_jobs()
|
|
145
|
+
|
|
146
|
+
# 暂停/恢复
|
|
147
|
+
scheduler.pause_job("my_task")
|
|
148
|
+
scheduler.resume_job("my_task")
|
|
149
|
+
|
|
150
|
+
# 移除
|
|
151
|
+
scheduler.remove_job("my_task")
|
|
152
|
+
|
|
153
|
+
# 重新调度
|
|
154
|
+
scheduler.reschedule_job("my_task", CronTrigger(hour=3))
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
## 监听器(高级)
|
|
158
|
+
|
|
159
|
+
通过底层 APScheduler 实例访问:
|
|
160
|
+
|
|
161
|
+
```python
|
|
162
|
+
from apscheduler.events import EVENT_JOB_EXECUTED, EVENT_JOB_ERROR
|
|
163
|
+
|
|
164
|
+
def job_listener(event):
|
|
165
|
+
if event.exception:
|
|
166
|
+
logger.error(f"任务失败: {event.job_id}")
|
|
167
|
+
else:
|
|
168
|
+
logger.info(f"任务完成: {event.job_id}")
|
|
169
|
+
|
|
170
|
+
scheduler.scheduler.add_listener(job_listener, EVENT_JOB_EXECUTED | EVENT_JOB_ERROR)
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
## 实践建议
|
|
174
|
+
|
|
175
|
+
1. **使用明确的 ID**:便于管理和调试
|
|
176
|
+
2. **合理设置间隔**:避免太频繁的任务
|
|
177
|
+
3. **异常处理**:在任务函数内捕获异常,避免影响调度器
|
|
178
|
+
4. **超时保护**:长运行任务使用 `asyncio.wait_for`
|
|
@@ -19,44 +19,8 @@ logger.exception("异常信息") # 自动记录堆栈
|
|
|
19
19
|
2024-01-15 12:00:00 | INFO | app.service:create:42 | abc123 - 操作成功
|
|
20
20
|
```
|
|
21
21
|
|
|
22
|
-
## 11.2 注入用户信息
|
|
23
22
|
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
```python
|
|
27
|
-
# app/auth/context.py
|
|
28
|
-
from contextvars import ContextVar
|
|
29
|
-
from aury.boot.common.logging import register_request_context
|
|
30
|
-
|
|
31
|
-
_user_id: ContextVar[str] = ContextVar("user_id", default="")
|
|
32
|
-
|
|
33
|
-
def set_user_id(uid: str) -> None:
|
|
34
|
-
_user_id.set(uid)
|
|
35
|
-
|
|
36
|
-
# 启动时注册(只需一次)
|
|
37
|
-
register_request_context("user_id", _user_id.get)
|
|
38
|
-
```
|
|
39
|
-
|
|
40
|
-
在认证中间件中设置(order < 100 以在日志中间件前执行):
|
|
41
|
-
|
|
42
|
-
```python
|
|
43
|
-
class AuthMiddleware(Middleware):
|
|
44
|
-
order = 50 # 在日志中间件(order=100)之前执行
|
|
45
|
-
|
|
46
|
-
async def dispatch(self, request, call_next):
|
|
47
|
-
user = await verify_token(request)
|
|
48
|
-
if user:
|
|
49
|
-
set_user_id(str(user.id))
|
|
50
|
-
return await call_next(request)
|
|
51
|
-
```
|
|
52
|
-
|
|
53
|
-
结果:
|
|
54
|
-
```
|
|
55
|
-
← GET /api/users | 状态: 200 | 耗时: 0.05s | Trace-ID: abc123
|
|
56
|
-
[REQUEST_CONTEXT] Trace-ID: abc123 | user_id: 123
|
|
57
|
-
```
|
|
58
|
-
|
|
59
|
-
## 11.3 性能监控装饰器
|
|
23
|
+
## 11.2 性能监控装饰器
|
|
60
24
|
|
|
61
25
|
```python
|
|
62
26
|
from aury.boot.common.logging import log_performance, log_exceptions
|
|
@@ -70,7 +34,7 @@ async def risky_operation():
|
|
|
70
34
|
...
|
|
71
35
|
```
|
|
72
36
|
|
|
73
|
-
## 11.
|
|
37
|
+
## 11.3 HTTP 请求日志
|
|
74
38
|
|
|
75
39
|
框架内置 `RequestLoggingMiddleware` 自动记录:
|
|
76
40
|
|
|
@@ -85,7 +49,7 @@ async def risky_operation():
|
|
|
85
49
|
慢请求: GET /api/reports | 耗时: 2.345s (超过1秒) | Trace-ID: abc123
|
|
86
50
|
```
|
|
87
51
|
|
|
88
|
-
## 11.
|
|
52
|
+
## 11.4 自定义日志文件
|
|
89
53
|
|
|
90
54
|
为特定业务创建独立的日志文件:
|
|
91
55
|
|
|
@@ -99,7 +63,7 @@ register_log_sink("payment", filter_key="payment")
|
|
|
99
63
|
logger.bind(payment=True).info(f"支付成功 | 订单: {{order_id}}")
|
|
100
64
|
```
|
|
101
65
|
|
|
102
|
-
## 11.
|
|
66
|
+
## 11.5 异步任务链路追踪
|
|
103
67
|
|
|
104
68
|
跨进程任务需要手动传递 trace_id:
|
|
105
69
|
|
|
@@ -117,7 +81,7 @@ async def process_order(order_id: str, trace_id: str | None = None):
|
|
|
117
81
|
logger.info(f"处理订单: {{order_id}}") # 自动包含 trace_id
|
|
118
82
|
```
|
|
119
83
|
|
|
120
|
-
## 11.
|
|
84
|
+
## 11.6 服务上下文隔离
|
|
121
85
|
|
|
122
86
|
日志自动按服务类型分离:
|
|
123
87
|
|
|
@@ -153,7 +153,7 @@ register_commands(app)
|
|
|
153
153
|
@app.command()
|
|
154
154
|
async def hello(name: str = "world") -> None:
|
|
155
155
|
"""示例:项目自定义命令。"""
|
|
156
|
-
print(f"Hello, {name} from {project_name_snake}!")
|
|
156
|
+
print(f"Hello, {{name}} from {project_name_snake}!")
|
|
157
157
|
```
|
|
158
158
|
|
|
159
159
|
> 注意:这里的 `app` 是 Typer 应用实例,`register_commands` 会把所有内置的 `init/generate/server/...` 等命令挂到你自己的 CLI 下。
|
|
@@ -6,13 +6,16 @@
|
|
|
6
6
|
也可通过 SCHEDULER_SCHEDULE_MODULES 环境变量指定自定义模块。
|
|
7
7
|
"""
|
|
8
8
|
|
|
9
|
+
# from apscheduler.triggers.cron import CronTrigger
|
|
10
|
+
# from apscheduler.triggers.interval import IntervalTrigger
|
|
11
|
+
#
|
|
9
12
|
# from aury.boot.common.logging import logger
|
|
10
13
|
# from aury.boot.infrastructure.scheduler import SchedulerManager
|
|
11
14
|
#
|
|
12
15
|
# scheduler = SchedulerManager.get_instance()
|
|
13
16
|
#
|
|
14
17
|
#
|
|
15
|
-
# @scheduler.scheduled_job(
|
|
18
|
+
# @scheduler.scheduled_job(IntervalTrigger(seconds=60))
|
|
16
19
|
# async def example_job():
|
|
17
20
|
# """示例定时任务,每 60 秒执行一次。"""
|
|
18
21
|
# logger.info("定时任务执行中...")
|
|
@@ -5,7 +5,6 @@
|
|
|
5
5
|
- 性能监控装饰器
|
|
6
6
|
- 异常日志装饰器
|
|
7
7
|
- 链路追踪 ID 支持
|
|
8
|
-
- 请求上下文注入(user_id 等)
|
|
9
8
|
- 自定义日志 sink 注册 API
|
|
10
9
|
|
|
11
10
|
日志文件:
|
|
@@ -27,10 +26,8 @@ logger.remove()
|
|
|
27
26
|
# 从子模块导入
|
|
28
27
|
from aury.boot.common.logging.context import (
|
|
29
28
|
ServiceContext,
|
|
30
|
-
get_request_contexts,
|
|
31
29
|
get_service_context,
|
|
32
30
|
get_trace_id,
|
|
33
|
-
register_request_context,
|
|
34
31
|
set_service_context,
|
|
35
32
|
set_trace_id,
|
|
36
33
|
)
|
|
@@ -55,7 +52,6 @@ __all__ = [
|
|
|
55
52
|
"ServiceContext",
|
|
56
53
|
"format_exception_java_style",
|
|
57
54
|
"get_class_logger",
|
|
58
|
-
"get_request_contexts",
|
|
59
55
|
"get_service_context",
|
|
60
56
|
"get_trace_id",
|
|
61
57
|
"log_exception",
|
|
@@ -63,7 +59,6 @@ __all__ = [
|
|
|
63
59
|
"log_performance",
|
|
64
60
|
"logger",
|
|
65
61
|
"register_log_sink",
|
|
66
|
-
"register_request_context",
|
|
67
62
|
"set_service_context",
|
|
68
63
|
"set_trace_id",
|
|
69
64
|
"setup_intercept",
|
|
@@ -1,11 +1,10 @@
|
|
|
1
1
|
"""日志上下文管理。
|
|
2
2
|
|
|
3
|
-
提供链路追踪 ID
|
|
3
|
+
提供链路追踪 ID、服务上下文的管理。
|
|
4
4
|
"""
|
|
5
5
|
|
|
6
6
|
from __future__ import annotations
|
|
7
7
|
|
|
8
|
-
from collections.abc import Callable
|
|
9
8
|
from contextvars import ContextVar
|
|
10
9
|
from enum import Enum
|
|
11
10
|
import uuid
|
|
@@ -24,8 +23,6 @@ _service_context: ContextVar[ServiceContext] = ContextVar("service_context", def
|
|
|
24
23
|
# 链路追踪 ID
|
|
25
24
|
_trace_id_var: ContextVar[str] = ContextVar("trace_id", default="")
|
|
26
25
|
|
|
27
|
-
# 请求上下文字段注册表(用户可注册自定义字段,如 user_id, tenant_id)
|
|
28
|
-
_request_context_getters: dict[str, Callable[[], str]] = {}
|
|
29
26
|
|
|
30
27
|
|
|
31
28
|
def get_service_context() -> ServiceContext:
|
|
@@ -75,58 +72,10 @@ def set_trace_id(trace_id: str) -> None:
|
|
|
75
72
|
_trace_id_var.set(trace_id)
|
|
76
73
|
|
|
77
74
|
|
|
78
|
-
def register_request_context(name: str, getter: Callable[[], str]) -> None:
|
|
79
|
-
"""注册请求上下文字段。
|
|
80
|
-
|
|
81
|
-
注册后,该字段会在每个请求结束时记录一次(与 trace_id 关联)。
|
|
82
|
-
适用于 user_id、tenant_id 等需要关联到请求但不需要每行日志都记录的信息。
|
|
83
|
-
|
|
84
|
-
Args:
|
|
85
|
-
name: 字段名(如 "user_id", "tenant_id")
|
|
86
|
-
getter: 获取当前值的函数(通常从 ContextVar 读取)
|
|
87
|
-
|
|
88
|
-
使用示例:
|
|
89
|
-
from contextvars import ContextVar
|
|
90
|
-
from aury.boot.common.logging import register_request_context
|
|
91
|
-
|
|
92
|
-
# 定义上下文变量
|
|
93
|
-
_user_id: ContextVar[str] = ContextVar("user_id", default="")
|
|
94
|
-
|
|
95
|
-
def set_user_id(uid: str):
|
|
96
|
-
_user_id.set(uid)
|
|
97
|
-
|
|
98
|
-
# 启动时注册(一次)
|
|
99
|
-
register_request_context("user_id", _user_id.get)
|
|
100
|
-
|
|
101
|
-
# Auth 中间件中设置(每次请求)
|
|
102
|
-
set_user_id(str(user.id))
|
|
103
|
-
"""
|
|
104
|
-
_request_context_getters[name] = getter
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
def get_request_contexts() -> dict[str, str]:
|
|
108
|
-
"""获取所有已注册的请求上下文当前值。
|
|
109
|
-
|
|
110
|
-
Returns:
|
|
111
|
-
字段名到值的字典(仅包含非空值)
|
|
112
|
-
"""
|
|
113
|
-
result = {}
|
|
114
|
-
for name, getter in _request_context_getters.items():
|
|
115
|
-
try:
|
|
116
|
-
value = getter()
|
|
117
|
-
if value: # 只包含非空值
|
|
118
|
-
result[name] = value
|
|
119
|
-
except Exception:
|
|
120
|
-
pass # 忽略获取失败的字段
|
|
121
|
-
return result
|
|
122
|
-
|
|
123
|
-
|
|
124
75
|
__all__ = [
|
|
125
76
|
"ServiceContext",
|
|
126
|
-
"get_request_contexts",
|
|
127
77
|
"get_service_context",
|
|
128
78
|
"get_trace_id",
|
|
129
|
-
"register_request_context",
|
|
130
79
|
"set_service_context",
|
|
131
80
|
"set_trace_id",
|
|
132
81
|
]
|
|
@@ -110,13 +110,15 @@ class CacheManager:
|
|
|
110
110
|
supported = ", ".join(b.value for b in CacheBackend)
|
|
111
111
|
raise ValueError(f"不支持的缓存后端: {backend}。支持: {supported}")
|
|
112
112
|
|
|
113
|
-
#
|
|
113
|
+
# 保存配置(用于启动横幅等场景展示)
|
|
114
114
|
self._config = {"CACHE_TYPE": backend.value}
|
|
115
115
|
|
|
116
116
|
# 根据后端类型构建配置并创建后端
|
|
117
117
|
if backend == CacheBackend.REDIS:
|
|
118
118
|
if not url:
|
|
119
119
|
raise ValueError("Redis 缓存需要提供 url 参数")
|
|
120
|
+
# 记录 URL 以便在启动横幅中展示(会通过 mask_url 脱敏)
|
|
121
|
+
self._config["CACHE_URL"] = url
|
|
120
122
|
self._backend = await CacheFactory.create(
|
|
121
123
|
"redis", url=url, serializer=serializer
|
|
122
124
|
)
|
|
@@ -128,6 +130,8 @@ class CacheManager:
|
|
|
128
130
|
cache_url = url or (servers[0] if servers else None)
|
|
129
131
|
if not cache_url:
|
|
130
132
|
raise ValueError("Memcached 缓存需要提供 url 参数")
|
|
133
|
+
# 同样记录 URL,便于在启动横幅中展示
|
|
134
|
+
self._config["CACHE_URL"] = cache_url
|
|
131
135
|
self._backend = await CacheFactory.create(
|
|
132
136
|
"memcached", servers=cache_url
|
|
133
137
|
)
|
|
@@ -10,7 +10,7 @@ from collections.abc import AsyncGenerator
|
|
|
10
10
|
from contextlib import asynccontextmanager
|
|
11
11
|
|
|
12
12
|
from sqlalchemy import text
|
|
13
|
-
from sqlalchemy.exc import DisconnectionError, OperationalError
|
|
13
|
+
from sqlalchemy.exc import DisconnectionError, OperationalError, SQLAlchemyError
|
|
14
14
|
from sqlalchemy.ext.asyncio import AsyncEngine, AsyncSession, async_sessionmaker, create_async_engine
|
|
15
15
|
|
|
16
16
|
from aury.boot.common.logging import logger
|
|
@@ -237,10 +237,16 @@ class DatabaseManager:
|
|
|
237
237
|
try:
|
|
238
238
|
await self._check_session_connection(session)
|
|
239
239
|
yield session
|
|
240
|
-
except
|
|
240
|
+
except SQLAlchemyError as exc:
|
|
241
|
+
# 只捕获数据库相关异常
|
|
241
242
|
await session.rollback()
|
|
242
243
|
logger.exception(f"数据库会话异常: {exc}")
|
|
243
244
|
raise
|
|
245
|
+
except Exception:
|
|
246
|
+
# 非数据库异常(如请求验证错误):仍需回滚以确保事务一致性
|
|
247
|
+
# 但不记录为数据库异常,直接传播
|
|
248
|
+
await session.rollback()
|
|
249
|
+
raise
|
|
244
250
|
finally:
|
|
245
251
|
await session.close()
|
|
246
252
|
|
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
- 生命周期管理
|
|
7
7
|
- 自动设置日志上下文(调度器任务日志自动写入 scheduler_xxx.log)
|
|
8
8
|
- 支持多个命名实例
|
|
9
|
+
- 支持 APScheduler 完整配置(jobstores、executors、job_defaults、timezone)
|
|
9
10
|
"""
|
|
10
11
|
|
|
11
12
|
from __future__ import annotations
|
|
@@ -20,52 +21,53 @@ from aury.boot.common.logging import logger, set_service_context
|
|
|
20
21
|
# 延迟导入 apscheduler(可选依赖)
|
|
21
22
|
try:
|
|
22
23
|
from apscheduler.schedulers.asyncio import AsyncIOScheduler
|
|
23
|
-
from apscheduler.triggers.cron import CronTrigger
|
|
24
|
-
from apscheduler.triggers.interval import IntervalTrigger
|
|
25
24
|
_APSCHEDULER_AVAILABLE = True
|
|
26
25
|
except ImportError:
|
|
27
26
|
_APSCHEDULER_AVAILABLE = False
|
|
28
|
-
# 创建占位符类型,避免类型检查错误
|
|
29
27
|
if TYPE_CHECKING:
|
|
30
28
|
from apscheduler.schedulers.asyncio import AsyncIOScheduler
|
|
31
|
-
from apscheduler.triggers.cron import CronTrigger
|
|
32
|
-
from apscheduler.triggers.interval import IntervalTrigger
|
|
33
29
|
else:
|
|
34
30
|
AsyncIOScheduler = None
|
|
35
|
-
CronTrigger = None
|
|
36
|
-
IntervalTrigger = None
|
|
37
31
|
|
|
38
32
|
|
|
39
33
|
class SchedulerManager:
|
|
40
34
|
"""调度器管理器(命名多实例)。
|
|
41
35
|
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
36
|
+
完全透传 APScheduler 的所有配置,支持:
|
|
37
|
+
- jobstores: 任务存储(内存/Redis/SQLAlchemy/MongoDB)
|
|
38
|
+
- executors: 执行器(AsyncIO/ThreadPool/ProcessPool)
|
|
39
|
+
- job_defaults: 任务默认配置(coalesce/max_instances/misfire_grace_time)
|
|
40
|
+
- timezone: 时区
|
|
47
41
|
|
|
48
42
|
使用示例:
|
|
49
|
-
|
|
43
|
+
from apscheduler.triggers.cron import CronTrigger
|
|
44
|
+
from apscheduler.triggers.interval import IntervalTrigger
|
|
45
|
+
from apscheduler.jobstores.redis import RedisJobStore
|
|
46
|
+
from apscheduler.executors.asyncio import AsyncIOExecutor
|
|
47
|
+
|
|
48
|
+
# 默认实例(内存存储)
|
|
50
49
|
scheduler = SchedulerManager.get_instance()
|
|
51
|
-
await scheduler.initialize()
|
|
52
50
|
|
|
53
|
-
#
|
|
54
|
-
|
|
55
|
-
|
|
51
|
+
# 带配置的实例
|
|
52
|
+
scheduler = SchedulerManager.get_instance(
|
|
53
|
+
"persistent",
|
|
54
|
+
jobstores={"default": RedisJobStore(host="localhost")},
|
|
55
|
+
executors={"default": AsyncIOExecutor()},
|
|
56
|
+
job_defaults={"coalesce": True, "max_instances": 3},
|
|
57
|
+
timezone="Asia/Shanghai",
|
|
58
|
+
)
|
|
56
59
|
|
|
57
60
|
# 注册任务
|
|
58
|
-
scheduler.
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
seconds=60
|
|
62
|
-
)
|
|
61
|
+
@scheduler.scheduled_job(IntervalTrigger(seconds=60))
|
|
62
|
+
async def my_task():
|
|
63
|
+
...
|
|
63
64
|
|
|
64
65
|
# 启动调度器
|
|
65
66
|
scheduler.start()
|
|
66
67
|
"""
|
|
67
68
|
|
|
68
69
|
_instances: dict[str, SchedulerManager] = {}
|
|
70
|
+
_instance_configs: dict[str, dict[str, Any]] = {} # 存储实例配置
|
|
69
71
|
|
|
70
72
|
def __init__(self, name: str = "default") -> None:
|
|
71
73
|
"""初始化调度器管理器。
|
|
@@ -80,16 +82,56 @@ class SchedulerManager:
|
|
|
80
82
|
self._started: bool = False # 调度器是否已启动
|
|
81
83
|
|
|
82
84
|
@classmethod
|
|
83
|
-
def get_instance(
|
|
85
|
+
def get_instance(
|
|
86
|
+
cls,
|
|
87
|
+
name: str = "default",
|
|
88
|
+
*,
|
|
89
|
+
jobstores: dict[str, Any] | None = None,
|
|
90
|
+
executors: dict[str, Any] | None = None,
|
|
91
|
+
job_defaults: dict[str, Any] | None = None,
|
|
92
|
+
timezone: str | Any | None = None,
|
|
93
|
+
) -> SchedulerManager:
|
|
84
94
|
"""获取指定名称的实例。
|
|
85
95
|
|
|
86
96
|
首次获取时会同步初始化调度器实例,使装饰器可以在模块导入时使用。
|
|
87
97
|
|
|
88
98
|
Args:
|
|
89
99
|
name: 实例名称,默认为 "default"
|
|
100
|
+
jobstores: APScheduler jobstores 配置,如 {"default": RedisJobStore(...)}
|
|
101
|
+
executors: APScheduler executors 配置,如 {"default": AsyncIOExecutor()}
|
|
102
|
+
job_defaults: 任务默认配置,如 {"coalesce": True, "max_instances": 3}
|
|
103
|
+
timezone: 时区,如 "Asia/Shanghai" 或 pytz 时区对象
|
|
90
104
|
|
|
91
105
|
Returns:
|
|
92
106
|
SchedulerManager: 调度器管理器实例
|
|
107
|
+
|
|
108
|
+
示例:
|
|
109
|
+
# 默认配置(内存存储)
|
|
110
|
+
scheduler = SchedulerManager.get_instance()
|
|
111
|
+
|
|
112
|
+
# Redis 持久化存储
|
|
113
|
+
from apscheduler.jobstores.redis import RedisJobStore
|
|
114
|
+
scheduler = SchedulerManager.get_instance(
|
|
115
|
+
"persistent",
|
|
116
|
+
jobstores={"default": RedisJobStore(host="localhost", port=6379)},
|
|
117
|
+
)
|
|
118
|
+
|
|
119
|
+
# SQLAlchemy 数据库存储
|
|
120
|
+
from apscheduler.jobstores.sqlalchemy import SQLAlchemyJobStore
|
|
121
|
+
scheduler = SchedulerManager.get_instance(
|
|
122
|
+
"db",
|
|
123
|
+
jobstores={"default": SQLAlchemyJobStore(url="sqlite:///jobs.db")},
|
|
124
|
+
)
|
|
125
|
+
|
|
126
|
+
# 完整配置
|
|
127
|
+
from apscheduler.executors.asyncio import AsyncIOExecutor
|
|
128
|
+
scheduler = SchedulerManager.get_instance(
|
|
129
|
+
"full",
|
|
130
|
+
jobstores={"default": RedisJobStore(host="localhost")},
|
|
131
|
+
executors={"default": AsyncIOExecutor()},
|
|
132
|
+
job_defaults={"coalesce": True, "max_instances": 3, "misfire_grace_time": 60},
|
|
133
|
+
timezone="Asia/Shanghai",
|
|
134
|
+
)
|
|
93
135
|
"""
|
|
94
136
|
if name not in cls._instances:
|
|
95
137
|
if not _APSCHEDULER_AVAILABLE:
|
|
@@ -97,9 +139,22 @@ class SchedulerManager:
|
|
|
97
139
|
"apscheduler 未安装。请安装可选依赖: pip install 'aury-boot[scheduler-apscheduler]'"
|
|
98
140
|
)
|
|
99
141
|
instance = cls(name)
|
|
100
|
-
|
|
142
|
+
|
|
143
|
+
# 构建 APScheduler 配置
|
|
144
|
+
scheduler_kwargs: dict[str, Any] = {}
|
|
145
|
+
if jobstores:
|
|
146
|
+
scheduler_kwargs["jobstores"] = jobstores
|
|
147
|
+
if executors:
|
|
148
|
+
scheduler_kwargs["executors"] = executors
|
|
149
|
+
if job_defaults:
|
|
150
|
+
scheduler_kwargs["job_defaults"] = job_defaults
|
|
151
|
+
if timezone:
|
|
152
|
+
scheduler_kwargs["timezone"] = timezone
|
|
153
|
+
|
|
154
|
+
instance._scheduler = AsyncIOScheduler(**scheduler_kwargs)
|
|
101
155
|
instance._initialized = True
|
|
102
156
|
cls._instances[name] = instance
|
|
157
|
+
cls._instance_configs[name] = scheduler_kwargs
|
|
103
158
|
logger.debug(f"调度器实例已创建: {name}")
|
|
104
159
|
return cls._instances[name]
|
|
105
160
|
|
|
@@ -114,8 +169,10 @@ class SchedulerManager:
|
|
|
114
169
|
"""
|
|
115
170
|
if name is None:
|
|
116
171
|
cls._instances.clear()
|
|
172
|
+
cls._instance_configs.clear()
|
|
117
173
|
elif name in cls._instances:
|
|
118
174
|
del cls._instances[name]
|
|
175
|
+
cls._instance_configs.pop(name, None)
|
|
119
176
|
|
|
120
177
|
async def initialize(self) -> SchedulerManager:
|
|
121
178
|
"""初始化调度器(链式调用)。
|
|
@@ -146,65 +203,43 @@ class SchedulerManager:
|
|
|
146
203
|
def add_job(
|
|
147
204
|
self,
|
|
148
205
|
func: Callable,
|
|
149
|
-
trigger:
|
|
206
|
+
trigger: Any,
|
|
150
207
|
*,
|
|
151
|
-
seconds: int | None = None,
|
|
152
|
-
minutes: int | None = None,
|
|
153
|
-
hours: int | None = None,
|
|
154
|
-
days: int | None = None,
|
|
155
|
-
cron: str | None = None,
|
|
156
208
|
id: str | None = None,
|
|
157
209
|
**kwargs: Any,
|
|
158
210
|
) -> None:
|
|
159
211
|
"""添加任务。
|
|
160
212
|
|
|
213
|
+
直接使用 APScheduler 原生 trigger 对象,不做任何封装。
|
|
214
|
+
|
|
161
215
|
Args:
|
|
162
216
|
func: 任务函数
|
|
163
|
-
trigger:
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
217
|
+
trigger: APScheduler 触发器对象,如:
|
|
218
|
+
- IntervalTrigger(seconds=60)
|
|
219
|
+
- CronTrigger(hour="*")
|
|
220
|
+
- CronTrigger.from_crontab("0 * * * *")
|
|
221
|
+
id: 任务ID(可选,默认使用函数完整路径)
|
|
222
|
+
**kwargs: 其他 APScheduler add_job 参数
|
|
223
|
+
|
|
224
|
+
示例:
|
|
225
|
+
from apscheduler.triggers.cron import CronTrigger
|
|
226
|
+
from apscheduler.triggers.interval import IntervalTrigger
|
|
227
|
+
|
|
228
|
+
# 每小时执行
|
|
229
|
+
scheduler.add_job(my_task, CronTrigger(hour="*"))
|
|
230
|
+
|
|
231
|
+
# 每 30 分钟执行
|
|
232
|
+
scheduler.add_job(my_task, IntervalTrigger(minutes=30))
|
|
233
|
+
|
|
234
|
+
# 每天凌晨 2 点执行
|
|
235
|
+
scheduler.add_job(my_task, CronTrigger(hour=2, minute=0))
|
|
236
|
+
|
|
237
|
+
# 使用 crontab 表达式
|
|
238
|
+
scheduler.add_job(my_task, CronTrigger.from_crontab("0 2 * * *"))
|
|
171
239
|
"""
|
|
172
240
|
if not self._initialized:
|
|
173
241
|
raise RuntimeError("调度器未初始化")
|
|
174
242
|
|
|
175
|
-
# 使用函数式编程构建触发器
|
|
176
|
-
def build_interval_trigger() -> IntervalTrigger:
|
|
177
|
-
"""构建间隔触发器。"""
|
|
178
|
-
if seconds:
|
|
179
|
-
return IntervalTrigger(seconds=seconds)
|
|
180
|
-
if minutes:
|
|
181
|
-
return IntervalTrigger(minutes=minutes)
|
|
182
|
-
if hours:
|
|
183
|
-
return IntervalTrigger(hours=hours)
|
|
184
|
-
if days:
|
|
185
|
-
return IntervalTrigger(days=days)
|
|
186
|
-
raise ValueError("必须指定间隔时间")
|
|
187
|
-
|
|
188
|
-
def build_cron_trigger() -> CronTrigger:
|
|
189
|
-
"""构建Cron触发器。"""
|
|
190
|
-
if not cron:
|
|
191
|
-
raise ValueError("Cron触发器必须提供cron表达式")
|
|
192
|
-
return CronTrigger.from_crontab(cron)
|
|
193
|
-
|
|
194
|
-
trigger_builders: dict[str, Callable[[], Any]] = {
|
|
195
|
-
"interval": build_interval_trigger,
|
|
196
|
-
"cron": build_cron_trigger,
|
|
197
|
-
}
|
|
198
|
-
|
|
199
|
-
builder = trigger_builders.get(trigger)
|
|
200
|
-
if builder is None:
|
|
201
|
-
available = ", ".join(trigger_builders.keys())
|
|
202
|
-
raise ValueError(
|
|
203
|
-
f"不支持的触发器类型: {trigger}。可用类型: {available}"
|
|
204
|
-
)
|
|
205
|
-
|
|
206
|
-
trigger_obj = builder()
|
|
207
|
-
|
|
208
243
|
# 包装任务函数,自动设置日志上下文
|
|
209
244
|
wrapped_func = self._wrap_with_context(func)
|
|
210
245
|
|
|
@@ -212,12 +247,12 @@ class SchedulerManager:
|
|
|
212
247
|
job_id = id or f"{func.__module__}.{func.__name__}"
|
|
213
248
|
self._scheduler.add_job(
|
|
214
249
|
func=wrapped_func,
|
|
215
|
-
trigger=
|
|
250
|
+
trigger=trigger,
|
|
216
251
|
id=job_id,
|
|
217
252
|
**kwargs,
|
|
218
253
|
)
|
|
219
254
|
|
|
220
|
-
logger.info(f"任务已注册: {job_id} | 触发器: {trigger}")
|
|
255
|
+
logger.info(f"任务已注册: {job_id} | 触发器: {type(trigger).__name__}")
|
|
221
256
|
|
|
222
257
|
def _wrap_with_context(self, func: Callable) -> Callable:
|
|
223
258
|
"""包装任务函数,自动设置 scheduler 日志上下文。"""
|
|
@@ -304,49 +339,19 @@ class SchedulerManager:
|
|
|
304
339
|
def reschedule_job(
|
|
305
340
|
self,
|
|
306
341
|
job_id: str,
|
|
307
|
-
trigger:
|
|
308
|
-
*,
|
|
309
|
-
seconds: int | None = None,
|
|
310
|
-
minutes: int | None = None,
|
|
311
|
-
hours: int | None = None,
|
|
312
|
-
days: int | None = None,
|
|
313
|
-
cron: str | None = None,
|
|
342
|
+
trigger: Any,
|
|
314
343
|
) -> None:
|
|
315
|
-
"""
|
|
344
|
+
"""重新调度任务。
|
|
316
345
|
|
|
317
346
|
Args:
|
|
318
347
|
job_id: 任务ID
|
|
319
|
-
trigger:
|
|
320
|
-
seconds: 间隔秒数
|
|
321
|
-
minutes: 间隔分钟数
|
|
322
|
-
hours: 间隔小时数
|
|
323
|
-
days: 间隔天数
|
|
324
|
-
cron: Cron表达式
|
|
348
|
+
trigger: APScheduler 触发器对象
|
|
325
349
|
"""
|
|
326
350
|
if not self._scheduler:
|
|
327
351
|
raise RuntimeError("调度器未初始化")
|
|
328
352
|
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
if seconds:
|
|
332
|
-
trigger_obj = IntervalTrigger(seconds=seconds)
|
|
333
|
-
elif minutes:
|
|
334
|
-
trigger_obj = IntervalTrigger(minutes=minutes)
|
|
335
|
-
elif hours:
|
|
336
|
-
trigger_obj = IntervalTrigger(hours=hours)
|
|
337
|
-
elif days:
|
|
338
|
-
trigger_obj = IntervalTrigger(days=days)
|
|
339
|
-
else:
|
|
340
|
-
raise ValueError("必须指定间隔时间")
|
|
341
|
-
elif trigger == "cron":
|
|
342
|
-
if not cron:
|
|
343
|
-
raise ValueError("Cron触发器必须提供cron表达式")
|
|
344
|
-
trigger_obj = CronTrigger.from_crontab(cron)
|
|
345
|
-
else:
|
|
346
|
-
raise ValueError(f"不支持的触发器类型: {trigger}")
|
|
347
|
-
|
|
348
|
-
self._scheduler.reschedule_job(job_id, trigger=trigger_obj)
|
|
349
|
-
logger.info(f"任务已重新调度: {job_id} | 触发器: {trigger}")
|
|
353
|
+
self._scheduler.reschedule_job(job_id, trigger=trigger)
|
|
354
|
+
logger.info(f"任务已重新调度: {job_id} | 触发器: {type(trigger).__name__}")
|
|
350
355
|
|
|
351
356
|
def pause_job(self, job_id: str) -> None:
|
|
352
357
|
"""暂停单个任务。
|
|
@@ -409,38 +414,35 @@ class SchedulerManager:
|
|
|
409
414
|
|
|
410
415
|
def scheduled_job(
|
|
411
416
|
self,
|
|
412
|
-
trigger:
|
|
417
|
+
trigger: Any,
|
|
413
418
|
*,
|
|
414
|
-
seconds: int | None = None,
|
|
415
|
-
minutes: int | None = None,
|
|
416
|
-
hours: int | None = None,
|
|
417
|
-
days: int | None = None,
|
|
418
|
-
cron: str | None = None,
|
|
419
419
|
id: str | None = None,
|
|
420
420
|
**kwargs: Any,
|
|
421
421
|
) -> Callable[[Callable], Callable]:
|
|
422
422
|
"""任务注册装饰器。
|
|
423
423
|
|
|
424
424
|
使用示例:
|
|
425
|
+
from apscheduler.triggers.cron import CronTrigger
|
|
426
|
+
from apscheduler.triggers.interval import IntervalTrigger
|
|
427
|
+
|
|
425
428
|
scheduler = SchedulerManager.get_instance()
|
|
426
429
|
|
|
427
|
-
@scheduler.scheduled_job(
|
|
430
|
+
@scheduler.scheduled_job(IntervalTrigger(seconds=60))
|
|
428
431
|
async def my_task():
|
|
429
432
|
print("Task executed")
|
|
430
433
|
|
|
431
|
-
@scheduler.scheduled_job(
|
|
434
|
+
@scheduler.scheduled_job(CronTrigger(hour="*"))
|
|
435
|
+
async def hourly_task():
|
|
436
|
+
print("Hourly task")
|
|
437
|
+
|
|
438
|
+
@scheduler.scheduled_job(CronTrigger.from_crontab("0 0 * * *"))
|
|
432
439
|
async def daily_task():
|
|
433
440
|
print("Daily task")
|
|
434
441
|
|
|
435
442
|
Args:
|
|
436
|
-
trigger:
|
|
437
|
-
seconds: 间隔秒数
|
|
438
|
-
minutes: 间隔分钟数
|
|
439
|
-
hours: 间隔小时数
|
|
440
|
-
days: 间隔天数
|
|
441
|
-
cron: Cron表达式
|
|
443
|
+
trigger: APScheduler 触发器对象
|
|
442
444
|
id: 任务ID
|
|
443
|
-
**kwargs:
|
|
445
|
+
**kwargs: 其他 APScheduler add_job 参数
|
|
444
446
|
|
|
445
447
|
Returns:
|
|
446
448
|
装饰器函数
|
|
@@ -449,11 +451,6 @@ class SchedulerManager:
|
|
|
449
451
|
job_config = {
|
|
450
452
|
"func": func,
|
|
451
453
|
"trigger": trigger,
|
|
452
|
-
"seconds": seconds,
|
|
453
|
-
"minutes": minutes,
|
|
454
|
-
"hours": hours,
|
|
455
|
-
"days": days,
|
|
456
|
-
"cron": cron,
|
|
457
454
|
"id": id,
|
|
458
455
|
**kwargs,
|
|
459
456
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
aury/boot/__init__.py,sha256=pCno-EInnpIBa1OtxNYF-JWf9j95Cd2h6vmu0xqa_-4,1791
|
|
2
|
-
aury/boot/_version.py,sha256=
|
|
2
|
+
aury/boot/_version.py,sha256=S11KPmKYJXKQX0D8GWJ9ou0JkWsMIQoAxzWzBzTnRp4,706
|
|
3
3
|
aury/boot/application/__init__.py,sha256=0o_XmiwFCeAu06VHggS8I1e7_nSMoRq0Hcm0fYfCywU,3071
|
|
4
4
|
aury/boot/application/adapter/__init__.py,sha256=e1bcSb1bxUMfofTwiCuHBZJk5-STkMCWPF2EJXHQ7UU,3976
|
|
5
5
|
aury/boot/application/adapter/base.py,sha256=Ar_66fiHPDEmV-1DKnqXKwc53p3pozG31bgTJTEUriY,15763
|
|
@@ -9,12 +9,12 @@ aury/boot/application/adapter/exceptions.py,sha256=Kzm-ytRxdUnSMIcWCSOHPxo4Jh_A6
|
|
|
9
9
|
aury/boot/application/adapter/http.py,sha256=4TADsSzdSRU63307dmmo-2U_JpVP12mwTFy66B5Ps-w,10759
|
|
10
10
|
aury/boot/application/app/__init__.py,sha256=I8FfCKDuDQsGzAK6BevyfdtAwieMUVYu6qgVQzBazpE,830
|
|
11
11
|
aury/boot/application/app/base.py,sha256=wBF7q_kZp5uWUeWy95oElBAsc43xJ4RZ1s6tMGIiCac,17253
|
|
12
|
-
aury/boot/application/app/components.py,sha256
|
|
12
|
+
aury/boot/application/app/components.py,sha256=-blDjMF87GYFmFyk4DHm2KJDcm27HpMxGpNaEFluwEE,22403
|
|
13
13
|
aury/boot/application/app/middlewares.py,sha256=BXe2H14FHzJUVpQM6DZUm-zfZRXSXIi1QIZ4_3izfHw,3306
|
|
14
|
-
aury/boot/application/app/startup.py,sha256=
|
|
14
|
+
aury/boot/application/app/startup.py,sha256=DHKt3C2G7V5XfFr1SQMl14tNzcuDd9MqUVAxi274HDQ,7873
|
|
15
15
|
aury/boot/application/config/__init__.py,sha256=Dd-myRSBCM18DXXsi863h0cJG5VFrI10xMRtjnvelGo,1894
|
|
16
16
|
aury/boot/application/config/multi_instance.py,sha256=RXSp-xP8-bKMDEhq3SeL7T3lS8-vpRlvBEVBuZVjVK4,6475
|
|
17
|
-
aury/boot/application/config/settings.py,sha256=
|
|
17
|
+
aury/boot/application/config/settings.py,sha256=WILVLfZCMyximKKZ6uuzxa82mcdhPNqa2G5iX90L1Wc,30619
|
|
18
18
|
aury/boot/application/constants/__init__.py,sha256=DCXs13_VVaQWHqO-qpJoZwRd7HIexiirtw_nu8msTXE,340
|
|
19
19
|
aury/boot/application/constants/components.py,sha256=hDRs3YxpnfIFcGaUa1DYqBRwmV2_dceOlcCXabHE3fk,1006
|
|
20
20
|
aury/boot/application/constants/scheduler.py,sha256=S77FBIvHlyruvlabRWZJ2J1YAs2xWXPQI2yuGdGUDNA,471
|
|
@@ -29,7 +29,7 @@ aury/boot/application/interfaces/__init__.py,sha256=EGbiCL8IoGseylLVZO29Lkt3luyg
|
|
|
29
29
|
aury/boot/application/interfaces/egress.py,sha256=t8FK17V649rsm65uAeBruYr2mhfcqJiIzkS8UPsOzlc,5346
|
|
30
30
|
aury/boot/application/interfaces/ingress.py,sha256=rlflJ4nxAZ2Mc3Iy8ZX__GRgfAWcMYYzLhHL2NSk4_U,2425
|
|
31
31
|
aury/boot/application/middleware/__init__.py,sha256=T01fmbcdO0Sm6JE74g23uuDyebBGYA4DMZMDBl0L00w,258
|
|
32
|
-
aury/boot/application/middleware/logging.py,sha256=
|
|
32
|
+
aury/boot/application/middleware/logging.py,sha256=d3wbprbCxFJ6TpeEDomTSnjQ7sIadYxXnnKS6b_Wj38,11899
|
|
33
33
|
aury/boot/application/migrations/__init__.py,sha256=Z5Gizx7f3AImRcl3cooiIDAZcNi5W-6GvB7mK5w1TNA,204
|
|
34
34
|
aury/boot/application/migrations/manager.py,sha256=G7mzkNA3MFjyQmM2UwY0ZFNgGGVS4W5GoG2Sbj5AUXk,23685
|
|
35
35
|
aury/boot/application/migrations/setup.py,sha256=cxzBIHGzRXhlIYs4_ZW6P85Z9A7uVnTq_dmQFKp3ha4,6485
|
|
@@ -61,7 +61,7 @@ aury/boot/commands/templates/generate/model.py.tpl,sha256=knFwMyGZ7wMpzH4_bQD_V1
|
|
|
61
61
|
aury/boot/commands/templates/generate/repository.py.tpl,sha256=xoEg6lPAaLIRDeFy4I0FBsPPVLSy91h6xosAlaCL_mM,590
|
|
62
62
|
aury/boot/commands/templates/generate/schema.py.tpl,sha256=HIaY5B0UG_S188nQLrZDEJ0q73WPdb7BmCdc0tseZA4,545
|
|
63
63
|
aury/boot/commands/templates/generate/service.py.tpl,sha256=2hwQ8e4a5d_bIMx_jGDobdmKPMFLBlfQrQVQH4Ym5k4,1842
|
|
64
|
-
aury/boot/commands/templates/project/AGENTS.md.tpl,sha256=
|
|
64
|
+
aury/boot/commands/templates/project/AGENTS.md.tpl,sha256=AzjUt98ojms1CSUT1kNzCNLZFLBW5BGXpSwm0yJzYeI,7797
|
|
65
65
|
aury/boot/commands/templates/project/README.md.tpl,sha256=oCeBiukk6Pa3hrCKybkfM2sIRHsPZ15nlwuFTUSFDwY,2459
|
|
66
66
|
aury/boot/commands/templates/project/admin_console_init.py.tpl,sha256=K81L14thyEhRA8lFCQJVZL_NU22-sBz0xS68MJPeoCo,1541
|
|
67
67
|
aury/boot/commands/templates/project/config.py.tpl,sha256=H_B05FypBJxTjb7qIL91zC1C9e37Pk7C9gO0-b3CqNs,1009
|
|
@@ -76,16 +76,16 @@ aury/boot/commands/templates/project/aury_docs/04-schema.md.tpl,sha256=ZwwKhUbLI
|
|
|
76
76
|
aury/boot/commands/templates/project/aury_docs/05-api.md.tpl,sha256=oPzda3V6ZPDDEW-5MwyzmsMRuu5mXrsRGEq3lj0M-58,2997
|
|
77
77
|
aury/boot/commands/templates/project/aury_docs/06-exception.md.tpl,sha256=Tv_Q5lsScHzvtcaFWmuQzN4YqvpcWZIdXS8jw99K29E,3340
|
|
78
78
|
aury/boot/commands/templates/project/aury_docs/07-cache.md.tpl,sha256=EQMI7vJIwJT-VdG4p1GMCDEo58DCO1n6V-MvUzGSaS0,3411
|
|
79
|
-
aury/boot/commands/templates/project/aury_docs/08-scheduler.md.tpl,sha256=
|
|
79
|
+
aury/boot/commands/templates/project/aury_docs/08-scheduler.md.tpl,sha256=A6H-V4HKXysnFawtBI4rLUeQLpuV1Igi8V7wOUSNmCM,4438
|
|
80
80
|
aury/boot/commands/templates/project/aury_docs/09-tasks.md.tpl,sha256=swHOQ_pWPtW8Bsy1arPu2OeIgs1FoKsJ2AsVSYUWPHY,931
|
|
81
81
|
aury/boot/commands/templates/project/aury_docs/10-storage.md.tpl,sha256=mhe0j0S51ndPJLjaQ6yD8OPYBEO02NHumJVbBvz2qkw,4320
|
|
82
|
-
aury/boot/commands/templates/project/aury_docs/11-logging.md.tpl,sha256=
|
|
82
|
+
aury/boot/commands/templates/project/aury_docs/11-logging.md.tpl,sha256=bwxFCGQsO9cTEbwqJF1xcjsZKP82HRWhIMRUS0c9_ZI,2435
|
|
83
83
|
aury/boot/commands/templates/project/aury_docs/12-admin.md.tpl,sha256=6z3mN54qP2jtpTFOJBLVexvEv0ZHXYKjncvpZG4yOdw,1883
|
|
84
84
|
aury/boot/commands/templates/project/aury_docs/13-channel.md.tpl,sha256=rdtlog3ajcSsT0fq9a_US3MPcZhTvDo72eT6hetg4aI,3376
|
|
85
85
|
aury/boot/commands/templates/project/aury_docs/14-mq.md.tpl,sha256=4bxLQBbCi0Fue0VQWOPt6acZ5P00BoLkCoLPQe_8k4U,2396
|
|
86
86
|
aury/boot/commands/templates/project/aury_docs/15-events.md.tpl,sha256=a4wQRgVPuYUGTGmw_lX1HJH_yFTbD30mBz7Arc4zgfs,3361
|
|
87
87
|
aury/boot/commands/templates/project/aury_docs/16-adapter.md.tpl,sha256=pkmJkZw2Ca6_uYk2jZvAb8DozjBa2tWq_t3gtq1lFSk,11456
|
|
88
|
-
aury/boot/commands/templates/project/aury_docs/99-cli.md.tpl,sha256=
|
|
88
|
+
aury/boot/commands/templates/project/aury_docs/99-cli.md.tpl,sha256=9JSdiAbu3SGmmF_iCslw5LBPlks2DYf4WYw05pA_2_I,5673
|
|
89
89
|
aury/boot/commands/templates/project/env_templates/_header.tpl,sha256=Pt0X_I25o1th3CLR228L2-nlcC-lIkN8cPailohBEkU,513
|
|
90
90
|
aury/boot/commands/templates/project/env_templates/admin.tpl,sha256=wWt3iybOpBHtuw6CkoUJ1bzEL0aNgOzKDEkMKhI2oag,2032
|
|
91
91
|
aury/boot/commands/templates/project/env_templates/cache.tpl,sha256=_sK-p_FECj4mVvggNvgb4Wu0yGii0Ocz560syG7DU2c,498
|
|
@@ -99,14 +99,14 @@ aury/boot/commands/templates/project/env_templates/storage.tpl,sha256=x983u0Y2AF
|
|
|
99
99
|
aury/boot/commands/templates/project/env_templates/third_party.tpl,sha256=w5lcKLYRHrA_iB-FmqMM_0WMs6bvxpf3ZwgicthFxgU,1959
|
|
100
100
|
aury/boot/commands/templates/project/modules/api.py.tpl,sha256=G_IE-UC_pRhN7oOxy3dl_VLmR_omlKmHhWYi-AlyZIQ,471
|
|
101
101
|
aury/boot/commands/templates/project/modules/exceptions.py.tpl,sha256=TKY3XaQU50Z-sDHWi3_Ns-A4v50PFru08H2lzmKxAUw,2646
|
|
102
|
-
aury/boot/commands/templates/project/modules/schedules.py.tpl,sha256=
|
|
102
|
+
aury/boot/commands/templates/project/modules/schedules.py.tpl,sha256=CizmYzeuibWBsa_XxJTycr_WuzLPKtjJE1Vc78Lhdic,752
|
|
103
103
|
aury/boot/commands/templates/project/modules/tasks.py.tpl,sha256=w16VsW0K1_ukZe1Md2A_DnNPCAQUTNuo1JYfHOb7ZTI,564
|
|
104
104
|
aury/boot/common/__init__.py,sha256=MhNP3c_nwx8CyDkDF6p1f4DcTZ1CZZScg66FWdbdaZI,629
|
|
105
105
|
aury/boot/common/exceptions/__init__.py,sha256=aS3rIXWc5qNNJbfMs_PNmBlFsyNdKUMErziNMd1yoB8,3176
|
|
106
106
|
aury/boot/common/i18n/__init__.py,sha256=2cy4kteU-1YsAHkuMDTr2c5o4G33fvtYUGKtzEy1Q6c,394
|
|
107
107
|
aury/boot/common/i18n/translator.py,sha256=_vEDL2SjEI1vwMNHbnJb0xErKUPLm7VmhyOuMBeCqRM,8412
|
|
108
|
-
aury/boot/common/logging/__init__.py,sha256=
|
|
109
|
-
aury/boot/common/logging/context.py,sha256=
|
|
108
|
+
aury/boot/common/logging/__init__.py,sha256=Z6pMdjXZMWXz6w-1ev0h6QN3G3c8Cz7EpOqZh42kgQ0,1612
|
|
109
|
+
aury/boot/common/logging/context.py,sha256=U78XwjVcDP7Y96VTGclCcP09p0ZWCmiEYzVhp1Obytw,2058
|
|
110
110
|
aury/boot/common/logging/decorators.py,sha256=UaGMhRJdARNJ2VgCuRwaNX0DD5wIc1gAl6NDj7u8K2c,3354
|
|
111
111
|
aury/boot/common/logging/format.py,sha256=V1VoZCPFAaAXy20n3WehOsZGTHuboYMHnSFGa0GxhdU,9648
|
|
112
112
|
aury/boot/common/logging/setup.py,sha256=ZPxOHJD8Fw4KxKPgsf8ZHQ2mWuXxKh4EJtXXvGY7Hgo,9422
|
|
@@ -137,7 +137,7 @@ aury/boot/infrastructure/cache/backends.py,sha256=9QMQ8G9DtZgzVXZ_Ng7n1gXRu-_OQZ
|
|
|
137
137
|
aury/boot/infrastructure/cache/base.py,sha256=Yn-h_SGcOoGGZW1unOnz_zgcuHaMKOEmwiUP0P7_pIM,1624
|
|
138
138
|
aury/boot/infrastructure/cache/exceptions.py,sha256=KZsFIHXW3_kOh_KB93EVZJKbiDvDw8aloAefJ3kasP8,622
|
|
139
139
|
aury/boot/infrastructure/cache/factory.py,sha256=aF74JoiiSKFgctqqh2Z8OtGRS2Am_ou-I40GyygLzC0,2489
|
|
140
|
-
aury/boot/infrastructure/cache/manager.py,sha256=
|
|
140
|
+
aury/boot/infrastructure/cache/manager.py,sha256=GGoOgYyIdWKMmhej5cRvEfpNeMN1GaSaU9hc0dy8_sA,12106
|
|
141
141
|
aury/boot/infrastructure/channel/__init__.py,sha256=Ztcfn1-TomgV91qhePpFK-3_nKgBt862yEFYUzIwPlo,566
|
|
142
142
|
aury/boot/infrastructure/channel/base.py,sha256=lBpP6vQB2AReoE7pJorkj9mAylXgC31B9Iwhyy2XKlk,2087
|
|
143
143
|
aury/boot/infrastructure/channel/manager.py,sha256=aZ-5lVn2lVRnq_tyCcEgBjZngrt_B6tuoWxlDOf3ykw,6260
|
|
@@ -154,7 +154,7 @@ aury/boot/infrastructure/clients/redis/manager.py,sha256=Dmfu6OWGe_PrBr9wbOhl3su
|
|
|
154
154
|
aury/boot/infrastructure/database/__init__.py,sha256=MsHNyrJ2CZJT-lbVZzOAJ0nFfFEmHrJqC0zw-cFS768,888
|
|
155
155
|
aury/boot/infrastructure/database/config.py,sha256=5LYy4DuLL0XNjVnX2HUcrMh3c71eeZa-vWGM8QCkL0U,1408
|
|
156
156
|
aury/boot/infrastructure/database/exceptions.py,sha256=hUjsU23c0eMwogSDrKq_bQ6zvnY7PQSGaitbCEhhDZQ,766
|
|
157
|
-
aury/boot/infrastructure/database/manager.py,sha256=
|
|
157
|
+
aury/boot/infrastructure/database/manager.py,sha256=lRSKL9jDkSdaA99DjD1k4EoQuQsn2vbh9XQQBOt9dM0,10317
|
|
158
158
|
aury/boot/infrastructure/database/query_tools/__init__.py,sha256=D-8Wxm8x48rg9G95aH_b4re7S4_IGJO9zznArYXldFo,5500
|
|
159
159
|
aury/boot/infrastructure/database/strategies/__init__.py,sha256=foj_2xEsgLZxshpK65YAhdJ2UZyh1tKvGRq6sre8pQY,5909
|
|
160
160
|
aury/boot/infrastructure/di/__init__.py,sha256=qFYlk265d6_rS8OiX37_wOc7mBFw8hk3yipDYNkyjQg,231
|
|
@@ -176,7 +176,7 @@ aury/boot/infrastructure/mq/backends/rabbitmq.py,sha256=0NWgPKEwtbmI63EVvKINdfXX
|
|
|
176
176
|
aury/boot/infrastructure/mq/backends/redis.py,sha256=i8KECToIFEZ6CnHyNCk34_xdff5ioK172_knOy6EeUU,5279
|
|
177
177
|
aury/boot/infrastructure/scheduler/__init__.py,sha256=eTRJ5dSPcKvyFvLVtraoQteXTTDDGwIrmw06J2hoNdA,323
|
|
178
178
|
aury/boot/infrastructure/scheduler/exceptions.py,sha256=ROltrhSctVWA-6ulnjuYeHAk3ZF-sykDoesuierYzew,634
|
|
179
|
-
aury/boot/infrastructure/scheduler/manager.py,sha256=
|
|
179
|
+
aury/boot/infrastructure/scheduler/manager.py,sha256=xXSFdzliLoFKVsKkpriig5PJjyro9CMT0SQGaMnTAec,16290
|
|
180
180
|
aury/boot/infrastructure/storage/__init__.py,sha256=bA-n3v2S1FX6XsQbLqt0hkgx512MjUn_b8kJo53G6gA,1238
|
|
181
181
|
aury/boot/infrastructure/storage/base.py,sha256=X9aswSMWtKZ6TdG5Rrh6qJpqIVLt1QcFkQAKdyUWPi0,5039
|
|
182
182
|
aury/boot/infrastructure/storage/exceptions.py,sha256=Av1r94bRkeeeDo6vgAD9e_9YA9Ge6D7F2U1qzUs-8FE,622
|
|
@@ -192,7 +192,7 @@ aury/boot/testing/client.py,sha256=KOg1EemuIVsBG68G5y0DjSxZGcIQVdWQ4ASaHE3o1R0,4
|
|
|
192
192
|
aury/boot/testing/factory.py,sha256=8GvwX9qIDu0L65gzJMlrWB0xbmJ-7zPHuwk3eECULcg,5185
|
|
193
193
|
aury/boot/toolkit/__init__.py,sha256=AcyVb9fDf3CaEmJPNkWC4iGv32qCPyk4BuFKSuNiJRQ,334
|
|
194
194
|
aury/boot/toolkit/http/__init__.py,sha256=zIPmpIZ9Qbqe25VmEr7jixoY2fkRbLm7NkCB9vKpg6I,11039
|
|
195
|
-
aury_boot-0.0.
|
|
196
|
-
aury_boot-0.0.
|
|
197
|
-
aury_boot-0.0.
|
|
198
|
-
aury_boot-0.0.
|
|
195
|
+
aury_boot-0.0.17.dist-info/METADATA,sha256=rtOVvSoKspMrTpr0Z7QMmoVzLFKipvFXjrlhU_tGehM,7981
|
|
196
|
+
aury_boot-0.0.17.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
|
|
197
|
+
aury_boot-0.0.17.dist-info/entry_points.txt,sha256=f9KXEkDIGc0BGkgBvsNx_HMz9VhDjNxu26q00jUpDwQ,49
|
|
198
|
+
aury_boot-0.0.17.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|