pirateface 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.
@@ -0,0 +1,4 @@
1
+ __pycache__/
2
+ *.pyc
3
+ .venv/
4
+ dist/
@@ -0,0 +1 @@
1
+ 3.12
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Pirate Face
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,89 @@
1
+ Metadata-Version: 2.5
2
+ Name: pirateface
3
+ Version: 0.1.0
4
+ Summary: Download Hugging Face models over BitTorrent from Pirate Face seeders. No code changes.
5
+ Project-URL: Homepage, https://pirateface.co
6
+ Project-URL: Source, https://github.com/pirateface/pirateface_hub
7
+ Project-URL: Issues, https://github.com/pirateface/pirateface_hub/issues
8
+ License-Expression: MIT
9
+ License-File: LICENSE
10
+ Keywords: bittorrent,download,huggingface,libtorrent,models
11
+ Classifier: Development Status :: 3 - Alpha
12
+ Classifier: Intended Audience :: Developers
13
+ Classifier: Programming Language :: Python :: 3
14
+ Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
15
+ Requires-Python: >=3.12
16
+ Requires-Dist: huggingface-hub>=2.0
17
+ Requires-Dist: libtorrent>=2.0
18
+ Requires-Dist: tqdm>=4.42
19
+ Description-Content-Type: text/markdown
20
+
21
+ # Pirate Face Hub
22
+
23
+ **Plug and play torrent downloads for Hugging Face models.** Add one package. Change nothing else.
24
+
25
+ ```sh
26
+ uv run --with pirateface python your_script.py
27
+ ```
28
+
29
+ or install it once and forget about it:
30
+
31
+ ```sh
32
+ pip install pirateface
33
+ ```
34
+
35
+ That's the whole setup. No code changes, no config, no new API. Your script still calls transformers or
36
+ `huggingface_hub` like it always did, and the files now come from [Pirate Face](https://pirateface.co)
37
+ seeders over BitTorrent:
38
+
39
+ ```python
40
+ from transformers import AutoModelForCausalLM
41
+
42
+ model = AutoModelForCausalLM.from_pretrained("org/model") # unchanged, now from the swarm
43
+ ```
44
+
45
+ The same goes for anything that downloads through `huggingface_hub`: diffusers, sentence-transformers,
46
+ `hf_hub_download`, `snapshot_download` and the `hf download` CLI.
47
+
48
+ Every file is checked against Hugging Face's own fingerprint (SHA-256 for large files, git SHA-1 for the
49
+ rest) before it goes into the normal Hugging Face cache. If Pirate Face doesn't have the model, a peer is
50
+ missing, or a check fails, that file comes from Hugging Face as usual. Uninstall it and your code
51
+ runs exactly as before.
52
+
53
+ There's also a small CLI:
54
+
55
+ ```sh
56
+ pf download org/model [--revision REV]
57
+ ```
58
+
59
+ ## How it hooks in
60
+
61
+ The package installs `pirateface.pth`, which Python runs at startup. It does nothing until something
62
+ imports `huggingface_hub`, then fetches each file's bytes from the swarm. Hugging Face still answers
63
+ which commit and fingerprint to expect, and every file from the swarm is checked against it. If Pirate
64
+ Face has no torrent, or no peer sends the file list within 30 s, the file comes from Hugging Face as usual.
65
+ Cache layout, symlinks and locks stay with `huggingface_hub`.
66
+
67
+ ## Settings
68
+
69
+ | Variable | Default | |
70
+ |---|---|---|
71
+ | `PF_DISABLE` | off | Turn the hook off |
72
+ | `PF_QUIET` | off | Print nothing |
73
+ | `PF_NO_RESCUE` | off | Don't ask Pirate Face to add models it doesn't have yet |
74
+ | `PF_DEBUG` | off | Print libtorrent's log |
75
+ | `PF_CACHE` | `~/.cache/pirateface` | Where torrent data and file lists are kept |
76
+ | `PF_API` | `https://pirateface.co` | Pirate Face server |
77
+ | `PF_LISTEN` | any free port | libtorrent listen interfaces |
78
+ | `PF_METADATA_TIMEOUT` | `30` | Seconds to wait for a peer to send the file list |
79
+ | `PF_STALL_TIMEOUT` | `60` | Seconds without progress before falling back to Hugging Face |
80
+
81
+ Flags are on for any value except empty or `0`.
82
+
83
+ Needs Python 3.12 or 3.13 (libtorrent has no wheels for 3.14 yet).
84
+
85
+ ## Development
86
+
87
+ ```sh
88
+ uv run --group dev ruff check
89
+ ```
@@ -0,0 +1,69 @@
1
+ # Pirate Face Hub
2
+
3
+ **Plug and play torrent downloads for Hugging Face models.** Add one package. Change nothing else.
4
+
5
+ ```sh
6
+ uv run --with pirateface python your_script.py
7
+ ```
8
+
9
+ or install it once and forget about it:
10
+
11
+ ```sh
12
+ pip install pirateface
13
+ ```
14
+
15
+ That's the whole setup. No code changes, no config, no new API. Your script still calls transformers or
16
+ `huggingface_hub` like it always did, and the files now come from [Pirate Face](https://pirateface.co)
17
+ seeders over BitTorrent:
18
+
19
+ ```python
20
+ from transformers import AutoModelForCausalLM
21
+
22
+ model = AutoModelForCausalLM.from_pretrained("org/model") # unchanged, now from the swarm
23
+ ```
24
+
25
+ The same goes for anything that downloads through `huggingface_hub`: diffusers, sentence-transformers,
26
+ `hf_hub_download`, `snapshot_download` and the `hf download` CLI.
27
+
28
+ Every file is checked against Hugging Face's own fingerprint (SHA-256 for large files, git SHA-1 for the
29
+ rest) before it goes into the normal Hugging Face cache. If Pirate Face doesn't have the model, a peer is
30
+ missing, or a check fails, that file comes from Hugging Face as usual. Uninstall it and your code
31
+ runs exactly as before.
32
+
33
+ There's also a small CLI:
34
+
35
+ ```sh
36
+ pf download org/model [--revision REV]
37
+ ```
38
+
39
+ ## How it hooks in
40
+
41
+ The package installs `pirateface.pth`, which Python runs at startup. It does nothing until something
42
+ imports `huggingface_hub`, then fetches each file's bytes from the swarm. Hugging Face still answers
43
+ which commit and fingerprint to expect, and every file from the swarm is checked against it. If Pirate
44
+ Face has no torrent, or no peer sends the file list within 30 s, the file comes from Hugging Face as usual.
45
+ Cache layout, symlinks and locks stay with `huggingface_hub`.
46
+
47
+ ## Settings
48
+
49
+ | Variable | Default | |
50
+ |---|---|---|
51
+ | `PF_DISABLE` | off | Turn the hook off |
52
+ | `PF_QUIET` | off | Print nothing |
53
+ | `PF_NO_RESCUE` | off | Don't ask Pirate Face to add models it doesn't have yet |
54
+ | `PF_DEBUG` | off | Print libtorrent's log |
55
+ | `PF_CACHE` | `~/.cache/pirateface` | Where torrent data and file lists are kept |
56
+ | `PF_API` | `https://pirateface.co` | Pirate Face server |
57
+ | `PF_LISTEN` | any free port | libtorrent listen interfaces |
58
+ | `PF_METADATA_TIMEOUT` | `30` | Seconds to wait for a peer to send the file list |
59
+ | `PF_STALL_TIMEOUT` | `60` | Seconds without progress before falling back to Hugging Face |
60
+
61
+ Flags are on for any value except empty or `0`.
62
+
63
+ Needs Python 3.12 or 3.13 (libtorrent has no wheels for 3.14 yet).
64
+
65
+ ## Development
66
+
67
+ ```sh
68
+ uv run --group dev ruff check
69
+ ```
@@ -0,0 +1,49 @@
1
+ [project]
2
+ name = "pirateface"
3
+ version = "0.1.0"
4
+ description = "Download Hugging Face models over BitTorrent from Pirate Face seeders. No code changes."
5
+ readme = "README.md"
6
+ license = "MIT"
7
+ license-files = ["LICENSE"]
8
+ requires-python = ">=3.12"
9
+ keywords = ["huggingface", "bittorrent", "libtorrent", "models", "download"]
10
+ classifiers = [
11
+ "Development Status :: 3 - Alpha",
12
+ "Intended Audience :: Developers",
13
+ "Programming Language :: Python :: 3",
14
+ "Topic :: Scientific/Engineering :: Artificial Intelligence",
15
+ ]
16
+ dependencies = [
17
+ "huggingface-hub>=2.0",
18
+ "libtorrent>=2.0",
19
+ "tqdm>=4.42",
20
+ ]
21
+
22
+ [project.urls]
23
+ Homepage = "https://pirateface.co"
24
+ Source = "https://github.com/pirateface/pirateface_hub"
25
+ Issues = "https://github.com/pirateface/pirateface_hub/issues"
26
+
27
+ [project.scripts]
28
+ pf = "pirateface.cli:main"
29
+
30
+ [build-system]
31
+ requires = ["hatchling>=1.27"]
32
+ build-backend = "hatchling.build"
33
+
34
+ [tool.hatch.build.targets.wheel]
35
+ packages = ["src/pirateface"]
36
+
37
+ [tool.hatch.build.targets.wheel.force-include]
38
+ "src/pirateface.pth" = "pirateface.pth"
39
+
40
+ [dependency-groups]
41
+ dev = ["ruff>=0.6"]
42
+
43
+ [tool.ruff]
44
+ line-length = 120
45
+
46
+ [tool.ruff.lint]
47
+ extend-select = ["B", "I", "UP", "SIM", "PTH", "RUF"]
48
+ # Broad excepts are deliberate: any failure falls back to Hugging Face.
49
+ ignore = ["BLE001"]
@@ -0,0 +1,47 @@
1
+ """Runs at every Python startup via pirateface.pth, so it must be cheap and never raise.
2
+ Patches huggingface_hub lazily, when something imports it."""
3
+
4
+ import sys
5
+
6
+ from pirateface.config import flag
7
+
8
+ TARGETS = {"huggingface_hub.file_download"}
9
+
10
+
11
+ def _install(name, module):
12
+ try:
13
+ from pirateface import huggingface
14
+
15
+ huggingface.install(module)
16
+ except Exception as e:
17
+ print(f"[pirateface] hook disabled: {e}", file=sys.stderr)
18
+
19
+
20
+ class _PatchOnImport:
21
+ def find_spec(self, name, path=None, target=None):
22
+ if name not in TARGETS:
23
+ return None
24
+ sys.meta_path.remove(self)
25
+ try:
26
+ import importlib.util
27
+
28
+ spec = importlib.util.find_spec(name)
29
+ finally:
30
+ sys.meta_path.insert(0, self)
31
+ if spec is None or spec.loader is None:
32
+ return None
33
+ exec_module = spec.loader.exec_module
34
+
35
+ def exec_and_patch(module):
36
+ exec_module(module)
37
+ _install(name, module)
38
+
39
+ spec.loader.exec_module = exec_and_patch
40
+ return spec
41
+
42
+
43
+ if not flag("PF_DISABLE"):
44
+ sys.meta_path.insert(0, _PatchOnImport())
45
+ for name in TARGETS:
46
+ if name in sys.modules:
47
+ _install(name, sys.modules[name])
@@ -0,0 +1,16 @@
1
+ import argparse
2
+
3
+ import pirateface._boot # noqa: F401 # uv tools skip .pth files
4
+
5
+
6
+ def main():
7
+ p = argparse.ArgumentParser(prog="pf")
8
+ sub = p.add_subparsers(dest="cmd", required=True)
9
+ d = sub.add_parser("download", help="download a model into the Hugging Face cache")
10
+ d.add_argument("repo_id")
11
+ d.add_argument("--revision")
12
+ args = p.parse_args()
13
+
14
+ from huggingface_hub import snapshot_download
15
+
16
+ print(snapshot_download(args.repo_id, revision=args.revision, max_workers=1))
File without changes
@@ -0,0 +1,63 @@
1
+ """Everything that talks to pirateface.co."""
2
+
3
+ import atexit
4
+ import contextlib
5
+ import functools
6
+ import json
7
+ import threading
8
+ import urllib.error
9
+ import urllib.parse
10
+ import urllib.request
11
+
12
+ from pirateface.config import API, flag
13
+
14
+ NOT_LISTED = object()
15
+
16
+
17
+ @functools.cache
18
+ def _lookup(repo_id):
19
+ """Pirate Face's entry for the repo, NOT_LISTED, or None if unknown (network error)."""
20
+ url = f"{API}/api/torrents?" + urllib.parse.urlencode({"repo": repo_id})
21
+ try:
22
+ with urllib.request.urlopen(url, timeout=10) as r:
23
+ return json.load(r)
24
+ except urllib.error.HTTPError as e:
25
+ return NOT_LISTED if e.code == 404 else None
26
+ except Exception:
27
+ return None
28
+
29
+
30
+ def lookup_magnet(repo_id):
31
+ """The magnet, NOT_LISTED, or None if unknown (network error, or license terms to accept first)."""
32
+ entry = _lookup(repo_id)
33
+ return entry.get("magnet") if isinstance(entry, dict) else entry
34
+
35
+
36
+ _rescues = []
37
+
38
+
39
+ def _send_rescue(repo_id):
40
+ req = urllib.request.Request(
41
+ f"{API}/api/rescue",
42
+ data=json.dumps({"repoId": repo_id}).encode(),
43
+ headers={"Content-Type": "application/json"},
44
+ method="POST",
45
+ )
46
+ with contextlib.suppress(Exception):
47
+ urllib.request.urlopen(req, timeout=10).close()
48
+
49
+
50
+ def request_rescue(repo_id):
51
+ """Ask Pirate Face to add a model it doesn't have yet, in the background."""
52
+ if flag("PF_NO_RESCUE"):
53
+ return False
54
+ t = threading.Thread(target=_send_rescue, args=(repo_id,), daemon=True)
55
+ t.start()
56
+ _rescues.append(t)
57
+ return True
58
+
59
+
60
+ @atexit.register
61
+ def _wait_for_rescues():
62
+ for t in _rescues:
63
+ t.join(timeout=2)
@@ -0,0 +1,206 @@
1
+ import atexit
2
+ import os
3
+ import shutil
4
+ import sys
5
+ import threading
6
+ import time
7
+ from importlib.metadata import version
8
+ from pathlib import Path
9
+
10
+ import libtorrent as lt
11
+
12
+ from pirateface import config, ui
13
+ from pirateface.client import api
14
+
15
+ CACHE_DIR = Path(config.CACHE_DIR)
16
+ _lock = threading.Lock()
17
+ _session = None
18
+ _handles = {} # repo_id -> handle, or None if not on Pirate Face
19
+ _wanted = {}
20
+
21
+
22
+ class Unavailable(Exception):
23
+ pass
24
+
25
+
26
+ def _get_session():
27
+ global _session
28
+ if _session is None:
29
+ debug = config.flag("PF_DEBUG")
30
+ _session = lt.session(
31
+ {
32
+ "listen_interfaces": config.LISTEN,
33
+ "user_agent": f"pirateface/{version('pirateface')}",
34
+ "alert_mask": lt.alert.category_t.all_categories if debug else lt.alert.category_t.storage_notification,
35
+ # The torrent goes "finished" between files and drops the web seed; reconnect in 2s, not 30s.
36
+ # Keep close_redundant_connections on: idle web seeds otherwise never request the next file.
37
+ "urlseed_wait_retry": 2,
38
+ }
39
+ )
40
+ threading.Thread(target=_pump_alerts, args=(_session, debug), daemon=True).start()
41
+ return _session
42
+
43
+
44
+ # libtorrent reports pieces done before they're written, so files can read as zeros until flushed.
45
+ # cache_flushed_alert doesn't say which flush it answers, so only one runs at a time.
46
+ _flush = threading.Condition()
47
+ _flush_one_at_a_time = threading.Lock()
48
+ _flush_done = {}
49
+
50
+
51
+ def _pump_alerts(session, debug):
52
+ """The only reader of libtorrent's alerts."""
53
+ noisy = ("PIECE", "HAVE", "REQUEST", "picker", "ALLOWED_FAST", "dht", "portmap", "stats", "block")
54
+ while True:
55
+ session.wait_for_alert(1000)
56
+ for a in session.pop_alerts():
57
+ if isinstance(a, lt.cache_flushed_alert):
58
+ with _flush:
59
+ key = str(a.handle.info_hashes().v1)
60
+ _flush_done[key] = _flush_done.get(key, 0) + 1
61
+ _flush.notify_all()
62
+ if debug:
63
+ msg = a.message()
64
+ if not any(k in msg or k in type(a).__name__ for k in noisy):
65
+ print(f"[lt] {msg[:220]}", file=sys.stderr, flush=True)
66
+
67
+
68
+ @atexit.register
69
+ def _cleanup():
70
+ for h in _handles.values():
71
+ if h is None:
72
+ continue
73
+ path = Path(h.status().save_path) / h.torrent_file().name()
74
+ _session.remove_torrent(h)
75
+ if path.is_dir():
76
+ shutil.rmtree(path, ignore_errors=True)
77
+ else:
78
+ path.unlink(missing_ok=True)
79
+
80
+
81
+ def _flush_to_disk(h, timeout=30):
82
+ key = str(h.info_hashes().v1)
83
+ with _flush_one_at_a_time, _flush:
84
+ before = _flush_done.get(key, 0)
85
+ h.flush_cache()
86
+ if not _flush.wait_for(lambda: _flush_done.get(key, 0) > before, timeout):
87
+ raise TimeoutError("libtorrent didn't confirm the file was written to disk")
88
+
89
+
90
+ def _handle(repo_id):
91
+ with _lock:
92
+ if repo_id in _handles:
93
+ return _handles[repo_id]
94
+ magnet = api.lookup_magnet(repo_id)
95
+ if magnet is api.NOT_LISTED:
96
+ ui.not_on_pirateface(repo_id, api.request_rescue(repo_id))
97
+ _handles[repo_id] = None
98
+ return None
99
+ if not magnet:
100
+ ui.not_on_pirateface(repo_id)
101
+ _handles[repo_id] = None
102
+ return None
103
+ atp = lt.parse_magnet_uri(magnet)
104
+ atp.save_path = str(CACHE_DIR)
105
+ atp.flags |= lt.torrent_flags.default_dont_download
106
+ # Cache the file list: a magnet alone needs a live peer to get it.
107
+ saved = CACHE_DIR / "torrents" / f"{atp.info_hashes.v1}.torrent"
108
+ if saved.exists():
109
+ atp.ti = lt.torrent_info(str(saved))
110
+ h = _get_session().add_torrent(atp)
111
+ deadline = time.monotonic() + config.METADATA_TIMEOUT
112
+ while not h.status().has_metadata:
113
+ if time.monotonic() > deadline:
114
+ _get_session().remove_torrent(h)
115
+ _handles[repo_id] = None
116
+ raise TimeoutError("no peer sent the file list in time")
117
+ time.sleep(0.2)
118
+ if not saved.exists():
119
+ saved.parent.mkdir(parents=True, exist_ok=True)
120
+ saved.write_bytes(lt.bencode({"info": lt.bdecode(h.torrent_file().info_section())}))
121
+ _handles[repo_id] = h
122
+ threading.Thread(target=_show_status, args=(repo_id, h), daemon=True).start()
123
+ fs = h.torrent_file().files()
124
+ real = [i for i in range(fs.num_files()) if not fs.file_flags(i) & fs.flag_pad_file]
125
+ ui.banner(repo_id, len(real), sum(fs.file_size(i) for i in real))
126
+ return h
127
+
128
+
129
+ def _show_status(repo_id, h, every=0.5, recent=3.0):
130
+ """Sources = who sent bytes recently, not who is connected: the web seed reconnects per file and flickers."""
131
+ fs = h.torrent_file().files()
132
+ sent = {}
133
+ last_seen = {}
134
+ bar = None
135
+ while True:
136
+ time.sleep(every)
137
+ with _lock:
138
+ wanted = list(_wanted.get(repo_id, ()))
139
+ progress = h.file_progress(lt.torrent_handle.piece_granularity)
140
+ total = sum(fs.file_size(i) for i in wanted)
141
+ done = sum(min(progress[i], fs.file_size(i)) for i in wanted)
142
+ files_done = sum(progress[i] >= fs.file_size(i) for i in wanted)
143
+ if not wanted or files_done == len(wanted):
144
+ if bar is not None:
145
+ bar.close()
146
+ bar = None
147
+ continue
148
+ now = time.monotonic()
149
+ for p in h.get_peer_info():
150
+ conn = tuple(p.ip)
151
+ if p.total_download > sent.get(conn, 0):
152
+ last_seen[(conn[0], p.connection_type != lt.peer_info.standard_bittorrent)] = now
153
+ sent[conn] = p.total_download
154
+ active = [web for (_, web), t in last_seen.items() if now - t < recent]
155
+ label = ui.sources_label(any(active), active.count(False))
156
+ if bar is None:
157
+ bar = ui.repo_progress(repo_id)
158
+ bar.total, bar.n = total, done
159
+ ui.set_status(
160
+ bar,
161
+ f"{files_done}/{len(wanted)} files · {done / 1e6:.1f}/{total / 1e6:.1f} MB · {label} · {h.status().download_rate / 1e6:.1f} MB/s",
162
+ )
163
+
164
+
165
+ def _file_index(ti, filename):
166
+ fs = ti.files()
167
+ for i in range(fs.num_files()):
168
+ path = fs.file_path(i).replace(os.sep, "/")
169
+ rel = path.split("/", 1)[1] if fs.num_files() > 1 and "/" in path else path
170
+ if rel == filename:
171
+ return i
172
+ return None
173
+
174
+
175
+ def fetch_file(repo_id, filename):
176
+ h = _handle(repo_id)
177
+ if h is None:
178
+ raise Unavailable()
179
+ ti = h.torrent_file()
180
+ i = _file_index(ti, filename)
181
+ if i is None:
182
+ raise Unavailable("not in the torrent")
183
+ size = ti.files().file_size(i)
184
+ path = Path(h.status().save_path) / ti.files().file_path(i)
185
+ # Set all priorities in one call (all-skipped for a moment drops the web seed), from our own set:
186
+ # libtorrent applies them lazily, so reading them back across threads loses updates.
187
+ with _lock:
188
+ wanted = _wanted.setdefault(repo_id, set())
189
+ wanted.add(i)
190
+ h.prioritize_files([4 if f in wanted else 0 for f in range(ti.num_files())])
191
+
192
+ last_done, last_change = -1, time.monotonic()
193
+ while True:
194
+ done = h.file_progress(lt.torrent_handle.piece_granularity)[i]
195
+ if done >= size:
196
+ break
197
+ st = h.status()
198
+ if st.errc.value():
199
+ raise RuntimeError(f"torrent error: {st.errc.message()}")
200
+ if done != last_done:
201
+ last_done, last_change = done, time.monotonic()
202
+ elif time.monotonic() - last_change > config.STALL_TIMEOUT:
203
+ raise TimeoutError(f"stalled for {config.STALL_TIMEOUT:.0f}s")
204
+ time.sleep(0.25)
205
+ _flush_to_disk(h)
206
+ return path
@@ -0,0 +1,12 @@
1
+ import os
2
+
3
+
4
+ def flag(name):
5
+ return os.environ.get(name, "") not in ("", "0")
6
+
7
+
8
+ API = os.environ.get("PF_API", "https://pirateface.co")
9
+ CACHE_DIR = os.environ.get("PF_CACHE") or os.path.expanduser("~/.cache/pirateface") # noqa: PTH111 # pathlib slows every Python startup
10
+ LISTEN = os.environ.get("PF_LISTEN", "0.0.0.0:0,[::]:0")
11
+ METADATA_TIMEOUT = float(os.environ.get("PF_METADATA_TIMEOUT", "30"))
12
+ STALL_TIMEOUT = float(os.environ.get("PF_STALL_TIMEOUT", "60"))
@@ -0,0 +1,111 @@
1
+ """Swap huggingface_hub's file download for a torrent download, checked against Hugging Face's fingerprint."""
2
+
3
+ import contextvars
4
+ import functools
5
+ import hashlib
6
+ import inspect
7
+ import shutil
8
+ import sys
9
+ from pathlib import Path
10
+
11
+ from pirateface import ui
12
+ from pirateface.client import torrent
13
+
14
+ FUNC = "_download_to_tmp_and_move"
15
+ # FUNC isn't passed the repo id, so the outer functions record it here.
16
+ OUTER = ("_hf_hub_download_to_cache_dir", "_hf_hub_download_to_local_dir")
17
+ _repo = contextvars.ContextVar("pirateface_repo", default=None)
18
+
19
+
20
+ def log(msg):
21
+ print(f"[pirateface] {msg}", file=sys.stderr, flush=True)
22
+
23
+
24
+ def install(file_download):
25
+ original = getattr(file_download, FUNC, None)
26
+ if original is None:
27
+ log(f"huggingface_hub has no {FUNC}; torrent downloads disabled")
28
+ return
29
+
30
+ signature = inspect.signature(original)
31
+
32
+ @functools.wraps(original)
33
+ def patched(*args, **kwargs):
34
+ bound = signature.bind(*args, **kwargs).arguments
35
+ dest = Path(bound["destination_path"])
36
+ if dest.exists() and not bound.get("force_download"):
37
+ return original(*args, **kwargs)
38
+ name = bound["filename"]
39
+ try:
40
+ if _from_torrent(name, bound["etag"], dest):
41
+ return None
42
+ except torrent.Unavailable as e:
43
+ ui.from_hf(name, str(e) or None)
44
+ except Exception as e:
45
+ ui.from_hf(name, str(e) or type(e).__name__)
46
+ return original(*args, **kwargs)
47
+
48
+ setattr(file_download, FUNC, patched)
49
+ _hide_token_nag()
50
+ for name in OUTER:
51
+ setattr(file_download, name, _remember_repo(getattr(file_download, name)))
52
+
53
+
54
+ def _hide_token_nag():
55
+ """Silence the "set a HF_TOKEN" server warning only; other server warnings still show."""
56
+ try:
57
+ from huggingface_hub.utils import _http
58
+
59
+ _http._WARNED_TOPICS.add("unauthenticated")
60
+ except (ImportError, AttributeError):
61
+ pass
62
+
63
+
64
+ def _remember_repo(outer):
65
+ @functools.wraps(outer)
66
+ def wrapper(*args, **kwargs):
67
+ token = _repo.set((kwargs.get("repo_type"), kwargs.get("repo_id")))
68
+ try:
69
+ return outer(*args, **kwargs)
70
+ finally:
71
+ _repo.reset(token)
72
+
73
+ return wrapper
74
+
75
+
76
+ def _from_torrent(filename, etag, dest):
77
+ repo_type, repo_id = _repo.get() or (None, None)
78
+ if repo_id is None or repo_type not in (None, "model"):
79
+ return False
80
+ src = torrent.fetch_file(repo_id, filename)
81
+ if not _matches(src, etag):
82
+ raise ValueError("torrent copy didn't match Hugging Face's fingerprint")
83
+ _place(src, dest)
84
+ ui.from_swarm(filename, src.stat().st_size)
85
+ return True
86
+
87
+
88
+ def _matches(path, etag):
89
+ """HF etags are SHA-256 for LFS files, git blob SHA-1 otherwise."""
90
+ etag = (etag or "").removeprefix("W/").strip('"')
91
+ if len(etag) == 64:
92
+ h = hashlib.sha256()
93
+ elif len(etag) == 40:
94
+ h = hashlib.sha1(b"blob %d\0" % path.stat().st_size)
95
+ else:
96
+ return False
97
+ with path.open("rb") as f:
98
+ while chunk := f.read(1 << 20):
99
+ h.update(chunk)
100
+ return h.hexdigest() == etag
101
+
102
+
103
+ def _place(src, dest):
104
+ """Copy, not hard-link: HF makes its blobs read-only, which would lock libtorrent out of a shared inode."""
105
+ dest.parent.mkdir(parents=True, exist_ok=True)
106
+ tmp = dest.with_name(dest.name + ".pf.incomplete")
107
+ try:
108
+ shutil.copyfile(src, tmp)
109
+ tmp.replace(dest)
110
+ finally:
111
+ tmp.unlink(missing_ok=True)
@@ -0,0 +1,112 @@
1
+ import os
2
+ import sys
3
+ import threading
4
+
5
+ from tqdm.auto import tqdm
6
+
7
+ from pirateface.config import flag
8
+
9
+ QUIET = flag("PF_QUIET")
10
+ _color = sys.stderr.isatty() and "NO_COLOR" not in os.environ
11
+ _lock = threading.Lock()
12
+ _announced = set()
13
+
14
+ SPLASH = r'''
15
+ _.-""""-._
16
+ __.-' (xx) '-.__
17
+ '-.________________.-'
18
+ / \
19
+ | ^ (###)===|
20
+ \ \______/ /
21
+ '-.__________.-'
22
+ '''[1:].splitlines()
23
+ TAGLINE = ("pirateface", " Open-weight model downloads, from the swarm")
24
+
25
+
26
+ def _c(code, text):
27
+ return f"\033[{code}m{text}\033[0m" if _color else text
28
+
29
+
30
+ def _print(line):
31
+ if not QUIET:
32
+ tqdm.write(line, file=sys.stderr)
33
+
34
+
35
+ def _bar():
36
+ return _c("35", "┃")
37
+
38
+
39
+ def mb(n):
40
+ return f"{n / 1e6:.1f} MB" if n >= 1e5 else f"{n / 1e3:.1f} kB"
41
+
42
+
43
+ def splash():
44
+ if _announced or not sys.stderr.isatty():
45
+ return
46
+ pad = " " * ((len("".join(TAGLINE)) - max(map(len, SPLASH))) // 2)
47
+ art = [pad + (_c("1", line) if i < 3 else _c("33", line)) for i, line in enumerate(SPLASH)]
48
+ _print("\n".join(art) + "\n" + _c("1;35", TAGLINE[0]) + _c("2", TAGLINE[1]) + "\n")
49
+
50
+
51
+ def banner(repo_id, files, size):
52
+ with _lock:
53
+ if repo_id in _announced:
54
+ return
55
+ splash()
56
+ _announced.add(repo_id)
57
+ _print(
58
+ _bar()
59
+ + " "
60
+ + _c("1;35", "🏴‍☠️ pirateface")
61
+ + f" {repo_id} is on Pirate Face ({files} files, {mb(size)}), downloading from the swarm"
62
+ )
63
+
64
+
65
+ def not_on_pirateface(repo_id, rescue_requested=False):
66
+ with _lock:
67
+ if repo_id in _announced:
68
+ return
69
+ _announced.add(repo_id)
70
+ line = _bar() + " " + _c("1;35", "🏴‍☠️ pirateface") + f" {repo_id} isn't on Pirate Face yet, using Hugging Face"
71
+ if rescue_requested:
72
+ line += _c("2", " · requested a rescue (turn off: PF_NO_RESCUE=1)")
73
+ _print(line)
74
+
75
+
76
+ def repo_progress(repo_id):
77
+ return tqdm(
78
+ total=1,
79
+ leave=False,
80
+ file=sys.stderr,
81
+ disable=QUIET or not sys.stderr.isatty(),
82
+ desc=f"{_bar()} 🏴‍☠️ {repo_id}",
83
+ bar_format="{desc} {bar:20}",
84
+ colour="magenta",
85
+ )
86
+
87
+
88
+ def set_status(bar, text):
89
+ # Not tqdm's postfix: it always prepends ", ".
90
+ bar.bar_format = "{desc} {bar:20} " + text.replace("{", "{{").replace("}", "}}")
91
+ bar.refresh()
92
+
93
+
94
+ def sources_label(webseed, peers):
95
+ if webseed and peers:
96
+ return f"web seed + {peers} peer{'s' * (peers > 1)}"
97
+ if webseed:
98
+ return "web seed"
99
+ if peers:
100
+ return f"{peers} peer{'s' * (peers > 1)}"
101
+ return "finding sources…"
102
+
103
+
104
+ def from_swarm(filename, size):
105
+ _print(
106
+ f"{_bar()} {_c('32', '✔')} {filename} {_c('2', mb(size) + ' from the swarm, fingerprint matches Hugging Face')}"
107
+ )
108
+
109
+
110
+ def from_hf(filename, reason=None):
111
+ if reason:
112
+ _print(f"{_bar()} {_c('33', '↩')} {filename} {_c('2', 'from Hugging Face (' + reason + ')')}")
@@ -0,0 +1 @@
1
+ import pirateface._boot
@@ -0,0 +1,313 @@
1
+ version = 1
2
+ revision = 3
3
+ requires-python = ">=3.12"
4
+
5
+ [[package]]
6
+ name = "anyio"
7
+ version = "4.15.1"
8
+ source = { registry = "https://pypi.org/simple" }
9
+ dependencies = [
10
+ { name = "idna" },
11
+ { name = "typing-extensions", marker = "python_full_version < '3.15'" },
12
+ ]
13
+ sdist = { url = "https://files.pythonhosted.org/packages/a9/d2/f4d173e22df740bc37b1db102b386ba719b66e95b0f0d751f556b387e6d2/anyio-4.15.1.tar.gz", hash = "sha256:9f28306018cbd6d329e64a36d58256edff76dd996fe423bc957326e578b82a94", size = 276966, upload-time = "2026-09-05T10:42:39.44Z" }
14
+ wheels = [
15
+ { url = "https://files.pythonhosted.org/packages/12/b8/4bd346e22b28902df4d651910f5242c28d84e4a5c2435ca5c3f797ed7e2e/anyio-4.15.1-py3-none-any.whl", hash = "sha256:6152fdbbf9a77fdec97731721bebf7c4c44f7c29b424b0065826173efc7ed101", size = 132079, upload-time = "2026-09-05T10:42:37.923Z" },
16
+ ]
17
+
18
+ [[package]]
19
+ name = "click"
20
+ version = "8.5.0"
21
+ source = { registry = "https://pypi.org/simple" }
22
+ sdist = { url = "https://files.pythonhosted.org/packages/c7/0e/7fa0ef50764b67090eca4114772a2abf8b6148198475e54c660b97caeee6/click-8.5.0.tar.gz", hash = "sha256:ba0d2089de75ea0310e2dde03160e6ca10009947fb95a182f9b54021bb272e34", size = 382235, upload-time = "2026-08-26T13:33:14.56Z" }
23
+ wheels = [
24
+ { url = "https://files.pythonhosted.org/packages/58/50/6c0d534c5f134586a8e1ba4e330569e32f057e33372ae556463212fb4cd3/click-8.5.0-py3-none-any.whl", hash = "sha256:255bc9599cf7748b4b1a446ccc735421bd08a2ae529a8b88597d3de5664ee360", size = 125251, upload-time = "2026-08-26T13:33:12.928Z" },
25
+ ]
26
+
27
+ [[package]]
28
+ name = "colorama"
29
+ version = "0.4.6"
30
+ source = { registry = "https://pypi.org/simple" }
31
+ sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697, upload-time = "2022-10-25T02:36:22.414Z" }
32
+ wheels = [
33
+ { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" },
34
+ ]
35
+
36
+ [[package]]
37
+ name = "filelock"
38
+ version = "4.1.0"
39
+ source = { registry = "https://pypi.org/simple" }
40
+ sdist = { url = "https://files.pythonhosted.org/packages/4c/58/6fd434bec86eff7c38a3168454cb132b762b2bea9b3ac094101a2f7bc32a/filelock-4.1.0.tar.gz", hash = "sha256:ad7f724afef953e731b1cc39bcd3a09166d72ed7fcdf29e6e88b1c3235c6715d", size = 561277, upload-time = "2026-10-09T19:57:20.34Z" }
41
+ wheels = [
42
+ { url = "https://files.pythonhosted.org/packages/ee/86/032133892a5de43b5a98200b01aadcad68cc255e274a762f08b8a76d2912/filelock-4.1.0-py3-none-any.whl", hash = "sha256:2ce9818e3e2d8f284c1a964414447ef148d42a5fd5e2a477a7118e574b293ec1", size = 133003, upload-time = "2026-10-09T19:57:18.716Z" },
43
+ ]
44
+
45
+ [[package]]
46
+ name = "fsspec"
47
+ version = "2026.9.0"
48
+ source = { registry = "https://pypi.org/simple" }
49
+ sdist = { url = "https://files.pythonhosted.org/packages/77/cd/9be253869fc42e764de7f3dedd6969af7d44ff9c3375214a3442a6f3fc08/fsspec-2026.9.0.tar.gz", hash = "sha256:0f08147951c8cb31d844c3547d631053b127863b60be04cf06e121333ee0e2fe", size = 333545, upload-time = "2026-09-18T17:50:42.825Z" }
50
+ wheels = [
51
+ { url = "https://files.pythonhosted.org/packages/6c/c0/a98505f18594f1bce828bb159cec0fcf9860562f1a2c85913409fc8f3d9e/fsspec-2026.9.0-py3-none-any.whl", hash = "sha256:8dd6e646e99ea382bd85f97a45e6b526a442d79423a7dc673f1e2756d05fcb5f", size = 221738, upload-time = "2026-09-18T17:50:41.341Z" },
52
+ ]
53
+
54
+ [[package]]
55
+ name = "h11"
56
+ version = "0.16.0"
57
+ source = { registry = "https://pypi.org/simple" }
58
+ sdist = { url = "https://files.pythonhosted.org/packages/01/ee/02a2c011bdab74c6fb3c75474d40b3052059d95df7e73351460c8588d963/h11-0.16.0.tar.gz", hash = "sha256:4e35b956cf45792e4caa5885e69fba00bdbc6ffafbfa020300e549b208ee5ff1", size = 101250, upload-time = "2025-04-24T03:35:25.427Z" }
59
+ wheels = [
60
+ { url = "https://files.pythonhosted.org/packages/04/4b/29cac41a4d98d144bf5f6d33995617b185d14b22401f75ca86f384e87ff1/h11-0.16.0-py3-none-any.whl", hash = "sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86", size = 37515, upload-time = "2025-04-24T03:35:24.344Z" },
61
+ ]
62
+
63
+ [[package]]
64
+ name = "hf-xet"
65
+ version = "1.7.0"
66
+ source = { registry = "https://pypi.org/simple" }
67
+ sdist = { url = "https://files.pythonhosted.org/packages/9e/27/06d899ea7bd721d272f84aac98bdb238de98af4cc767a69056d967d68c71/hf_xet-1.7.0.tar.gz", hash = "sha256:d406ec79053c0871817f700c2ac8c36ba0d87f9c34b7458b0f0063bb218b0466", size = 985689, upload-time = "2026-10-06T20:18:43.89Z" }
68
+ wheels = [
69
+ { url = "https://files.pythonhosted.org/packages/9f/7c/3e45174942e6793adde6cba4daa7fb037275cf02a944d9eadfcf9ff33b86/hf_xet-1.7.0-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:fa029678be1ba7f953c409b0b27bf15cc69cd1c9b3a674fbd78856ebefca1052", size = 3803919, upload-time = "2026-10-06T20:18:09.844Z" },
70
+ { url = "https://files.pythonhosted.org/packages/ff/3a/5e8b363391adcbb002e191dbf924dab31464ea9c45adfeb73502afc36d35/hf_xet-1.7.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:57bc157b8b7fe3bee9dcb9af7f3da8de41801c3b31a9ef68a77a33c6a6be382f", size = 3553588, upload-time = "2026-10-06T20:18:13.376Z" },
71
+ { url = "https://files.pythonhosted.org/packages/e5/c2/0d1eaa5da13bbf9c896badc7f380601c7d973a87a6ffb4d100267c4536c1/hf_xet-1.7.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:87dab080f8f7d32781c2586904e3603f4e60d09bfc727706c3ae419e0829beeb", size = 4201962, upload-time = "2026-10-06T20:18:16.11Z" },
72
+ { url = "https://files.pythonhosted.org/packages/23/2d/225d5b11a9ca7d31b9470a57f2b2be1a5cef8b84325a2146aeb4589e226c/hf_xet-1.7.0-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:b01fe18dbbd151a2403d2c64ed30dc6547b00d6babab9a617d77c7acdb81ee66", size = 3982978, upload-time = "2026-10-06T20:18:18.092Z" },
73
+ { url = "https://files.pythonhosted.org/packages/93/34/9d681f0e3dac0b5dae0d7dea748429266f24e52415446523f464fbaa828e/hf_xet-1.7.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:4ee5e05a627f5ab5bad7a86582277d645556ea1e199903aae19e033a392aa13a", size = 4181558, upload-time = "2026-10-06T20:18:20.082Z" },
74
+ { url = "https://files.pythonhosted.org/packages/de/f0/277f039b7d72027bc2ed277f1b62a2f70f740a5aac2a3e7243e5b6854c5d/hf_xet-1.7.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:19c0e64f14175ccb6a1aff69e0d2ab9ec5269a560e6687abaf2b3fa4f73de7cd", size = 4411546, upload-time = "2026-10-06T20:18:21.999Z" },
75
+ { url = "https://files.pythonhosted.org/packages/3d/7f/832d3ddb49326114175b7bcc50daea8565c09fd21ac03a02b211c09fefb7/hf_xet-1.7.0-cp314-cp314t-win_amd64.whl", hash = "sha256:757168feb5679647c0bb13ee5d0faebe799c4dff9051419885a566ebd79f949d", size = 3812809, upload-time = "2026-10-06T20:18:24.288Z" },
76
+ { url = "https://files.pythonhosted.org/packages/3d/c4/310c3c29e5beae7c049e63947bd1923d597883b41c9ec4718589920812c4/hf_xet-1.7.0-cp314-cp314t-win_arm64.whl", hash = "sha256:b91569d5f1b61c34b043687da02c05dd3604f3d329e7868510bf3f7971599006", size = 3646174, upload-time = "2026-10-06T20:18:26.279Z" },
77
+ { url = "https://files.pythonhosted.org/packages/9c/0b/b03be21ffaada749ba0d3197d8aefbf1aa698bac149580421c15239b299e/hf_xet-1.7.0-cp38-abi3-macosx_10_12_x86_64.whl", hash = "sha256:e3e88a7a75d7d95cbee1f37dc31341d6201124cf21c6c4b1dfab8ccba9b09e0f", size = 3796096, upload-time = "2026-10-06T20:18:28.43Z" },
78
+ { url = "https://files.pythonhosted.org/packages/c3/47/a26ebdce7056a61e931f228439bc0ab08cbec239d1690f965e5e637cba79/hf_xet-1.7.0-cp38-abi3-macosx_11_0_arm64.whl", hash = "sha256:59fba37039233c7fcbe196817d6cdcf1b40dfb17b410f229d85b0cf0a1848da4", size = 3560352, upload-time = "2026-10-06T20:18:30.365Z" },
79
+ { url = "https://files.pythonhosted.org/packages/a3/4c/2bf3b66c215d409655f28de1622393dde04c9461280d48c7924bb3b2decd/hf_xet-1.7.0-cp38-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:2814a6e999d13464c4d679b788cc5d784eb5a4edfc638a31f10e9a11ab531ef8", size = 4212180, upload-time = "2026-10-06T20:18:32.292Z" },
80
+ { url = "https://files.pythonhosted.org/packages/49/0c/a2f703a5a78267556e89e03316fa0805c86b72b50829bc67665746e8ebf0/hf_xet-1.7.0-cp38-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:fcfd6c22418e57dd5b3aea649e813b2e2cfb2aebf317b210d90f1fe4b3018b52", size = 3990011, upload-time = "2026-10-06T20:18:34.21Z" },
81
+ { url = "https://files.pythonhosted.org/packages/a4/77/e52e4201b1cbf571530a61cc57f70182045a39a230089ee5f1df182a4de2/hf_xet-1.7.0-cp38-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:80f79dae613ce9e0ea1fd1ae15616ca9ac74aed4c770aabc199c4f03ebecc863", size = 4190628, upload-time = "2026-10-06T20:18:36.062Z" },
82
+ { url = "https://files.pythonhosted.org/packages/6c/dc/03a21b89f118664a0926ff25b0f8e44a519bf22724a6a8fc7a9abbc188b6/hf_xet-1.7.0-cp38-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:0a9e802f33bf50c851abe45fc5380e61f959e2d369647d6742b79ad9d6c27cab", size = 4418814, upload-time = "2026-10-06T20:18:37.888Z" },
83
+ { url = "https://files.pythonhosted.org/packages/4d/59/b35106dfa71b6eef605dc88bd038fe99c7f86fb132a15b60d0bf2f235b2c/hf_xet-1.7.0-cp38-abi3-win_amd64.whl", hash = "sha256:2b7bb5727889b0f2436dbaaad8fc4c3e66b8240d992716989e0c086b4278b1bc", size = 3822644, upload-time = "2026-10-06T20:18:40.052Z" },
84
+ { url = "https://files.pythonhosted.org/packages/48/cd/072313585f74fe9d441e2eb5e0a4703c30586cd709810ea369675f61b74e/hf_xet-1.7.0-cp38-abi3-win_arm64.whl", hash = "sha256:acc3851cf2576a8fb2ae926da863f4efabe21303cf292e9a44332802ab0dcc6a", size = 3662436, upload-time = "2026-10-06T20:18:42.205Z" },
85
+ ]
86
+
87
+ [[package]]
88
+ name = "httpcore2"
89
+ version = "2.13.1"
90
+ source = { registry = "https://pypi.org/simple" }
91
+ dependencies = [
92
+ { name = "h11" },
93
+ { name = "truststore" },
94
+ ]
95
+ sdist = { url = "https://files.pythonhosted.org/packages/cb/f3/1db7aa2bc2524062192bb0e0323969492d1883152a232fe36eea65f4e35c/httpcore2-2.13.1.tar.gz", hash = "sha256:e0aa977abe17e69a3b820a24542a6fa88702676d83880b8d194dcd18408e5103", size = 68071, upload-time = "2026-09-23T07:47:22.372Z" }
96
+ wheels = [
97
+ { url = "https://files.pythonhosted.org/packages/09/ba/a4568248771ce81957bfb7cc600264a40fbcda092391ee1c415c50be4bea/httpcore2-2.13.1-py3-none-any.whl", hash = "sha256:e1e05d4f25f7d7d496bfb96748f6f4b67657b03da069b3a68c36069f3db73d0a", size = 83423, upload-time = "2026-09-23T07:47:19.365Z" },
98
+ ]
99
+
100
+ [[package]]
101
+ name = "httpx2"
102
+ version = "2.13.1"
103
+ source = { registry = "https://pypi.org/simple" }
104
+ dependencies = [
105
+ { name = "anyio", marker = "sys_platform != 'emscripten'" },
106
+ { name = "httpcore2", marker = "sys_platform != 'emscripten'" },
107
+ { name = "httpx2-jsfetch", marker = "sys_platform == 'emscripten'" },
108
+ { name = "idna" },
109
+ { name = "truststore", marker = "sys_platform != 'emscripten'" },
110
+ { name = "typing-extensions", marker = "python_full_version < '3.13'" },
111
+ ]
112
+ sdist = { url = "https://files.pythonhosted.org/packages/d5/44/474bef2a0e9d90f1715d32cb98b0738695ca17ba324095fb2497ed7fbd59/httpx2-2.13.1.tar.gz", hash = "sha256:e48744a19e3af5ee48313d0ce5fe941d5422fae5705ea922a4aabf94d7800dfa", size = 100405, upload-time = "2026-09-23T07:47:23.052Z" }
113
+ wheels = [
114
+ { url = "https://files.pythonhosted.org/packages/d8/9c/6fe8931fd9f381042a9e4c7d5a7b4cbf7016b252bec0c99a49fce42c3326/httpx2-2.13.1-py3-none-any.whl", hash = "sha256:6dff50fabc270ee5fd25d845d0b078ed20564579744d6d962850975996d2f9a4", size = 95597, upload-time = "2026-09-23T07:47:20.995Z" },
115
+ ]
116
+
117
+ [[package]]
118
+ name = "httpx2-jsfetch"
119
+ version = "1.0"
120
+ source = { registry = "https://pypi.org/simple" }
121
+ sdist = { url = "https://files.pythonhosted.org/packages/cd/c4/0e5636363151a2a1795e0a77617168b9ca438e1748ec05fc9b5687f93d64/httpx2_jsfetch-1.0.tar.gz", hash = "sha256:70a0e3eabfef7cce5ad9c629f7d01ca05e418f586646f4ddf14782e4c1454c60", size = 6872, upload-time = "2026-08-07T00:13:07.492Z" }
122
+ wheels = [
123
+ { url = "https://files.pythonhosted.org/packages/9b/43/832f631d32e4f1211caa2ba368317739fe71f0b8530e4c9d15dc454bac2a/httpx2_jsfetch-1.0-py3-none-any.whl", hash = "sha256:cb916b707601e69a07721aabc8f3f6659be3a6893bc1ff5c6f9e02241df2da32", size = 6382, upload-time = "2026-08-07T00:13:06.567Z" },
124
+ ]
125
+
126
+ [[package]]
127
+ name = "huggingface-hub"
128
+ version = "2.2.0"
129
+ source = { registry = "https://pypi.org/simple" }
130
+ dependencies = [
131
+ { name = "click" },
132
+ { name = "filelock" },
133
+ { name = "fsspec" },
134
+ { name = "hf-xet", marker = "platform_machine == 'AMD64' or platform_machine == 'ARM64' or platform_machine == 'aarch64' or platform_machine == 'amd64' or platform_machine == 'arm64' or platform_machine == 'x86_64'" },
135
+ { name = "httpx2" },
136
+ { name = "packaging" },
137
+ { name = "pyyaml" },
138
+ { name = "tqdm" },
139
+ { name = "typing-extensions" },
140
+ ]
141
+ sdist = { url = "https://files.pythonhosted.org/packages/12/47/6858d63643e66fb4f6585c3cfd4029c0b2bc1ae21688cee9b3335f20a10d/huggingface_hub-2.2.0.tar.gz", hash = "sha256:5d1b47537394e4215cb858aa12fd493d0f7ef7f58990f5dcd24bc173107b2871", size = 1041026, upload-time = "2026-10-08T15:30:59.971Z" }
142
+ wheels = [
143
+ { url = "https://files.pythonhosted.org/packages/eb/b0/0f7b430fd100b3a3b037fdbb314878200241082e607b3383c63d91a13a72/huggingface_hub-2.2.0-py3-none-any.whl", hash = "sha256:1667f145dc56dc210d60966069397df9ecfca9607a5d43db88b308c89dae56b3", size = 839884, upload-time = "2026-10-08T15:30:57.914Z" },
144
+ ]
145
+
146
+ [[package]]
147
+ name = "idna"
148
+ version = "3.20"
149
+ source = { registry = "https://pypi.org/simple" }
150
+ sdist = { url = "https://files.pythonhosted.org/packages/f5/08/8eea9d4b8302028f3abb2c0813953f7aec26d33b7a8960ed760e65ff29fa/idna-3.20.tar.gz", hash = "sha256:a7db850025b95ded1eae8a46181a1a6c56c92c96f0e2b005d9ff8dc0210cab44", size = 216463, upload-time = "2026-09-17T14:11:04.752Z" }
151
+ wheels = [
152
+ { url = "https://files.pythonhosted.org/packages/58/a2/bb081bab032533a855d44de1d56f8e8426114ff1ba5d1f07a438a0a654f8/idna-3.20-py3-none-any.whl", hash = "sha256:ab7ae7122974553370f0bdb919e1a960b2cd1bc1ef0276416d896db81c14582c", size = 69583, upload-time = "2026-09-17T14:11:03.168Z" },
153
+ ]
154
+
155
+ [[package]]
156
+ name = "libtorrent"
157
+ version = "2.1.1"
158
+ source = { registry = "https://pypi.org/simple" }
159
+ wheels = [
160
+ { url = "https://files.pythonhosted.org/packages/c2/10/77694ec3f50d3678cf2a7143f380f8a043ee62f653bfce8977ce9b307ea8/libtorrent-2.1.1-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:768375f62631acd298a118d6e4f3e288af8220880d6432a49125f0539ce446d4", size = 7418245, upload-time = "2026-08-10T17:37:49.616Z" },
161
+ { url = "https://files.pythonhosted.org/packages/f5/78/bad3873f65ba26586cd380a81ed002498e74848db92b0bec1676768c105b/libtorrent-2.1.1-cp312-cp312-macosx_15_0_arm64.whl", hash = "sha256:188aa0f03a2d6e967956a45534cbfd04d81641deb7b92b816e56723fc7ae5078", size = 7372771, upload-time = "2026-08-10T17:37:51.311Z" },
162
+ { url = "https://files.pythonhosted.org/packages/6c/78/ca385948e656f2307b01aa2e139df4c1828e7645d5a1f8ffe09fbcd35bb3/libtorrent-2.1.1-cp312-cp312-macosx_15_0_x86_64.whl", hash = "sha256:c8d843dc910a31e5cfc2b8e3d1bd5b7ae8b02a1100eeb19764157b7a5ed81308", size = 7392110, upload-time = "2026-08-10T17:37:53.162Z" },
163
+ { url = "https://files.pythonhosted.org/packages/21/e2/105cab89d59a94bbaaac322bee2f507eda0b5af32756deb333112dfb58a5/libtorrent-2.1.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:580b07d04b30c9d85f73ceaab5184035e815ee4ff3f37ba5b8f887292ae949f1", size = 8423265, upload-time = "2026-08-10T17:37:54.705Z" },
164
+ { url = "https://files.pythonhosted.org/packages/0c/58/345ed75b4b4d908b488f9c5cf46dad249b3940be1c5ee16c38a5d87f2e85/libtorrent-2.1.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:90c11511a67c118e2fd6043c717d42663e60da03ab8d4851b2ad5a500a5e0134", size = 8245815, upload-time = "2026-08-10T17:37:56.411Z" },
165
+ { url = "https://files.pythonhosted.org/packages/f2/96/d623ecc609eafcdaf78e12410b3a81dcd543fc94a73e9db11d59d25524d1/libtorrent-2.1.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:08ce0c0275e60ec1aa0d13f46099e41ddc184273af175cc0a7d3812c31bc9134", size = 14159891, upload-time = "2026-08-10T17:37:58.206Z" },
166
+ { url = "https://files.pythonhosted.org/packages/36/65/0bcc5613669084b5cfe53fa399176a0a6cc809f7319859d95d216677c5b2/libtorrent-2.1.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:0a2bc058e53e11ef38b1dafbdb8595b5a1d6c7881c0b04fd01afae99e2845be5", size = 14301049, upload-time = "2026-08-10T17:38:00.299Z" },
167
+ { url = "https://files.pythonhosted.org/packages/51/dc/04c867f00154fb679d704293c9fb1bf11824ca906dbba0a9df352d24def7/libtorrent-2.1.1-cp312-cp312-win32.whl", hash = "sha256:2845d60a35ea1a97a96c70c5817d898c0d9fe5e50fe1cd86d3ca995b3e1afa0d", size = 4323872, upload-time = "2026-08-10T17:38:02.333Z" },
168
+ { url = "https://files.pythonhosted.org/packages/5a/8f/a262d268098f58f17a221c8b8c632eaf187bc810b4cf2a633937b3c6bd3e/libtorrent-2.1.1-cp312-cp312-win_amd64.whl", hash = "sha256:493954405d15f14b9d9f9b1762ae78e7fecc5a44699f74cb433a9800fa3a7b45", size = 5018631, upload-time = "2026-08-10T17:38:03.893Z" },
169
+ { url = "https://files.pythonhosted.org/packages/8b/31/e38b29f397ef5ba615136a9ae1c9795d71d3d5856c43c12a20aab058e513/libtorrent-2.1.1-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:94767ff3e8bf4fb35cff3f638188ba5ca69dec331bf3b96254289bee635102a1", size = 7418386, upload-time = "2026-08-10T17:38:05.307Z" },
170
+ { url = "https://files.pythonhosted.org/packages/56/e7/8c92ca04c63f6e909f768dceae64559b9e73ed615a084bc04ca53a3ef60d/libtorrent-2.1.1-cp313-cp313-macosx_15_0_arm64.whl", hash = "sha256:bae8a30fa1b4988881124c76867f8e61f9887a42e55638693d7a1b0038bdebe8", size = 7372882, upload-time = "2026-08-10T17:38:06.77Z" },
171
+ { url = "https://files.pythonhosted.org/packages/5d/3f/a8b5ac66bc80f143b4ea734dbc6f19d9d80dd04ca1a078fda8c8772faae2/libtorrent-2.1.1-cp313-cp313-macosx_15_0_x86_64.whl", hash = "sha256:baea3ba15ad692d8d87b6cf6685c91611f0ce0367ce6213c4ce52f20e62a5b8c", size = 7391859, upload-time = "2026-08-10T17:38:08.303Z" },
172
+ { url = "https://files.pythonhosted.org/packages/f0/2e/674ea2526d6bf8c3728848eeefc0b66e8ae3cd8d01e67317bac3c005a4ce/libtorrent-2.1.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d456d5c772e64267211eca995f5a61cfa9e476de9494b300b42fd6fdf71675ca", size = 8423724, upload-time = "2026-08-10T17:38:09.887Z" },
173
+ { url = "https://files.pythonhosted.org/packages/7b/e4/ef1bacf71afb4feba617aebe699ad75e9ae36f025f047fd078df3730c45e/libtorrent-2.1.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c81d768e04915c1627ac108a6abfa64c740bdd1a1634d5fffcdc56ed6e6d915f", size = 8246041, upload-time = "2026-08-10T17:38:11.701Z" },
174
+ { url = "https://files.pythonhosted.org/packages/8f/bc/d35c775bb7b111623d7402e8c589353642bd0c474a07d50a985aa35612cc/libtorrent-2.1.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f4104e121fd20949ea681abb63377de5dcdbbeda68f0d27ceb074b24877ddaa9", size = 14159928, upload-time = "2026-08-10T17:38:13.604Z" },
175
+ { url = "https://files.pythonhosted.org/packages/7d/02/42a632b930a61c96a4aed9ca3e451ed61a46ef19ef9d8d31e95772c624ba/libtorrent-2.1.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:98a0cccb5da53e855c8ea6d8b06f190f05de3219732882a1cd2643df15faa22a", size = 14301102, upload-time = "2026-08-10T17:38:15.511Z" },
176
+ { url = "https://files.pythonhosted.org/packages/c2/5b/b84a328fe45983ff2f6a77d6d466ad78d39127894f0fd6f44bd64269fffa/libtorrent-2.1.1-cp313-cp313-win32.whl", hash = "sha256:ee9030a6062b671a71ce1bf873e3b62481b7ef9abbe6bce1768c2efb8426f8f8", size = 4323886, upload-time = "2026-08-10T17:38:18.005Z" },
177
+ { url = "https://files.pythonhosted.org/packages/f0/2b/b843874837440dadb6d6cde2374a311367efdb40f86375de46cf9a0e59ea/libtorrent-2.1.1-cp313-cp313-win_amd64.whl", hash = "sha256:fa11aa801d5b9184f067f559c465d3074a4e878d9c671b00dd4930d6dfce2b46", size = 5018669, upload-time = "2026-08-10T17:38:19.403Z" },
178
+ ]
179
+
180
+ [[package]]
181
+ name = "packaging"
182
+ version = "26.3"
183
+ source = { registry = "https://pypi.org/simple" }
184
+ sdist = { url = "https://files.pythonhosted.org/packages/7d/fa/3944b40b07da9ce895c0e6303a5ab7d53da063554f534556b134a54d6093/packaging-26.3.tar.gz", hash = "sha256:94edc256424af38762eb31306eed28beb9f0efc50a8837492c9d6fd6004aed79", size = 313412, upload-time = "2026-08-04T18:15:28.737Z" }
185
+ wheels = [
186
+ { url = "https://files.pythonhosted.org/packages/63/34/ba1c580383c9eada3711951fef0795c80b829a078d72188184bcab9dd527/packaging-26.3-py3-none-any.whl", hash = "sha256:d7193f7c8e4e93f444fde0262bf90af30e16fa0ad0ad44cb553c87339b23cd1c", size = 129956, upload-time = "2026-08-04T18:15:27.159Z" },
187
+ ]
188
+
189
+ [[package]]
190
+ name = "pirateface"
191
+ version = "0.1.0"
192
+ source = { editable = "." }
193
+ dependencies = [
194
+ { name = "huggingface-hub" },
195
+ { name = "libtorrent" },
196
+ { name = "tqdm" },
197
+ ]
198
+
199
+ [package.dev-dependencies]
200
+ dev = [
201
+ { name = "ruff" },
202
+ ]
203
+
204
+ [package.metadata]
205
+ requires-dist = [
206
+ { name = "huggingface-hub", specifier = ">=2.0" },
207
+ { name = "libtorrent", specifier = ">=2.0" },
208
+ { name = "tqdm", specifier = ">=4.42" },
209
+ ]
210
+
211
+ [package.metadata.requires-dev]
212
+ dev = [{ name = "ruff", specifier = ">=0.6" }]
213
+
214
+ [[package]]
215
+ name = "pyyaml"
216
+ version = "6.0.3"
217
+ source = { registry = "https://pypi.org/simple" }
218
+ sdist = { url = "https://files.pythonhosted.org/packages/05/8e/961c0007c59b8dd7729d542c61a4d537767a59645b82a0b521206e1e25c2/pyyaml-6.0.3.tar.gz", hash = "sha256:d76623373421df22fb4cf8817020cbb7ef15c725b9d5e45f17e189bfc384190f", size = 130960, upload-time = "2025-09-25T21:33:16.546Z" }
219
+ wheels = [
220
+ { url = "https://files.pythonhosted.org/packages/d1/33/422b98d2195232ca1826284a76852ad5a86fe23e31b009c9886b2d0fb8b2/pyyaml-6.0.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:7f047e29dcae44602496db43be01ad42fc6f1cc0d8cd6c83d342306c32270196", size = 182063, upload-time = "2025-09-25T21:32:11.445Z" },
221
+ { url = "https://files.pythonhosted.org/packages/89/a0/6cf41a19a1f2f3feab0e9c0b74134aa2ce6849093d5517a0c550fe37a648/pyyaml-6.0.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:fc09d0aa354569bc501d4e787133afc08552722d3ab34836a80547331bb5d4a0", size = 173973, upload-time = "2025-09-25T21:32:12.492Z" },
222
+ { url = "https://files.pythonhosted.org/packages/ed/23/7a778b6bd0b9a8039df8b1b1d80e2e2ad78aa04171592c8a5c43a56a6af4/pyyaml-6.0.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9149cad251584d5fb4981be1ecde53a1ca46c891a79788c0df828d2f166bda28", size = 775116, upload-time = "2025-09-25T21:32:13.652Z" },
223
+ { url = "https://files.pythonhosted.org/packages/65/30/d7353c338e12baef4ecc1b09e877c1970bd3382789c159b4f89d6a70dc09/pyyaml-6.0.3-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5fdec68f91a0c6739b380c83b951e2c72ac0197ace422360e6d5a959d8d97b2c", size = 844011, upload-time = "2025-09-25T21:32:15.21Z" },
224
+ { url = "https://files.pythonhosted.org/packages/8b/9d/b3589d3877982d4f2329302ef98a8026e7f4443c765c46cfecc8858c6b4b/pyyaml-6.0.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ba1cc08a7ccde2d2ec775841541641e4548226580ab850948cbfda66a1befcdc", size = 807870, upload-time = "2025-09-25T21:32:16.431Z" },
225
+ { url = "https://files.pythonhosted.org/packages/05/c0/b3be26a015601b822b97d9149ff8cb5ead58c66f981e04fedf4e762f4bd4/pyyaml-6.0.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8dc52c23056b9ddd46818a57b78404882310fb473d63f17b07d5c40421e47f8e", size = 761089, upload-time = "2025-09-25T21:32:17.56Z" },
226
+ { url = "https://files.pythonhosted.org/packages/be/8e/98435a21d1d4b46590d5459a22d88128103f8da4c2d4cb8f14f2a96504e1/pyyaml-6.0.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:41715c910c881bc081f1e8872880d3c650acf13dfa8214bad49ed4cede7c34ea", size = 790181, upload-time = "2025-09-25T21:32:18.834Z" },
227
+ { url = "https://files.pythonhosted.org/packages/74/93/7baea19427dcfbe1e5a372d81473250b379f04b1bd3c4c5ff825e2327202/pyyaml-6.0.3-cp312-cp312-win32.whl", hash = "sha256:96b533f0e99f6579b3d4d4995707cf36df9100d67e0c8303a0c55b27b5f99bc5", size = 137658, upload-time = "2025-09-25T21:32:20.209Z" },
228
+ { url = "https://files.pythonhosted.org/packages/86/bf/899e81e4cce32febab4fb42bb97dcdf66bc135272882d1987881a4b519e9/pyyaml-6.0.3-cp312-cp312-win_amd64.whl", hash = "sha256:5fcd34e47f6e0b794d17de1b4ff496c00986e1c83f7ab2fb8fcfe9616ff7477b", size = 154003, upload-time = "2025-09-25T21:32:21.167Z" },
229
+ { url = "https://files.pythonhosted.org/packages/1a/08/67bd04656199bbb51dbed1439b7f27601dfb576fb864099c7ef0c3e55531/pyyaml-6.0.3-cp312-cp312-win_arm64.whl", hash = "sha256:64386e5e707d03a7e172c0701abfb7e10f0fb753ee1d773128192742712a98fd", size = 140344, upload-time = "2025-09-25T21:32:22.617Z" },
230
+ { url = "https://files.pythonhosted.org/packages/d1/11/0fd08f8192109f7169db964b5707a2f1e8b745d4e239b784a5a1dd80d1db/pyyaml-6.0.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:8da9669d359f02c0b91ccc01cac4a67f16afec0dac22c2ad09f46bee0697eba8", size = 181669, upload-time = "2025-09-25T21:32:23.673Z" },
231
+ { url = "https://files.pythonhosted.org/packages/b1/16/95309993f1d3748cd644e02e38b75d50cbc0d9561d21f390a76242ce073f/pyyaml-6.0.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:2283a07e2c21a2aa78d9c4442724ec1eb15f5e42a723b99cb3d822d48f5f7ad1", size = 173252, upload-time = "2025-09-25T21:32:25.149Z" },
232
+ { url = "https://files.pythonhosted.org/packages/50/31/b20f376d3f810b9b2371e72ef5adb33879b25edb7a6d072cb7ca0c486398/pyyaml-6.0.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ee2922902c45ae8ccada2c5b501ab86c36525b883eff4255313a253a3160861c", size = 767081, upload-time = "2025-09-25T21:32:26.575Z" },
233
+ { url = "https://files.pythonhosted.org/packages/49/1e/a55ca81e949270d5d4432fbbd19dfea5321eda7c41a849d443dc92fd1ff7/pyyaml-6.0.3-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a33284e20b78bd4a18c8c2282d549d10bc8408a2a7ff57653c0cf0b9be0afce5", size = 841159, upload-time = "2025-09-25T21:32:27.727Z" },
234
+ { url = "https://files.pythonhosted.org/packages/74/27/e5b8f34d02d9995b80abcef563ea1f8b56d20134d8f4e5e81733b1feceb2/pyyaml-6.0.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0f29edc409a6392443abf94b9cf89ce99889a1dd5376d94316ae5145dfedd5d6", size = 801626, upload-time = "2025-09-25T21:32:28.878Z" },
235
+ { url = "https://files.pythonhosted.org/packages/f9/11/ba845c23988798f40e52ba45f34849aa8a1f2d4af4b798588010792ebad6/pyyaml-6.0.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f7057c9a337546edc7973c0d3ba84ddcdf0daa14533c2065749c9075001090e6", size = 753613, upload-time = "2025-09-25T21:32:30.178Z" },
236
+ { url = "https://files.pythonhosted.org/packages/3d/e0/7966e1a7bfc0a45bf0a7fb6b98ea03fc9b8d84fa7f2229e9659680b69ee3/pyyaml-6.0.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:eda16858a3cab07b80edaf74336ece1f986ba330fdb8ee0d6c0d68fe82bc96be", size = 794115, upload-time = "2025-09-25T21:32:31.353Z" },
237
+ { url = "https://files.pythonhosted.org/packages/de/94/980b50a6531b3019e45ddeada0626d45fa85cbe22300844a7983285bed3b/pyyaml-6.0.3-cp313-cp313-win32.whl", hash = "sha256:d0eae10f8159e8fdad514efdc92d74fd8d682c933a6dd088030f3834bc8e6b26", size = 137427, upload-time = "2025-09-25T21:32:32.58Z" },
238
+ { url = "https://files.pythonhosted.org/packages/97/c9/39d5b874e8b28845e4ec2202b5da735d0199dbe5b8fb85f91398814a9a46/pyyaml-6.0.3-cp313-cp313-win_amd64.whl", hash = "sha256:79005a0d97d5ddabfeeea4cf676af11e647e41d81c9a7722a193022accdb6b7c", size = 154090, upload-time = "2025-09-25T21:32:33.659Z" },
239
+ { url = "https://files.pythonhosted.org/packages/73/e8/2bdf3ca2090f68bb3d75b44da7bbc71843b19c9f2b9cb9b0f4ab7a5a4329/pyyaml-6.0.3-cp313-cp313-win_arm64.whl", hash = "sha256:5498cd1645aa724a7c71c8f378eb29ebe23da2fc0d7a08071d89469bf1d2defb", size = 140246, upload-time = "2025-09-25T21:32:34.663Z" },
240
+ { url = "https://files.pythonhosted.org/packages/9d/8c/f4bd7f6465179953d3ac9bc44ac1a8a3e6122cf8ada906b4f96c60172d43/pyyaml-6.0.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:8d1fab6bb153a416f9aeb4b8763bc0f22a5586065f86f7664fc23339fc1c1fac", size = 181814, upload-time = "2025-09-25T21:32:35.712Z" },
241
+ { url = "https://files.pythonhosted.org/packages/bd/9c/4d95bb87eb2063d20db7b60faa3840c1b18025517ae857371c4dd55a6b3a/pyyaml-6.0.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:34d5fcd24b8445fadc33f9cf348c1047101756fd760b4dacb5c3e99755703310", size = 173809, upload-time = "2025-09-25T21:32:36.789Z" },
242
+ { url = "https://files.pythonhosted.org/packages/92/b5/47e807c2623074914e29dabd16cbbdd4bf5e9b2db9f8090fa64411fc5382/pyyaml-6.0.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:501a031947e3a9025ed4405a168e6ef5ae3126c59f90ce0cd6f2bfc477be31b7", size = 766454, upload-time = "2025-09-25T21:32:37.966Z" },
243
+ { url = "https://files.pythonhosted.org/packages/02/9e/e5e9b168be58564121efb3de6859c452fccde0ab093d8438905899a3a483/pyyaml-6.0.3-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:b3bc83488de33889877a0f2543ade9f70c67d66d9ebb4ac959502e12de895788", size = 836355, upload-time = "2025-09-25T21:32:39.178Z" },
244
+ { url = "https://files.pythonhosted.org/packages/88/f9/16491d7ed2a919954993e48aa941b200f38040928474c9e85ea9e64222c3/pyyaml-6.0.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c458b6d084f9b935061bc36216e8a69a7e293a2f1e68bf956dcd9e6cbcd143f5", size = 794175, upload-time = "2025-09-25T21:32:40.865Z" },
245
+ { url = "https://files.pythonhosted.org/packages/dd/3f/5989debef34dc6397317802b527dbbafb2b4760878a53d4166579111411e/pyyaml-6.0.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7c6610def4f163542a622a73fb39f534f8c101d690126992300bf3207eab9764", size = 755228, upload-time = "2025-09-25T21:32:42.084Z" },
246
+ { url = "https://files.pythonhosted.org/packages/d7/ce/af88a49043cd2e265be63d083fc75b27b6ed062f5f9fd6cdc223ad62f03e/pyyaml-6.0.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:5190d403f121660ce8d1d2c1bb2ef1bd05b5f68533fc5c2ea899bd15f4399b35", size = 789194, upload-time = "2025-09-25T21:32:43.362Z" },
247
+ { url = "https://files.pythonhosted.org/packages/23/20/bb6982b26a40bb43951265ba29d4c246ef0ff59c9fdcdf0ed04e0687de4d/pyyaml-6.0.3-cp314-cp314-win_amd64.whl", hash = "sha256:4a2e8cebe2ff6ab7d1050ecd59c25d4c8bd7e6f400f5f82b96557ac0abafd0ac", size = 156429, upload-time = "2025-09-25T21:32:57.844Z" },
248
+ { url = "https://files.pythonhosted.org/packages/f4/f4/a4541072bb9422c8a883ab55255f918fa378ecf083f5b85e87fc2b4eda1b/pyyaml-6.0.3-cp314-cp314-win_arm64.whl", hash = "sha256:93dda82c9c22deb0a405ea4dc5f2d0cda384168e466364dec6255b293923b2f3", size = 143912, upload-time = "2025-09-25T21:32:59.247Z" },
249
+ { url = "https://files.pythonhosted.org/packages/7c/f9/07dd09ae774e4616edf6cda684ee78f97777bdd15847253637a6f052a62f/pyyaml-6.0.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:02893d100e99e03eda1c8fd5c441d8c60103fd175728e23e431db1b589cf5ab3", size = 189108, upload-time = "2025-09-25T21:32:44.377Z" },
250
+ { url = "https://files.pythonhosted.org/packages/4e/78/8d08c9fb7ce09ad8c38ad533c1191cf27f7ae1effe5bb9400a46d9437fcf/pyyaml-6.0.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:c1ff362665ae507275af2853520967820d9124984e0f7466736aea23d8611fba", size = 183641, upload-time = "2025-09-25T21:32:45.407Z" },
251
+ { url = "https://files.pythonhosted.org/packages/7b/5b/3babb19104a46945cf816d047db2788bcaf8c94527a805610b0289a01c6b/pyyaml-6.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6adc77889b628398debc7b65c073bcb99c4a0237b248cacaf3fe8a557563ef6c", size = 831901, upload-time = "2025-09-25T21:32:48.83Z" },
252
+ { url = "https://files.pythonhosted.org/packages/8b/cc/dff0684d8dc44da4d22a13f35f073d558c268780ce3c6ba1b87055bb0b87/pyyaml-6.0.3-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a80cb027f6b349846a3bf6d73b5e95e782175e52f22108cfa17876aaeff93702", size = 861132, upload-time = "2025-09-25T21:32:50.149Z" },
253
+ { url = "https://files.pythonhosted.org/packages/b1/5e/f77dc6b9036943e285ba76b49e118d9ea929885becb0a29ba8a7c75e29fe/pyyaml-6.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:00c4bdeba853cc34e7dd471f16b4114f4162dc03e6b7afcc2128711f0eca823c", size = 839261, upload-time = "2025-09-25T21:32:51.808Z" },
254
+ { url = "https://files.pythonhosted.org/packages/ce/88/a9db1376aa2a228197c58b37302f284b5617f56a5d959fd1763fb1675ce6/pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:66e1674c3ef6f541c35191caae2d429b967b99e02040f5ba928632d9a7f0f065", size = 805272, upload-time = "2025-09-25T21:32:52.941Z" },
255
+ { url = "https://files.pythonhosted.org/packages/da/92/1446574745d74df0c92e6aa4a7b0b3130706a4142b2d1a5869f2eaa423c6/pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:16249ee61e95f858e83976573de0f5b2893b3677ba71c9dd36b9cf8be9ac6d65", size = 829923, upload-time = "2025-09-25T21:32:54.537Z" },
256
+ { url = "https://files.pythonhosted.org/packages/f0/7a/1c7270340330e575b92f397352af856a8c06f230aa3e76f86b39d01b416a/pyyaml-6.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:4ad1906908f2f5ae4e5a8ddfce73c320c2a1429ec52eafd27138b7f1cbe341c9", size = 174062, upload-time = "2025-09-25T21:32:55.767Z" },
257
+ { url = "https://files.pythonhosted.org/packages/f1/12/de94a39c2ef588c7e6455cfbe7343d3b2dc9d6b6b2f40c4c6565744c873d/pyyaml-6.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:ebc55a14a21cb14062aa4162f906cd962b28e2e9ea38f9b4391244cd8de4ae0b", size = 149341, upload-time = "2025-09-25T21:32:56.828Z" },
258
+ ]
259
+
260
+ [[package]]
261
+ name = "ruff"
262
+ version = "0.17.0"
263
+ source = { registry = "https://pypi.org/simple" }
264
+ sdist = { url = "https://files.pythonhosted.org/packages/e9/a7/70debb024dfacda67b8e560cc7511f52b34b9348a8cc8c1ec23036dcd51d/ruff-0.17.0.tar.gz", hash = "sha256:5cd03240d8208a557c2a9655a5cb07ebe36aa6bb35065f97d48c1f6adef5a322", size = 5320554, upload-time = "2026-10-09T19:47:29.248Z" }
265
+ wheels = [
266
+ { url = "https://files.pythonhosted.org/packages/bc/f8/ee5ab9da6089eae2a33e6008b01deb1eda19992c1c8e10661e98cee1640f/ruff-0.17.0-py3-none-linux_armv6l.whl", hash = "sha256:0e271826af9a20d18c6cfae8c51e82959167c24859686ddd3eb9a7f0842ce81e", size = 10148907, upload-time = "2026-10-09T19:46:38.695Z" },
267
+ { url = "https://files.pythonhosted.org/packages/9f/d9/2f81fb5a9d580afbb11b1c8ff915233a11f2a1b27405d7991f183c5e1976/ruff-0.17.0-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:5f0ca4a40f81403689c04f12966e22f44e329ae362072d8f1587b7bda87f603b", size = 10224208, upload-time = "2026-10-09T19:46:41.711Z" },
268
+ { url = "https://files.pythonhosted.org/packages/a7/20/643f3c8f75594f937b2bf74801241c56a2e2b8e139d24dff8b66b28cdd7f/ruff-0.17.0-py3-none-macosx_11_0_arm64.whl", hash = "sha256:cbf7149e0927dc3295d5d64679a4765576eef71b00782b2ae969ef82274d6bb9", size = 9273324, upload-time = "2026-10-09T19:46:44.323Z" },
269
+ { url = "https://files.pythonhosted.org/packages/ec/91/627700b233d367736cb274f1bd0b47d1f2b12f68878192812bd875adadc3/ruff-0.17.0-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:13ee90156522998c3037059d8f66885c8adeeaf7643bdce2caceee196ecd23e0", size = 9953190, upload-time = "2026-10-09T19:46:47.155Z" },
270
+ { url = "https://files.pythonhosted.org/packages/cd/92/91f7b5ed39490f89d6cbf56e1f543c383667a725efa8e2c2dee0f01f5591/ruff-0.17.0-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:3d8e4a002a94cd9d0dc48b51dc69d807a172b5b9bf2b668e656424dc5b55ead1", size = 9961746, upload-time = "2026-10-09T19:46:50.098Z" },
271
+ { url = "https://files.pythonhosted.org/packages/87/c5/7310f9fc63ce11ff6394edbd5e85433dfb0e14c9fbf6ccc97f1538491bc7/ruff-0.17.0-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c0b8a60c06a218c337e1161638d34757f83449243e2db161483ddf948e53ad14", size = 10625483, upload-time = "2026-10-09T19:46:53.379Z" },
272
+ { url = "https://files.pythonhosted.org/packages/a5/8d/97443f0dca4a03a0bc7629fd396fd494a1cb6666121e38c5075acb217d8f/ruff-0.17.0-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a330178bdffc4205dbf3bda11d93e059e388fd6546f8cdd304501a9160363c0d", size = 11350129, upload-time = "2026-10-09T19:46:56.486Z" },
273
+ { url = "https://files.pythonhosted.org/packages/9c/0a/c525efd9777be4b6b012e6969a3012648468e7e6c4b3e5b46af69f46e8eb/ruff-0.17.0-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7bb08489e234876fa2da67ae3ea938e9a2156da80293e0e4365abd6973d98329", size = 10906223, upload-time = "2026-10-09T19:47:00.263Z" },
274
+ { url = "https://files.pythonhosted.org/packages/2c/3c/4a01195d93420cad1175bedad13a515dc8a56f95a6e39789b92e582682f5/ruff-0.17.0-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bc73e7c133e82d55b5f15897b2a442d72c0cb4a0c886c46801ce3c247150b60c", size = 10386314, upload-time = "2026-10-09T19:47:03.057Z" },
275
+ { url = "https://files.pythonhosted.org/packages/c7/72/1a3951665485a921f6375f91e754a1854d5a645d41acc3642668064ff64d/ruff-0.17.0-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:db4f74c533403ab70fe4007873f6ae0c9f94a8b03158cf48d78788e47cdbe399", size = 10565464, upload-time = "2026-10-09T19:47:05.831Z" },
276
+ { url = "https://files.pythonhosted.org/packages/90/8c/539b4d8c082f57e18db8ae2be85a460d77861c79dcd5798e32b536a6a06f/ruff-0.17.0-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:3d8cc360e666d1914e47b0777c6906d70cf18891a55532bd0a16844195d70859", size = 10079468, upload-time = "2026-10-09T19:47:08.617Z" },
277
+ { url = "https://files.pythonhosted.org/packages/e0/b8/84286966db79434e8c26b585b0a0f6897cb3ab1c51a4aa4df10c28488b62/ruff-0.17.0-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:d66de796b726c4801e05fa99a2a8d7a780e107be222486c304ab61765561e866", size = 9958556, upload-time = "2026-10-09T19:47:11.324Z" },
278
+ { url = "https://files.pythonhosted.org/packages/69/50/27b6eed27b83fcdd5bfa0d52b83231e29094754374698da404d094487ae3/ruff-0.17.0-py3-none-musllinux_1_2_i686.whl", hash = "sha256:c3f268baf004aea944f040623327119527ea231af15f7fb7890e82cea0679589", size = 10352173, upload-time = "2026-10-09T19:47:14.188Z" },
279
+ { url = "https://files.pythonhosted.org/packages/2a/fa/955399fd13044cd827862044117d784a59e3196f6cce7424908ac9a7f914/ruff-0.17.0-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:864b6c1acb6b0bccf94b5a3938a1531fd09aaca5e5659a2e7bf0f3cf2a685540", size = 10791761, upload-time = "2026-10-09T19:47:16.931Z" },
280
+ { url = "https://files.pythonhosted.org/packages/ae/bf/024e01e1f6aec87768696725e648ed5b438941341eea8f8100beb681961f/ruff-0.17.0-py3-none-win32.whl", hash = "sha256:5e50aa5b84decd9fe5b0bb0e6f71c3b592f1767ed09faa4b7207d933961e35cd", size = 9904112, upload-time = "2026-10-09T19:47:19.75Z" },
281
+ { url = "https://files.pythonhosted.org/packages/cc/77/1ee73df41dcc8d1cdb686ee4bc46ea29704ea175feb6b95c78420f631ab8/ruff-0.17.0-py3-none-win_amd64.whl", hash = "sha256:8ab76bcda86dfd28e13776cb5de3c7bcdcf1ae3d37ed761113d1a5a415dc134c", size = 10115585, upload-time = "2026-10-09T19:47:22.698Z" },
282
+ { url = "https://files.pythonhosted.org/packages/fd/71/eb4f0ccc844aece56963e8578df9c95d4c00f580547d52329d4035d3af18/ruff-0.17.0-py3-none-win_arm64.whl", hash = "sha256:c154c73ff43f9854395e24cac507af13078962e53d2b511605058d22af1fdb88", size = 9872035, upload-time = "2026-10-09T19:47:26.306Z" },
283
+ ]
284
+
285
+ [[package]]
286
+ name = "tqdm"
287
+ version = "4.70.1"
288
+ source = { registry = "https://pypi.org/simple" }
289
+ dependencies = [
290
+ { name = "colorama", marker = "sys_platform == 'win32'" },
291
+ ]
292
+ sdist = { url = "https://files.pythonhosted.org/packages/0d/ea/b2a5bd54b28a324dae8211928b2d730b6547500342c7e6c6dea08bd0a485/tqdm-4.70.1.tar.gz", hash = "sha256:cefd0eca11b2a37a3aee776544d4f4ae913f02688135b5556b8788dfa474afc4", size = 171846, upload-time = "2026-09-11T07:25:16.601Z" }
293
+ wheels = [
294
+ { url = "https://files.pythonhosted.org/packages/a7/03/921a3d3c75785aca9ebfbfcabfbc3a1be12e2ab5265deb026d55a5a3f83e/tqdm-4.70.1-py3-none-any.whl", hash = "sha256:c293e525e6fef9c20e8728fd4612df02a0aa31bb5fe91ecd93e123b1b7bffa73", size = 80199, upload-time = "2026-09-11T07:25:14.599Z" },
295
+ ]
296
+
297
+ [[package]]
298
+ name = "truststore"
299
+ version = "0.10.4"
300
+ source = { registry = "https://pypi.org/simple" }
301
+ sdist = { url = "https://files.pythonhosted.org/packages/53/a3/1585216310e344e8102c22482f6060c7a6ea0322b63e026372e6dcefcfd6/truststore-0.10.4.tar.gz", hash = "sha256:9d91bd436463ad5e4ee4aba766628dd6cd7010cf3e2461756b3303710eebc301", size = 26169, upload-time = "2025-08-12T18:49:02.73Z" }
302
+ wheels = [
303
+ { url = "https://files.pythonhosted.org/packages/19/97/56608b2249fe206a67cd573bc93cd9896e1efb9e98bce9c163bcdc704b88/truststore-0.10.4-py3-none-any.whl", hash = "sha256:adaeaecf1cbb5f4de3b1959b42d41f6fab57b2b1666adb59e89cb0b53361d981", size = 18660, upload-time = "2025-08-12T18:49:01.46Z" },
304
+ ]
305
+
306
+ [[package]]
307
+ name = "typing-extensions"
308
+ version = "4.16.0"
309
+ source = { registry = "https://pypi.org/simple" }
310
+ sdist = { url = "https://files.pythonhosted.org/packages/f6/cc/6253133b5bb138fc3306cebfbda2c520f545d36b5be2c7255cc528bb45d6/typing_extensions-4.16.0.tar.gz", hash = "sha256:dc983d19a509c94dba722ee6abd33940f7c05a89e243c47e907eb4db6f1a43e5", size = 113555, upload-time = "2026-07-02T08:40:05.92Z" }
311
+ wheels = [
312
+ { url = "https://files.pythonhosted.org/packages/49/d3/b8441a820a491ddfc024b0b0cf0393375b75ea13866d9c66727e54c2fc80/typing_extensions-4.16.0-py3-none-any.whl", hash = "sha256:481caa481374e813c1b176ada14e97f1f67a4539ce9cfeb3f350d78d6370c2e8", size = 45571, upload-time = "2026-07-02T08:40:04.659Z" },
313
+ ]