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/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