sci-protocol 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.
- pysci/__init__.py +77 -0
- pysci/__main__.py +8 -0
- pysci/_cli/__init__.py +5 -0
- pysci/_cli/checks.py +173 -0
- pysci/_cli/clients/__init__.py +0 -0
- pysci/_cli/clients/archive.py +456 -0
- pysci/_cli/clients/credentials.py +54 -0
- pysci/_cli/clients/github.py +734 -0
- pysci/_cli/clients/hosting.py +238 -0
- pysci/_cli/clients/http.py +481 -0
- pysci/_cli/clients/huggingface.py +492 -0
- pysci/_cli/clients/index.py +435 -0
- pysci/_cli/clients/tsa.py +223 -0
- pysci/_cli/commands/__init__.py +44 -0
- pysci/_cli/commands/check.py +115 -0
- pysci/_cli/commands/claim.py +179 -0
- pysci/_cli/commands/common.py +91 -0
- pysci/_cli/commands/folders.py +83 -0
- pysci/_cli/commands/index.py +51 -0
- pysci/_cli/commands/keygen.py +58 -0
- pysci/_cli/commands/license.py +75 -0
- pysci/_cli/commands/protocol.py +72 -0
- pysci/_cli/commands/publish.py +282 -0
- pysci/_cli/commands/pull.py +337 -0
- pysci/_cli/commands/read.py +364 -0
- pysci/_cli/commands/revoke.py +207 -0
- pysci/_cli/commands/search.py +400 -0
- pysci/_cli/commands/sign.py +284 -0
- pysci/_cli/commands/submit.py +172 -0
- pysci/_cli/commands/timestamp.py +196 -0
- pysci/_cli/commands/verify.py +163 -0
- pysci/_cli/config.py +233 -0
- pysci/_cli/context.py +79 -0
- pysci/_cli/destinations.py +163 -0
- pysci/_cli/errors.py +168 -0
- pysci/_cli/main.py +229 -0
- pysci/_cli/output.py +80 -0
- pysci/_cli/retrieval.py +51 -0
- pysci/_cli/spec.py +113 -0
- pysci/_cli/storage/__init__.py +0 -0
- pysci/_cli/storage/catalog.py +371 -0
- pysci/_cli/storage/files.py +252 -0
- pysci/_cli/storage/inputs.py +205 -0
- pysci/_cli/storage/keys.py +132 -0
- pysci/_cli/storage/local_publish.py +88 -0
- pysci/accounts.py +135 -0
- pysci/baseline.py +142 -0
- pysci/bundle.py +878 -0
- pysci/citations.py +233 -0
- pysci/data/freetsa-cacert.pem +45 -0
- pysci/data/sci-baseline-LICENSE.txt +457 -0
- pysci/data/sci-protocol.md +613 -0
- pysci/der.py +146 -0
- pysci/discovery.py +461 -0
- pysci/draft.py +374 -0
- pysci/ed25519.py +153 -0
- pysci/errors.py +204 -0
- pysci/ids.py +61 -0
- pysci/jsonstrict.py +225 -0
- pysci/keys.py +247 -0
- pysci/manifest.py +302 -0
- pysci/metadata.py +406 -0
- pysci/paths.py +136 -0
- pysci/png.py +217 -0
- pysci/py.typed +0 -0
- pysci/records.py +384 -0
- pysci/rfc3339.py +66 -0
- pysci/schemas.py +75 -0
- pysci/source.py +166 -0
- pysci/specification.py +58 -0
- pysci/submission.py +225 -0
- pysci/templates.py +129 -0
- pysci/text.py +226 -0
- pysci/timestamps.py +867 -0
- pysci/zipformat.py +400 -0
- sci_protocol-0.2.0.dist-info/METADATA +112 -0
- sci_protocol-0.2.0.dist-info/RECORD +80 -0
- sci_protocol-0.2.0.dist-info/WHEEL +4 -0
- sci_protocol-0.2.0.dist-info/entry_points.txt +2 -0
- sci_protocol-0.2.0.dist-info/licenses/LICENSE +202 -0
|
@@ -0,0 +1,492 @@
|
|
|
1
|
+
"""Hugging Face dataset archive publishing through the Hub HTTP API and Git LFS."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import base64
|
|
6
|
+
import functools
|
|
7
|
+
import hashlib
|
|
8
|
+
import json
|
|
9
|
+
import urllib.parse
|
|
10
|
+
from dataclasses import dataclass
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
from typing import Any
|
|
13
|
+
|
|
14
|
+
from ..errors import CliError, Exit
|
|
15
|
+
from .hosting import (
|
|
16
|
+
ARCHIVE_PATH,
|
|
17
|
+
LIST_PATH,
|
|
18
|
+
ItemReport,
|
|
19
|
+
Prepared,
|
|
20
|
+
RemoteFile,
|
|
21
|
+
check_descriptor,
|
|
22
|
+
confirm_public,
|
|
23
|
+
descriptor_bytes,
|
|
24
|
+
git_blob_sha1,
|
|
25
|
+
merged_list,
|
|
26
|
+
plan_reader_files,
|
|
27
|
+
)
|
|
28
|
+
from .http import Deadline, HttpClient, Response
|
|
29
|
+
|
|
30
|
+
HUB = "https://huggingface.co"
|
|
31
|
+
JSON_LIMIT = 32 << 20
|
|
32
|
+
LFS_TYPE = "application/vnd.git-lfs+json"
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def descriptor_for(owner: str, repo: str) -> dict[str, str]:
|
|
36
|
+
base = f"{HUB}/datasets/{owner}/{repo}/resolve/main"
|
|
37
|
+
return {
|
|
38
|
+
"schema": "sci-archive/0.1",
|
|
39
|
+
"bundle_url": f"{base}/bundles/{{id2}}/{{id}}.zip",
|
|
40
|
+
"entry_url": f"{base}/sci/publications/{{id2}}/{{id}}/{{+path}}",
|
|
41
|
+
"list_url": f"{base}/sci/publications.txt",
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def bundle_path(digest: str) -> str:
|
|
46
|
+
return f"bundles/{digest[:2]}/{digest}.zip"
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
@dataclass(frozen=True, slots=True)
|
|
50
|
+
class LfsFile:
|
|
51
|
+
path: str
|
|
52
|
+
sha256: str
|
|
53
|
+
size: int
|
|
54
|
+
source: Path | bytes
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def _hub_error(response: Response, what: str) -> CliError:
|
|
58
|
+
message = ""
|
|
59
|
+
try:
|
|
60
|
+
record = json.loads(response.body)
|
|
61
|
+
if isinstance(record, dict) and isinstance(record.get("error"), str):
|
|
62
|
+
message = ": " + record["error"][:300]
|
|
63
|
+
except ValueError:
|
|
64
|
+
pass
|
|
65
|
+
temporary = response.status >= 500 or response.status == 429
|
|
66
|
+
if response.status == 401:
|
|
67
|
+
return CliError("unauthorized", f"Hugging Face {what}: HTTP 401{message}", Exit.USAGE)
|
|
68
|
+
if response.status == 403:
|
|
69
|
+
return CliError("forbidden", f"Hugging Face {what}: HTTP 403{message}", Exit.REFUSED)
|
|
70
|
+
return CliError(
|
|
71
|
+
"temporary_failure" if temporary else "huggingface_error",
|
|
72
|
+
f"Hugging Face {what} returned HTTP {response.status}{message}",
|
|
73
|
+
Exit.INCOMPLETE if temporary else Exit.INVALID,
|
|
74
|
+
retryable=temporary,
|
|
75
|
+
)
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
class _NotFastForward(Exception):
|
|
79
|
+
pass
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
class HuggingFaceArchive:
|
|
83
|
+
def __init__(self, http: HttpClient, owner: str, repo: str, token: str) -> None:
|
|
84
|
+
self.http = http
|
|
85
|
+
self.owner = owner
|
|
86
|
+
self.repo = repo
|
|
87
|
+
self.repo_id = f"{owner}/{repo}"
|
|
88
|
+
self.token = token
|
|
89
|
+
self.descriptor = descriptor_for(owner, repo)
|
|
90
|
+
self.descriptor_url = f"{HUB}/datasets/{self.repo_id}/resolve/main/{ARCHIVE_PATH}"
|
|
91
|
+
self.api = f"{HUB}/api/datasets/{self.repo_id}"
|
|
92
|
+
|
|
93
|
+
def _call(
|
|
94
|
+
self,
|
|
95
|
+
method: str,
|
|
96
|
+
url: str,
|
|
97
|
+
deadline: Deadline,
|
|
98
|
+
*,
|
|
99
|
+
document: object = None,
|
|
100
|
+
body: Any = None,
|
|
101
|
+
headers: dict[str, str] | None = None,
|
|
102
|
+
max_bytes: int = JSON_LIMIT,
|
|
103
|
+
auth: bool = True,
|
|
104
|
+
) -> Response:
|
|
105
|
+
all_headers: dict[str, str] = {"Accept": "application/json"}
|
|
106
|
+
if document is not None:
|
|
107
|
+
body = json.dumps(document).encode("utf-8")
|
|
108
|
+
all_headers["Content-Type"] = "application/json"
|
|
109
|
+
all_headers.update(headers or {})
|
|
110
|
+
return self.http.request(
|
|
111
|
+
method,
|
|
112
|
+
url,
|
|
113
|
+
deadline=deadline,
|
|
114
|
+
headers=all_headers,
|
|
115
|
+
body=body,
|
|
116
|
+
auth=f"Bearer {self.token}" if auth else None,
|
|
117
|
+
max_bytes=max_bytes,
|
|
118
|
+
retry=method in ("GET", "PUT"),
|
|
119
|
+
)
|
|
120
|
+
|
|
121
|
+
def _json(self, response: Response, what: str, ok: tuple[int, ...] = (200,)) -> Any:
|
|
122
|
+
if response.status not in ok:
|
|
123
|
+
raise _hub_error(response, what)
|
|
124
|
+
return response.json(f"Hugging Face {what}")
|
|
125
|
+
|
|
126
|
+
# ------------------------------------------------------------------ repository
|
|
127
|
+
|
|
128
|
+
def ensure_repository(self, create: bool, deadline: Deadline) -> dict[str, Any]:
|
|
129
|
+
response = self._call("GET", self.api, deadline)
|
|
130
|
+
created = False
|
|
131
|
+
if response.status in (401, 404):
|
|
132
|
+
if not create:
|
|
133
|
+
raise CliError(
|
|
134
|
+
"repository_missing",
|
|
135
|
+
f"Hugging Face dataset {self.repo_id} does not exist (or is not accessible); "
|
|
136
|
+
"pass --create",
|
|
137
|
+
Exit.USAGE,
|
|
138
|
+
)
|
|
139
|
+
who = self._json(self._call("GET", f"{HUB}/api/whoami-v2", deadline), "whoami")
|
|
140
|
+
name = who.get("name") if isinstance(who, dict) else None
|
|
141
|
+
body: dict[str, Any] = {"type": "dataset", "name": self.repo, "private": False}
|
|
142
|
+
if not isinstance(name, str) or name.lower() != self.owner.lower():
|
|
143
|
+
body["organization"] = self.owner
|
|
144
|
+
self._json(
|
|
145
|
+
self._call("POST", f"{HUB}/api/repos/create", deadline, document=body),
|
|
146
|
+
"repository creation",
|
|
147
|
+
(200, 201),
|
|
148
|
+
)
|
|
149
|
+
created = True
|
|
150
|
+
response = self._call("GET", self.api, deadline)
|
|
151
|
+
info = self._json(response, "repository lookup")
|
|
152
|
+
if not isinstance(info, dict):
|
|
153
|
+
raise CliError("invalid_response", "repository info is not an object", Exit.INVALID)
|
|
154
|
+
if info.get("private") is not False:
|
|
155
|
+
raise CliError(
|
|
156
|
+
"repository_not_public",
|
|
157
|
+
f"Hugging Face dataset {self.repo_id} is not public; its visibility is left "
|
|
158
|
+
"unchanged",
|
|
159
|
+
Exit.USAGE,
|
|
160
|
+
)
|
|
161
|
+
if info.get("gated") not in (None, False):
|
|
162
|
+
raise CliError(
|
|
163
|
+
"repository_not_public",
|
|
164
|
+
f"Hugging Face dataset {self.repo_id} is gated; readers need open access",
|
|
165
|
+
Exit.USAGE,
|
|
166
|
+
)
|
|
167
|
+
info["_created"] = created
|
|
168
|
+
return info
|
|
169
|
+
|
|
170
|
+
def head(self, deadline: Deadline) -> str | None:
|
|
171
|
+
response = self._call("GET", f"{self.api}/revision/main", deadline)
|
|
172
|
+
if response.status == 404:
|
|
173
|
+
return None
|
|
174
|
+
record = self._json(response, "revision lookup")
|
|
175
|
+
sha = record.get("sha") if isinstance(record, dict) else None
|
|
176
|
+
return sha if isinstance(sha, str) else None
|
|
177
|
+
|
|
178
|
+
def paths_info(self, paths: list[str], deadline: Deadline) -> dict[str, RemoteFile]:
|
|
179
|
+
found: dict[str, RemoteFile] = {}
|
|
180
|
+
for start in range(0, len(paths), 50):
|
|
181
|
+
chunk = paths[start : start + 50]
|
|
182
|
+
form = urllib.parse.urlencode([*(("paths", p) for p in chunk), ("expand", "true")])
|
|
183
|
+
response = self._call(
|
|
184
|
+
"POST",
|
|
185
|
+
f"{self.api}/paths-info/main",
|
|
186
|
+
deadline,
|
|
187
|
+
body=form.encode("ascii"),
|
|
188
|
+
headers={"Content-Type": "application/x-www-form-urlencoded"},
|
|
189
|
+
)
|
|
190
|
+
if response.status == 404:
|
|
191
|
+
return found
|
|
192
|
+
records = self._json(response, "paths-info")
|
|
193
|
+
for record in records if isinstance(records, list) else []:
|
|
194
|
+
if not isinstance(record, dict) or record.get("type") != "file":
|
|
195
|
+
continue
|
|
196
|
+
lfs = record.get("lfs")
|
|
197
|
+
sha256 = lfs.get("oid") if isinstance(lfs, dict) else None
|
|
198
|
+
size = record.get("size")
|
|
199
|
+
found[str(record.get("path"))] = RemoteFile(
|
|
200
|
+
size=size if isinstance(size, int) else None,
|
|
201
|
+
git_sha1=None if sha256 else str(record.get("oid")),
|
|
202
|
+
sha256=str(sha256) if sha256 else None,
|
|
203
|
+
)
|
|
204
|
+
return found
|
|
205
|
+
|
|
206
|
+
def read(self, path: str, deadline: Deadline) -> bytes | None:
|
|
207
|
+
response = self._call(
|
|
208
|
+
"GET",
|
|
209
|
+
f"{HUB}/datasets/{self.repo_id}/resolve/main/{urllib.parse.quote(path)}",
|
|
210
|
+
deadline,
|
|
211
|
+
max_bytes=64 << 20,
|
|
212
|
+
)
|
|
213
|
+
if response.status == 404:
|
|
214
|
+
return None
|
|
215
|
+
if response.status != 200:
|
|
216
|
+
raise _hub_error(response, f"download of {path}")
|
|
217
|
+
return response.body
|
|
218
|
+
|
|
219
|
+
def upload_modes(self, files: dict[str, bytes], deadline: Deadline) -> dict[str, str]:
|
|
220
|
+
if not files:
|
|
221
|
+
return {}
|
|
222
|
+
document = {
|
|
223
|
+
"files": [
|
|
224
|
+
{"path": p, "size": len(d), "sample": base64.b64encode(d[:512]).decode("ascii")}
|
|
225
|
+
for p, d in files.items()
|
|
226
|
+
]
|
|
227
|
+
}
|
|
228
|
+
record = self._json(
|
|
229
|
+
self._call("POST", f"{self.api}/preupload/main", deadline, document=document),
|
|
230
|
+
"preupload",
|
|
231
|
+
)
|
|
232
|
+
modes: dict[str, str] = {}
|
|
233
|
+
for entry in record.get("files", []) if isinstance(record, dict) else []:
|
|
234
|
+
if isinstance(entry, dict):
|
|
235
|
+
modes[str(entry.get("path"))] = str(entry.get("uploadMode", "regular"))
|
|
236
|
+
return modes
|
|
237
|
+
|
|
238
|
+
def lfs_upload(self, item: LfsFile, deadline: Deadline) -> str:
|
|
239
|
+
"""Upload one LFS object through the batch API; return uploaded/present."""
|
|
240
|
+
batch = self._json(
|
|
241
|
+
self._call(
|
|
242
|
+
"POST",
|
|
243
|
+
f"{HUB}/datasets/{self.repo_id}.git/info/lfs/objects/batch",
|
|
244
|
+
deadline,
|
|
245
|
+
document={
|
|
246
|
+
"operation": "upload",
|
|
247
|
+
"transfers": ["basic"],
|
|
248
|
+
"objects": [{"oid": item.sha256, "size": item.size}],
|
|
249
|
+
"hash_algo": "sha256",
|
|
250
|
+
"ref": {"name": "main"},
|
|
251
|
+
},
|
|
252
|
+
headers={"Accept": LFS_TYPE, "Content-Type": LFS_TYPE},
|
|
253
|
+
),
|
|
254
|
+
"LFS batch",
|
|
255
|
+
)
|
|
256
|
+
objects = batch.get("objects") if isinstance(batch, dict) else None
|
|
257
|
+
if not isinstance(objects, list) or not objects or not isinstance(objects[0], dict):
|
|
258
|
+
raise CliError("invalid_response", "LFS batch response has no objects", Exit.INVALID)
|
|
259
|
+
obj = objects[0]
|
|
260
|
+
if "error" in obj:
|
|
261
|
+
raise CliError(
|
|
262
|
+
"lfs_error",
|
|
263
|
+
f"LFS refused {item.path}: {obj['error']}",
|
|
264
|
+
Exit.INCOMPLETE,
|
|
265
|
+
retryable=True,
|
|
266
|
+
)
|
|
267
|
+
actions = obj.get("actions") or {}
|
|
268
|
+
upload = actions.get("upload") if isinstance(actions, dict) else None
|
|
269
|
+
if upload is None:
|
|
270
|
+
return "present"
|
|
271
|
+
header = dict(upload.get("header") or {})
|
|
272
|
+
if "chunk_size" in header:
|
|
273
|
+
raise CliError(
|
|
274
|
+
"unsupported_capability",
|
|
275
|
+
"the Hub requested a multipart LFS upload, which this CLI does not support",
|
|
276
|
+
Exit.UNSUPPORTED,
|
|
277
|
+
)
|
|
278
|
+
response = self.http.request(
|
|
279
|
+
"PUT",
|
|
280
|
+
str(upload["href"]),
|
|
281
|
+
deadline=deadline,
|
|
282
|
+
headers={str(k): str(v) for k, v in header.items()},
|
|
283
|
+
body=item.source,
|
|
284
|
+
)
|
|
285
|
+
if response.status not in (200, 201):
|
|
286
|
+
raise _hub_error(response, f"LFS upload of {item.path}")
|
|
287
|
+
verify = actions.get("verify")
|
|
288
|
+
if isinstance(verify, dict):
|
|
289
|
+
response = self.http.request(
|
|
290
|
+
"POST",
|
|
291
|
+
str(verify["href"]),
|
|
292
|
+
deadline=deadline,
|
|
293
|
+
headers={
|
|
294
|
+
"Accept": LFS_TYPE,
|
|
295
|
+
"Content-Type": LFS_TYPE,
|
|
296
|
+
**{str(k): str(v) for k, v in (verify.get("header") or {}).items()},
|
|
297
|
+
},
|
|
298
|
+
body=json.dumps({"oid": item.sha256, "size": item.size}).encode("ascii"),
|
|
299
|
+
auth=f"Bearer {self.token}",
|
|
300
|
+
)
|
|
301
|
+
if response.status != 200:
|
|
302
|
+
raise _hub_error(response, f"LFS verification of {item.path}")
|
|
303
|
+
return "uploaded"
|
|
304
|
+
|
|
305
|
+
def commit(
|
|
306
|
+
self,
|
|
307
|
+
parent: str | None,
|
|
308
|
+
regular: dict[str, bytes],
|
|
309
|
+
lfs: list[LfsFile],
|
|
310
|
+
message: str,
|
|
311
|
+
deadline: Deadline,
|
|
312
|
+
) -> str:
|
|
313
|
+
header: dict[str, Any] = {"summary": message, "description": ""}
|
|
314
|
+
if parent is not None:
|
|
315
|
+
header["parentCommit"] = parent
|
|
316
|
+
lines = [json.dumps({"key": "header", "value": header})]
|
|
317
|
+
for path, data in sorted(regular.items()):
|
|
318
|
+
lines.append(
|
|
319
|
+
json.dumps(
|
|
320
|
+
{
|
|
321
|
+
"key": "file",
|
|
322
|
+
"value": {
|
|
323
|
+
"path": path,
|
|
324
|
+
"content": base64.b64encode(data).decode("ascii"),
|
|
325
|
+
"encoding": "base64",
|
|
326
|
+
},
|
|
327
|
+
}
|
|
328
|
+
)
|
|
329
|
+
)
|
|
330
|
+
for item in lfs:
|
|
331
|
+
lines.append(
|
|
332
|
+
json.dumps(
|
|
333
|
+
{
|
|
334
|
+
"key": "lfsFile",
|
|
335
|
+
"value": {
|
|
336
|
+
"path": item.path,
|
|
337
|
+
"algo": "sha256",
|
|
338
|
+
"oid": item.sha256,
|
|
339
|
+
"size": item.size,
|
|
340
|
+
},
|
|
341
|
+
}
|
|
342
|
+
)
|
|
343
|
+
)
|
|
344
|
+
response = self._call(
|
|
345
|
+
"POST",
|
|
346
|
+
f"{self.api}/commit/main",
|
|
347
|
+
deadline,
|
|
348
|
+
body=("\n".join(lines) + "\n").encode("utf-8"),
|
|
349
|
+
headers={"Content-Type": "application/x-ndjson"},
|
|
350
|
+
)
|
|
351
|
+
if response.status in (409, 412):
|
|
352
|
+
raise _NotFastForward()
|
|
353
|
+
record = self._json(response, "commit", (200, 201))
|
|
354
|
+
oid = record.get("commitOid") if isinstance(record, dict) else None
|
|
355
|
+
return str(oid) if oid else ""
|
|
356
|
+
|
|
357
|
+
# ------------------------------------------------------------------ workflow
|
|
358
|
+
|
|
359
|
+
def publish(
|
|
360
|
+
self, pubs: list[Prepared], *, create: bool, timeout: float
|
|
361
|
+
) -> tuple[dict[str, Any], list[ItemReport]]:
|
|
362
|
+
deadline = Deadline(timeout)
|
|
363
|
+
repo = self.ensure_repository(create, deadline)
|
|
364
|
+
head = self.head(deadline)
|
|
365
|
+
managed = self.paths_info([ARCHIVE_PATH, LIST_PATH], deadline)
|
|
366
|
+
archive_existing = self.read(ARCHIVE_PATH, deadline) if ARCHIVE_PATH in managed else None
|
|
367
|
+
write_archive = check_descriptor(archive_existing, self.descriptor)
|
|
368
|
+
list_existing = self.read(LIST_PATH, deadline) if LIST_PATH in managed else None
|
|
369
|
+
merged_list(list_existing, [])
|
|
370
|
+
reports: list[ItemReport] = []
|
|
371
|
+
regular: dict[str, bytes] = {}
|
|
372
|
+
lfs: list[LfsFile] = []
|
|
373
|
+
completed: list[str] = []
|
|
374
|
+
for pub in pubs:
|
|
375
|
+
report = ItemReport(pub.item.publication_id)
|
|
376
|
+
reports.append(report)
|
|
377
|
+
item_deadline = Deadline(timeout)
|
|
378
|
+
try:
|
|
379
|
+
paths = [*pub.remote_files(), bundle_path(pub.digest)]
|
|
380
|
+
remote = self.paths_info(paths, item_deadline)
|
|
381
|
+
planned = plan_reader_files(
|
|
382
|
+
pub, remote, functools.partial(self.read, deadline=item_deadline), report
|
|
383
|
+
)
|
|
384
|
+
if report.status == "failed":
|
|
385
|
+
continue
|
|
386
|
+
bundle_remote = remote.get(bundle_path(pub.digest))
|
|
387
|
+
item_lfs: list[LfsFile] = []
|
|
388
|
+
if bundle_remote is not None:
|
|
389
|
+
if bundle_remote.sha256 != pub.zip_sha256:
|
|
390
|
+
raise CliError(
|
|
391
|
+
"destination_conflict",
|
|
392
|
+
f"{bundle_path(pub.digest)} exists with different bytes",
|
|
393
|
+
Exit.INVALID,
|
|
394
|
+
)
|
|
395
|
+
report.bundle = "reused"
|
|
396
|
+
else:
|
|
397
|
+
item_lfs.append(
|
|
398
|
+
LfsFile(bundle_path(pub.digest), pub.zip_sha256, pub.zip_size, pub.zip_path)
|
|
399
|
+
)
|
|
400
|
+
modes = self.upload_modes(planned, item_deadline)
|
|
401
|
+
item_regular: dict[str, bytes] = {}
|
|
402
|
+
for path, data in planned.items():
|
|
403
|
+
if modes.get(path) == "lfs":
|
|
404
|
+
item_lfs.append(
|
|
405
|
+
LfsFile(path, hashlib.sha256(data).hexdigest(), len(data), data)
|
|
406
|
+
)
|
|
407
|
+
else:
|
|
408
|
+
item_regular[path] = data
|
|
409
|
+
for item in item_lfs:
|
|
410
|
+
state = self.lfs_upload(item, item_deadline)
|
|
411
|
+
if item.path == bundle_path(pub.digest):
|
|
412
|
+
report.bundle = "uploaded" if state == "uploaded" else "reused"
|
|
413
|
+
except CliError as exc:
|
|
414
|
+
report.fail(exc.records[0], exc.exit_code)
|
|
415
|
+
continue
|
|
416
|
+
regular.update(item_regular)
|
|
417
|
+
lfs.extend(item_lfs)
|
|
418
|
+
report.uploaded = sorted(planned)
|
|
419
|
+
completed.append(pub.digest)
|
|
420
|
+
report.status = "pending_commit"
|
|
421
|
+
commit_oid = None
|
|
422
|
+
for _attempt in range(4):
|
|
423
|
+
commit_deadline = Deadline(timeout)
|
|
424
|
+
files = dict(regular)
|
|
425
|
+
current = self.paths_info([ARCHIVE_PATH, LIST_PATH], commit_deadline)
|
|
426
|
+
if write_archive and ARCHIVE_PATH not in current:
|
|
427
|
+
files[ARCHIVE_PATH] = descriptor_bytes(self.descriptor)
|
|
428
|
+
existing_list = self.read(LIST_PATH, commit_deadline) if LIST_PATH in current else None
|
|
429
|
+
new_list = merged_list(existing_list, completed)
|
|
430
|
+
if new_list != existing_list:
|
|
431
|
+
files[LIST_PATH] = new_list
|
|
432
|
+
if not files and not lfs:
|
|
433
|
+
break
|
|
434
|
+
try:
|
|
435
|
+
commit_oid = self.commit(
|
|
436
|
+
head,
|
|
437
|
+
files,
|
|
438
|
+
lfs,
|
|
439
|
+
f"Publish {len(completed)} SCI publication(s)",
|
|
440
|
+
commit_deadline,
|
|
441
|
+
)
|
|
442
|
+
break
|
|
443
|
+
except _NotFastForward:
|
|
444
|
+
head = self.head(commit_deadline)
|
|
445
|
+
else:
|
|
446
|
+
raise CliError(
|
|
447
|
+
"concurrent_update",
|
|
448
|
+
"main kept moving during publishing; retry",
|
|
449
|
+
Exit.INCOMPLETE,
|
|
450
|
+
retryable=True,
|
|
451
|
+
)
|
|
452
|
+
for report in reports:
|
|
453
|
+
if report.status == "pending_commit":
|
|
454
|
+
report.status = "hosted"
|
|
455
|
+
checks: list[tuple[str, str, bytes | None]] = [
|
|
456
|
+
("descriptor", self.descriptor_url, descriptor_bytes(self.descriptor))
|
|
457
|
+
]
|
|
458
|
+
for pub, report in zip(pubs, reports, strict=True):
|
|
459
|
+
if report.status != "hosted":
|
|
460
|
+
continue
|
|
461
|
+
entry = (
|
|
462
|
+
self.descriptor["entry_url"]
|
|
463
|
+
.replace("{id2}", pub.digest[:2])
|
|
464
|
+
.replace("{id}", pub.digest)
|
|
465
|
+
)
|
|
466
|
+
checks.append(
|
|
467
|
+
("manifest", entry.replace("{+path}", "manifest.json"), pub.reader["manifest.json"])
|
|
468
|
+
)
|
|
469
|
+
checks.append(
|
|
470
|
+
(
|
|
471
|
+
"bundle",
|
|
472
|
+
self.descriptor["bundle_url"]
|
|
473
|
+
.replace("{id2}", pub.digest[:2])
|
|
474
|
+
.replace("{id}", pub.digest),
|
|
475
|
+
None,
|
|
476
|
+
)
|
|
477
|
+
)
|
|
478
|
+
availability = confirm_public(self.http, checks, Deadline(timeout))
|
|
479
|
+
return (
|
|
480
|
+
{
|
|
481
|
+
"repository": f"{HUB}/datasets/{self.repo_id}",
|
|
482
|
+
"created": bool(repo.get("_created")),
|
|
483
|
+
"descriptor_url": self.descriptor_url,
|
|
484
|
+
"descriptor": self.descriptor,
|
|
485
|
+
"commit": commit_oid,
|
|
486
|
+
"availability": availability,
|
|
487
|
+
},
|
|
488
|
+
reports,
|
|
489
|
+
)
|
|
490
|
+
|
|
491
|
+
|
|
492
|
+
__all__ = ["HuggingFaceArchive", "descriptor_for", "git_blob_sha1"]
|