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.
@@ -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,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -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,6 @@
1
+ """Allow ``python -m xes_assets``."""
2
+
3
+ from .cli import main
4
+
5
+ if __name__ == "__main__":
6
+ raise SystemExit(main())
@@ -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,2 @@
1
+ [console_scripts]
2
+ xes-assets = xes_assets.cli:main
@@ -0,0 +1 @@
1
+ xes_assets