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
pysci/zipformat.py
ADDED
|
@@ -0,0 +1,400 @@
|
|
|
1
|
+
"""The canonical single-file bundle ZIP (protocol §2.2).
|
|
2
|
+
|
|
3
|
+
:func:`parse_zip_layout` checks every structural rule of §2.2 except CRC-32
|
|
4
|
+
values and entry ordering against the manifest (those need entry data and
|
|
5
|
+
the manifest; see :mod:`pysci.bundle`). It reads only the end record, the
|
|
6
|
+
central directory, and each local header, checking bounds before every read,
|
|
7
|
+
and verifies that headers and data account for every byte.
|
|
8
|
+
|
|
9
|
+
:func:`write_canonical_zip` produces the exact bytes defined by §2.2 and
|
|
10
|
+
:func:`canonical_zip_size` evaluates the §2.1 size formula.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
import struct
|
|
16
|
+
import zlib
|
|
17
|
+
from collections.abc import Iterable, Sequence
|
|
18
|
+
from dataclasses import dataclass
|
|
19
|
+
from typing import Final, Protocol
|
|
20
|
+
|
|
21
|
+
from .errors import Incomplete, Invalid, Issue, Outcome
|
|
22
|
+
from .source import ByteSource, iter_chunks
|
|
23
|
+
|
|
24
|
+
__all__ = [
|
|
25
|
+
"BASELINE_ZIP_LIMIT",
|
|
26
|
+
"SupportsWrite",
|
|
27
|
+
"ZipEntry",
|
|
28
|
+
"canonical_zip_size",
|
|
29
|
+
"crc32_of",
|
|
30
|
+
"parse_zip_layout",
|
|
31
|
+
"write_canonical_zip",
|
|
32
|
+
]
|
|
33
|
+
|
|
34
|
+
BASELINE_ZIP_LIMIT: Final = 64 * 1024 * 1024
|
|
35
|
+
|
|
36
|
+
_LOCAL_SIG: Final = 0x04034B50
|
|
37
|
+
_CENTRAL_SIG: Final = 0x02014B50
|
|
38
|
+
_EOCD_SIG: Final = 0x06054B50
|
|
39
|
+
_LOCAL: Final = struct.Struct("<IHHHHHIIIHH")
|
|
40
|
+
_CENTRAL: Final = struct.Struct("<IHHHHHHIIIHHHHHII")
|
|
41
|
+
_EOCD: Final = struct.Struct("<IHHHHIIH")
|
|
42
|
+
_VERSION: Final = 10 # version 1.0
|
|
43
|
+
_MADE_BY: Final = 0x000A # MS-DOS host, version 1.0
|
|
44
|
+
_DOS_DATE: Final = 0x0021 # 1980-01-01
|
|
45
|
+
_DOS_TIME: Final = 0x0000
|
|
46
|
+
_MAX_U16: Final = 0xFFFF
|
|
47
|
+
_MAX_U32: Final = 0xFFFFFFFF
|
|
48
|
+
ENTRY_OVERHEAD: Final = _LOCAL.size + _CENTRAL.size # 76 bytes, plus 2 * path length
|
|
49
|
+
EOCD_SIZE: Final = _EOCD.size # 22
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
@dataclass(frozen=True, slots=True)
|
|
53
|
+
class ZipEntry:
|
|
54
|
+
"""Location of one stored entry inside a structurally valid ZIP."""
|
|
55
|
+
|
|
56
|
+
name: bytes
|
|
57
|
+
local_offset: int
|
|
58
|
+
data_offset: int
|
|
59
|
+
size: int
|
|
60
|
+
crc32: int
|
|
61
|
+
|
|
62
|
+
@property
|
|
63
|
+
def path(self) -> str:
|
|
64
|
+
"""The entry name decoded as ASCII (validated by the parser)."""
|
|
65
|
+
return self.name.decode("ascii")
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def _invalid(code: str, requirement: str, observed: object = None) -> Invalid:
|
|
69
|
+
return Invalid(
|
|
70
|
+
Issue(
|
|
71
|
+
code,
|
|
72
|
+
requirement,
|
|
73
|
+
section="2.2",
|
|
74
|
+
file="bundle.zip",
|
|
75
|
+
observed=None if observed is None else str(observed),
|
|
76
|
+
)
|
|
77
|
+
)
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def parse_zip_layout(
|
|
81
|
+
source: ByteSource,
|
|
82
|
+
*,
|
|
83
|
+
max_entries: int = _MAX_U16 - 1,
|
|
84
|
+
max_central_bytes: int = 64 * 1024 * 1024,
|
|
85
|
+
) -> list[ZipEntry]:
|
|
86
|
+
"""Parse and check the §2.2 layout without reading entry data.
|
|
87
|
+
|
|
88
|
+
Raises:
|
|
89
|
+
Invalid: any layout, field, or consistency violation.
|
|
90
|
+
Incomplete: the central directory exceeds a local limit.
|
|
91
|
+
"""
|
|
92
|
+
size = source.size
|
|
93
|
+
if size < EOCD_SIZE:
|
|
94
|
+
raise _invalid(
|
|
95
|
+
"zip_truncated",
|
|
96
|
+
"a ZIP ends with a 22-byte end-of-central-directory record",
|
|
97
|
+
f"{size} bytes",
|
|
98
|
+
)
|
|
99
|
+
eocd_offset = size - EOCD_SIZE
|
|
100
|
+
sig, disk, cd_disk, count_disk, count_total, cd_size, cd_offset, comment_len = _EOCD.unpack(
|
|
101
|
+
source.read_at(eocd_offset, EOCD_SIZE)
|
|
102
|
+
)
|
|
103
|
+
if sig != _EOCD_SIG:
|
|
104
|
+
raise _invalid(
|
|
105
|
+
"zip_end_record",
|
|
106
|
+
"one end-of-central-directory record must end at the last byte, with an empty comment",
|
|
107
|
+
)
|
|
108
|
+
if comment_len != 0:
|
|
109
|
+
raise _invalid("zip_comment", "the archive comment must be empty", comment_len)
|
|
110
|
+
if disk != 0 or cd_disk != 0:
|
|
111
|
+
raise _invalid("zip_disk_number", "all disk-number fields must be zero")
|
|
112
|
+
if _MAX_U16 in (count_disk, count_total) or _MAX_U32 in (cd_size, cd_offset):
|
|
113
|
+
raise _invalid("zip64", "ZIP64 records and sentinel values are prohibited")
|
|
114
|
+
if count_disk != count_total:
|
|
115
|
+
raise _invalid(
|
|
116
|
+
"zip_entry_count",
|
|
117
|
+
"both entry-count fields must give the actual count",
|
|
118
|
+
f"{count_disk} != {count_total}",
|
|
119
|
+
)
|
|
120
|
+
if cd_offset > eocd_offset or cd_size != eocd_offset - cd_offset:
|
|
121
|
+
raise _invalid(
|
|
122
|
+
"zip_central_extent",
|
|
123
|
+
"the central directory must lie between the last "
|
|
124
|
+
"entry data and the end record, with its stated offset and length",
|
|
125
|
+
)
|
|
126
|
+
if count_total > max_entries:
|
|
127
|
+
raise Incomplete(
|
|
128
|
+
Issue(
|
|
129
|
+
"zip_entry_limit",
|
|
130
|
+
"ZIP entry count exceeds the local limit",
|
|
131
|
+
section="6",
|
|
132
|
+
file="bundle.zip",
|
|
133
|
+
observed=str(count_total),
|
|
134
|
+
outcome=Outcome.INCOMPLETE,
|
|
135
|
+
)
|
|
136
|
+
)
|
|
137
|
+
if cd_size > max_central_bytes:
|
|
138
|
+
raise Incomplete(
|
|
139
|
+
Issue(
|
|
140
|
+
"zip_central_limit",
|
|
141
|
+
"central directory exceeds the local limit",
|
|
142
|
+
section="6",
|
|
143
|
+
file="bundle.zip",
|
|
144
|
+
observed=str(cd_size),
|
|
145
|
+
outcome=Outcome.INCOMPLETE,
|
|
146
|
+
)
|
|
147
|
+
)
|
|
148
|
+
central = source.read_at(cd_offset, cd_size)
|
|
149
|
+
records = _parse_central(central, count_total)
|
|
150
|
+
return _check_locals(source, records, cd_offset)
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
@dataclass(frozen=True, slots=True)
|
|
154
|
+
class _Central:
|
|
155
|
+
name: bytes
|
|
156
|
+
crc32: int
|
|
157
|
+
size: int
|
|
158
|
+
offset: int
|
|
159
|
+
fields: tuple[int, ...] # version needed .. uncompressed size, shared with local header
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
def _parse_central(central: bytes, count: int) -> list[_Central]:
|
|
163
|
+
records: list[_Central] = []
|
|
164
|
+
position = 0
|
|
165
|
+
for index in range(count):
|
|
166
|
+
if len(central) - position < _CENTRAL.size:
|
|
167
|
+
raise _invalid(
|
|
168
|
+
"zip_central_truncated",
|
|
169
|
+
"central directory must contain one header per entry",
|
|
170
|
+
f"entry {index}",
|
|
171
|
+
)
|
|
172
|
+
(
|
|
173
|
+
sig,
|
|
174
|
+
made_by,
|
|
175
|
+
needed,
|
|
176
|
+
flags,
|
|
177
|
+
method,
|
|
178
|
+
mtime,
|
|
179
|
+
mdate,
|
|
180
|
+
crc,
|
|
181
|
+
csize,
|
|
182
|
+
usize,
|
|
183
|
+
name_len,
|
|
184
|
+
extra_len,
|
|
185
|
+
comment_len,
|
|
186
|
+
disk_start,
|
|
187
|
+
internal,
|
|
188
|
+
external,
|
|
189
|
+
offset,
|
|
190
|
+
) = _CENTRAL.unpack_from(central, position)
|
|
191
|
+
if sig != _CENTRAL_SIG:
|
|
192
|
+
raise _invalid(
|
|
193
|
+
"zip_central_signature", "central file header signature expected", f"entry {index}"
|
|
194
|
+
)
|
|
195
|
+
_check_common(index, needed, flags, method, mtime, mdate, csize, usize)
|
|
196
|
+
if made_by != _MADE_BY:
|
|
197
|
+
raise _invalid(
|
|
198
|
+
"zip_version_made_by", "version made by must be 0x000A", f"0x{made_by:04X}"
|
|
199
|
+
)
|
|
200
|
+
if extra_len != 0 or comment_len != 0:
|
|
201
|
+
raise _invalid(
|
|
202
|
+
"zip_extra_field", "extra fields and file comments must be empty", f"entry {index}"
|
|
203
|
+
)
|
|
204
|
+
if disk_start != 0:
|
|
205
|
+
raise _invalid("zip_disk_number", "all disk-number fields must be zero")
|
|
206
|
+
if internal != 0 or external != 0:
|
|
207
|
+
raise _invalid(
|
|
208
|
+
"zip_attributes",
|
|
209
|
+
"internal and external file attributes must be zero",
|
|
210
|
+
f"entry {index}",
|
|
211
|
+
)
|
|
212
|
+
if offset == _MAX_U32:
|
|
213
|
+
raise _invalid("zip64", "ZIP64 sentinel values are prohibited")
|
|
214
|
+
start = position + _CENTRAL.size
|
|
215
|
+
if name_len > len(central) - start:
|
|
216
|
+
raise _invalid(
|
|
217
|
+
"zip_central_truncated", "file name exceeds the central directory", f"entry {index}"
|
|
218
|
+
)
|
|
219
|
+
name = central[start : start + name_len]
|
|
220
|
+
records.append(
|
|
221
|
+
_Central(
|
|
222
|
+
name,
|
|
223
|
+
crc,
|
|
224
|
+
usize,
|
|
225
|
+
offset,
|
|
226
|
+
(needed, flags, method, mtime, mdate, crc, csize, usize, name_len, extra_len),
|
|
227
|
+
)
|
|
228
|
+
)
|
|
229
|
+
position = start + name_len
|
|
230
|
+
if position != len(central):
|
|
231
|
+
raise _invalid(
|
|
232
|
+
"zip_unaccounted_bytes",
|
|
233
|
+
"central directory length must match its headers exactly",
|
|
234
|
+
f"{len(central) - position} extra bytes",
|
|
235
|
+
)
|
|
236
|
+
return records
|
|
237
|
+
|
|
238
|
+
|
|
239
|
+
def _check_common(
|
|
240
|
+
index: int, needed: int, flags: int, method: int, mtime: int, mdate: int, csize: int, usize: int
|
|
241
|
+
) -> None:
|
|
242
|
+
if needed != _VERSION:
|
|
243
|
+
raise _invalid("zip_version_needed", "version needed to extract must be 1.0 (10)", needed)
|
|
244
|
+
if flags != 0:
|
|
245
|
+
raise _invalid(
|
|
246
|
+
"zip_flags",
|
|
247
|
+
"general-purpose flags must be zero (no data descriptors)",
|
|
248
|
+
f"0x{flags:04X}",
|
|
249
|
+
)
|
|
250
|
+
if method != 0:
|
|
251
|
+
raise _invalid("zip_method", "compression method must be 0 (stored)", method)
|
|
252
|
+
if mtime != _DOS_TIME:
|
|
253
|
+
raise _invalid("zip_mod_time", "last-modification time must be 0x0000", f"0x{mtime:04X}")
|
|
254
|
+
if mdate != _DOS_DATE:
|
|
255
|
+
raise _invalid("zip_mod_date", "last-modification date must be 0x0021", f"0x{mdate:04X}")
|
|
256
|
+
if _MAX_U32 in (csize, usize):
|
|
257
|
+
raise _invalid("zip64", "ZIP64 sentinel values are prohibited")
|
|
258
|
+
if csize != usize:
|
|
259
|
+
raise _invalid(
|
|
260
|
+
"zip_size_mismatch",
|
|
261
|
+
"stored entries have equal compressed and uncompressed sizes",
|
|
262
|
+
f"entry {index}",
|
|
263
|
+
)
|
|
264
|
+
|
|
265
|
+
|
|
266
|
+
def _check_locals(
|
|
267
|
+
source: ByteSource, records: Sequence[_Central], cd_offset: int
|
|
268
|
+
) -> list[ZipEntry]:
|
|
269
|
+
entries: list[ZipEntry] = []
|
|
270
|
+
position = 0
|
|
271
|
+
for index, record in enumerate(records):
|
|
272
|
+
if record.offset != position:
|
|
273
|
+
raise _invalid(
|
|
274
|
+
"zip_local_offset",
|
|
275
|
+
"each central entry must point to its local "
|
|
276
|
+
"header, stored consecutively from byte zero",
|
|
277
|
+
f"entry {index}: offset {record.offset}, expected {position}",
|
|
278
|
+
)
|
|
279
|
+
if cd_offset - position < _LOCAL.size:
|
|
280
|
+
raise _invalid(
|
|
281
|
+
"zip_local_truncated", "local header exceeds the entry region", f"entry {index}"
|
|
282
|
+
)
|
|
283
|
+
(sig, *fields) = _LOCAL.unpack(source.read_at(position, _LOCAL.size))
|
|
284
|
+
if sig != _LOCAL_SIG:
|
|
285
|
+
raise _invalid(
|
|
286
|
+
"zip_local_signature", "local file header signature expected", f"entry {index}"
|
|
287
|
+
)
|
|
288
|
+
needed, flags, method, mtime, mdate, _crc, csize, usize, _nlen, _xlen = fields
|
|
289
|
+
_check_common(index, needed, flags, method, mtime, mdate, csize, usize)
|
|
290
|
+
if tuple(fields) != record.fields:
|
|
291
|
+
raise _invalid(
|
|
292
|
+
"zip_header_mismatch",
|
|
293
|
+
"fields shared by local and central headers must agree byte for byte",
|
|
294
|
+
f"entry {index}",
|
|
295
|
+
)
|
|
296
|
+
name_start = position + _LOCAL.size
|
|
297
|
+
name_len = len(record.name)
|
|
298
|
+
if name_len > cd_offset - name_start:
|
|
299
|
+
raise _invalid(
|
|
300
|
+
"zip_local_truncated", "file name exceeds the entry region", f"entry {index}"
|
|
301
|
+
)
|
|
302
|
+
if source.read_at(name_start, name_len) != record.name:
|
|
303
|
+
raise _invalid(
|
|
304
|
+
"zip_header_mismatch", "local and central file names must agree", f"entry {index}"
|
|
305
|
+
)
|
|
306
|
+
data_offset = name_start + name_len
|
|
307
|
+
if record.size > cd_offset - data_offset:
|
|
308
|
+
raise _invalid(
|
|
309
|
+
"zip_data_truncated",
|
|
310
|
+
"entry data must end before the central directory",
|
|
311
|
+
f"entry {index}",
|
|
312
|
+
)
|
|
313
|
+
if not record.name.isascii():
|
|
314
|
+
raise _invalid("zip_name", "entry names must be ASCII paths", record.name)
|
|
315
|
+
entries.append(ZipEntry(record.name, position, data_offset, record.size, record.crc32))
|
|
316
|
+
position = data_offset + record.size
|
|
317
|
+
if position != cd_offset:
|
|
318
|
+
raise _invalid(
|
|
319
|
+
"zip_unaccounted_bytes",
|
|
320
|
+
"local headers and data must account for every byte before the central directory",
|
|
321
|
+
f"{cd_offset - position} unaccounted bytes",
|
|
322
|
+
)
|
|
323
|
+
return entries
|
|
324
|
+
|
|
325
|
+
|
|
326
|
+
def crc32_of(source: ByteSource) -> int:
|
|
327
|
+
"""CRC-32 of a whole source, streamed."""
|
|
328
|
+
crc = 0
|
|
329
|
+
for chunk in iter_chunks(source):
|
|
330
|
+
crc = zlib.crc32(chunk, crc)
|
|
331
|
+
return crc
|
|
332
|
+
|
|
333
|
+
|
|
334
|
+
class SupportsWrite(Protocol):
|
|
335
|
+
def write(self, data: bytes, /) -> object: ...
|
|
336
|
+
|
|
337
|
+
|
|
338
|
+
def canonical_zip_size(entries: Iterable[tuple[str, int]]) -> int:
|
|
339
|
+
"""Exact canonical ZIP length: ``22 + sum(file_bytes + 76 + 2 * path_bytes)`` (§2.1).
|
|
340
|
+
|
|
341
|
+
``entries`` must include ``manifest.json`` and ``manifest.json.sig``.
|
|
342
|
+
"""
|
|
343
|
+
return EOCD_SIZE + sum(
|
|
344
|
+
size + ENTRY_OVERHEAD + 2 * len(path.encode("utf-8")) for path, size in entries
|
|
345
|
+
)
|
|
346
|
+
|
|
347
|
+
|
|
348
|
+
def write_canonical_zip(sink: SupportsWrite, entries: Sequence[tuple[str, ByteSource]]) -> int:
|
|
349
|
+
"""Write the canonical ZIP for entries already in §2.2 order; return its length.
|
|
350
|
+
|
|
351
|
+
Each source is read twice (CRC pass, then copy) in bounded chunks. The
|
|
352
|
+
caller is responsible for the entry order (manifest, signature, then
|
|
353
|
+
listed paths in ascending ASCII order); see
|
|
354
|
+
:func:`pysci.bundle.canonical_entries`.
|
|
355
|
+
|
|
356
|
+
Raises:
|
|
357
|
+
ValueError: the entries cannot be represented in classic ZIP.
|
|
358
|
+
"""
|
|
359
|
+
if len(entries) >= _MAX_U16:
|
|
360
|
+
raise ValueError("too many entries for classic ZIP")
|
|
361
|
+
central = bytearray()
|
|
362
|
+
position = 0
|
|
363
|
+
for name, source in entries:
|
|
364
|
+
name_bytes = name.encode("ascii")
|
|
365
|
+
size = source.size
|
|
366
|
+
if size >= _MAX_U32 or len(name_bytes) > _MAX_U16:
|
|
367
|
+
raise ValueError(f"entry {name!r} cannot be represented without ZIP64")
|
|
368
|
+
crc = crc32_of(source)
|
|
369
|
+
header = _LOCAL.pack(
|
|
370
|
+
_LOCAL_SIG, _VERSION, 0, 0, _DOS_TIME, _DOS_DATE, crc, size, size, len(name_bytes), 0
|
|
371
|
+
)
|
|
372
|
+
sink.write(header + name_bytes)
|
|
373
|
+
for chunk in iter_chunks(source):
|
|
374
|
+
sink.write(chunk)
|
|
375
|
+
central += _CENTRAL.pack(
|
|
376
|
+
_CENTRAL_SIG,
|
|
377
|
+
_MADE_BY,
|
|
378
|
+
_VERSION,
|
|
379
|
+
0,
|
|
380
|
+
0,
|
|
381
|
+
_DOS_TIME,
|
|
382
|
+
_DOS_DATE,
|
|
383
|
+
crc,
|
|
384
|
+
size,
|
|
385
|
+
size,
|
|
386
|
+
len(name_bytes),
|
|
387
|
+
0,
|
|
388
|
+
0,
|
|
389
|
+
0,
|
|
390
|
+
0,
|
|
391
|
+
0,
|
|
392
|
+
position,
|
|
393
|
+
)
|
|
394
|
+
central += name_bytes
|
|
395
|
+
position += _LOCAL.size + len(name_bytes) + size
|
|
396
|
+
if position >= _MAX_U32:
|
|
397
|
+
raise ValueError("archive too large for classic ZIP")
|
|
398
|
+
sink.write(bytes(central))
|
|
399
|
+
sink.write(_EOCD.pack(_EOCD_SIG, 0, 0, len(entries), len(entries), len(central), position, 0))
|
|
400
|
+
return position + len(central) + EOCD_SIZE
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: sci-protocol
|
|
3
|
+
Version: 0.2.0
|
|
4
|
+
Summary: Python library and sci CLI for SCI research publications.
|
|
5
|
+
Project-URL: Repository, https://github.com/azaho/sci
|
|
6
|
+
Project-URL: Documentation, https://github.com/azaho/sci/blob/main/README.md
|
|
7
|
+
Author: SCI contributors
|
|
8
|
+
License-Expression: Apache-2.0
|
|
9
|
+
License-File: LICENSE
|
|
10
|
+
Classifier: Development Status :: 3 - Alpha
|
|
11
|
+
Classifier: Environment :: Console
|
|
12
|
+
Classifier: Programming Language :: Python :: 3
|
|
13
|
+
Classifier: Typing :: Typed
|
|
14
|
+
Requires-Python: >=3.11
|
|
15
|
+
Requires-Dist: asn1crypto>=1.5.1
|
|
16
|
+
Requires-Dist: cryptography>=46.0.3
|
|
17
|
+
Requires-Dist: idna>=3.11
|
|
18
|
+
Description-Content-Type: text/markdown
|
|
19
|
+
|
|
20
|
+
<h1 align="center">sci</h1>
|
|
21
|
+
|
|
22
|
+
<p align="center">Signed, verifiable research publications for agents and humans.</p>
|
|
23
|
+
|
|
24
|
+
<p align="center">
|
|
25
|
+
<a href="https://github.com/azaho/sci/actions/workflows/checks.yml"><img src="https://github.com/azaho/sci/actions/workflows/checks.yml/badge.svg?branch=main" alt="Checks"></a>
|
|
26
|
+
<a href="https://github.com/azaho/sci/actions/workflows/test-vectors.yml"><img src="https://github.com/azaho/sci/actions/workflows/test-vectors.yml/badge.svg?branch=main" alt="Test vectors"></a>
|
|
27
|
+
<a href="pyproject.toml"><img src="https://img.shields.io/badge/Python-3.11%2B-blue?logo=python&logoColor=white" alt="Python 3.11+"></a>
|
|
28
|
+
<a href="protocol/README.md"><img src="https://img.shields.io/badge/Protocol-0.1_draft-orange" alt="Protocol 0.1 draft"></a>
|
|
29
|
+
<a href="LICENSE"><img src="https://img.shields.io/badge/License-Apache_2.0-blue" alt="Software license: Apache 2.0"></a>
|
|
30
|
+
</p>
|
|
31
|
+
|
|
32
|
+
SCI is an open, decentralized publication system. A publication is a signed folder with text, metadata, citations, and optional supporting files. Its ID is the SHA-256 hash of its signed manifest. Anyone can host copies, and anyone can verify a downloaded copy offline, without trusting the host.
|
|
33
|
+
|
|
34
|
+
| Component | Contents |
|
|
35
|
+
| --- | --- |
|
|
36
|
+
| [`protocol/`](protocol/README.md) | Protocol specification (draft 0.1), baseline publication license, and the shared conformance corpus |
|
|
37
|
+
| [CLI guide](cli/README.md) | The `sci` command: prepare, sign, verify, publish, submit, search, and retrieve publications |
|
|
38
|
+
| [`python/`](python/README.md) | `pysci`, the Python library and private CLI implementation |
|
|
39
|
+
| [`typescript/`](typescript/README.md) | TypeScript protocol library for browsers, Deno, Workers, and Node |
|
|
40
|
+
|
|
41
|
+
Both implementations use the shared 680-case [conformance corpus](protocol/test-vectors/README.md).
|
|
42
|
+
The TypeScript adapter covers all 680 cases; the Python adapter covers 670,
|
|
43
|
+
excluding 10 stateful indexer scenarios.
|
|
44
|
+
|
|
45
|
+
## Install
|
|
46
|
+
|
|
47
|
+
Requires Python 3.11 or later. One package, `sci-protocol`, contains both the Python
|
|
48
|
+
library and the `sci` command. The CLI implementation is private; use
|
|
49
|
+
`import pysci` for the public Python API.
|
|
50
|
+
|
|
51
|
+
After the first PyPI release, install the standalone command with:
|
|
52
|
+
|
|
53
|
+
```sh
|
|
54
|
+
uv tool install sci-protocol
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
Or install both the library and command in a project virtual environment:
|
|
58
|
+
|
|
59
|
+
```sh
|
|
60
|
+
pip install sci-protocol
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
`uv tool install` isolates the command; it does not add `pysci` to your
|
|
64
|
+
project or notebook environment.
|
|
65
|
+
|
|
66
|
+
Until the PyPI release, install from this checkout at the repository root:
|
|
67
|
+
|
|
68
|
+
```sh
|
|
69
|
+
uv tool install .
|
|
70
|
+
# Or, inside a project virtual environment:
|
|
71
|
+
pip install .
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
For development, use `uv sync` and `uv run sci --help`. Package version 0.2.0
|
|
75
|
+
introduces this unified distribution; the SCI protocol remains at draft 0.1.
|
|
76
|
+
|
|
77
|
+
## First workflow
|
|
78
|
+
|
|
79
|
+
Prepare a draft folder:
|
|
80
|
+
|
|
81
|
+
```text
|
|
82
|
+
draft/
|
|
83
|
+
├── sci.json # required: {"title": "...", "abstract": "...", "citations": [...]}
|
|
84
|
+
├── README.md # required: main text in UTF-8 Markdown
|
|
85
|
+
└── files/ # optional: code, data, and figures
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
Sign it, verify it, and keep the bundle in a local campaign folder:
|
|
89
|
+
|
|
90
|
+
```sh
|
|
91
|
+
sci keygen # create a signing key in ~/.sci/
|
|
92
|
+
sci license ./draft # select the SCI baseline license
|
|
93
|
+
sci check ./draft # report every problem before signing
|
|
94
|
+
sci sign ./draft --output ./campaign # sign, timestamp with FreeTSA, save <digest>.zip
|
|
95
|
+
sci verify ./campaign/<digest>.zip # complete offline verification
|
|
96
|
+
sci search "neural decoding" --in ./campaign # search verified local bundles
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
Host the bundles in a public archive and register them with an index:
|
|
100
|
+
|
|
101
|
+
```sh
|
|
102
|
+
sci publish ./campaign --to https://github.com/lab/research --create
|
|
103
|
+
sci submit ./campaign --to https://sciscape.io \
|
|
104
|
+
--archive https://github.com/lab/research
|
|
105
|
+
sci pull sha256:<digest> --in https://sciscape.io # retrieve and verify from any registered host
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
See the [CLI instructions](cli/README.md) for every command, and [CONTRIBUTING.md](CONTRIBUTING.md) for development.
|
|
109
|
+
|
|
110
|
+
## License
|
|
111
|
+
|
|
112
|
+
Software in this repository is licensed under the [Apache License, Version 2.0](LICENSE). Publications keep their own licenses; the [SCI baseline publication license](protocol/sci-baseline-LICENSE.txt) is CC BY 4.0 for text, figures, and data and MIT for code.
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
pysci/__init__.py,sha256=6iAWMgbVnvk85K4xjfujSyaYih_fZiCVCyVu9nzTgYg,2232
|
|
2
|
+
pysci/__main__.py,sha256=0wXYxQ_bSRnD7NvHIB139Pw9xn5RrNdaUZ4PeDddSKw,172
|
|
3
|
+
pysci/accounts.py,sha256=gBPJyVmYqxs6KRBvXzNFkUwHx2SnK2JeM99oVBP6KHk,4697
|
|
4
|
+
pysci/baseline.py,sha256=lXeEXFhVAVoAoO621fCUDMViHcZO-yEoIMhz2F6QD1Q,4841
|
|
5
|
+
pysci/bundle.py,sha256=0A5GHVa3tluN7CmYJEaHMvKm6Tm6HQ4QMGLPDoxnSN0,30701
|
|
6
|
+
pysci/citations.py,sha256=nWkbWt4_lona6-1O_JnogorAjEaPRse0HcCaeDokXpU,8241
|
|
7
|
+
pysci/der.py,sha256=0fllx-VtUMVP_AaOTbDJp8GbFl3znJhtDz0HQBV0lCo,4758
|
|
8
|
+
pysci/discovery.py,sha256=2vTCbRPjWtI7riXT08MfR-PUvFDzjE1c6g8-3-EVxUc,15545
|
|
9
|
+
pysci/draft.py,sha256=L4Lj9qXjIpyvdYRHcpx6Jd2-ALDwjPwPBBPpsQzmRh0,13680
|
|
10
|
+
pysci/ed25519.py,sha256=9Uu5TdsGEwYFdmUPFtCvEFXOXQ2L-OLEL87YdoqogKc,5297
|
|
11
|
+
pysci/errors.py,sha256=xvv0FoLeI4hB4LYH9AqprILtjCr4_4OgJvkrUZn5N-8,6081
|
|
12
|
+
pysci/ids.py,sha256=W7r3ZMJvnUFmggmqZ1nlAjQJztepwiOrtPGxwi-PFgY,1971
|
|
13
|
+
pysci/jsonstrict.py,sha256=FHBCagb-LnQSNVhtfOfnoQ_ztaW5PYFUfgaUzrQ4_xA,6832
|
|
14
|
+
pysci/keys.py,sha256=2Ja2o0CDwbD2bO4NZ2KJaWnlyNSyl3B8McYTqG-_ag4,7785
|
|
15
|
+
pysci/manifest.py,sha256=tP7pAG0xeRU25P-u4PqgOO29AZMitOouKN_x733Cfbc,10745
|
|
16
|
+
pysci/metadata.py,sha256=_YxvH1e2tXImDRGfo0jNQlctdgymyukl3gtrfkbaGEE,13729
|
|
17
|
+
pysci/paths.py,sha256=bOnqOZL8drUpt8Vr9srGw3yhDzY0g6ZEuMZJsHnUi6w,4660
|
|
18
|
+
pysci/png.py,sha256=Ef9NYvbFu_aZ00LTLeAp2PrbyhJwSUP4nB5GyQRJOvk,8329
|
|
19
|
+
pysci/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
20
|
+
pysci/records.py,sha256=RrDMPYNGkNUdJmodiK1WpKh6-oya72ECkfCDgQ6fO-g,12866
|
|
21
|
+
pysci/rfc3339.py,sha256=n0VrVeI8XdxFyic8txOLeRUWggteFwhuGICFftVbJ6g,2535
|
|
22
|
+
pysci/schemas.py,sha256=LHeLns0CgJMqV3J0qSjbh_P7bccJksf9gDcOVFpsgFU,2259
|
|
23
|
+
pysci/source.py,sha256=MErNuSO5V9_FXY9swwv8HzyzFt6eFGAz9NoP10BFp0I,5189
|
|
24
|
+
pysci/specification.py,sha256=83lSZPXb0jTMSoRufwNWD0JulWrZvJg6mkJL1ihWChE,2147
|
|
25
|
+
pysci/submission.py,sha256=I8ypqxM38WDGI0aXrMF0cK51SVKiLqzMQc4Sl58M9sY,7867
|
|
26
|
+
pysci/templates.py,sha256=OGbePyPlLDzLOwEIGUsttdRbdtMH7ymmNcS4oOPt9so,4502
|
|
27
|
+
pysci/text.py,sha256=IH12DJSr0XfRvjEWHzFKwJbSYQGyyMktCutWzsFgIKE,7601
|
|
28
|
+
pysci/timestamps.py,sha256=sN4YzcvC54uSKLpksnDw0Euhsc5N4oq_BxDP2-y0CY0,33031
|
|
29
|
+
pysci/zipformat.py,sha256=mI3QFLh-3isORPbFMkr1KAig2IuYScjWnUhC3xXLt5c,14168
|
|
30
|
+
pysci/_cli/__init__.py,sha256=izaso6Nx_amnnDox2STr3CbZuwn6QQotopadF7qiHdA,148
|
|
31
|
+
pysci/_cli/checks.py,sha256=XlKTAo_t89kIoRGcjCcEsOtzyL6cwlPv40ksDD479Ec,6647
|
|
32
|
+
pysci/_cli/config.py,sha256=9z92ixo_8bLYq8MjjWynz0wpNwGvqBFq_PNcqd3cUs4,8011
|
|
33
|
+
pysci/_cli/context.py,sha256=d8A0UxlBdNVoQj0fsgYHYYfEYAiIOx8SqJMWBSop-WA,2638
|
|
34
|
+
pysci/_cli/destinations.py,sha256=Ugn8oOS0RELZ8GV0hID5eyqJ_5sBU6FkKg4jarWNuAM,5569
|
|
35
|
+
pysci/_cli/errors.py,sha256=W-zF6-lOmPFXw7zh3GgRAH7fv8P1gUrcJGzSXENtwTE,5273
|
|
36
|
+
pysci/_cli/main.py,sha256=peLjTx_vcHkYMxJDCOE-8tnt7nC4UXaH-0Uf8oW6O3U,9498
|
|
37
|
+
pysci/_cli/output.py,sha256=rM-trm5DKrNYWde91TKYMNdctx5aoXOQSrwGt5Pmt5I,2706
|
|
38
|
+
pysci/_cli/retrieval.py,sha256=cOprG0-O_VyZ3Y4SzlPPmEz9vbVx1yrjz-hplnNGAAE,1551
|
|
39
|
+
pysci/_cli/spec.py,sha256=aSe8f9Z2TvjAZZcLloG8MCDZ_-PTW1xs5xVTvzNLBF0,3915
|
|
40
|
+
pysci/_cli/clients/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
41
|
+
pysci/_cli/clients/archive.py,sha256=7_Qgu8iUFjrm98WwHQJaJEzhjO8CUxTP0sa1mmFd6rM,17249
|
|
42
|
+
pysci/_cli/clients/credentials.py,sha256=cB2jIUBUTz97tLlpUFToNSLX78xO3RI3Q7kymQNSUy0,1780
|
|
43
|
+
pysci/_cli/clients/github.py,sha256=tyGEwR_6ccaKl7Cf5F1_ZUMZB_oeM2hkjfZArbrD9M4,28519
|
|
44
|
+
pysci/_cli/clients/hosting.py,sha256=5iTeZqnrksilOSg5NwlIVuo_k8QVbL7xtM_hqSSZkB0,7753
|
|
45
|
+
pysci/_cli/clients/http.py,sha256=f0K6KMeZeu0z5XG7mFHoiAOqKJb2TTdYkGT35Fh8Fhw,16730
|
|
46
|
+
pysci/_cli/clients/huggingface.py,sha256=5qRVigeru4WkHbdSHvSC72iAFi7sHsZxtyBwhgn20WI,19202
|
|
47
|
+
pysci/_cli/clients/index.py,sha256=nit0vIasrmHFzsYec3YWumonnr44MeX0p1weKNv_kdY,16624
|
|
48
|
+
pysci/_cli/clients/tsa.py,sha256=Gkywequh62h1p_M5I_xapCCZlwVwGnWruZ7An5Z9EMs,7180
|
|
49
|
+
pysci/_cli/commands/__init__.py,sha256=-C7eOIBhzHcp_jjaa1OuU9xVAvmmYsmKHn9grfcfMpQ,692
|
|
50
|
+
pysci/_cli/commands/check.py,sha256=0-_sQYm_86mAkLXEkcCKkYADgXIcGUOQ-4w08257dm0,4600
|
|
51
|
+
pysci/_cli/commands/claim.py,sha256=nS_sYXo3jnOaAfEeUCKMwa1NlwsjfuRAY0eY2WMNLH8,7415
|
|
52
|
+
pysci/_cli/commands/common.py,sha256=C4Nvy3hWJsTt9CMrMSa2PTsjud5aCsufrGG-YCd8AA4,2895
|
|
53
|
+
pysci/_cli/commands/folders.py,sha256=ciTc_joMLo5t4rQu_ik9PK__PUc-XwsZBLK6WqkEwV8,3284
|
|
54
|
+
pysci/_cli/commands/index.py,sha256=EKDkmk9mjpL5waCuVFGGcBCgWMUzqB985uGgksytLz8,1808
|
|
55
|
+
pysci/_cli/commands/keygen.py,sha256=QqbzyHkpF_gzuBuPlodaYuq8I9J0byDrlsDKdZXPQEo,2071
|
|
56
|
+
pysci/_cli/commands/license.py,sha256=XP0_7lWtAGgVHntjyUszsFhx78qR1Br8UHd6Vn5mbWA,2928
|
|
57
|
+
pysci/_cli/commands/protocol.py,sha256=6of-D8M-doJY0CqEbgbVr6Iz8ZtZp8Ny2g2ijw7vrvc,2694
|
|
58
|
+
pysci/_cli/commands/publish.py,sha256=Owmx9v6nhrxYDLXrH7ai9gMEmAAsYVinBe0gdhpnHSs,12075
|
|
59
|
+
pysci/_cli/commands/pull.py,sha256=ZNvftjI87D9wvSQb75smsfWf4qSspV33sNtduaf25-U,14207
|
|
60
|
+
pysci/_cli/commands/read.py,sha256=YUpe0Rh6lU5N0yO8hQBa_UGEM8w7n6l_gSGv4ezX5Pk,14885
|
|
61
|
+
pysci/_cli/commands/revoke.py,sha256=hFPqUZO9TPr2kTCLtIcNcC0NtU-rVT6Y6jP-D3OPhDk,8988
|
|
62
|
+
pysci/_cli/commands/search.py,sha256=KXPIW_NiRNgNMOg6U0nzEkFcANHiaLcE1sYB82JE_2Q,15593
|
|
63
|
+
pysci/_cli/commands/sign.py,sha256=nYsil1CV52fsMGYZsTsQEdsReEso3U87iRMZXlToKz0,12241
|
|
64
|
+
pysci/_cli/commands/submit.py,sha256=x5wwpaw1v7uKdq8niQlgkl4wXwgvg5OePLw_Fh7PMy8,6756
|
|
65
|
+
pysci/_cli/commands/timestamp.py,sha256=y0miNXKGhlYgwkxq7lPXVR18HeHB5F82w9jFu_9ojb0,8573
|
|
66
|
+
pysci/_cli/commands/verify.py,sha256=HSnmytpaFXwAJAxPn2nAFe3luC9bKSBZTBClWR4l4kg,6336
|
|
67
|
+
pysci/_cli/storage/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
68
|
+
pysci/_cli/storage/catalog.py,sha256=xDFyTGSoM-bLz6mgUhc3ysYb01OofG7FRI-EmgLDsyU,13540
|
|
69
|
+
pysci/_cli/storage/files.py,sha256=Sd_TwFZGdK4zThQLpfU25arKxob_GChPLKFuruGGPzQ,8381
|
|
70
|
+
pysci/_cli/storage/inputs.py,sha256=UgNP4LsJQwMD4pXh6QhDvmgeBPtuc4VhBMVV1JZjTWQ,7382
|
|
71
|
+
pysci/_cli/storage/keys.py,sha256=roa-HOky4PxlZBmMVXeJ2PSxlNDRfLjpVLsjO-Ys-9c,4442
|
|
72
|
+
pysci/_cli/storage/local_publish.py,sha256=YtcQ2veSTsMeqHdbjjzfQ83WiS4FgNyy6uGUAOWrksU,3422
|
|
73
|
+
pysci/data/freetsa-cacert.pem,sha256=IVG2ETf_qGv2ZGkbpn59oLGfmMdY49Io1djr8n4ERDg,2833
|
|
74
|
+
pysci/data/sci-baseline-LICENSE.txt,sha256=jM_V_aqT5ecdyIEr8R4KcZi9ClNPpkuJcP0t5Y5sJHg,22183
|
|
75
|
+
pysci/data/sci-protocol.md,sha256=MR5AcgOSVwk4_uP_Z9U-bO-TORayKGU9Enmpw_Uq_A0,39097
|
|
76
|
+
sci_protocol-0.2.0.dist-info/METADATA,sha256=ANCHYOdOVmmqO3SZX7-9228fZ2It8812zJqt76V5QFU,5061
|
|
77
|
+
sci_protocol-0.2.0.dist-info/WHEEL,sha256=2iZrJkDIkeDh0k9SikKMqFuRP-ua6mb4Ir1AsER4BEw,87
|
|
78
|
+
sci_protocol-0.2.0.dist-info/entry_points.txt,sha256=bgSpT-ID776Fyzmv4GU3C82Y1v43Ca_dD0xAsO17C4c,45
|
|
79
|
+
sci_protocol-0.2.0.dist-info/licenses/LICENSE,sha256=z8d0m5b2O9McPEK1xHG_dWgUBT6EfBDz6wA0F7xSPTA,11358
|
|
80
|
+
sci_protocol-0.2.0.dist-info/RECORD,,
|