xes-assets 1.0.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.
- xes_assets-1.0.0/LICENSE +21 -0
- xes_assets-1.0.0/PKG-INFO +110 -0
- xes_assets-1.0.0/README.md +80 -0
- xes_assets-1.0.0/pyproject.toml +54 -0
- xes_assets-1.0.0/setup.cfg +4 -0
- xes_assets-1.0.0/src/xes_assets/__init__.py +363 -0
- xes_assets-1.0.0/src/xes_assets/__main__.py +6 -0
- xes_assets-1.0.0/src/xes_assets/cli.py +100 -0
- xes_assets-1.0.0/src/xes_assets.egg-info/PKG-INFO +110 -0
- xes_assets-1.0.0/src/xes_assets.egg-info/SOURCES.txt +11 -0
- xes_assets-1.0.0/src/xes_assets.egg-info/dependency_links.txt +1 -0
- xes_assets-1.0.0/src/xes_assets.egg-info/entry_points.txt +2 -0
- xes_assets-1.0.0/src/xes_assets.egg-info/top_level.txt +1 -0
xes_assets-1.0.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 xes-assets 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,110 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: xes-assets
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: Zero-dependency downloader for the XES Python course asset files (RAR + ZIP), with size and SHA-256 verification.
|
|
5
|
+
Author: xes-assets
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Homepage, https://pypi.org/project/xes-assets/
|
|
8
|
+
Project-URL: Source, https://livefile.xesimg.com/programme/python_assets/
|
|
9
|
+
Keywords: xes,xesimg,assets,downloader,python-assets,no-dependencies,zero-dependency,sha256
|
|
10
|
+
Classifier: Development Status :: 5 - Production/Stable
|
|
11
|
+
Classifier: Environment :: Console
|
|
12
|
+
Classifier: Intended Audience :: Developers
|
|
13
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
14
|
+
Classifier: Natural Language :: Chinese (Simplified)
|
|
15
|
+
Classifier: Natural Language :: English
|
|
16
|
+
Classifier: Operating System :: OS Independent
|
|
17
|
+
Classifier: Programming Language :: Python :: 3
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.8
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
21
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
22
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
23
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
24
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
25
|
+
Classifier: Topic :: System :: Archiving
|
|
26
|
+
Requires-Python: >=3.8
|
|
27
|
+
Description-Content-Type: text/markdown
|
|
28
|
+
License-File: LICENSE
|
|
29
|
+
Dynamic: license-file
|
|
30
|
+
|
|
31
|
+
# xes-assets
|
|
32
|
+
|
|
33
|
+
零依赖下载器 / Zero-dependency downloader for the XES Python course asset files:
|
|
34
|
+
|
|
35
|
+
- `https://livefile.xesimg.com/programme/python_assets/3f51c62b04d3aacc27b0d53f294ecc3b.rar`
|
|
36
|
+
- `https://livefile.xesimg.com/programme/python_assets/a3635617bd88675041ba69f8398df5a0.zip`
|
|
37
|
+
|
|
38
|
+
只用 Python 标准库(`urllib`、`hashlib`、`zipfile`),**没有任何第三方依赖**。
|
|
39
|
+
Uses nothing but the Python standard library — **no third-party dependency at all**.
|
|
40
|
+
|
|
41
|
+
## 安装 / Install
|
|
42
|
+
|
|
43
|
+
```bash
|
|
44
|
+
pip install xes-assets
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
## 命令行 / CLI
|
|
48
|
+
|
|
49
|
+
```bash
|
|
50
|
+
xes-assets --list # 查看两个文件的信息
|
|
51
|
+
xes-assets zip -d D:\xes # 下载 ZIP
|
|
52
|
+
xes-assets all -d D:\xes -x # 下载两个并解压
|
|
53
|
+
xes-assets rar --verify-only -d D:\xes # 只校验已下载文件
|
|
54
|
+
python -m xes_assets all -d D:\xes # 等价写法
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
| 参数 | 说明 |
|
|
58
|
+
| --- | --- |
|
|
59
|
+
| `ASSET`(位置参数) | `rar`、`zip`、`all`、文件名或 URL;省略时等于 `all` |
|
|
60
|
+
| `-d, --dest DIR` | 输出目录(默认当前目录) |
|
|
61
|
+
| `-x, --extract` | 下载后解压(RAR 需要外部解压工具,见下) |
|
|
62
|
+
| `-f, --overwrite` | 覆盖已存在文件 |
|
|
63
|
+
| `--no-verify` | 跳过 SHA-256 校验 |
|
|
64
|
+
| `-q, --quiet` | 不显示进度 |
|
|
65
|
+
| `--verify-only` | 只校验不下载 |
|
|
66
|
+
| `--list` | 列出已知文件 |
|
|
67
|
+
|
|
68
|
+
## Python API
|
|
69
|
+
|
|
70
|
+
```python
|
|
71
|
+
from xes_assets import ASSETS, download, download_all, extract, verify
|
|
72
|
+
|
|
73
|
+
download("zip", r"D:\xes") # -> Path,含大小 + SHA-256 校验
|
|
74
|
+
download_all(r"D:\xes") # 两个都下
|
|
75
|
+
verify("rar", r"D:\xes") # -> True/False
|
|
76
|
+
extract("zip", r"D:\xes") # 标准库解压,返回目录
|
|
77
|
+
extract("rar", r"D:\xes") # 需外部 7z / UnRAR(见下)
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
单个函数的行为:
|
|
81
|
+
|
|
82
|
+
- `download(key, dest=".", *, overwrite=False, verify_checksum=True, progress=None, timeout=60, retries=3)`
|
|
83
|
+
先写 `<文件名>.part`,校验大小与 SHA-256 通过后才原子改名为正式文件;断点续传用 HTTP `Range`
|
|
84
|
+
(服务器返回 206 时继续,否则从头开始),网络中断自动重试。
|
|
85
|
+
- `download_all(dest=".", *, keys=None, **kwargs)` → `List[Path]`
|
|
86
|
+
- `extract(key, dest=None, *, archive=None, overwrite=False)` → 解压目录 `Path`
|
|
87
|
+
- `verify(key, path=None, dest=".")` → `bool`
|
|
88
|
+
- `asset_path(key, dest=".")` → 预期的本地路径
|
|
89
|
+
- `ASSETS` / `ASSET_ORDER` 为元数据常量;`XesAssetError` 为统一异常
|
|
90
|
+
|
|
91
|
+
## 文件与校验值 / Files
|
|
92
|
+
|
|
93
|
+
| key | 文件名 | 大小 | SHA-256 |
|
|
94
|
+
| --- | --- | --- | --- |
|
|
95
|
+
| `rar` | `3f51c62b04d3aacc27b0d53f294ecc3b.rar` | 568,456,521 B | `ea41b34bd193cf0039386c0cfe22f93c2225bb43ef01d69740106a35e05d7c85` |
|
|
96
|
+
| `zip` | `a3635617bd88675041ba69f8398df5a0.zip` | 39,463,900 B | `63fdddb6c37849606ae32d08e522bbd2a627be90971005946b6c332a7715ff93` |
|
|
97
|
+
|
|
98
|
+
校验值在主文件被上游替换前一直有效;上游变更时下载会明确报 `checksum mismatch`
|
|
99
|
+
(不会静默留下坏文件)。
|
|
100
|
+
|
|
101
|
+
## 关于 RAR 解压 / About RAR extraction
|
|
102
|
+
|
|
103
|
+
**下载**永远只需要标准库。**解压 `.rar`** 时本包会依次查找
|
|
104
|
+
`7z` / `7zz` / `7za` / `unrar` / `unar` / `bsdtar`(PATH 中或常见安装路径
|
|
105
|
+
`C:\Program Files\7-Zip\7z.exe`、`C:\Program Files\WinRAR\UnRAR.exe`),
|
|
106
|
+
找不到就抛 `XesAssetError` 并提示手工解压。ZIP 解压完全由标准库完成。
|
|
107
|
+
|
|
108
|
+
## License
|
|
109
|
+
|
|
110
|
+
MIT
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
# xes-assets
|
|
2
|
+
|
|
3
|
+
零依赖下载器 / Zero-dependency downloader for the XES Python course asset files:
|
|
4
|
+
|
|
5
|
+
- `https://livefile.xesimg.com/programme/python_assets/3f51c62b04d3aacc27b0d53f294ecc3b.rar`
|
|
6
|
+
- `https://livefile.xesimg.com/programme/python_assets/a3635617bd88675041ba69f8398df5a0.zip`
|
|
7
|
+
|
|
8
|
+
只用 Python 标准库(`urllib`、`hashlib`、`zipfile`),**没有任何第三方依赖**。
|
|
9
|
+
Uses nothing but the Python standard library — **no third-party dependency at all**.
|
|
10
|
+
|
|
11
|
+
## 安装 / Install
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
pip install xes-assets
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
## 命令行 / CLI
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
xes-assets --list # 查看两个文件的信息
|
|
21
|
+
xes-assets zip -d D:\xes # 下载 ZIP
|
|
22
|
+
xes-assets all -d D:\xes -x # 下载两个并解压
|
|
23
|
+
xes-assets rar --verify-only -d D:\xes # 只校验已下载文件
|
|
24
|
+
python -m xes_assets all -d D:\xes # 等价写法
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
| 参数 | 说明 |
|
|
28
|
+
| --- | --- |
|
|
29
|
+
| `ASSET`(位置参数) | `rar`、`zip`、`all`、文件名或 URL;省略时等于 `all` |
|
|
30
|
+
| `-d, --dest DIR` | 输出目录(默认当前目录) |
|
|
31
|
+
| `-x, --extract` | 下载后解压(RAR 需要外部解压工具,见下) |
|
|
32
|
+
| `-f, --overwrite` | 覆盖已存在文件 |
|
|
33
|
+
| `--no-verify` | 跳过 SHA-256 校验 |
|
|
34
|
+
| `-q, --quiet` | 不显示进度 |
|
|
35
|
+
| `--verify-only` | 只校验不下载 |
|
|
36
|
+
| `--list` | 列出已知文件 |
|
|
37
|
+
|
|
38
|
+
## Python API
|
|
39
|
+
|
|
40
|
+
```python
|
|
41
|
+
from xes_assets import ASSETS, download, download_all, extract, verify
|
|
42
|
+
|
|
43
|
+
download("zip", r"D:\xes") # -> Path,含大小 + SHA-256 校验
|
|
44
|
+
download_all(r"D:\xes") # 两个都下
|
|
45
|
+
verify("rar", r"D:\xes") # -> True/False
|
|
46
|
+
extract("zip", r"D:\xes") # 标准库解压,返回目录
|
|
47
|
+
extract("rar", r"D:\xes") # 需外部 7z / UnRAR(见下)
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
单个函数的行为:
|
|
51
|
+
|
|
52
|
+
- `download(key, dest=".", *, overwrite=False, verify_checksum=True, progress=None, timeout=60, retries=3)`
|
|
53
|
+
先写 `<文件名>.part`,校验大小与 SHA-256 通过后才原子改名为正式文件;断点续传用 HTTP `Range`
|
|
54
|
+
(服务器返回 206 时继续,否则从头开始),网络中断自动重试。
|
|
55
|
+
- `download_all(dest=".", *, keys=None, **kwargs)` → `List[Path]`
|
|
56
|
+
- `extract(key, dest=None, *, archive=None, overwrite=False)` → 解压目录 `Path`
|
|
57
|
+
- `verify(key, path=None, dest=".")` → `bool`
|
|
58
|
+
- `asset_path(key, dest=".")` → 预期的本地路径
|
|
59
|
+
- `ASSETS` / `ASSET_ORDER` 为元数据常量;`XesAssetError` 为统一异常
|
|
60
|
+
|
|
61
|
+
## 文件与校验值 / Files
|
|
62
|
+
|
|
63
|
+
| key | 文件名 | 大小 | SHA-256 |
|
|
64
|
+
| --- | --- | --- | --- |
|
|
65
|
+
| `rar` | `3f51c62b04d3aacc27b0d53f294ecc3b.rar` | 568,456,521 B | `ea41b34bd193cf0039386c0cfe22f93c2225bb43ef01d69740106a35e05d7c85` |
|
|
66
|
+
| `zip` | `a3635617bd88675041ba69f8398df5a0.zip` | 39,463,900 B | `63fdddb6c37849606ae32d08e522bbd2a627be90971005946b6c332a7715ff93` |
|
|
67
|
+
|
|
68
|
+
校验值在主文件被上游替换前一直有效;上游变更时下载会明确报 `checksum mismatch`
|
|
69
|
+
(不会静默留下坏文件)。
|
|
70
|
+
|
|
71
|
+
## 关于 RAR 解压 / About RAR extraction
|
|
72
|
+
|
|
73
|
+
**下载**永远只需要标准库。**解压 `.rar`** 时本包会依次查找
|
|
74
|
+
`7z` / `7zz` / `7za` / `unrar` / `unar` / `bsdtar`(PATH 中或常见安装路径
|
|
75
|
+
`C:\Program Files\7-Zip\7z.exe`、`C:\Program Files\WinRAR\UnRAR.exe`),
|
|
76
|
+
找不到就抛 `XesAssetError` 并提示手工解压。ZIP 解压完全由标准库完成。
|
|
77
|
+
|
|
78
|
+
## License
|
|
79
|
+
|
|
80
|
+
MIT
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=61.0"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "xes-assets"
|
|
7
|
+
version = "1.0.0"
|
|
8
|
+
description = "Zero-dependency downloader for the XES Python course asset files (RAR + ZIP), with size and SHA-256 verification."
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.8"
|
|
11
|
+
license = { text = "MIT" }
|
|
12
|
+
authors = [{ name = "xes-assets" }]
|
|
13
|
+
keywords = [
|
|
14
|
+
"xes",
|
|
15
|
+
"xesimg",
|
|
16
|
+
"assets",
|
|
17
|
+
"downloader",
|
|
18
|
+
"python-assets",
|
|
19
|
+
"no-dependencies",
|
|
20
|
+
"zero-dependency",
|
|
21
|
+
"sha256",
|
|
22
|
+
]
|
|
23
|
+
classifiers = [
|
|
24
|
+
"Development Status :: 5 - Production/Stable",
|
|
25
|
+
"Environment :: Console",
|
|
26
|
+
"Intended Audience :: Developers",
|
|
27
|
+
"License :: OSI Approved :: MIT License",
|
|
28
|
+
"Natural Language :: Chinese (Simplified)",
|
|
29
|
+
"Natural Language :: English",
|
|
30
|
+
"Operating System :: OS Independent",
|
|
31
|
+
"Programming Language :: Python :: 3",
|
|
32
|
+
"Programming Language :: Python :: 3.8",
|
|
33
|
+
"Programming Language :: Python :: 3.9",
|
|
34
|
+
"Programming Language :: Python :: 3.10",
|
|
35
|
+
"Programming Language :: Python :: 3.11",
|
|
36
|
+
"Programming Language :: Python :: 3.12",
|
|
37
|
+
"Programming Language :: Python :: 3.13",
|
|
38
|
+
"Topic :: Software Development :: Libraries :: Python Modules",
|
|
39
|
+
"Topic :: System :: Archiving",
|
|
40
|
+
]
|
|
41
|
+
dependencies = []
|
|
42
|
+
|
|
43
|
+
[project.urls]
|
|
44
|
+
Homepage = "https://pypi.org/project/xes-assets/"
|
|
45
|
+
Source = "https://livefile.xesimg.com/programme/python_assets/"
|
|
46
|
+
|
|
47
|
+
[project.scripts]
|
|
48
|
+
xes-assets = "xes_assets.cli:main"
|
|
49
|
+
|
|
50
|
+
[tool.setuptools]
|
|
51
|
+
package-dir = { "" = "src" }
|
|
52
|
+
|
|
53
|
+
[tool.setuptools.packages.find]
|
|
54
|
+
where = ["src"]
|
|
@@ -0,0 +1,363 @@
|
|
|
1
|
+
"""xes-assets - zero-dependency downloader for the XES Python course assets.
|
|
2
|
+
|
|
3
|
+
The package knows about two public asset files published under
|
|
4
|
+
``https://livefile.xesimg.com/programme/python_assets/`` and downloads them with
|
|
5
|
+
nothing but the Python standard library (no third-party dependency):
|
|
6
|
+
|
|
7
|
+
====================== ============ ================== ==================================
|
|
8
|
+
key type size sha256
|
|
9
|
+
====================== ============ ================== ==================================
|
|
10
|
+
``rar`` RAR archive 568,456,521 bytes ea41b34bd193cf0039386c0cfe22f93c2225bb43ef01d69740106a35e05d7c85
|
|
11
|
+
``zip`` ZIP archive 39,463,900 bytes 63fdddb6c37849606ae32d08e522bbd2a627be90971005946b6c332a7715ff93
|
|
12
|
+
====================== ============ ================== ==================================
|
|
13
|
+
|
|
14
|
+
Quick start::
|
|
15
|
+
|
|
16
|
+
from xes_assets import download, download_all, extract
|
|
17
|
+
|
|
18
|
+
download("zip", r"D:\\xes") # one file, integrity checked
|
|
19
|
+
download_all(r"D:\\xes") # both files
|
|
20
|
+
extract("zip", r"D:\\xes") # stdlib unzip; RAR needs an external tool
|
|
21
|
+
|
|
22
|
+
Command line::
|
|
23
|
+
|
|
24
|
+
python -m xes_assets --all --dest D:\\xes --extract
|
|
25
|
+
"""
|
|
26
|
+
|
|
27
|
+
from __future__ import annotations
|
|
28
|
+
|
|
29
|
+
import hashlib
|
|
30
|
+
import os
|
|
31
|
+
import shutil
|
|
32
|
+
import subprocess
|
|
33
|
+
import sys
|
|
34
|
+
import time
|
|
35
|
+
import urllib.error
|
|
36
|
+
import urllib.request
|
|
37
|
+
import zipfile
|
|
38
|
+
from pathlib import Path
|
|
39
|
+
from typing import Dict, Iterable, List, Optional, Tuple, Union
|
|
40
|
+
|
|
41
|
+
__version__ = "1.0.0"
|
|
42
|
+
|
|
43
|
+
__all__ = [
|
|
44
|
+
"ASSETS",
|
|
45
|
+
"ASSET_ORDER",
|
|
46
|
+
"BASE_URL",
|
|
47
|
+
"XesAssetError",
|
|
48
|
+
"asset_path",
|
|
49
|
+
"download",
|
|
50
|
+
"download_all",
|
|
51
|
+
"extract",
|
|
52
|
+
"get_asset",
|
|
53
|
+
"iter_assets",
|
|
54
|
+
"verify",
|
|
55
|
+
]
|
|
56
|
+
|
|
57
|
+
BASE_URL = "https://livefile.xesimg.com/programme/python_assets/"
|
|
58
|
+
_USER_AGENT = "xes-assets/" + __version__ + " (+https://pypi.org/project/xes-assets/)"
|
|
59
|
+
_CHUNK = 1 << 20
|
|
60
|
+
_RAR_TOOLS = ("7z", "7zz", "7za", "unrar", "unar", "bsdtar")
|
|
61
|
+
_RAR_TOOL_PATHS = (
|
|
62
|
+
r"C:\Program Files\7-Zip\7z.exe",
|
|
63
|
+
r"C:\Program Files (x86)\7-Zip\7z.exe",
|
|
64
|
+
r"C:\Program Files\WinRAR\UnRAR.exe",
|
|
65
|
+
r"C:\Program Files (x86)\WinRAR\UnRAR.exe",
|
|
66
|
+
)
|
|
67
|
+
|
|
68
|
+
PathLike = Union[str, "os.PathLike[str]"]
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
class XesAssetError(RuntimeError):
|
|
72
|
+
"""Raised when an asset cannot be downloaded, verified or unpacked."""
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
#: Metadata of every known asset, keyed by short name.
|
|
76
|
+
ASSETS: Dict[str, dict] = {
|
|
77
|
+
"rar": {
|
|
78
|
+
"key": "rar",
|
|
79
|
+
"filename": "3f51c62b04d3aacc27b0d53f294ecc3b.rar",
|
|
80
|
+
"url": BASE_URL + "3f51c62b04d3aacc27b0d53f294ecc3b.rar",
|
|
81
|
+
"size": 568456521,
|
|
82
|
+
"sha256": "ea41b34bd193cf0039386c0cfe22f93c2225bb43ef01d69740106a35e05d7c85",
|
|
83
|
+
"archive": "rar",
|
|
84
|
+
"description": "Lesson asset archive (~542 MiB), served as RAR.",
|
|
85
|
+
},
|
|
86
|
+
"zip": {
|
|
87
|
+
"key": "zip",
|
|
88
|
+
"filename": "a3635617bd88675041ba69f8398df5a0.zip",
|
|
89
|
+
"url": BASE_URL + "a3635617bd88675041ba69f8398df5a0.zip",
|
|
90
|
+
"size": 39463900,
|
|
91
|
+
"sha256": "63fdddb6c37849606ae32d08e522bbd2a627be90971005946b6c332a7715ff93",
|
|
92
|
+
"archive": "zip",
|
|
93
|
+
"description": "Lesson asset archive (~38 MiB), served as ZIP.",
|
|
94
|
+
},
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
#: Preferred download order (biggest first, so the rest can run while it finishes).
|
|
98
|
+
ASSET_ORDER: Tuple[str, ...] = ("rar", "zip")
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def _human(n: Union[int, float]) -> str:
|
|
102
|
+
"""Format a byte count as a short human readable string."""
|
|
103
|
+
n = float(n)
|
|
104
|
+
for unit in ("B", "KiB", "MiB", "GiB", "TiB"):
|
|
105
|
+
if n < 1024.0 or unit == "TiB":
|
|
106
|
+
return ("%.0f %s" if unit == "B" else "%.2f %s") % (n, unit)
|
|
107
|
+
n /= 1024.0
|
|
108
|
+
return "%.2f TiB" % n
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def get_asset(key: str) -> dict:
|
|
112
|
+
"""Return the metadata dict for ``key``.
|
|
113
|
+
|
|
114
|
+
``key`` may be a short key (``"rar"`` / ``"zip"``), a file name, a file
|
|
115
|
+
stem or a URL. Unknown values raise :class:`KeyError`.
|
|
116
|
+
"""
|
|
117
|
+
if not isinstance(key, str):
|
|
118
|
+
raise TypeError("asset key must be a string, got %r" % (type(key).__name__,))
|
|
119
|
+
name = key.strip()
|
|
120
|
+
low = name.lower()
|
|
121
|
+
if low in ASSETS:
|
|
122
|
+
return ASSETS[low]
|
|
123
|
+
base = os.path.basename(low)
|
|
124
|
+
stem = base.rsplit(".", 1)[0]
|
|
125
|
+
for info in ASSETS.values():
|
|
126
|
+
if base == info["filename"].lower() or stem == info["filename"].rsplit(".", 1)[0].lower():
|
|
127
|
+
return info
|
|
128
|
+
for info in ASSETS.values():
|
|
129
|
+
if name == info["url"]:
|
|
130
|
+
return info
|
|
131
|
+
raise KeyError("unknown asset %r (known: %s)" % (key, ", ".join(ASSET_ORDER)))
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
def iter_assets() -> Iterable[dict]:
|
|
135
|
+
"""Yield asset metadata dicts in :data:`ASSET_ORDER`."""
|
|
136
|
+
for key in ASSET_ORDER:
|
|
137
|
+
yield ASSETS[key]
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
def asset_path(key: str, dest: PathLike = ".") -> Path:
|
|
141
|
+
"""Return the local path ``key`` would be downloaded to inside ``dest``."""
|
|
142
|
+
info = get_asset(key)
|
|
143
|
+
return Path(dest).expanduser() / info["filename"]
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
def _sha256(path: PathLike) -> str:
|
|
147
|
+
"""Stream ``path`` through SHA-256 and return the hex digest."""
|
|
148
|
+
digest = hashlib.sha256()
|
|
149
|
+
with open(path, "rb") as fh:
|
|
150
|
+
while True:
|
|
151
|
+
block = fh.read(_CHUNK)
|
|
152
|
+
if not block:
|
|
153
|
+
break
|
|
154
|
+
digest.update(block)
|
|
155
|
+
return digest.hexdigest()
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
def verify(key: str, path: Optional[PathLike] = None, dest: PathLike = ".") -> bool:
|
|
159
|
+
"""Check size and SHA-256 of a downloaded asset.
|
|
160
|
+
|
|
161
|
+
Returns ``True`` when the file matches the expected size and digest.
|
|
162
|
+
"""
|
|
163
|
+
info = get_asset(key)
|
|
164
|
+
target = Path(path).expanduser() if path is not None else asset_path(key, dest)
|
|
165
|
+
if not target.is_file():
|
|
166
|
+
return False
|
|
167
|
+
if target.stat().st_size != info["size"]:
|
|
168
|
+
return False
|
|
169
|
+
return _sha256(target) == info["sha256"]
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
def _progress_printer(label: str, total: int, stream):
|
|
173
|
+
state = {"last": 0.0, "start": time.time()}
|
|
174
|
+
|
|
175
|
+
def report(done: int, force: bool = False) -> None:
|
|
176
|
+
now = time.time()
|
|
177
|
+
if not force and now - state["last"] < 0.5 and done < total:
|
|
178
|
+
return
|
|
179
|
+
state["last"] = now
|
|
180
|
+
elapsed = max(now - state["start"], 1e-6)
|
|
181
|
+
pct = (100.0 * done / total) if total else 0.0
|
|
182
|
+
spe = done / elapsed
|
|
183
|
+
stream.write(
|
|
184
|
+
"\r%s %6.2f%% %s / %s (%s/s)"
|
|
185
|
+
% (label, pct, _human(done), _human(total), _human(spe))
|
|
186
|
+
)
|
|
187
|
+
stream.flush()
|
|
188
|
+
|
|
189
|
+
return report
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
def download(
|
|
193
|
+
key: str,
|
|
194
|
+
dest: PathLike = ".",
|
|
195
|
+
*,
|
|
196
|
+
overwrite: bool = False,
|
|
197
|
+
verify_checksum: bool = True,
|
|
198
|
+
progress: Optional[bool] = None,
|
|
199
|
+
timeout: float = 60.0,
|
|
200
|
+
retries: int = 3,
|
|
201
|
+
) -> Path:
|
|
202
|
+
"""Download one asset into ``dest`` and return its local path.
|
|
203
|
+
|
|
204
|
+
The transfer goes to ``<name>.part`` and is renamed only after the size and
|
|
205
|
+
(unless ``verify_checksum=False``) the SHA-256 digest match, so an
|
|
206
|
+
interrupted or corrupted download never leaves a usable-looking file.
|
|
207
|
+
Partial files are resumed with an HTTP ``Range`` request when the server
|
|
208
|
+
supports it. ``progress`` defaults to ``True`` only when stderr is a TTY.
|
|
209
|
+
"""
|
|
210
|
+
info = get_asset(key)
|
|
211
|
+
if progress is None:
|
|
212
|
+
progress = bool(getattr(sys.stderr, "isatty", lambda: False)())
|
|
213
|
+
dest_dir = Path(dest).expanduser()
|
|
214
|
+
dest_dir.mkdir(parents=True, exist_ok=True)
|
|
215
|
+
target = dest_dir / info["filename"]
|
|
216
|
+
part = target.with_name(target.name + ".part")
|
|
217
|
+
label = info["key"].ljust(4)
|
|
218
|
+
|
|
219
|
+
if target.exists() and not overwrite:
|
|
220
|
+
if not verify_checksum or verify(info["key"], target):
|
|
221
|
+
if progress:
|
|
222
|
+
sys.stderr.write("%s already downloaded: %s\n" % (label, target))
|
|
223
|
+
return target
|
|
224
|
+
raise XesAssetError(
|
|
225
|
+
"%s exists but does not match the expected checksum; pass overwrite=True "
|
|
226
|
+
"to replace it: %s" % (info["key"], target)
|
|
227
|
+
)
|
|
228
|
+
|
|
229
|
+
printer = _progress_printer(label, info["size"], sys.stderr) if progress else None
|
|
230
|
+
last_error: Optional[BaseException] = None
|
|
231
|
+
|
|
232
|
+
for attempt in range(1, max(1, retries) + 1):
|
|
233
|
+
done = part.stat().st_size if part.is_file() else 0
|
|
234
|
+
if done >= info["size"]:
|
|
235
|
+
done = 0
|
|
236
|
+
if done and overwrite:
|
|
237
|
+
done = 0
|
|
238
|
+
if done == 0 and part.exists():
|
|
239
|
+
part.unlink()
|
|
240
|
+
headers = {"User-Agent": _USER_AGENT}
|
|
241
|
+
mode = "wb"
|
|
242
|
+
if done:
|
|
243
|
+
headers["Range"] = "bytes=%d-" % done
|
|
244
|
+
mode = "ab"
|
|
245
|
+
request = urllib.request.Request(info["url"], headers=headers)
|
|
246
|
+
try:
|
|
247
|
+
with urllib.request.urlopen(request, timeout=timeout) as response:
|
|
248
|
+
if done and getattr(response, "status", 200) != 206:
|
|
249
|
+
# Server ignored Range: start over from the beginning.
|
|
250
|
+
done = 0
|
|
251
|
+
mode = "wb"
|
|
252
|
+
if mode == "wb":
|
|
253
|
+
part.unlink(missing_ok=True)
|
|
254
|
+
with open(part, mode) as out:
|
|
255
|
+
while True:
|
|
256
|
+
block = response.read(_CHUNK)
|
|
257
|
+
if not block:
|
|
258
|
+
break
|
|
259
|
+
out.write(block)
|
|
260
|
+
done += len(block)
|
|
261
|
+
if printer:
|
|
262
|
+
printer(done)
|
|
263
|
+
if printer:
|
|
264
|
+
printer(done, force=True)
|
|
265
|
+
sys.stderr.write("\n")
|
|
266
|
+
size = part.stat().st_size
|
|
267
|
+
if size != info["size"]:
|
|
268
|
+
raise XesAssetError(
|
|
269
|
+
"%s size mismatch: got %d bytes, expected %d"
|
|
270
|
+
% (info["key"], size, info["size"])
|
|
271
|
+
)
|
|
272
|
+
if verify_checksum:
|
|
273
|
+
got = _sha256(part)
|
|
274
|
+
if got != info["sha256"]:
|
|
275
|
+
raise XesAssetError(
|
|
276
|
+
"%s checksum mismatch: got %s, expected %s"
|
|
277
|
+
% (info["key"], got, info["sha256"])
|
|
278
|
+
)
|
|
279
|
+
os.replace(part, target)
|
|
280
|
+
return target
|
|
281
|
+
except XesAssetError:
|
|
282
|
+
raise
|
|
283
|
+
except Exception as exc: # network hiccup: keep .part and resume
|
|
284
|
+
last_error = exc
|
|
285
|
+
if attempt >= max(1, retries):
|
|
286
|
+
break
|
|
287
|
+
if printer:
|
|
288
|
+
sys.stderr.write("\n%s attempt %d failed (%s); retrying\n" % (label, attempt, exc))
|
|
289
|
+
time.sleep(min(2 ** attempt, 10))
|
|
290
|
+
|
|
291
|
+
raise XesAssetError(
|
|
292
|
+
"failed to download %s from %s after %d attempt(s): %s"
|
|
293
|
+
% (info["key"], info["url"], max(1, retries), last_error)
|
|
294
|
+
)
|
|
295
|
+
|
|
296
|
+
|
|
297
|
+
def download_all(
|
|
298
|
+
dest: PathLike = ".",
|
|
299
|
+
*,
|
|
300
|
+
keys: Optional[Iterable[str]] = None,
|
|
301
|
+
**kwargs,
|
|
302
|
+
) -> List[Path]:
|
|
303
|
+
"""Download several assets (all by default) and return their paths."""
|
|
304
|
+
wanted = list(keys) if keys is not None else list(ASSET_ORDER)
|
|
305
|
+
return [download(key, dest, **kwargs) for key in wanted]
|
|
306
|
+
|
|
307
|
+
|
|
308
|
+
def _find_rar_tool() -> Optional[str]:
|
|
309
|
+
"""Return the path of an available RAR extractor, or ``None``."""
|
|
310
|
+
for tool in _RAR_TOOLS:
|
|
311
|
+
found = shutil.which(tool)
|
|
312
|
+
if found:
|
|
313
|
+
return found
|
|
314
|
+
for candidate in _RAR_TOOL_PATHS:
|
|
315
|
+
if os.path.isfile(candidate):
|
|
316
|
+
return candidate
|
|
317
|
+
return None
|
|
318
|
+
|
|
319
|
+
|
|
320
|
+
def extract(
|
|
321
|
+
key: str,
|
|
322
|
+
dest: Optional[PathLike] = None,
|
|
323
|
+
*,
|
|
324
|
+
archive: Optional[PathLike] = None,
|
|
325
|
+
overwrite: bool = False,
|
|
326
|
+
) -> Path:
|
|
327
|
+
"""Unpack a downloaded asset.
|
|
328
|
+
|
|
329
|
+
``dest`` defaults to the folder that holds the archive. ZIP files are
|
|
330
|
+
unpacked with the standard library; RAR files need an external extractor
|
|
331
|
+
(``7z``/``7zz``/``7za``/``unrar``/``unar``/``bsdtar``) in ``PATH`` and raise
|
|
332
|
+
:class:`XesAssetError` when none is found. Returns the output directory.
|
|
333
|
+
"""
|
|
334
|
+
info = get_asset(key)
|
|
335
|
+
source = Path(archive).expanduser() if archive is not None else asset_path(key, dest if dest else ".")
|
|
336
|
+
out_dir = Path(dest).expanduser() if dest is not None else source.parent
|
|
337
|
+
if not source.is_file():
|
|
338
|
+
raise XesAssetError("archive not found: %s (run download() first)" % source)
|
|
339
|
+
out_dir.mkdir(parents=True, exist_ok=True)
|
|
340
|
+
|
|
341
|
+
if info["archive"] == "zip":
|
|
342
|
+
with zipfile.ZipFile(source) as zf:
|
|
343
|
+
zf.extractall(out_dir)
|
|
344
|
+
return out_dir
|
|
345
|
+
|
|
346
|
+
tool = _find_rar_tool()
|
|
347
|
+
if tool is None:
|
|
348
|
+
raise XesAssetError(
|
|
349
|
+
"no RAR extractor found; install 7-Zip or WinRAR (or keep one of %s), "
|
|
350
|
+
"or unpack %s manually" % (", ".join(_RAR_TOOL_PATHS), source)
|
|
351
|
+
)
|
|
352
|
+
exe = os.path.basename(tool).lower()
|
|
353
|
+
if exe.startswith(("7z", "7zz", "7za")):
|
|
354
|
+
cmd = [tool, "x", "-y", "-bso0", "-bsp0", "-o" + str(out_dir), str(source)]
|
|
355
|
+
else: # unrar / unar / bsdtar style: unrar x -y <archive> <dest\\>
|
|
356
|
+
cmd = [tool, "x", "-y", str(source), str(out_dir) + os.sep]
|
|
357
|
+
proc = subprocess.run(cmd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
|
|
358
|
+
if proc.returncode != 0:
|
|
359
|
+
raise XesAssetError(
|
|
360
|
+
"RAR extraction failed (%s): %s"
|
|
361
|
+
% (" ".join(cmd), proc.stdout.decode("utf-8", "replace").strip())
|
|
362
|
+
)
|
|
363
|
+
return out_dir
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
"""Command line interface for :mod:`xes_assets`."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import argparse
|
|
6
|
+
import sys
|
|
7
|
+
|
|
8
|
+
from . import (
|
|
9
|
+
ASSET_ORDER,
|
|
10
|
+
ASSETS,
|
|
11
|
+
__version__,
|
|
12
|
+
download,
|
|
13
|
+
extract,
|
|
14
|
+
get_asset,
|
|
15
|
+
verify,
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
_EPILOG = """examples:
|
|
19
|
+
xes-assets --list show the known assets
|
|
20
|
+
xes-assets zip -d D:\\xes download the ZIP file
|
|
21
|
+
xes-assets all -d D:\\xes -x download both files, then unpack
|
|
22
|
+
xes-assets rar -d D:\\xes --verify-only check an already downloaded file
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def _build_parser() -> argparse.ArgumentParser:
|
|
27
|
+
parser = argparse.ArgumentParser(
|
|
28
|
+
prog="xes-assets",
|
|
29
|
+
description="Download the XES Python course asset files (no dependencies).",
|
|
30
|
+
epilog=_EPILOG,
|
|
31
|
+
formatter_class=argparse.RawDescriptionHelpFormatter,
|
|
32
|
+
)
|
|
33
|
+
parser.add_argument(
|
|
34
|
+
"assets",
|
|
35
|
+
nargs="*",
|
|
36
|
+
metavar="ASSET",
|
|
37
|
+
help="asset key or file name: %s, or 'all' (default)" % ", ".join(ASSET_ORDER),
|
|
38
|
+
)
|
|
39
|
+
parser.add_argument("-d", "--dest", default=".", help="output directory (default: current)")
|
|
40
|
+
parser.add_argument("-x", "--extract", action="store_true", help="unpack each file after download")
|
|
41
|
+
parser.add_argument("-f", "--overwrite", action="store_true", help="replace existing files")
|
|
42
|
+
parser.add_argument("--no-verify", action="store_true", help="skip the SHA-256 check")
|
|
43
|
+
parser.add_argument("-q", "--quiet", action="store_true", help="hide the progress bar")
|
|
44
|
+
parser.add_argument("--verify-only", action="store_true", help="only check already downloaded files")
|
|
45
|
+
parser.add_argument("--list", action="store_true", help="list the known assets and exit")
|
|
46
|
+
parser.add_argument("--version", action="version", version="xes-assets " + __version__)
|
|
47
|
+
return parser
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def _resolve(names) -> list:
|
|
51
|
+
keys = []
|
|
52
|
+
for name in names or ["all"]:
|
|
53
|
+
if name.strip().lower() in ("all", "*"):
|
|
54
|
+
keys.extend(k for k in ASSET_ORDER if k not in keys)
|
|
55
|
+
continue
|
|
56
|
+
key = get_asset(name)["key"]
|
|
57
|
+
if key not in keys:
|
|
58
|
+
keys.append(key)
|
|
59
|
+
return keys
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def main(argv=None) -> int:
|
|
63
|
+
args = _build_parser().parse_args(argv)
|
|
64
|
+
|
|
65
|
+
if args.list:
|
|
66
|
+
for info in (ASSETS[k] for k in ASSET_ORDER):
|
|
67
|
+
print(
|
|
68
|
+
"%-4s %-46s %12s %s"
|
|
69
|
+
% (info["key"], info["filename"], info["size"], info["url"])
|
|
70
|
+
)
|
|
71
|
+
return 0
|
|
72
|
+
|
|
73
|
+
keys = _resolve(args.assets)
|
|
74
|
+
failures = 0
|
|
75
|
+
for key in keys:
|
|
76
|
+
try:
|
|
77
|
+
if args.verify_only:
|
|
78
|
+
ok = verify(key, dest=args.dest)
|
|
79
|
+
print("%s %s: %s" % ("[OK] " if ok else "[FAIL]", key, args.dest))
|
|
80
|
+
failures += 0 if ok else 1
|
|
81
|
+
continue
|
|
82
|
+
path = download(
|
|
83
|
+
key,
|
|
84
|
+
args.dest,
|
|
85
|
+
overwrite=args.overwrite,
|
|
86
|
+
verify_checksum=not args.no_verify,
|
|
87
|
+
progress=not args.quiet,
|
|
88
|
+
)
|
|
89
|
+
print("[OK] %s -> %s" % (key, path))
|
|
90
|
+
if args.extract:
|
|
91
|
+
out = extract(key, archive=path)
|
|
92
|
+
print("[OK] %s unpacked into %s" % (key, out))
|
|
93
|
+
except Exception as exc: # noqa: BLE001 - one guard for the whole CLI loop
|
|
94
|
+
sys.stderr.write("[FAIL] %s: %s\n" % (key, exc))
|
|
95
|
+
failures += 1
|
|
96
|
+
return 1 if failures else 0
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
if __name__ == "__main__": # pragma: no cover
|
|
100
|
+
raise SystemExit(main())
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: xes-assets
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: Zero-dependency downloader for the XES Python course asset files (RAR + ZIP), with size and SHA-256 verification.
|
|
5
|
+
Author: xes-assets
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Homepage, https://pypi.org/project/xes-assets/
|
|
8
|
+
Project-URL: Source, https://livefile.xesimg.com/programme/python_assets/
|
|
9
|
+
Keywords: xes,xesimg,assets,downloader,python-assets,no-dependencies,zero-dependency,sha256
|
|
10
|
+
Classifier: Development Status :: 5 - Production/Stable
|
|
11
|
+
Classifier: Environment :: Console
|
|
12
|
+
Classifier: Intended Audience :: Developers
|
|
13
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
14
|
+
Classifier: Natural Language :: Chinese (Simplified)
|
|
15
|
+
Classifier: Natural Language :: English
|
|
16
|
+
Classifier: Operating System :: OS Independent
|
|
17
|
+
Classifier: Programming Language :: Python :: 3
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.8
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
21
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
22
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
23
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
24
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
25
|
+
Classifier: Topic :: System :: Archiving
|
|
26
|
+
Requires-Python: >=3.8
|
|
27
|
+
Description-Content-Type: text/markdown
|
|
28
|
+
License-File: LICENSE
|
|
29
|
+
Dynamic: license-file
|
|
30
|
+
|
|
31
|
+
# xes-assets
|
|
32
|
+
|
|
33
|
+
零依赖下载器 / Zero-dependency downloader for the XES Python course asset files:
|
|
34
|
+
|
|
35
|
+
- `https://livefile.xesimg.com/programme/python_assets/3f51c62b04d3aacc27b0d53f294ecc3b.rar`
|
|
36
|
+
- `https://livefile.xesimg.com/programme/python_assets/a3635617bd88675041ba69f8398df5a0.zip`
|
|
37
|
+
|
|
38
|
+
只用 Python 标准库(`urllib`、`hashlib`、`zipfile`),**没有任何第三方依赖**。
|
|
39
|
+
Uses nothing but the Python standard library — **no third-party dependency at all**.
|
|
40
|
+
|
|
41
|
+
## 安装 / Install
|
|
42
|
+
|
|
43
|
+
```bash
|
|
44
|
+
pip install xes-assets
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
## 命令行 / CLI
|
|
48
|
+
|
|
49
|
+
```bash
|
|
50
|
+
xes-assets --list # 查看两个文件的信息
|
|
51
|
+
xes-assets zip -d D:\xes # 下载 ZIP
|
|
52
|
+
xes-assets all -d D:\xes -x # 下载两个并解压
|
|
53
|
+
xes-assets rar --verify-only -d D:\xes # 只校验已下载文件
|
|
54
|
+
python -m xes_assets all -d D:\xes # 等价写法
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
| 参数 | 说明 |
|
|
58
|
+
| --- | --- |
|
|
59
|
+
| `ASSET`(位置参数) | `rar`、`zip`、`all`、文件名或 URL;省略时等于 `all` |
|
|
60
|
+
| `-d, --dest DIR` | 输出目录(默认当前目录) |
|
|
61
|
+
| `-x, --extract` | 下载后解压(RAR 需要外部解压工具,见下) |
|
|
62
|
+
| `-f, --overwrite` | 覆盖已存在文件 |
|
|
63
|
+
| `--no-verify` | 跳过 SHA-256 校验 |
|
|
64
|
+
| `-q, --quiet` | 不显示进度 |
|
|
65
|
+
| `--verify-only` | 只校验不下载 |
|
|
66
|
+
| `--list` | 列出已知文件 |
|
|
67
|
+
|
|
68
|
+
## Python API
|
|
69
|
+
|
|
70
|
+
```python
|
|
71
|
+
from xes_assets import ASSETS, download, download_all, extract, verify
|
|
72
|
+
|
|
73
|
+
download("zip", r"D:\xes") # -> Path,含大小 + SHA-256 校验
|
|
74
|
+
download_all(r"D:\xes") # 两个都下
|
|
75
|
+
verify("rar", r"D:\xes") # -> True/False
|
|
76
|
+
extract("zip", r"D:\xes") # 标准库解压,返回目录
|
|
77
|
+
extract("rar", r"D:\xes") # 需外部 7z / UnRAR(见下)
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
单个函数的行为:
|
|
81
|
+
|
|
82
|
+
- `download(key, dest=".", *, overwrite=False, verify_checksum=True, progress=None, timeout=60, retries=3)`
|
|
83
|
+
先写 `<文件名>.part`,校验大小与 SHA-256 通过后才原子改名为正式文件;断点续传用 HTTP `Range`
|
|
84
|
+
(服务器返回 206 时继续,否则从头开始),网络中断自动重试。
|
|
85
|
+
- `download_all(dest=".", *, keys=None, **kwargs)` → `List[Path]`
|
|
86
|
+
- `extract(key, dest=None, *, archive=None, overwrite=False)` → 解压目录 `Path`
|
|
87
|
+
- `verify(key, path=None, dest=".")` → `bool`
|
|
88
|
+
- `asset_path(key, dest=".")` → 预期的本地路径
|
|
89
|
+
- `ASSETS` / `ASSET_ORDER` 为元数据常量;`XesAssetError` 为统一异常
|
|
90
|
+
|
|
91
|
+
## 文件与校验值 / Files
|
|
92
|
+
|
|
93
|
+
| key | 文件名 | 大小 | SHA-256 |
|
|
94
|
+
| --- | --- | --- | --- |
|
|
95
|
+
| `rar` | `3f51c62b04d3aacc27b0d53f294ecc3b.rar` | 568,456,521 B | `ea41b34bd193cf0039386c0cfe22f93c2225bb43ef01d69740106a35e05d7c85` |
|
|
96
|
+
| `zip` | `a3635617bd88675041ba69f8398df5a0.zip` | 39,463,900 B | `63fdddb6c37849606ae32d08e522bbd2a627be90971005946b6c332a7715ff93` |
|
|
97
|
+
|
|
98
|
+
校验值在主文件被上游替换前一直有效;上游变更时下载会明确报 `checksum mismatch`
|
|
99
|
+
(不会静默留下坏文件)。
|
|
100
|
+
|
|
101
|
+
## 关于 RAR 解压 / About RAR extraction
|
|
102
|
+
|
|
103
|
+
**下载**永远只需要标准库。**解压 `.rar`** 时本包会依次查找
|
|
104
|
+
`7z` / `7zz` / `7za` / `unrar` / `unar` / `bsdtar`(PATH 中或常见安装路径
|
|
105
|
+
`C:\Program Files\7-Zip\7z.exe`、`C:\Program Files\WinRAR\UnRAR.exe`),
|
|
106
|
+
找不到就抛 `XesAssetError` 并提示手工解压。ZIP 解压完全由标准库完成。
|
|
107
|
+
|
|
108
|
+
## License
|
|
109
|
+
|
|
110
|
+
MIT
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
LICENSE
|
|
2
|
+
README.md
|
|
3
|
+
pyproject.toml
|
|
4
|
+
src/xes_assets/__init__.py
|
|
5
|
+
src/xes_assets/__main__.py
|
|
6
|
+
src/xes_assets/cli.py
|
|
7
|
+
src/xes_assets.egg-info/PKG-INFO
|
|
8
|
+
src/xes_assets.egg-info/SOURCES.txt
|
|
9
|
+
src/xes_assets.egg-info/dependency_links.txt
|
|
10
|
+
src/xes_assets.egg-info/entry_points.txt
|
|
11
|
+
src/xes_assets.egg-info/top_level.txt
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
xes_assets
|