darsay 0.6.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.
- darsay/__init__.py +23 -0
- darsay/archiver.py +493 -0
- darsay/cli.py +493 -0
- darsay/estimate.py +274 -0
- darsay/export.py +210 -0
- darsay/hashing.py +73 -0
- darsay/hydrate.py +574 -0
- darsay/licensing.py +158 -0
- darsay/metadata.py +230 -0
- darsay/providers/__init__.py +1 -0
- darsay/providers/base.py +157 -0
- darsay/providers/huggingface.py +546 -0
- darsay/readme_gen.py +461 -0
- darsay/runners/__init__.py +8 -0
- darsay/runners/llama_cpp_runner.py +119 -0
- darsay/runners/transformers_runner.py +165 -0
- darsay/safetensors_meta.py +62 -0
- darsay/schema.py +92 -0
- darsay/smoke.py +196 -0
- darsay/sources.py +139 -0
- darsay/transfer.py +1192 -0
- darsay/verify.py +148 -0
- darsay-0.6.0.dist-info/METADATA +454 -0
- darsay-0.6.0.dist-info/RECORD +29 -0
- darsay-0.6.0.dist-info/WHEEL +5 -0
- darsay-0.6.0.dist-info/entry_points.txt +2 -0
- darsay-0.6.0.dist-info/licenses/LICENSE +201 -0
- darsay-0.6.0.dist-info/licenses/NOTICE +4 -0
- darsay-0.6.0.dist-info/top_level.txt +1 -0
darsay/__init__.py
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
"""darsay: the genesis machine of archives."""
|
|
2
|
+
|
|
3
|
+
__version__ = "0.6.0"
|
|
4
|
+
|
|
5
|
+
# Version of the manifest.json schema, independent of the tool version.
|
|
6
|
+
# Bump the major component on breaking layout changes; consumers should
|
|
7
|
+
# check this before parsing.
|
|
8
|
+
# 1.1.0: defined the shape of runtime.tested_hardware entries (written by
|
|
9
|
+
# `darsay run`); previously always null.
|
|
10
|
+
# 1.2.0: dataset artifact type (payload under data/, dataset_metadata section,
|
|
11
|
+
# smoke_tests.structure, dataset relationships) and the model-side
|
|
12
|
+
# relationships.training_datasets field. Additive.
|
|
13
|
+
# 1.3.0: source.access (Hub gate status at archive time); structured lineage —
|
|
14
|
+
# relationships.base_models (all parents) and base_model_relation (the
|
|
15
|
+
# model-tree edge label), with finetuned_from now set only when the
|
|
16
|
+
# declared relation is `finetune`; licensing needs_manual_review/notes
|
|
17
|
+
# are gate-aware. Additive.
|
|
18
|
+
# 1.4.0: source.transfer session/accounting summary; source.mirrors_used is
|
|
19
|
+
# populated for verified sibling-bundle copies; archive-time checksum
|
|
20
|
+
# verification records its per-file timing. Additive.
|
|
21
|
+
# 1.5.0: source.provider and source.address (canonical provider-qualified
|
|
22
|
+
# ref). origin remains the provider id. Additive.
|
|
23
|
+
SCHEMA_VERSION = "1.5.0"
|
darsay/archiver.py
ADDED
|
@@ -0,0 +1,493 @@
|
|
|
1
|
+
"""Archive a source into a reproducible, auditable bundle.
|
|
2
|
+
|
|
3
|
+
Bundle layout:
|
|
4
|
+
|
|
5
|
+
vault/<bundle-name>/<revision12>/
|
|
6
|
+
model/ immutable payload: exact snapshot of the upstream repo
|
|
7
|
+
(data/ for dataset bundles — the registry's payload_root)
|
|
8
|
+
manifest.json machine-readable record (schema.py / SCHEMA_VERSION)
|
|
9
|
+
README.md human-readable summary, regenerable from the manifest
|
|
10
|
+
VERIFICATION.md latest verification report
|
|
11
|
+
verification.json verification history
|
|
12
|
+
curation.md curator notes; the only file meant to be hand-edited
|
|
13
|
+
|
|
14
|
+
The payload is treated as immutable after archiving; the bundle hash covers it
|
|
15
|
+
alone. Metadata at the bundle root is mutable by design.
|
|
16
|
+
|
|
17
|
+
Acquisition is provider-dispatched (``sources.parse_source``). Hugging Face is
|
|
18
|
+
the first provider, not the archive format.
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
from __future__ import annotations
|
|
22
|
+
|
|
23
|
+
import json
|
|
24
|
+
import platform
|
|
25
|
+
import re
|
|
26
|
+
import shutil
|
|
27
|
+
import socket
|
|
28
|
+
from datetime import datetime, timezone
|
|
29
|
+
from pathlib import Path
|
|
30
|
+
|
|
31
|
+
from . import SCHEMA_VERSION, __version__
|
|
32
|
+
from .hashing import bundle_hash
|
|
33
|
+
from .licensing import build_licensing_record
|
|
34
|
+
from .metadata import estimate_runtime, extract_dataset_metadata, extract_model_metadata
|
|
35
|
+
from .schema import ARTIFACT_TYPES, BUNDLE_METADATA_FILES, check_completeness, payload_root_for
|
|
36
|
+
from .sources import (
|
|
37
|
+
SourceGatedError,
|
|
38
|
+
SourceNotFoundError,
|
|
39
|
+
SourceRef,
|
|
40
|
+
get_provider,
|
|
41
|
+
parse_source,
|
|
42
|
+
source_from_ledger,
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def utc_now() -> str:
|
|
47
|
+
return datetime.now(timezone.utc).isoformat(timespec="seconds")
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def parse_repo_ref(ref: str) -> tuple[str, str]:
|
|
51
|
+
"""Return ``(artifact_type, locator)``. Prefer ``parse_source`` for new code."""
|
|
52
|
+
source = parse_source(ref)
|
|
53
|
+
return source.artifact_type, source.locator
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def hub_url(repo_id: str, repo_type: str = "model") -> str:
|
|
57
|
+
"""Hugging Face URL for a locator. Prefer ``parse_source(...).url``."""
|
|
58
|
+
path = f"datasets/{repo_id}" if repo_type == "dataset" else repo_id
|
|
59
|
+
return f"https://huggingface.co/{path}"
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def bundle_name_for(repo_id: str, repo_type: str = "model") -> str:
|
|
63
|
+
"""Vault directory name for a Hugging Face-shaped locator.
|
|
64
|
+
|
|
65
|
+
Prefer ``parse_source(...).bundle_name``. Hugging Face bundle names are
|
|
66
|
+
unchanged (``owner--name``, ``datasets--owner--name``) so existing vaults
|
|
67
|
+
keep resuming. Other providers include their id in the name.
|
|
68
|
+
"""
|
|
69
|
+
loc = f"datasets/{repo_id}" if repo_type == "dataset" else repo_id
|
|
70
|
+
return parse_source(loc).bundle_name
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def bundle_dir_for(vault: Path, source: str | SourceRef, revision: str, repo_type: str | None = None) -> Path:
|
|
74
|
+
"""Return the bundle directory for a source + pinned revision.
|
|
75
|
+
|
|
76
|
+
``repo_type`` is accepted only so older callers that passed a bare
|
|
77
|
+
``repo_id`` string still work; new code passes a SourceRef or source ref.
|
|
78
|
+
"""
|
|
79
|
+
if isinstance(source, SourceRef):
|
|
80
|
+
ref = source
|
|
81
|
+
elif repo_type is not None:
|
|
82
|
+
loc = f"datasets/{source}" if repo_type == "dataset" else source
|
|
83
|
+
ref = parse_source(loc)
|
|
84
|
+
else:
|
|
85
|
+
ref = parse_source(source)
|
|
86
|
+
return vault / ref.bundle_name / revision[:12]
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def write_manifest(bundle_dir: Path, manifest: dict) -> None:
|
|
90
|
+
path = bundle_dir / "manifest.json"
|
|
91
|
+
path.write_text(json.dumps(manifest, indent=2, ensure_ascii=False) + "\n", encoding="utf-8")
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def load_manifest(bundle_dir: Path) -> dict:
|
|
95
|
+
return json.loads((bundle_dir / "manifest.json").read_text(encoding="utf-8"))
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def _guess_version(repo_name: str, model_type: str | None) -> str | None:
|
|
99
|
+
# "Qwen3-0.6B" with model_type "qwen3" -> "3"
|
|
100
|
+
if model_type:
|
|
101
|
+
m = re.search(r"(\d+(?:\.\d+)?)$", model_type)
|
|
102
|
+
if m:
|
|
103
|
+
return m.group(1)
|
|
104
|
+
m = re.search(r"[a-zA-Z](\d+(?:\.\d+)?)[-_]", repo_name)
|
|
105
|
+
return m.group(1) if m else None
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def archive(
|
|
109
|
+
source: str | SourceRef,
|
|
110
|
+
revision: str | None = None,
|
|
111
|
+
vault: Path = Path("vault"),
|
|
112
|
+
force: bool = False,
|
|
113
|
+
dry_run: bool = False,
|
|
114
|
+
max_bytes: int | None = None,
|
|
115
|
+
max_minutes: float | None = None,
|
|
116
|
+
rehash: bool = False,
|
|
117
|
+
jobs: int = 4,
|
|
118
|
+
shard: tuple[int, int] | None = None,
|
|
119
|
+
progress=print,
|
|
120
|
+
) -> Path | None:
|
|
121
|
+
"""Archive a source through pin → reconcile → transfer → register."""
|
|
122
|
+
from .transfer import (
|
|
123
|
+
LedgerError,
|
|
124
|
+
CleanStop,
|
|
125
|
+
PartialTransfer,
|
|
126
|
+
StopController,
|
|
127
|
+
add_disk_preflight,
|
|
128
|
+
begin_session,
|
|
129
|
+
find_resume,
|
|
130
|
+
finish_session,
|
|
131
|
+
load_ledger,
|
|
132
|
+
new_ledger,
|
|
133
|
+
reconcile,
|
|
134
|
+
record_event,
|
|
135
|
+
save_ledger,
|
|
136
|
+
print_plan,
|
|
137
|
+
print_shard_plan,
|
|
138
|
+
transfer_all,
|
|
139
|
+
transfer_lock,
|
|
140
|
+
transfer_plan,
|
|
141
|
+
)
|
|
142
|
+
|
|
143
|
+
ref = source if isinstance(source, SourceRef) else parse_source(source)
|
|
144
|
+
provider = get_provider(ref.provider)
|
|
145
|
+
root = payload_root_for(ref.artifact_type)
|
|
146
|
+
resume = find_resume(vault, ref, revision, root)
|
|
147
|
+
pinned = resume[1] if resume else None
|
|
148
|
+
orphan_dir = resume[0] if resume and pinned is None else None
|
|
149
|
+
|
|
150
|
+
if pinned is not None:
|
|
151
|
+
bundle_dir = resume[0]
|
|
152
|
+
progress(
|
|
153
|
+
f"Resuming {ref.artifact_type} {ref.canonical} @ pinned revision "
|
|
154
|
+
f"{pinned['revision'][:12]} (no metadata refresh) ..."
|
|
155
|
+
)
|
|
156
|
+
snapshot = None
|
|
157
|
+
else:
|
|
158
|
+
pin_revision = orphan_dir.name if orphan_dir is not None else revision
|
|
159
|
+
progress(f"Resolving {ref.canonical} @ {pin_revision or provider.default_revision} ...")
|
|
160
|
+
try:
|
|
161
|
+
snapshot = provider.pin(ref, pin_revision, require_access=True)
|
|
162
|
+
except SourceGatedError as exc:
|
|
163
|
+
if orphan_dir is not None:
|
|
164
|
+
shutil.rmtree(orphan_dir, ignore_errors=True)
|
|
165
|
+
raise SystemExit(str(exc)) from None
|
|
166
|
+
except SourceNotFoundError as exc:
|
|
167
|
+
raise SystemExit(str(exc)) from None
|
|
168
|
+
bundle_dir = bundle_dir_for(vault, ref, snapshot.revision)
|
|
169
|
+
|
|
170
|
+
payload_dir = bundle_dir / root
|
|
171
|
+
with transfer_lock(bundle_dir, progress=progress):
|
|
172
|
+
manifest_path = bundle_dir / "manifest.json"
|
|
173
|
+
if manifest_path.exists() and not force and not dry_run:
|
|
174
|
+
raise SystemExit(f"Bundle already exists: {bundle_dir} (use --force to re-archive)")
|
|
175
|
+
|
|
176
|
+
if pinned is not None:
|
|
177
|
+
ledger = load_ledger(bundle_dir)
|
|
178
|
+
else:
|
|
179
|
+
if manifest_path.exists() and dry_run and not force:
|
|
180
|
+
try:
|
|
181
|
+
ledger = load_ledger(bundle_dir)
|
|
182
|
+
except LedgerError:
|
|
183
|
+
assert snapshot is not None
|
|
184
|
+
ledger = new_ledger(snapshot)
|
|
185
|
+
elif force:
|
|
186
|
+
assert snapshot is not None
|
|
187
|
+
ledger = new_ledger(snapshot)
|
|
188
|
+
save_ledger(bundle_dir, ledger)
|
|
189
|
+
else:
|
|
190
|
+
try:
|
|
191
|
+
ledger = load_ledger(bundle_dir)
|
|
192
|
+
except LedgerError:
|
|
193
|
+
assert snapshot is not None
|
|
194
|
+
ledger = new_ledger(snapshot)
|
|
195
|
+
save_ledger(bundle_dir, ledger)
|
|
196
|
+
if force and manifest_path.exists():
|
|
197
|
+
manifest_path.unlink()
|
|
198
|
+
|
|
199
|
+
if dry_run:
|
|
200
|
+
import copy
|
|
201
|
+
|
|
202
|
+
dry_ledger = copy.deepcopy(ledger)
|
|
203
|
+
dry_session = {"bytes_adopted": 0, "files_completed": 0}
|
|
204
|
+
plan = reconcile(
|
|
205
|
+
bundle_dir,
|
|
206
|
+
payload_dir,
|
|
207
|
+
dry_ledger,
|
|
208
|
+
dry_session,
|
|
209
|
+
progress=progress,
|
|
210
|
+
apply=False,
|
|
211
|
+
rehash=rehash,
|
|
212
|
+
)
|
|
213
|
+
add_disk_preflight(bundle_dir, plan)
|
|
214
|
+
print_plan(plan, progress=progress)
|
|
215
|
+
if shard is not None:
|
|
216
|
+
print_shard_plan(ledger, shard, progress=progress)
|
|
217
|
+
return None
|
|
218
|
+
|
|
219
|
+
stop_controller = StopController(max_bytes=max_bytes, max_minutes=max_minutes)
|
|
220
|
+
stop_controller.start()
|
|
221
|
+
with stop_controller.sigint_handler():
|
|
222
|
+
session = begin_session(bundle_dir, ledger, shard=shard)
|
|
223
|
+
session_finished = False
|
|
224
|
+
try:
|
|
225
|
+
plan = reconcile(
|
|
226
|
+
bundle_dir,
|
|
227
|
+
payload_dir,
|
|
228
|
+
ledger,
|
|
229
|
+
session,
|
|
230
|
+
progress=progress,
|
|
231
|
+
rehash=rehash,
|
|
232
|
+
)
|
|
233
|
+
add_disk_preflight(bundle_dir, plan)
|
|
234
|
+
print_plan(plan, progress=progress)
|
|
235
|
+
if shard is not None:
|
|
236
|
+
print_shard_plan(ledger, shard, progress=progress)
|
|
237
|
+
if plan["disk"]["verdict"] == "insufficient":
|
|
238
|
+
progress("WARNING: disk preflight is insufficient; transfer may end with ENOSPC")
|
|
239
|
+
stop_controller.check(session)
|
|
240
|
+
plan = transfer_all(
|
|
241
|
+
bundle_dir,
|
|
242
|
+
payload_dir,
|
|
243
|
+
ledger,
|
|
244
|
+
session,
|
|
245
|
+
progress=progress,
|
|
246
|
+
stop_controller=stop_controller,
|
|
247
|
+
jobs=jobs,
|
|
248
|
+
shard=shard,
|
|
249
|
+
)
|
|
250
|
+
if not plan["complete"]:
|
|
251
|
+
raise RuntimeError("transfer ended without verifying every pinned file")
|
|
252
|
+
|
|
253
|
+
finish_session(bundle_dir, ledger, session, "complete")
|
|
254
|
+
session_finished = True
|
|
255
|
+
return _register_bundle(bundle_dir, payload_dir, ledger, progress)
|
|
256
|
+
except CleanStop as stop:
|
|
257
|
+
plan = add_disk_preflight(bundle_dir, transfer_plan(payload_dir, ledger))
|
|
258
|
+
if plan["complete"]:
|
|
259
|
+
finish_session(bundle_dir, ledger, session, "complete")
|
|
260
|
+
session_finished = True
|
|
261
|
+
return _register_bundle(bundle_dir, payload_dir, ledger, progress)
|
|
262
|
+
session["stop_detail"] = stop.detail
|
|
263
|
+
finish_session(bundle_dir, ledger, session, stop.reason)
|
|
264
|
+
session_finished = True
|
|
265
|
+
print_plan(plan, progress=progress)
|
|
266
|
+
raise PartialTransfer(bundle_dir, stop.reason, stop.detail, plan)
|
|
267
|
+
except SourceGatedError as exc:
|
|
268
|
+
record_event(
|
|
269
|
+
ledger,
|
|
270
|
+
None,
|
|
271
|
+
"gated",
|
|
272
|
+
"upstream access was denied during transfer; partial archive retained",
|
|
273
|
+
)
|
|
274
|
+
finish_session(bundle_dir, ledger, session, "error")
|
|
275
|
+
session_finished = True
|
|
276
|
+
raise SystemExit(str(exc)) from None
|
|
277
|
+
except BaseException:
|
|
278
|
+
if not session_finished:
|
|
279
|
+
finish_session(bundle_dir, ledger, session, "error")
|
|
280
|
+
raise
|
|
281
|
+
|
|
282
|
+
|
|
283
|
+
def archive_model(
|
|
284
|
+
repo_id: str,
|
|
285
|
+
revision: str | None = None,
|
|
286
|
+
vault: Path = Path("vault"),
|
|
287
|
+
force: bool = False,
|
|
288
|
+
repo_type: str = "model",
|
|
289
|
+
**kwargs,
|
|
290
|
+
) -> Path | None:
|
|
291
|
+
"""Backward-compatible wrapper: ``repo_id`` + ``repo_type`` → ``archive(source)``."""
|
|
292
|
+
loc = f"datasets/{repo_id}" if repo_type == "dataset" else repo_id
|
|
293
|
+
return archive(loc, revision=revision, vault=vault, force=force, **kwargs)
|
|
294
|
+
|
|
295
|
+
|
|
296
|
+
def _register_bundle(bundle_dir: Path, payload_dir: Path, ledger: dict, progress) -> Path:
|
|
297
|
+
"""Run completion-time extraction and register from pinned ledger facts."""
|
|
298
|
+
from .transfer import file_records as ledger_file_records, local_mirrors, transfer_summary
|
|
299
|
+
|
|
300
|
+
source = source_from_ledger(ledger)
|
|
301
|
+
provider = get_provider(source.provider)
|
|
302
|
+
repo_type = source.artifact_type
|
|
303
|
+
commit = ledger["revision"]
|
|
304
|
+
root = payload_root_for(repo_type)
|
|
305
|
+
metadata = ledger["metadata"]
|
|
306
|
+
card = metadata.get("card_data") or {}
|
|
307
|
+
tags = list(metadata.get("tags") or [])
|
|
308
|
+
gated = metadata.get("gated", False)
|
|
309
|
+
|
|
310
|
+
# Provider transfer caches own resumable partials until every expected
|
|
311
|
+
# file is verified. They are not archival payload.
|
|
312
|
+
shutil.rmtree(payload_dir / ".cache", ignore_errors=True)
|
|
313
|
+
file_records, upstream_mismatches = ledger_file_records(ledger, root)
|
|
314
|
+
inventory_paths = [r["path"] for r in file_records]
|
|
315
|
+
total_size = sum(r["size"] for r in file_records)
|
|
316
|
+
completeness = check_completeness(repo_type, inventory_paths)
|
|
317
|
+
if repo_type == "dataset":
|
|
318
|
+
model_metadata = runtime = None
|
|
319
|
+
dataset_metadata = extract_dataset_metadata(payload_dir, card, file_records)
|
|
320
|
+
else:
|
|
321
|
+
dataset_metadata = None
|
|
322
|
+
model_metadata = extract_model_metadata(payload_dir, card)
|
|
323
|
+
runtime = estimate_runtime(payload_dir, model_metadata)
|
|
324
|
+
license_id = card.get("license")
|
|
325
|
+
if isinstance(license_id, list):
|
|
326
|
+
license_id = license_id[0] if license_id else None
|
|
327
|
+
licensing = build_licensing_record(license_id, payload_dir, gated=bool(gated))
|
|
328
|
+
|
|
329
|
+
for lf in licensing["license_files"]:
|
|
330
|
+
name = Path(lf).name
|
|
331
|
+
if name.upper().startswith(("LICENSE", "LICENCE", "COPYING")):
|
|
332
|
+
shutil.copy2(bundle_dir / lf, bundle_dir / "LICENSE")
|
|
333
|
+
break
|
|
334
|
+
|
|
335
|
+
progress("Querying downstream ecosystem ...")
|
|
336
|
+
relationships = provider.relationships(source, metadata)
|
|
337
|
+
|
|
338
|
+
now = utc_now()
|
|
339
|
+
bundle_id = f"{source.bundle_name}@{commit[:12]}"
|
|
340
|
+
model_type = model_metadata.get("model_type") if model_metadata else None
|
|
341
|
+
aliases = [source.canonical, source.locator]
|
|
342
|
+
if source.url not in aliases:
|
|
343
|
+
aliases.append(source.url)
|
|
344
|
+
|
|
345
|
+
downloader = {
|
|
346
|
+
"tool": "darsay",
|
|
347
|
+
"version": __version__,
|
|
348
|
+
"python": platform.python_version(),
|
|
349
|
+
"platform": platform.platform(),
|
|
350
|
+
"provider": provider.name,
|
|
351
|
+
**provider.downloader_versions(),
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
manifest = {
|
|
355
|
+
"schema_version": SCHEMA_VERSION,
|
|
356
|
+
"artifact_type": repo_type,
|
|
357
|
+
"bundle_id": bundle_id,
|
|
358
|
+
"identity": {
|
|
359
|
+
"model_name": source.name,
|
|
360
|
+
"family": model_type or source.name.split("-")[0].lower(),
|
|
361
|
+
"publisher": source.publisher,
|
|
362
|
+
"version": _guess_version(source.name, model_type),
|
|
363
|
+
"release_date": metadata.get("created_at"),
|
|
364
|
+
"aliases": aliases,
|
|
365
|
+
},
|
|
366
|
+
"source": {
|
|
367
|
+
"origin": provider.name,
|
|
368
|
+
"provider": provider.name,
|
|
369
|
+
"address": source.canonical,
|
|
370
|
+
"repo_id": source.locator,
|
|
371
|
+
"upstream_url": source.url,
|
|
372
|
+
"revision": commit,
|
|
373
|
+
"revision_ref": ledger["revision_ref"],
|
|
374
|
+
"last_modified_upstream": metadata.get("last_modified"),
|
|
375
|
+
"download_timestamp": now,
|
|
376
|
+
"transfer": transfer_summary(ledger),
|
|
377
|
+
"downloader": downloader,
|
|
378
|
+
"mirrors_used": local_mirrors(ledger),
|
|
379
|
+
"signatures": None,
|
|
380
|
+
"access": provider.access_record(metadata),
|
|
381
|
+
"upstream_stats_at_archive": {
|
|
382
|
+
"downloads_last_month": metadata.get("downloads"),
|
|
383
|
+
"likes": metadata.get("likes"),
|
|
384
|
+
},
|
|
385
|
+
"upstream_tags": tags or None,
|
|
386
|
+
},
|
|
387
|
+
"licensing": licensing,
|
|
388
|
+
"inventory": {
|
|
389
|
+
"file_count": len(file_records),
|
|
390
|
+
"total_size_bytes": total_size,
|
|
391
|
+
"bundle_hash": bundle_hash(file_records, root),
|
|
392
|
+
"layout": {
|
|
393
|
+
"payload_root": ARTIFACT_TYPES[repo_type]["payload_root"],
|
|
394
|
+
"mutable_metadata": [*BUNDLE_METADATA_FILES, "LICENSE"],
|
|
395
|
+
},
|
|
396
|
+
"files": file_records,
|
|
397
|
+
},
|
|
398
|
+
**({"dataset_metadata": dataset_metadata} if repo_type == "dataset"
|
|
399
|
+
else {"model_metadata": model_metadata, "runtime": runtime}),
|
|
400
|
+
"validation": {
|
|
401
|
+
"checksum_verification": {
|
|
402
|
+
"at": now,
|
|
403
|
+
"method": "per-file at download completion",
|
|
404
|
+
"status": "pass" if not upstream_mismatches else "fail",
|
|
405
|
+
"files_checked": len(file_records),
|
|
406
|
+
"upstream_mismatches": upstream_mismatches,
|
|
407
|
+
},
|
|
408
|
+
"completeness": completeness,
|
|
409
|
+
"smoke_tests": ({"structure": {"status": "not-run"}} if repo_type == "dataset"
|
|
410
|
+
else {"tokenizer": {"status": "not-run"},
|
|
411
|
+
"inference": {"status": "not-run"}}),
|
|
412
|
+
},
|
|
413
|
+
"relationships": relationships,
|
|
414
|
+
"archive": {
|
|
415
|
+
"date_archived": now,
|
|
416
|
+
"archived_by": None,
|
|
417
|
+
"location": str(bundle_dir.resolve()),
|
|
418
|
+
"host": socket.gethostname(),
|
|
419
|
+
"storage_tier": "local-disk",
|
|
420
|
+
"backup_status": "none",
|
|
421
|
+
"replicas": [],
|
|
422
|
+
"last_integrity_check": now,
|
|
423
|
+
"last_accessed": now,
|
|
424
|
+
},
|
|
425
|
+
"security": {
|
|
426
|
+
"integrity_status": "verified-against-upstream" if not upstream_mismatches else "upstream-mismatch",
|
|
427
|
+
"unexpected_changes": [],
|
|
428
|
+
"trust_level": "unreviewed",
|
|
429
|
+
"reviewed_by": None,
|
|
430
|
+
"review_notes": None,
|
|
431
|
+
},
|
|
432
|
+
"curation": {
|
|
433
|
+
"historical_significance": None,
|
|
434
|
+
"major_capabilities": None,
|
|
435
|
+
"known_limitations": None,
|
|
436
|
+
"successor_models": None,
|
|
437
|
+
"personal_notes": None,
|
|
438
|
+
"curation_file": "curation.md",
|
|
439
|
+
},
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
write_manifest(bundle_dir, manifest)
|
|
443
|
+
_write_curation_template(bundle_dir, manifest)
|
|
444
|
+
|
|
445
|
+
from .readme_gen import write_bundle_readme
|
|
446
|
+
write_bundle_readme(bundle_dir, manifest)
|
|
447
|
+
|
|
448
|
+
from .verify import write_verification_report
|
|
449
|
+
write_verification_report(bundle_dir, manifest["validation"]["checksum_verification"], completeness, first_run=True)
|
|
450
|
+
|
|
451
|
+
progress(f"Archived {bundle_id}: {len(file_records)} files, {total_size / 1024**2:.1f} MiB")
|
|
452
|
+
if upstream_mismatches:
|
|
453
|
+
progress(f"WARNING: {len(upstream_mismatches)} files did not match upstream checksums!")
|
|
454
|
+
return bundle_dir
|
|
455
|
+
|
|
456
|
+
|
|
457
|
+
def _write_curation_template(bundle_dir: Path, manifest: dict) -> None:
|
|
458
|
+
path = bundle_dir / "curation.md"
|
|
459
|
+
if path.exists():
|
|
460
|
+
return
|
|
461
|
+
path.write_text(
|
|
462
|
+
f"""# Curation notes — {manifest['bundle_id']}
|
|
463
|
+
|
|
464
|
+
_This is the curator's file: edit it freely. `darsay regen` folds it into
|
|
465
|
+
README.md; nothing here is machine-generated after this template._
|
|
466
|
+
|
|
467
|
+
## Historical significance
|
|
468
|
+
|
|
469
|
+
_Why this model matters._
|
|
470
|
+
|
|
471
|
+
## Derivation & alignment changes
|
|
472
|
+
|
|
473
|
+
_How this artifact differs from its base — finetune, quantization, merge,
|
|
474
|
+
alignment modifications (e.g. abliteration) — beyond what upstream tags say._
|
|
475
|
+
|
|
476
|
+
## Major capabilities
|
|
477
|
+
|
|
478
|
+
_What it was known to do well._
|
|
479
|
+
|
|
480
|
+
## Known limitations
|
|
481
|
+
|
|
482
|
+
_Where it fell short._
|
|
483
|
+
|
|
484
|
+
## Successor models
|
|
485
|
+
|
|
486
|
+
_What replaced it._
|
|
487
|
+
|
|
488
|
+
## Personal notes
|
|
489
|
+
|
|
490
|
+
_Anything else worth remembering._
|
|
491
|
+
""",
|
|
492
|
+
encoding="utf-8",
|
|
493
|
+
)
|