aury-boot 0.0.15__py3-none-any.whl → 0.0.18__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 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.15'
32
- __version_tuple__ = version_tuple = (0, 0, 15)
31
+ __version__ = version = '0.0.18'
32
+ __version_tuple__ = version_tuple = (0, 0, 18)
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") if instance._config else None,
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):
@@ -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("interval", seconds=60)
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("cron", hour=0, minute=0)
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("cron", day_of_week="mon", hour=9)
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
- 启用方式:配置 `SCHEDULER_ENABLED=true`,框架自动加载 `{package_name}/schedules/` 模块。
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`
@@ -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("interval", seconds=60)
18
+ # @scheduler.scheduled_job(IntervalTrigger(seconds=60))
16
19
  # async def example_job():
17
20
  # """示例定时任务,每 60 秒执行一次。"""
18
21
  # logger.info("定时任务执行中...")
@@ -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
  )
@@ -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
- 1. 管理调度器实例
44
- 2. 注册任务
45
- 3. 生命周期管理
46
- 4. 支持多个命名实例,如不同业务线的调度器
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
- report_scheduler = SchedulerManager.get_instance("report")
55
- cleanup_scheduler = SchedulerManager.get_instance("cleanup")
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.add_job(
59
- func=my_task,
60
- trigger="interval",
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(cls, name: str = "default") -> SchedulerManager:
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
- instance._scheduler = AsyncIOScheduler()
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: str = "interval",
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: 触发器类型(interval/cron)
164
- seconds: 间隔秒数
165
- minutes: 间隔分钟数
166
- hours: 间隔小时数
167
- days: 间隔天数
168
- cron: Cron表达式(如 "0 0 * * *")
169
- id: 任务ID
170
- **kwargs: 其他参数
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=trigger_obj,
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: str = "interval",
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: 触发器类型(interval/cron)
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
- if trigger == "interval":
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: str = "interval",
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("interval", seconds=60)
430
+ @scheduler.scheduled_job(IntervalTrigger(seconds=60))
428
431
  async def my_task():
429
432
  print("Task executed")
430
433
 
431
- @scheduler.scheduled_job("cron", cron="0 0 * * *")
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: 触发器类型(interval/cron)
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,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: aury-boot
3
- Version: 0.0.15
3
+ Version: 0.0.18
4
4
  Summary: Aury Boot - 基于 FastAPI 生态的企业级 API 开发框架
5
5
  Requires-Python: >=3.13
6
6
  Requires-Dist: alembic>=1.17.2
@@ -1,5 +1,5 @@
1
1
  aury/boot/__init__.py,sha256=pCno-EInnpIBa1OtxNYF-JWf9j95Cd2h6vmu0xqa_-4,1791
2
- aury/boot/_version.py,sha256=vo0_oMD-jYQGv4X3gUeREI1I3x7jd_dShIWAxsGI-zY,706
2
+ aury/boot/_version.py,sha256=tzG3REjXftgEO32YP2rgLirgULNff34B-C5Yb8oweEc,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=tt4a4ZVcRJZbvStj5W9AYf9omGqKVu0qFu5pn1q2VSU,20312
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=bkRT4cCzXPnTOBSNs-TLcKvFGwqBgwXeO8_gQq9Gc1s,7895
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=CckslmXl0H1XN58DKK4IBK9WqKX72nhDSIpx0dxtFPA,29544
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
@@ -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=lzRh23-8Buw5JgZ_BRuMH15gh6WmxJRU4dbZ57fyntA,7309
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,7 +76,7 @@ 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=zk7RHjtx_QGjmeLy04Nk_qSc8sofTrubS2Tg7DxfEl4,858
79
+ aury/boot/commands/templates/project/aury_docs/08-scheduler.md.tpl,sha256=ocltWA8ubYwPWwfmhYmhgF-N9KFUO8PKCvE2DxA5BiM,4444
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
82
  aury/boot/commands/templates/project/aury_docs/11-logging.md.tpl,sha256=bwxFCGQsO9cTEbwqJF1xcjsZKP82HRWhIMRUS0c9_ZI,2435
@@ -85,7 +85,7 @@ aury/boot/commands/templates/project/aury_docs/13-channel.md.tpl,sha256=rdtlog3a
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=LEZ-ygl_pF_t_abrpajlaEkGv1qD4onylmxG6p5wzgc,5671
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,7 +99,7 @@ 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=P-R-0SDsoQ_lWfKYJXZT5DoNAVKGUjYiC3HBbUZCc3Y,633
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
@@ -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=H2Gi6EEoZBSP5s5BoRlx0MPqKXoNg-mBvogoXmO8w0Y,11822
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
@@ -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=vvVditIJC4WaeqWWpUXSmMdkqnlUHREPd0BRPSNpnyY,15554
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.15.dist-info/METADATA,sha256=KpgZbUBGqUhwH-65xmNrP3H8699sx82WkG3cj-a5-gw,7981
196
- aury_boot-0.0.15.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
197
- aury_boot-0.0.15.dist-info/entry_points.txt,sha256=f9KXEkDIGc0BGkgBvsNx_HMz9VhDjNxu26q00jUpDwQ,49
198
- aury_boot-0.0.15.dist-info/RECORD,,
195
+ aury_boot-0.0.18.dist-info/METADATA,sha256=rT6cWP49gdwbb-55IWIRuoD4c_Cl1O3bSaqD3fnh4Zs,7981
196
+ aury_boot-0.0.18.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
197
+ aury_boot-0.0.18.dist-info/entry_points.txt,sha256=f9KXEkDIGc0BGkgBvsNx_HMz9VhDjNxu26q00jUpDwQ,49
198
+ aury_boot-0.0.18.dist-info/RECORD,,