pygpu-scheduling 0.1.0__tar.gz
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.
- pygpu_scheduling-0.1.0/.gitignore +12 -0
- pygpu_scheduling-0.1.0/.python-version +1 -0
- pygpu_scheduling-0.1.0/AGENTS.md +33 -0
- pygpu_scheduling-0.1.0/CHANGELOG.md +11 -0
- pygpu_scheduling-0.1.0/LICENSE +21 -0
- pygpu_scheduling-0.1.0/PKG-INFO +93 -0
- pygpu_scheduling-0.1.0/README.md +72 -0
- pygpu_scheduling-0.1.0/docs/api.md +106 -0
- pygpu_scheduling-0.1.0/docs/maintaining.md +85 -0
- pygpu_scheduling-0.1.0/examples/artifacts.py +60 -0
- pygpu_scheduling-0.1.0/examples/basic.py +20 -0
- pygpu_scheduling-0.1.0/pyproject.toml +43 -0
- pygpu_scheduling-0.1.0/scripts/check_project.py +62 -0
- pygpu_scheduling-0.1.0/scripts/check_release.py +18 -0
- pygpu_scheduling-0.1.0/scripts/smoke_test.py +48 -0
- pygpu_scheduling-0.1.0/src/task_scheduling/__init__.py +14 -0
- pygpu_scheduling-0.1.0/src/task_scheduling/gpu.py +90 -0
- pygpu_scheduling-0.1.0/src/task_scheduling/memory.py +45 -0
- pygpu_scheduling-0.1.0/src/task_scheduling/policy.py +46 -0
- pygpu_scheduling-0.1.0/src/task_scheduling/progress.py +131 -0
- pygpu_scheduling-0.1.0/src/task_scheduling/runtime.py +37 -0
- pygpu_scheduling-0.1.0/src/task_scheduling/scheduler.py +461 -0
- pygpu_scheduling-0.1.0/src/task_scheduling/storage.py +49 -0
- pygpu_scheduling-0.1.0/src/task_scheduling/task.py +197 -0
- pygpu_scheduling-0.1.0/tests/test_failures.py +86 -0
- pygpu_scheduling-0.1.0/tests/test_resources.py +93 -0
- pygpu_scheduling-0.1.0/tests/test_scheduler.py +221 -0
- pygpu_scheduling-0.1.0/uv.lock +98 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
3.11
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
# 项目约定
|
|
2
|
+
|
|
3
|
+
PyGPUScheduling 是独立的单机 CPU/NVIDIA GPU 调度库,运行时核心只依赖 Python 标准库。
|
|
4
|
+
|
|
5
|
+
## 开始工作
|
|
6
|
+
|
|
7
|
+
- 先读 `README.md`、`docs/api.md` 和 `docs/maintaining.md`。
|
|
8
|
+
- 环境:`uv sync --locked`。
|
|
9
|
+
- 检查:`uv run python scripts/check_project.py`、`uv run pytest`。
|
|
10
|
+
- 构建:`uv build --no-sources`。
|
|
11
|
+
- 安装包验证:按 `docs/maintaining.md` 运行 wheel 和 sdist 冒烟测试。
|
|
12
|
+
|
|
13
|
+
## 代码边界
|
|
14
|
+
|
|
15
|
+
- 不在代码中留下注释;文档使用清楚、简洁的中文。
|
|
16
|
+
- 保持高可读、高复用、高维护、低耦合和高内聚。
|
|
17
|
+
- 核心在 `src/task_scheduling`;公共导出在 `__init__.py`。
|
|
18
|
+
- 不引入 Torch、Optuna、模型注册表、训练配置或具体产物协议。
|
|
19
|
+
- 业务规则由外部 `TaskHandler` 和事件订阅者实现。
|
|
20
|
+
- 保持核心运行时依赖为空,进度显示依赖只能作为可选 extra。
|
|
21
|
+
- 任务定义、处理器名称、订阅者名称和事件 ID 是恢复契约。
|
|
22
|
+
- 更新状态文件格式时必须说明旧状态的兼容与迁移方式。
|
|
23
|
+
- 示例放 `examples`,通用测试放 `tests`,维护工具放 `scripts`。
|
|
24
|
+
- 不把业务仓库的模型测试、数据或账号配置复制进来。
|
|
25
|
+
|
|
26
|
+
## 文档与版本
|
|
27
|
+
|
|
28
|
+
- README 保留定位、安装和最小示例,API 细节只在 `docs/api.md` 维护。
|
|
29
|
+
- 开发、发布与剩余账号配置只在 `docs/maintaining.md` 维护。
|
|
30
|
+
- 版本唯一来源是 `pyproject.toml`;发布 tag 必须等于 `v<version>`。
|
|
31
|
+
- 开发未发布的变化记录在 `CHANGELOG.md` 的 `Unreleased`。
|
|
32
|
+
- 不自动移动已发布 tag,不把本地路径依赖作为正式发布来源。
|
|
33
|
+
- 当前交接状态及下一步见 `docs/maintaining.md#当前交接状态`。
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
# 变更记录
|
|
2
|
+
|
|
3
|
+
## Unreleased
|
|
4
|
+
|
|
5
|
+
## 0.1.0 — 2026-11-11
|
|
6
|
+
|
|
7
|
+
- 从 DMMBaseline 的 `2781c6a` 提取独立调度核心,保持源码行为与导入名。
|
|
8
|
+
- 分发包命名为 `pygpu-scheduling`,采用 MIT 许可证。
|
|
9
|
+
- 支持任务依赖、动态提交、GPU 显存预算、重试、状态恢复和生命周期处理器。
|
|
10
|
+
- 补充独立测试、示例、uv 开发环境及 GitHub Actions 构建与发布流程。
|
|
11
|
+
- 补齐公开仓库的包元数据与 README 链接,精简维护和接入说明。
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 PyGPUScheduling contributors
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: pygpu-scheduling
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Reusable local CPU and NVIDIA GPU task scheduling with dependency graphs and recovery hooks
|
|
5
|
+
Project-URL: Source, https://github.com/AllanJinYosoro/PyGPUScheduling
|
|
6
|
+
Project-URL: Issues, https://github.com/AllanJinYosoro/PyGPUScheduling/issues
|
|
7
|
+
Project-URL: Changelog, https://github.com/AllanJinYosoro/PyGPUScheduling/blob/main/CHANGELOG.md
|
|
8
|
+
Author: PyGPUScheduling contributors
|
|
9
|
+
License-Expression: MIT
|
|
10
|
+
License-File: LICENSE
|
|
11
|
+
Keywords: gpu,nvidia,scheduler,subprocess,workflow
|
|
12
|
+
Classifier: Development Status :: 3 - Alpha
|
|
13
|
+
Classifier: Intended Audience :: Developers
|
|
14
|
+
Classifier: Operating System :: POSIX
|
|
15
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
16
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
17
|
+
Requires-Python: >=3.11
|
|
18
|
+
Provides-Extra: progress
|
|
19
|
+
Requires-Dist: tqdm>=4.66; extra == 'progress'
|
|
20
|
+
Description-Content-Type: text/markdown
|
|
21
|
+
|
|
22
|
+
# PyGPUScheduling
|
|
23
|
+
|
|
24
|
+
单机 CPU 与 NVIDIA GPU 任务调度库。业务注册任务、依赖和生命周期处理器,调度器管理资源准入、子进程、重试及恢复。运行时仅依赖 Python 标准库,交互式进度条可选安装 `tqdm`。
|
|
25
|
+
|
|
26
|
+
分发包名为 `pygpu-scheduling`,Python 导入名为 `task_scheduling`。发布状态见[维护指南](https://github.com/AllanJinYosoro/PyGPUScheduling/blob/main/docs/maintaining.md#当前交接状态)。
|
|
27
|
+
|
|
28
|
+
## 环境与安装
|
|
29
|
+
|
|
30
|
+
需要 Python 3.11 或更新版本及 POSIX 系统。GPU 功能通过 `nvidia-smi` 工作,需要 NVIDIA 驱动;调度库不要求安装 CUDA Toolkit、Torch 或 Optuna。每个 GPU 任务使用一张卡,CPU 任务不查询 GPU。
|
|
31
|
+
|
|
32
|
+
在本仓库开发:
|
|
33
|
+
|
|
34
|
+
```bash
|
|
35
|
+
uv sync --locked
|
|
36
|
+
uv run pytest
|
|
37
|
+
uv run python examples/basic.py
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
需要交互式进度条时:
|
|
41
|
+
|
|
42
|
+
```bash
|
|
43
|
+
uv sync --locked --extra progress
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
在其他项目中安装:
|
|
47
|
+
|
|
48
|
+
```bash
|
|
49
|
+
uv add "pygpu-scheduling>=0.1.0,<0.2.0"
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
GitHub 固定 tag 接入及本地联调方式见[维护指南](https://github.com/AllanJinYosoro/PyGPUScheduling/blob/main/docs/maintaining.md#业务仓库接入)。
|
|
53
|
+
|
|
54
|
+
## 注册任务
|
|
55
|
+
|
|
56
|
+
```python
|
|
57
|
+
import sys
|
|
58
|
+
|
|
59
|
+
from task_scheduling import Scheduler, Scheduling, TaskSpec
|
|
60
|
+
|
|
61
|
+
scheduler = Scheduler("runs/example", Scheduling(max_parallel_tasks=2))
|
|
62
|
+
scheduler.submit_many([
|
|
63
|
+
TaskSpec("prepare", "command", payload={"args": [sys.executable, "prepare.py"]}),
|
|
64
|
+
TaskSpec("train", "command", dependencies=("prepare",),
|
|
65
|
+
payload={"args": [sys.executable, "train.py"]}),
|
|
66
|
+
])
|
|
67
|
+
records = scheduler.run()
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
`command` 是按退出码判定结果的内置处理器。`run()` 返回全部任务记录,调用方检查 `complete`、`failed`、`blocked` 等状态并决定业务退出码。
|
|
71
|
+
|
|
72
|
+
GPU 任务通过 `resources=ResourceRequest("cuda", 4096)` 声明 4096 MiB 预算。预算用于启动准入,不是显存硬限制。调度器不解释模型、训练轮数、业务参数或产物文件。
|
|
73
|
+
|
|
74
|
+
## 功能与边界
|
|
75
|
+
|
|
76
|
+
- 静态依赖图与动态追加任务,拒绝缺失依赖、重复定义和依赖环。
|
|
77
|
+
- 按优先级、显存预算和提交顺序选择任务,支持共享或独占 GPU。
|
|
78
|
+
- 启动前预留显存,结合实际进程占用更新预算,保留每卡安全余量。
|
|
79
|
+
- 子进程日志、超时、有限重试、GPU 冷却和故障隔离。
|
|
80
|
+
- 注册 `prepare`、`assess`、`commit`、`recover`,由业务提供校验及产物流程。
|
|
81
|
+
- 持久化任务状态,完成事件按至少一次语义重放。
|
|
82
|
+
|
|
83
|
+
当前提供单机本地子进程执行,不包含多机、抢占或单任务多卡调度。预算账本属于单个调度实例;跨实例不会合并预算。更完整的约束见[API 与恢复协议](https://github.com/AllanJinYosoro/PyGPUScheduling/blob/main/docs/api.md)。
|
|
84
|
+
|
|
85
|
+
## 文档与示例
|
|
86
|
+
|
|
87
|
+
- [API、调度策略和恢复协议](https://github.com/AllanJinYosoro/PyGPUScheduling/blob/main/docs/api.md)
|
|
88
|
+
- [开发与发布](https://github.com/AllanJinYosoro/PyGPUScheduling/blob/main/docs/maintaining.md)
|
|
89
|
+
- [变更记录](https://github.com/AllanJinYosoro/PyGPUScheduling/blob/main/CHANGELOG.md)
|
|
90
|
+
- [依赖顺序示例](https://github.com/AllanJinYosoro/PyGPUScheduling/blob/main/examples/basic.py)
|
|
91
|
+
- [业务产物处理示例](https://github.com/AllanJinYosoro/PyGPUScheduling/blob/main/examples/artifacts.py)
|
|
92
|
+
|
|
93
|
+
采用 [MIT 许可证](https://github.com/AllanJinYosoro/PyGPUScheduling/blob/main/LICENSE)。
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
# PyGPUScheduling
|
|
2
|
+
|
|
3
|
+
单机 CPU 与 NVIDIA GPU 任务调度库。业务注册任务、依赖和生命周期处理器,调度器管理资源准入、子进程、重试及恢复。运行时仅依赖 Python 标准库,交互式进度条可选安装 `tqdm`。
|
|
4
|
+
|
|
5
|
+
分发包名为 `pygpu-scheduling`,Python 导入名为 `task_scheduling`。发布状态见[维护指南](https://github.com/AllanJinYosoro/PyGPUScheduling/blob/main/docs/maintaining.md#当前交接状态)。
|
|
6
|
+
|
|
7
|
+
## 环境与安装
|
|
8
|
+
|
|
9
|
+
需要 Python 3.11 或更新版本及 POSIX 系统。GPU 功能通过 `nvidia-smi` 工作,需要 NVIDIA 驱动;调度库不要求安装 CUDA Toolkit、Torch 或 Optuna。每个 GPU 任务使用一张卡,CPU 任务不查询 GPU。
|
|
10
|
+
|
|
11
|
+
在本仓库开发:
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
uv sync --locked
|
|
15
|
+
uv run pytest
|
|
16
|
+
uv run python examples/basic.py
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
需要交互式进度条时:
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
uv sync --locked --extra progress
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
在其他项目中安装:
|
|
26
|
+
|
|
27
|
+
```bash
|
|
28
|
+
uv add "pygpu-scheduling>=0.1.0,<0.2.0"
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
GitHub 固定 tag 接入及本地联调方式见[维护指南](https://github.com/AllanJinYosoro/PyGPUScheduling/blob/main/docs/maintaining.md#业务仓库接入)。
|
|
32
|
+
|
|
33
|
+
## 注册任务
|
|
34
|
+
|
|
35
|
+
```python
|
|
36
|
+
import sys
|
|
37
|
+
|
|
38
|
+
from task_scheduling import Scheduler, Scheduling, TaskSpec
|
|
39
|
+
|
|
40
|
+
scheduler = Scheduler("runs/example", Scheduling(max_parallel_tasks=2))
|
|
41
|
+
scheduler.submit_many([
|
|
42
|
+
TaskSpec("prepare", "command", payload={"args": [sys.executable, "prepare.py"]}),
|
|
43
|
+
TaskSpec("train", "command", dependencies=("prepare",),
|
|
44
|
+
payload={"args": [sys.executable, "train.py"]}),
|
|
45
|
+
])
|
|
46
|
+
records = scheduler.run()
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
`command` 是按退出码判定结果的内置处理器。`run()` 返回全部任务记录,调用方检查 `complete`、`failed`、`blocked` 等状态并决定业务退出码。
|
|
50
|
+
|
|
51
|
+
GPU 任务通过 `resources=ResourceRequest("cuda", 4096)` 声明 4096 MiB 预算。预算用于启动准入,不是显存硬限制。调度器不解释模型、训练轮数、业务参数或产物文件。
|
|
52
|
+
|
|
53
|
+
## 功能与边界
|
|
54
|
+
|
|
55
|
+
- 静态依赖图与动态追加任务,拒绝缺失依赖、重复定义和依赖环。
|
|
56
|
+
- 按优先级、显存预算和提交顺序选择任务,支持共享或独占 GPU。
|
|
57
|
+
- 启动前预留显存,结合实际进程占用更新预算,保留每卡安全余量。
|
|
58
|
+
- 子进程日志、超时、有限重试、GPU 冷却和故障隔离。
|
|
59
|
+
- 注册 `prepare`、`assess`、`commit`、`recover`,由业务提供校验及产物流程。
|
|
60
|
+
- 持久化任务状态,完成事件按至少一次语义重放。
|
|
61
|
+
|
|
62
|
+
当前提供单机本地子进程执行,不包含多机、抢占或单任务多卡调度。预算账本属于单个调度实例;跨实例不会合并预算。更完整的约束见[API 与恢复协议](https://github.com/AllanJinYosoro/PyGPUScheduling/blob/main/docs/api.md)。
|
|
63
|
+
|
|
64
|
+
## 文档与示例
|
|
65
|
+
|
|
66
|
+
- [API、调度策略和恢复协议](https://github.com/AllanJinYosoro/PyGPUScheduling/blob/main/docs/api.md)
|
|
67
|
+
- [开发与发布](https://github.com/AllanJinYosoro/PyGPUScheduling/blob/main/docs/maintaining.md)
|
|
68
|
+
- [变更记录](https://github.com/AllanJinYosoro/PyGPUScheduling/blob/main/CHANGELOG.md)
|
|
69
|
+
- [依赖顺序示例](https://github.com/AllanJinYosoro/PyGPUScheduling/blob/main/examples/basic.py)
|
|
70
|
+
- [业务产物处理示例](https://github.com/AllanJinYosoro/PyGPUScheduling/blob/main/examples/artifacts.py)
|
|
71
|
+
|
|
72
|
+
采用 [MIT 许可证](https://github.com/AllanJinYosoro/PyGPUScheduling/blob/main/LICENSE)。
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
# API 与恢复协议
|
|
2
|
+
|
|
3
|
+
## 职责
|
|
4
|
+
|
|
5
|
+
调用方声明任务、依赖、资源需求、业务参数和处理器。调度器只负责依赖约束、资源准入、执行状态及事件,不解释业务参数中的内容。
|
|
6
|
+
|
|
7
|
+
子进程是执行边界;生命周期方法在调度进程内执行,应保持简短。训练、转换文件等耗时操作应由 `prepare` 返回的命令完成。
|
|
8
|
+
|
|
9
|
+
## 公共对象
|
|
10
|
+
|
|
11
|
+
| 对象 | 用途 |
|
|
12
|
+
| --- | --- |
|
|
13
|
+
| `Scheduler` | 注册处理器、提交任务、订阅事件、运行与恢复 |
|
|
14
|
+
| `Scheduling` | GPU 筛选、并发、重试、查询间隔等全局策略 |
|
|
15
|
+
| `TaskSpec` | 稳定 ID、处理器、显示名、JSON 参数、依赖、优先级和资源需求 |
|
|
16
|
+
| `ResourceRequest` | `cpu` 或 `cuda`,以及单进程显存预算 |
|
|
17
|
+
| `Command` | 命令参数列表、工作目录和环境变量 |
|
|
18
|
+
| `TaskContext` | 任务、根目录、当前状态、历史尝试以及最近一次的目录与 GPU |
|
|
19
|
+
| `Assessment` | 成功与否、能否重试、失败种类和信息 |
|
|
20
|
+
| `Recovery` | 外部校验后的恢复状态和 JSON 结果 |
|
|
21
|
+
| `TaskEvent` | 稳定事件 ID、任务、终态、历史尝试和业务发布结果 |
|
|
22
|
+
| `TaskRecord` | 任务快照、执行状态、尝试、结果和事件确认记录 |
|
|
23
|
+
| `LocalRuntime` | 本地子进程运行与可注入的 GPU 查询、停止操作 |
|
|
24
|
+
|
|
25
|
+
`TaskSpec` 的 `id`、`handler` 和 `name` 必须是非空字符串,`payload` 必须是可序列化的 JSON 对象。`dependencies` 是任务 ID 序列,`priority` 是整数。资源默认 `ResourceRequest("cpu", 0)`,GPU 任务应显式给出预算。
|
|
26
|
+
|
|
27
|
+
`Command.args` 是非空字符串序列;不经过 shell。`cwd` 省略时沿用调度进程的工作目录,`env` 用于覆盖继承的环境变量。核心最终设置 `CUDA_VISIBLE_DEVICES`,GPU 子进程只看到分配到的卡,CPU 子进程看不到 GPU。
|
|
28
|
+
|
|
29
|
+
## 注册、依赖和顺序
|
|
30
|
+
|
|
31
|
+
```python
|
|
32
|
+
from task_scheduling import CommandHandler, Scheduler
|
|
33
|
+
|
|
34
|
+
scheduler = Scheduler("runs/workflow")
|
|
35
|
+
scheduler.register_handler("custom", CommandHandler())
|
|
36
|
+
scheduler.subscribe("workflow", on_finished)
|
|
37
|
+
scheduler.submit_many(tasks)
|
|
38
|
+
records = scheduler.run()
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
同一批任务可向前引用依赖;依赖必须存在且不能形成环。批量校验失败时不提交这一批任务。相同 ID 和业务定义可重复提交;改变处理器、参数或依赖会报错。显示名、优先级和资源可以在任务未执行时调整,执行中的任务不能更换定义。
|
|
42
|
+
|
|
43
|
+
同层任务按优先级降序、显存预算降序及提交顺序选择。显存不足时继续尝试后续可放入的小任务。依赖全部 `complete` 后任务才会启动,前置任务最终 `failed` 或 `blocked` 时,下游成为 `blocked`。严格先后关系使用依赖表达。
|
|
44
|
+
|
|
45
|
+
## 生命周期处理器
|
|
46
|
+
|
|
47
|
+
自定义处理器实现 `TaskHandler` 协议,也可继承 `CommandHandler` 覆盖部分方法:
|
|
48
|
+
|
|
49
|
+
| 方法 | 契约 |
|
|
50
|
+
| --- | --- |
|
|
51
|
+
| `prepare(context)` | 检查输入、准备尝试目录,返回 `Command` |
|
|
52
|
+
| `assess(context, outcome)` | 检查退出码与业务产物,返回 `Assessment` |
|
|
53
|
+
| `commit(context)` | 幂等发布产物,返回可序列化的 JSON 对象 |
|
|
54
|
+
| `recover(context)` | 检查旧产物及旧状态,决定恢复或重新执行 |
|
|
55
|
+
|
|
56
|
+
`context.attempts` 是历史快照,可能为空。`context.directory` 返回最近一次尝试目录,没有尝试时返回调度根目录;`context.gpu` 返回 GPU UUID 或 `None`。
|
|
57
|
+
|
|
58
|
+
设备丢失和超时由核心直接判定为可重试失败;普通退出会交给 `assess`,内置处理器以退出码 0 为成功。处理器自身抛错会停止调度并保留现场,不会当成业务失败自动消耗重试预算。
|
|
59
|
+
|
|
60
|
+
业务产物完整示例见 [`examples/artifacts.py`](../examples/artifacts.py)。
|
|
61
|
+
|
|
62
|
+
## 状态与恢复
|
|
63
|
+
|
|
64
|
+
成功流程为 `pending → running → committing → complete`。核心先保存 `committing`,再调用业务发布方法。失败根据 `Assessment.retryable` 和剩余预算回到 `pending` 或进入 `failed`,中断尝试记录为 `interrupted`,不消耗失败重试次数。
|
|
65
|
+
|
|
66
|
+
`recover` 可返回:
|
|
67
|
+
|
|
68
|
+
- `Recovery("complete", result)`:业务确认已经完成,复用结果。
|
|
69
|
+
- `Recovery("committing")`:计算已结束,继续调用幂等 `commit`。
|
|
70
|
+
- `Recovery("failed")`:业务确认永久失败。
|
|
71
|
+
- `None`:按已保存的失败次数及可重试标记重新排队。
|
|
72
|
+
|
|
73
|
+
已有 `complete` 或 `committing` 状态无法恢复时应抛错,不能静默覆盖已发布结果。业务自己定义数据身份、文件校验及发布流程。内置命令处理器仅依靠保存的成功状态恢复,不检查业务文件。
|
|
74
|
+
|
|
75
|
+
`scheduler.json` 当前格式版本为 1,保存任务定义、尝试与事件确认信息;`scheduler.lock` 防止同一运行目录被重复调度。每次尝试目录包含 `worker.log`,以及 worker 选择上报的 `progress.json`。保留完整运行目录,并在恢复时重新注册同名处理器和订阅者。Python 函数、对象和闭包不写入状态文件。
|
|
76
|
+
|
|
77
|
+
`with Scheduler(...) as scheduler` 可显式控制锁持有范围;`reconcile()` 在持锁会话中恢复任务。`import_records()` 用于调用方迁入已有通用记录,只允许在尚无状态的新调度器中导入。状态包含绝对尝试路径,当前不承诺运行目录搬迁后仍可恢复。
|
|
78
|
+
|
|
79
|
+
## 完成事件与动态任务
|
|
80
|
+
|
|
81
|
+
`subscribe(name, listener)` 接收 `complete`、`failed`、`blocked` 终态事件。订阅回调可继续提交任务;新任务必须使用稳定 ID。
|
|
82
|
+
|
|
83
|
+
事件按至少一次语义交付:回调成功返回后才保存确认信息,期间中断会重复投递相同事件 ID。调用方按事件 ID 幂等更新业务状态。`observe(callback)` 提供通用状态快照,适合导出报告,不具有终态事件的确认语义。
|
|
84
|
+
|
|
85
|
+
## 策略默认值
|
|
86
|
+
|
|
87
|
+
| 字段 | 默认值及含义 |
|
|
88
|
+
| --- | --- |
|
|
89
|
+
| `allowed_gpus` | `None`,使用所有可见 GPU;可用索引或 `GPU-` UUID 前缀限制 |
|
|
90
|
+
| `min_free_memory_mib` | 4096,底层 `eligible` 可用性查询的默认门槛;任务自身使用显式资源预算 |
|
|
91
|
+
| `memory_margin_mib` | 0,每卡预留的安全余量 |
|
|
92
|
+
| `max_parallel_tasks` | 3,同时运行的 worker 总数 |
|
|
93
|
+
| `require_idle` | `False`,允许共享 GPU;`True` 要求空闲卡并使用独占锁 |
|
|
94
|
+
| `max_retries` | 3,可重试失败后的额外尝试次数 |
|
|
95
|
+
| `poll_interval_seconds` | 10,GPU 查询间隔 |
|
|
96
|
+
| `gpu_cooldown_seconds` | 60,GPU 普通失败后的冷却时间 |
|
|
97
|
+
| `task_timeout_seconds` | 0,关闭单次任务超时 |
|
|
98
|
+
| `wait_seconds` | 3600,无运行任务时等待 GPU 的时限 |
|
|
99
|
+
|
|
100
|
+
GPU 筛选同时遵守父进程 `CUDA_VISIBLE_DEVICES`。多卡可选时优先避开上次失败的卡,再选择剩余可调度容量最小的卡。查询失败时暂停启动新的 GPU 任务;设备连续两次查询消失后停止对应 worker 并隔离该卡。
|
|
101
|
+
|
|
102
|
+
可调度容量为实际空闲显存减去安全余量,再减去每个本实例运行任务尚未实际占用的预算。启动前预留完整预算,按 PID 查询实际显存以避免重复扣减。进程显存未知时保留完整预留。预算是准入策略,不是显存硬限制;多实例不共享预算,外部进程后续增长不受此预算约束。
|
|
103
|
+
|
|
104
|
+
## 进度
|
|
105
|
+
|
|
106
|
+
核心设置 `TASK_PROGRESS_FILE`。worker 可向对应路径原子写入 JSON,字段为 `current`、`total`、`phase`、`unit`。没有上报时显示初始化状态。TTY 且安装 `tqdm` 时使用交互式显示,其余情况输出状态和心跳日志。
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
# 开发与发布
|
|
2
|
+
|
|
3
|
+
## 开发验证
|
|
4
|
+
|
|
5
|
+
```bash
|
|
6
|
+
uv sync --locked
|
|
7
|
+
uv run python scripts/check_project.py
|
|
8
|
+
uv run pytest
|
|
9
|
+
uv run python examples/basic.py
|
|
10
|
+
uv run python examples/artifacts.py
|
|
11
|
+
uv build --no-sources
|
|
12
|
+
uv run --isolated --no-project --with dist/*.whl python scripts/smoke_test.py
|
|
13
|
+
uv run --isolated --no-project --with dist/*.tar.gz python scripts/smoke_test.py
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
`uv.lock` 固定开发及 CI 环境,业务仓库维护自己的锁文件。CI 在 Ubuntu 上测试 Python 3.11–3.14,并单独验证 wheel 和 sdist。`check_project.py` 检查语法、无代码注释、核心导入边界、元数据与文档本地链接;`smoke_test.py` 验证安装包的版本、CPU 依赖顺序、动态任务和恢复。
|
|
17
|
+
|
|
18
|
+
## 版本与发布
|
|
19
|
+
|
|
20
|
+
版本唯一来源为 `pyproject.toml`,tag 必须为 `v<version>`。已发布 tag 不移动,同一版本不使用不同内容重新发布。
|
|
21
|
+
|
|
22
|
+
- `0.1.x` 保持首批接口和状态格式兼容,patch 用于兼容错误修复。
|
|
23
|
+
- 开发阶段的接口调整使用新的 minor,并在变更记录明确说明。
|
|
24
|
+
- 稳定后发布 `1.0.0`,此后遵循语义化版本。
|
|
25
|
+
- 恢复兼容范围包括任务定义、处理器和订阅者名称、事件语义及 `scheduler.json` 格式;格式变化必须说明旧状态的兼容与迁移方式并验证恢复。
|
|
26
|
+
|
|
27
|
+
仓库为 [AllanJinYosoro/PyGPUScheduling](https://github.com/AllanJinYosoro/PyGPUScheduling)。开发变化写入 `CHANGELOG.md` 的 `Unreleased`。每次发布按顺序执行:
|
|
28
|
+
|
|
29
|
+
1. 用 `uv version <version>` 更新版本及锁文件,将变化归入带实际日期的版本段。
|
|
30
|
+
2. 完成开发验证,提交并推送 `main`,确认该提交的 [CI](https://github.com/AllanJinYosoro/PyGPUScheduling/actions/workflows/ci.yml) 全部通过。
|
|
31
|
+
3. 检查 tag 与版本一致后创建并推送 tag,以 `0.1.0` 为例:
|
|
32
|
+
|
|
33
|
+
```bash
|
|
34
|
+
uv run python scripts/check_release.py v0.1.0
|
|
35
|
+
git tag -a v0.1.0 -m "Release 0.1.0"
|
|
36
|
+
git push origin v0.1.0
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
4. 确认 [发布工作流](https://github.com/AllanJinYosoro/PyGPUScheduling/actions/workflows/release.yml) 成功,再从 PyPI 独立安装对应版本运行 `scripts/smoke_test.py`,核对 PyPI 的版本、文件及仓库链接。更新本页交接状态。
|
|
40
|
+
|
|
41
|
+
`release.yml` 验证 tag、运行测试、构建并验证分发包,再由独立 publish job 使用 OIDC 上传。只有 publish job 具有 `id-token: write`;无需长期 PyPI token。源码、wheel 与 tag 必须来自同一提交。
|
|
42
|
+
|
|
43
|
+
## PyPI 账号配置
|
|
44
|
+
|
|
45
|
+
发布使用以下绑定;迁移仓库或工作流时同步更新两端:
|
|
46
|
+
|
|
47
|
+
- [GitHub Environments](https://github.com/AllanJinYosoro/PyGPUScheduling/settings/environments):`pypi`,仅允许 `v*` tag 部署。
|
|
48
|
+
- PyPI Trusted Publisher:绑定字段如下。
|
|
49
|
+
|
|
50
|
+
| 字段 | 值 |
|
|
51
|
+
| --- | --- |
|
|
52
|
+
| PyPI project name | `pygpu-scheduling` |
|
|
53
|
+
| GitHub owner | `AllanJinYosoro` |
|
|
54
|
+
| Repository | `PyGPUScheduling` |
|
|
55
|
+
| Workflow filename | `release.yml` |
|
|
56
|
+
| Environment | `pypi` |
|
|
57
|
+
|
|
58
|
+
新项目在 [PyPI Publishing](https://pypi.org/manage/account/publishing/) 添加 Pending Publisher,首次成功上传后自动转为普通绑定。已有项目在项目设置中管理 Trusted Publisher。不把凭证写入仓库。
|
|
59
|
+
|
|
60
|
+
参考:[PyPI 首次项目与 Trusted Publisher](https://docs.pypi.org/trusted-publishers/creating-a-project-through-oidc/)、[uv 发布指南](https://docs.astral.sh/uv/guides/integration/github/#publishing-to-pypi)。
|
|
61
|
+
|
|
62
|
+
## 业务仓库接入
|
|
63
|
+
|
|
64
|
+
声明版本依赖:
|
|
65
|
+
|
|
66
|
+
```bash
|
|
67
|
+
uv add "pygpu-scheduling>=0.1.0,<0.2.0"
|
|
68
|
+
uv sync --locked
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
也可使用已发布的固定 GitHub tag:
|
|
72
|
+
|
|
73
|
+
```bash
|
|
74
|
+
uv add git+https://github.com/AllanJinYosoro/PyGPUScheduling.git --tag v0.1.0
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
联调时使用 `uv add --editable ../PyGPUScheduling`,提交业务仓库前恢复远程版本依赖,一同提交 `pyproject.toml` 与 `uv.lock`。迁移旧 `task-scheduling` 依赖时先移除旧分发包,避免两个包同时提供 `task_scheduling` 导入名;业务适配层留在调用方仓库。
|
|
78
|
+
|
|
79
|
+
## 当前交接状态
|
|
80
|
+
|
|
81
|
+
- 正在发布 `0.1.0`;初始核心来源记录在 `CHANGELOG.md`。
|
|
82
|
+
- GitHub 初始提交 `da3ad57` 的 Python 3.11–3.14 及安装包 [CI 已通过](https://github.com/AllanJinYosoro/PyGPUScheduling/actions/runs/42968843208),远程尚无 tag;发布准备改动仍需推送并确认对应 CI。
|
|
83
|
+
- 本地项目检查、版本检查、Python 3.11 下 29 项测试、两个示例、构建及 wheel/sdist 独立安装冒烟测试通过。
|
|
84
|
+
- GitHub 登录和仓库管理权限已核验;账号所有者已确认添加 PyPI Pending Publisher,等待首次 OIDC 上传验证。
|
|
85
|
+
- GPU 实机尚未验证;现有测试覆盖 CPU 子进程和模拟 GPU 行为。下游业务仓库迁移另行处理;本项目不维护独立 Agent 记忆。
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
from pathlib import Path
|
|
2
|
+
import sys
|
|
3
|
+
|
|
4
|
+
from task_scheduling import Assessment, Command, CommandHandler, Recovery, Scheduler, TaskSpec
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class TextArtifactHandler(CommandHandler):
|
|
8
|
+
def prepare(self, context):
|
|
9
|
+
artifact = context.directory / "artifact.txt"
|
|
10
|
+
content = context.task.payload["content"]
|
|
11
|
+
script = f"from pathlib import Path; Path({str(artifact)!r}).write_text({content!r})"
|
|
12
|
+
return Command((sys.executable, "-c", script))
|
|
13
|
+
|
|
14
|
+
def assess(self, context, outcome):
|
|
15
|
+
execution = super().assess(context, outcome)
|
|
16
|
+
if not execution.success:
|
|
17
|
+
return execution
|
|
18
|
+
artifact = context.directory / "artifact.txt"
|
|
19
|
+
valid = artifact.is_file() and artifact.read_text() == context.task.payload["content"]
|
|
20
|
+
return Assessment(valid, False, "artifact", "artifact content validation")
|
|
21
|
+
|
|
22
|
+
def commit(self, context):
|
|
23
|
+
output = Path(context.task.payload["output"])
|
|
24
|
+
if output.exists():
|
|
25
|
+
if output.read_text() != context.task.payload["content"]:
|
|
26
|
+
raise ValueError("Published artifact content changed")
|
|
27
|
+
else:
|
|
28
|
+
output.parent.mkdir(parents=True, exist_ok=True)
|
|
29
|
+
(context.directory / "artifact.txt").replace(output)
|
|
30
|
+
return {"output": str(output)}
|
|
31
|
+
|
|
32
|
+
def recover(self, context):
|
|
33
|
+
output = Path(context.task.payload["output"])
|
|
34
|
+
if output.exists():
|
|
35
|
+
if output.read_text() != context.task.payload["content"]:
|
|
36
|
+
raise ValueError("Published artifact content changed")
|
|
37
|
+
return Recovery("complete", {"output": str(output)})
|
|
38
|
+
if context.attempts:
|
|
39
|
+
artifact = context.directory / "artifact.txt"
|
|
40
|
+
if artifact.is_file() and artifact.read_text() == context.task.payload["content"]:
|
|
41
|
+
return Recovery("committing")
|
|
42
|
+
if context.status in {"complete", "committing"}:
|
|
43
|
+
raise ValueError("Completed artifact is missing")
|
|
44
|
+
return None
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def main():
|
|
48
|
+
root = Path("runs/artifacts").resolve()
|
|
49
|
+
scheduler = Scheduler(root / "queue")
|
|
50
|
+
scheduler.register_handler("text", TextArtifactHandler())
|
|
51
|
+
scheduler.submit(TaskSpec("publish-text", "text", payload={
|
|
52
|
+
"content": "Artifact lifecycle completed\n", "output": str(root / "published.txt")
|
|
53
|
+
}))
|
|
54
|
+
record, = scheduler.run()
|
|
55
|
+
print(record.result)
|
|
56
|
+
return 0 if record.status == "complete" else 1
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
if __name__ == "__main__":
|
|
60
|
+
raise SystemExit(main())
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import sys
|
|
2
|
+
|
|
3
|
+
from task_scheduling import Scheduler, Scheduling, TaskSpec
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def main():
|
|
7
|
+
scheduler = Scheduler("runs/basic", Scheduling(max_parallel_tasks=2))
|
|
8
|
+
scheduler.submit_many([
|
|
9
|
+
TaskSpec("first", "command", payload={"args": [sys.executable, "-c", "print('first completed')"]}),
|
|
10
|
+
TaskSpec("second", "command", dependencies=("first",),
|
|
11
|
+
payload={"args": [sys.executable, "-c", "print('second completed')"]}),
|
|
12
|
+
])
|
|
13
|
+
records = scheduler.run()
|
|
14
|
+
for record in records:
|
|
15
|
+
print(f"{record.spec.id}: {record.status}")
|
|
16
|
+
return 0 if all(record.status == "complete" for record in records) else 1
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
if __name__ == "__main__":
|
|
20
|
+
raise SystemExit(main())
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "pygpu-scheduling"
|
|
3
|
+
version = "0.1.0"
|
|
4
|
+
description = "Reusable local CPU and NVIDIA GPU task scheduling with dependency graphs and recovery hooks"
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
requires-python = ">=3.11"
|
|
7
|
+
license = "MIT"
|
|
8
|
+
license-files = ["LICENSE"]
|
|
9
|
+
authors = [{ name = "PyGPUScheduling contributors" }]
|
|
10
|
+
keywords = ["gpu", "scheduler", "subprocess", "workflow", "nvidia"]
|
|
11
|
+
classifiers = [
|
|
12
|
+
"Development Status :: 3 - Alpha",
|
|
13
|
+
"Intended Audience :: Developers",
|
|
14
|
+
"Operating System :: POSIX",
|
|
15
|
+
"Programming Language :: Python :: 3 :: Only",
|
|
16
|
+
"Topic :: Software Development :: Libraries :: Python Modules",
|
|
17
|
+
]
|
|
18
|
+
dependencies = []
|
|
19
|
+
|
|
20
|
+
[project.urls]
|
|
21
|
+
Source = "https://github.com/AllanJinYosoro/PyGPUScheduling"
|
|
22
|
+
Issues = "https://github.com/AllanJinYosoro/PyGPUScheduling/issues"
|
|
23
|
+
Changelog = "https://github.com/AllanJinYosoro/PyGPUScheduling/blob/main/CHANGELOG.md"
|
|
24
|
+
|
|
25
|
+
[project.optional-dependencies]
|
|
26
|
+
progress = ["tqdm>=4.66"]
|
|
27
|
+
|
|
28
|
+
[dependency-groups]
|
|
29
|
+
dev = ["pytest>=8,<10"]
|
|
30
|
+
|
|
31
|
+
[build-system]
|
|
32
|
+
requires = ["hatchling>=1.27"]
|
|
33
|
+
build-backend = "hatchling.build"
|
|
34
|
+
|
|
35
|
+
[tool.hatch.build.targets.wheel]
|
|
36
|
+
packages = ["src/task_scheduling"]
|
|
37
|
+
|
|
38
|
+
[tool.hatch.build.targets.sdist]
|
|
39
|
+
include = ["/src", "/tests", "/examples", "/scripts", "/docs", "/README.md", "/AGENTS.md", "/CHANGELOG.md", "/LICENSE", "/pyproject.toml", "/uv.lock", "/.python-version"]
|
|
40
|
+
|
|
41
|
+
[tool.pytest.ini_options]
|
|
42
|
+
testpaths = ["tests"]
|
|
43
|
+
addopts = "-ra"
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import ast
|
|
2
|
+
import io
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
import re
|
|
5
|
+
import tokenize
|
|
6
|
+
import tomllib
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
ROOT = Path(__file__).resolve().parents[1]
|
|
10
|
+
FORBIDDEN_IMPORTS = {"video_baselines", "torch", "optuna", "numpy", "yaml"}
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def check_python():
|
|
14
|
+
paths = [path for directory in ("src", "tests", "examples", "scripts")
|
|
15
|
+
for path in (ROOT / directory).rglob("*.py")]
|
|
16
|
+
for path in paths:
|
|
17
|
+
source = path.read_text()
|
|
18
|
+
tree = ast.parse(source, filename=str(path))
|
|
19
|
+
comments = [token for token in tokenize.generate_tokens(io.StringIO(source).readline)
|
|
20
|
+
if token.type == tokenize.COMMENT]
|
|
21
|
+
if comments:
|
|
22
|
+
raise ValueError(f"Code comments are not allowed: {path.relative_to(ROOT)}")
|
|
23
|
+
if path.is_relative_to(ROOT / "src"):
|
|
24
|
+
imports = [node.module for node in ast.walk(tree)
|
|
25
|
+
if isinstance(node, ast.ImportFrom) and node.module]
|
|
26
|
+
imports += [alias.name for node in ast.walk(tree)
|
|
27
|
+
if isinstance(node, ast.Import) for alias in node.names]
|
|
28
|
+
if any(name.split(".")[0] in FORBIDDEN_IMPORTS for name in imports):
|
|
29
|
+
raise ValueError(f"Business dependency in core: {path.relative_to(ROOT)}")
|
|
30
|
+
return len(paths)
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def check_links():
|
|
34
|
+
documents = [ROOT / "README.md", ROOT / "AGENTS.md", ROOT / "CHANGELOG.md", *(ROOT / "docs").glob("*.md")]
|
|
35
|
+
for document in documents:
|
|
36
|
+
for target in re.findall(r"\[[^\]]*\]\(([^)]+)\)", document.read_text()):
|
|
37
|
+
if "://" in target or target.startswith("#"):
|
|
38
|
+
continue
|
|
39
|
+
relative = target.split("#", 1)[0]
|
|
40
|
+
if relative and not (document.parent / relative).exists():
|
|
41
|
+
raise ValueError(f"Broken link in {document.relative_to(ROOT)}: {target}")
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def check_metadata():
|
|
45
|
+
project = tomllib.loads((ROOT / "pyproject.toml").read_text())["project"]
|
|
46
|
+
if project["name"] != "pygpu-scheduling" or project["dependencies"]:
|
|
47
|
+
raise ValueError("Distribution name or core runtime dependencies changed")
|
|
48
|
+
if project["license"] != "MIT" or not (ROOT / "LICENSE").is_file():
|
|
49
|
+
raise ValueError("MIT license metadata or license file is missing")
|
|
50
|
+
if not re.fullmatch(r"\d+\.\d+\.\d+(?:(?:a|b|rc)\d+)?", project["version"]):
|
|
51
|
+
raise ValueError("Version must have a release-compatible three-part format")
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def main():
|
|
55
|
+
count = check_python()
|
|
56
|
+
check_links()
|
|
57
|
+
check_metadata()
|
|
58
|
+
print(f"Project checks passed: {count} Python files, import boundaries, metadata and documentation links")
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
if __name__ == "__main__":
|
|
62
|
+
main()
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
from pathlib import Path
|
|
2
|
+
import sys
|
|
3
|
+
import tomllib
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def main():
|
|
7
|
+
if len(sys.argv) != 2:
|
|
8
|
+
raise SystemExit("Usage: python scripts/check_release.py v<version>")
|
|
9
|
+
root = Path(__file__).resolve().parents[1]
|
|
10
|
+
version = tomllib.loads((root / "pyproject.toml").read_text())["project"]["version"]
|
|
11
|
+
expected = f"v{version}"
|
|
12
|
+
if sys.argv[1] != expected:
|
|
13
|
+
raise SystemExit(f"Release tag must be {expected}, got {sys.argv[1]}")
|
|
14
|
+
print(f"Release tag matches package version: {expected}")
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
if __name__ == "__main__":
|
|
18
|
+
main()
|