model-mirror-cli 0.2.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- model_mirror/__init__.py +7 -0
- model_mirror/audit.py +214 -0
- model_mirror/checksums.py +371 -0
- model_mirror/cli.py +2436 -0
- model_mirror/config.py +300 -0
- model_mirror/hub.py +901 -0
- model_mirror/lock.py +119 -0
- model_mirror/mirror.py +201 -0
- model_mirror/progress.py +260 -0
- model_mirror/removal.py +164 -0
- model_mirror/repair.py +399 -0
- model_mirror/state.py +150 -0
- model_mirror/torrent.py +453 -0
- model_mirror/torrent_coverage.py +400 -0
- model_mirror/torrent_hashes.py +168 -0
- model_mirror/torrent_import.py +620 -0
- model_mirror/torrent_publication.py +582 -0
- model_mirror/torrent_seed.py +228 -0
- model_mirror/verify.py +153 -0
- model_mirror_cli-0.2.0.dist-info/METADATA +550 -0
- model_mirror_cli-0.2.0.dist-info/RECORD +24 -0
- model_mirror_cli-0.2.0.dist-info/WHEEL +4 -0
- model_mirror_cli-0.2.0.dist-info/entry_points.txt +2 -0
- model_mirror_cli-0.2.0.dist-info/licenses/LICENSE +21 -0
model_mirror/config.py
ADDED
|
@@ -0,0 +1,300 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
from dataclasses import dataclass
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from typing import Mapping
|
|
7
|
+
|
|
8
|
+
import yaml
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
DEFAULT_CONFIG_PATH = Path("~/.model-mirror.yaml").expanduser()
|
|
12
|
+
ARCHIVE_CONTROL_DIR = ".model-mirror"
|
|
13
|
+
TOKEN_SETUP_HINT = "model-mirror config set token-path /path/to/huggingface/token"
|
|
14
|
+
REPO_TYPE_DIRS = {
|
|
15
|
+
"model": "models",
|
|
16
|
+
"dataset": "datasets",
|
|
17
|
+
"space": "spaces",
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
@dataclass(slots=True)
|
|
22
|
+
class Config:
|
|
23
|
+
directory: Path | None = None
|
|
24
|
+
repo_type: str = "model"
|
|
25
|
+
revision: str = "main"
|
|
26
|
+
checksum: bool = True
|
|
27
|
+
checksum_workers: int = 1
|
|
28
|
+
download_workers: int = 1
|
|
29
|
+
stall_timeout_seconds: int = 600
|
|
30
|
+
stall_retries: int = 3
|
|
31
|
+
verify_after_mirror: bool = True
|
|
32
|
+
hf_xet_high_performance: bool = False
|
|
33
|
+
hf_xet_reconstruct_write_sequentially: bool = False
|
|
34
|
+
hf_xet_num_concurrent_range_gets: int | None = 1
|
|
35
|
+
token_path: Path | None = None
|
|
36
|
+
cache_dir: Path | None = None
|
|
37
|
+
tmp_dir: Path | None = None
|
|
38
|
+
|
|
39
|
+
def __post_init__(self) -> None:
|
|
40
|
+
if self.directory is None:
|
|
41
|
+
self.directory = Path.home() / ".local" / "share" / "model-mirror"
|
|
42
|
+
self.directory = Path(self.directory).expanduser()
|
|
43
|
+
if self.token_path is not None:
|
|
44
|
+
self.token_path = Path(self.token_path).expanduser()
|
|
45
|
+
if self.cache_dir is not None:
|
|
46
|
+
self.cache_dir = Path(self.cache_dir).expanduser()
|
|
47
|
+
if self.tmp_dir is not None:
|
|
48
|
+
self.tmp_dir = Path(self.tmp_dir).expanduser()
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def parse_bool(value: object) -> bool:
|
|
52
|
+
if isinstance(value, bool):
|
|
53
|
+
return value
|
|
54
|
+
if isinstance(value, str):
|
|
55
|
+
return value.strip().lower() in {"1", "true", "yes", "on"}
|
|
56
|
+
return bool(value)
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def load_config(path: Path | str | None = None, environ: Mapping[str, str] | None = None) -> Config:
|
|
60
|
+
config_path = Path(path).expanduser() if path is not None else DEFAULT_CONFIG_PATH
|
|
61
|
+
environ = os.environ if environ is None else environ
|
|
62
|
+
data: dict = {}
|
|
63
|
+
if config_path.exists():
|
|
64
|
+
loaded = yaml.safe_load(config_path.read_text(encoding="utf-8"))
|
|
65
|
+
if loaded:
|
|
66
|
+
if not isinstance(loaded, dict):
|
|
67
|
+
raise ValueError(f"Config file must contain a YAML mapping: {config_path}")
|
|
68
|
+
data = loaded
|
|
69
|
+
|
|
70
|
+
config = Config(
|
|
71
|
+
directory=_path_or_none(data.get("directory")),
|
|
72
|
+
repo_type=data.get("repo_type", "model"),
|
|
73
|
+
revision=data.get("revision", "main"),
|
|
74
|
+
checksum=parse_bool(data.get("checksum", True)),
|
|
75
|
+
checksum_workers=parse_positive_int(data.get("checksum_workers", 1), default=1),
|
|
76
|
+
download_workers=parse_positive_int(data.get("download_workers", 1), default=1),
|
|
77
|
+
stall_timeout_seconds=parse_nonnegative_int(data.get("stall_timeout_seconds", 600), default=600),
|
|
78
|
+
stall_retries=parse_nonnegative_int(data.get("stall_retries", 3), default=3),
|
|
79
|
+
verify_after_mirror=parse_bool(
|
|
80
|
+
data.get("verify_after_mirror", data.get("audit_after_mirror", True))
|
|
81
|
+
),
|
|
82
|
+
hf_xet_high_performance=parse_bool(data.get("hf_xet_high_performance", False)),
|
|
83
|
+
hf_xet_reconstruct_write_sequentially=parse_bool(
|
|
84
|
+
data.get("hf_xet_reconstruct_write_sequentially", False)
|
|
85
|
+
),
|
|
86
|
+
hf_xet_num_concurrent_range_gets=parse_optional_positive_int(
|
|
87
|
+
data.get("hf_xet_num_concurrent_range_gets", 1)
|
|
88
|
+
),
|
|
89
|
+
token_path=_path_or_none(data.get("token_path")),
|
|
90
|
+
cache_dir=_path_or_none(data.get("cache_dir")),
|
|
91
|
+
tmp_dir=_path_or_none(data.get("tmp_dir")),
|
|
92
|
+
)
|
|
93
|
+
|
|
94
|
+
if environ.get("MODEL_MIRROR_DIRECTORY"):
|
|
95
|
+
config.directory = Path(environ["MODEL_MIRROR_DIRECTORY"]).expanduser()
|
|
96
|
+
if environ.get("MODEL_MIRROR_REVISION"):
|
|
97
|
+
config.revision = environ["MODEL_MIRROR_REVISION"]
|
|
98
|
+
if environ.get("MODEL_MIRROR_REPO_TYPE"):
|
|
99
|
+
config.repo_type = environ["MODEL_MIRROR_REPO_TYPE"]
|
|
100
|
+
if environ.get("MODEL_MIRROR_TOKEN_PATH"):
|
|
101
|
+
config.token_path = Path(environ["MODEL_MIRROR_TOKEN_PATH"]).expanduser()
|
|
102
|
+
if environ.get("MODEL_MIRROR_HF_XET_HIGH_PERFORMANCE"):
|
|
103
|
+
config.hf_xet_high_performance = parse_bool(environ["MODEL_MIRROR_HF_XET_HIGH_PERFORMANCE"])
|
|
104
|
+
if environ.get("MODEL_MIRROR_HF_XET_RECONSTRUCT_WRITE_SEQUENTIALLY"):
|
|
105
|
+
config.hf_xet_reconstruct_write_sequentially = parse_bool(
|
|
106
|
+
environ["MODEL_MIRROR_HF_XET_RECONSTRUCT_WRITE_SEQUENTIALLY"]
|
|
107
|
+
)
|
|
108
|
+
if environ.get("MODEL_MIRROR_HF_XET_NUM_CONCURRENT_RANGE_GETS"):
|
|
109
|
+
config.hf_xet_num_concurrent_range_gets = parse_positive_int(
|
|
110
|
+
environ["MODEL_MIRROR_HF_XET_NUM_CONCURRENT_RANGE_GETS"], default=16
|
|
111
|
+
)
|
|
112
|
+
if environ.get("MODEL_MIRROR_CHECKSUM_WORKERS"):
|
|
113
|
+
config.checksum_workers = parse_positive_int(environ["MODEL_MIRROR_CHECKSUM_WORKERS"], default=1)
|
|
114
|
+
if environ.get("MODEL_MIRROR_DOWNLOAD_WORKERS"):
|
|
115
|
+
config.download_workers = parse_positive_int(environ["MODEL_MIRROR_DOWNLOAD_WORKERS"], default=1)
|
|
116
|
+
if environ.get("MODEL_MIRROR_STALL_TIMEOUT"):
|
|
117
|
+
config.stall_timeout_seconds = parse_nonnegative_int(environ["MODEL_MIRROR_STALL_TIMEOUT"], default=600)
|
|
118
|
+
if environ.get("MODEL_MIRROR_STALL_RETRIES"):
|
|
119
|
+
config.stall_retries = parse_nonnegative_int(environ["MODEL_MIRROR_STALL_RETRIES"], default=3)
|
|
120
|
+
|
|
121
|
+
return config
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
def parse_positive_int(value: object, *, default: int) -> int:
|
|
125
|
+
if value in {None, ""}:
|
|
126
|
+
return default
|
|
127
|
+
parsed = int(value)
|
|
128
|
+
if parsed < 1:
|
|
129
|
+
raise ValueError("value must be a positive integer")
|
|
130
|
+
return parsed
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
def parse_nonnegative_int(value: object, *, default: int) -> int:
|
|
134
|
+
if value in {None, ""}:
|
|
135
|
+
return default
|
|
136
|
+
parsed = int(value)
|
|
137
|
+
if parsed < 0:
|
|
138
|
+
raise ValueError("value must be zero or a positive integer")
|
|
139
|
+
return parsed
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
def parse_optional_positive_int(value: object) -> int | None:
|
|
143
|
+
if value in {None, ""}:
|
|
144
|
+
return None
|
|
145
|
+
return parse_positive_int(value, default=1)
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
def _path_or_none(value: object) -> Path | None:
|
|
149
|
+
if value in {None, ""}:
|
|
150
|
+
return None
|
|
151
|
+
return Path(str(value)).expanduser()
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
def token_path_candidates(environ: Mapping[str, str] | None = None) -> list[Path]:
|
|
155
|
+
env = os.environ if environ is None else environ
|
|
156
|
+
candidates: list[Path] = []
|
|
157
|
+
if env.get("HF_TOKEN_PATH"):
|
|
158
|
+
candidates.append(Path(env["HF_TOKEN_PATH"]).expanduser())
|
|
159
|
+
if env.get("MODEL_MIRROR_TOKEN_PATH"):
|
|
160
|
+
candidates.append(Path(env["MODEL_MIRROR_TOKEN_PATH"]).expanduser())
|
|
161
|
+
if env.get("HF_HOME"):
|
|
162
|
+
candidates.append(Path(env["HF_HOME"]).expanduser() / "token")
|
|
163
|
+
candidates.extend(
|
|
164
|
+
[
|
|
165
|
+
Path.home() / ".cache" / "huggingface" / "token",
|
|
166
|
+
Path.home() / ".huggingface" / "token",
|
|
167
|
+
]
|
|
168
|
+
)
|
|
169
|
+
|
|
170
|
+
deduped: list[Path] = []
|
|
171
|
+
seen = set()
|
|
172
|
+
for candidate in candidates:
|
|
173
|
+
key = str(candidate)
|
|
174
|
+
if key in seen:
|
|
175
|
+
continue
|
|
176
|
+
seen.add(key)
|
|
177
|
+
deduped.append(candidate)
|
|
178
|
+
return deduped
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
def detect_token_path(config: Config, environ: Mapping[str, str] | None = None) -> Path | None:
|
|
182
|
+
if config.token_path is not None:
|
|
183
|
+
return config.token_path
|
|
184
|
+
for candidate in token_path_candidates(environ):
|
|
185
|
+
if candidate.is_file():
|
|
186
|
+
return candidate
|
|
187
|
+
return None
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
def hf_token_available(environ: Mapping[str, str]) -> bool:
|
|
191
|
+
if environ.get("HF_TOKEN"):
|
|
192
|
+
return True
|
|
193
|
+
token_path = environ.get("HF_TOKEN_PATH")
|
|
194
|
+
if not token_path:
|
|
195
|
+
return False
|
|
196
|
+
try:
|
|
197
|
+
path = Path(token_path).expanduser()
|
|
198
|
+
return path.is_file() and path.stat().st_size > 0
|
|
199
|
+
except OSError:
|
|
200
|
+
return False
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
def save_config(config: Config, path: Path | str | None = None) -> None:
|
|
204
|
+
config_path = Path(path).expanduser() if path is not None else DEFAULT_CONFIG_PATH
|
|
205
|
+
config_path.parent.mkdir(parents=True, exist_ok=True)
|
|
206
|
+
data = {
|
|
207
|
+
"directory": str(config.directory),
|
|
208
|
+
"repo_type": config.repo_type,
|
|
209
|
+
"revision": config.revision,
|
|
210
|
+
"checksum": config.checksum,
|
|
211
|
+
"checksum_workers": config.checksum_workers,
|
|
212
|
+
"download_workers": config.download_workers,
|
|
213
|
+
"stall_timeout_seconds": config.stall_timeout_seconds,
|
|
214
|
+
"stall_retries": config.stall_retries,
|
|
215
|
+
"verify_after_mirror": config.verify_after_mirror,
|
|
216
|
+
"hf_xet_high_performance": config.hf_xet_high_performance,
|
|
217
|
+
"hf_xet_reconstruct_write_sequentially": config.hf_xet_reconstruct_write_sequentially,
|
|
218
|
+
}
|
|
219
|
+
if config.hf_xet_num_concurrent_range_gets is not None:
|
|
220
|
+
data["hf_xet_num_concurrent_range_gets"] = config.hf_xet_num_concurrent_range_gets
|
|
221
|
+
if config.token_path is not None:
|
|
222
|
+
data["token_path"] = str(config.token_path)
|
|
223
|
+
if config.cache_dir is not None:
|
|
224
|
+
data["cache_dir"] = str(config.cache_dir)
|
|
225
|
+
if config.tmp_dir is not None:
|
|
226
|
+
data["tmp_dir"] = str(config.tmp_dir)
|
|
227
|
+
config_path.write_text(yaml.safe_dump(data, sort_keys=True), encoding="utf-8")
|
|
228
|
+
|
|
229
|
+
|
|
230
|
+
def safe_repo_path(repo_id: str) -> Path:
|
|
231
|
+
parts = repo_id.split("/")
|
|
232
|
+
if not parts or any(part in {"", ".", ".."} for part in parts):
|
|
233
|
+
raise ValueError(f"Invalid repository id for local path: {repo_id!r}")
|
|
234
|
+
if any("\\" in part or "\x00" in part for part in parts):
|
|
235
|
+
raise ValueError(f"Invalid repository id for local path: {repo_id!r}")
|
|
236
|
+
return Path(*parts)
|
|
237
|
+
|
|
238
|
+
|
|
239
|
+
def archive_path(config: Config, repo_id: str, repo_type: str | None = None) -> Path:
|
|
240
|
+
selected_type = repo_type or config.repo_type
|
|
241
|
+
try:
|
|
242
|
+
type_dir = REPO_TYPE_DIRS[selected_type]
|
|
243
|
+
except KeyError as exc:
|
|
244
|
+
raise ValueError(f"Unsupported repo type: {selected_type!r}") from exc
|
|
245
|
+
return Path(config.directory) / type_dir / safe_repo_path(repo_id)
|
|
246
|
+
|
|
247
|
+
|
|
248
|
+
def archive_control_path(config: Config) -> Path:
|
|
249
|
+
return Path(config.directory) / ARCHIVE_CONTROL_DIR
|
|
250
|
+
|
|
251
|
+
|
|
252
|
+
def archive_runtime_cache_path(config: Config) -> Path:
|
|
253
|
+
if config.cache_dir is not None:
|
|
254
|
+
return Path(config.cache_dir)
|
|
255
|
+
return archive_control_path(config) / "cache"
|
|
256
|
+
|
|
257
|
+
|
|
258
|
+
def archive_runtime_tmp_path(config: Config) -> Path:
|
|
259
|
+
if config.tmp_dir is not None:
|
|
260
|
+
return Path(config.tmp_dir)
|
|
261
|
+
return archive_control_path(config) / "tmp"
|
|
262
|
+
|
|
263
|
+
|
|
264
|
+
def apply_hf_environment(config: Config, environ: Mapping[str, str] | None = None) -> dict[str, str]:
|
|
265
|
+
base_env = dict(os.environ if environ is None else environ)
|
|
266
|
+
token_path = detect_token_path(config, base_env)
|
|
267
|
+
env = dict(base_env)
|
|
268
|
+
cache_dir = archive_runtime_cache_path(config)
|
|
269
|
+
tmp_dir = archive_runtime_tmp_path(config)
|
|
270
|
+
|
|
271
|
+
env["HF_HOME"] = str(cache_dir)
|
|
272
|
+
env["HF_HUB_CACHE"] = str(cache_dir / "hub")
|
|
273
|
+
env["HF_ASSETS_CACHE"] = str(cache_dir / "assets")
|
|
274
|
+
env["HF_XET_CACHE"] = str(cache_dir / "xet")
|
|
275
|
+
env["TRANSFORMERS_CACHE"] = str(cache_dir / "transformers")
|
|
276
|
+
env["XDG_CACHE_HOME"] = str(cache_dir / "xdg")
|
|
277
|
+
env["TMPDIR"] = str(tmp_dir)
|
|
278
|
+
env.setdefault("HF_HUB_DISABLE_TELEMETRY", "1")
|
|
279
|
+
|
|
280
|
+
if token_path is not None:
|
|
281
|
+
env["HF_TOKEN_PATH"] = str(token_path)
|
|
282
|
+
|
|
283
|
+
if config.hf_xet_high_performance:
|
|
284
|
+
env["HF_XET_HIGH_PERFORMANCE"] = "1"
|
|
285
|
+
else:
|
|
286
|
+
env.pop("HF_XET_HIGH_PERFORMANCE", None)
|
|
287
|
+
if config.hf_xet_reconstruct_write_sequentially:
|
|
288
|
+
env["HF_XET_RECONSTRUCT_WRITE_SEQUENTIALLY"] = "1"
|
|
289
|
+
env["HF_XET_RECONSTRUCTION_USE_VECTORED_WRITE"] = "false"
|
|
290
|
+
else:
|
|
291
|
+
env.pop("HF_XET_RECONSTRUCT_WRITE_SEQUENTIALLY", None)
|
|
292
|
+
env.pop("HF_XET_RECONSTRUCTION_USE_VECTORED_WRITE", None)
|
|
293
|
+
if config.hf_xet_num_concurrent_range_gets is not None:
|
|
294
|
+
env["HF_XET_NUM_CONCURRENT_RANGE_GETS"] = str(config.hf_xet_num_concurrent_range_gets)
|
|
295
|
+
env["HF_XET_FIXED_DOWNLOAD_CONCURRENCY"] = str(config.hf_xet_num_concurrent_range_gets)
|
|
296
|
+
else:
|
|
297
|
+
env.pop("HF_XET_NUM_CONCURRENT_RANGE_GETS", None)
|
|
298
|
+
env.pop("HF_XET_FIXED_DOWNLOAD_CONCURRENCY", None)
|
|
299
|
+
|
|
300
|
+
return env
|