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.
Files changed (80) hide show
  1. pysci/__init__.py +77 -0
  2. pysci/__main__.py +8 -0
  3. pysci/_cli/__init__.py +5 -0
  4. pysci/_cli/checks.py +173 -0
  5. pysci/_cli/clients/__init__.py +0 -0
  6. pysci/_cli/clients/archive.py +456 -0
  7. pysci/_cli/clients/credentials.py +54 -0
  8. pysci/_cli/clients/github.py +734 -0
  9. pysci/_cli/clients/hosting.py +238 -0
  10. pysci/_cli/clients/http.py +481 -0
  11. pysci/_cli/clients/huggingface.py +492 -0
  12. pysci/_cli/clients/index.py +435 -0
  13. pysci/_cli/clients/tsa.py +223 -0
  14. pysci/_cli/commands/__init__.py +44 -0
  15. pysci/_cli/commands/check.py +115 -0
  16. pysci/_cli/commands/claim.py +179 -0
  17. pysci/_cli/commands/common.py +91 -0
  18. pysci/_cli/commands/folders.py +83 -0
  19. pysci/_cli/commands/index.py +51 -0
  20. pysci/_cli/commands/keygen.py +58 -0
  21. pysci/_cli/commands/license.py +75 -0
  22. pysci/_cli/commands/protocol.py +72 -0
  23. pysci/_cli/commands/publish.py +282 -0
  24. pysci/_cli/commands/pull.py +337 -0
  25. pysci/_cli/commands/read.py +364 -0
  26. pysci/_cli/commands/revoke.py +207 -0
  27. pysci/_cli/commands/search.py +400 -0
  28. pysci/_cli/commands/sign.py +284 -0
  29. pysci/_cli/commands/submit.py +172 -0
  30. pysci/_cli/commands/timestamp.py +196 -0
  31. pysci/_cli/commands/verify.py +163 -0
  32. pysci/_cli/config.py +233 -0
  33. pysci/_cli/context.py +79 -0
  34. pysci/_cli/destinations.py +163 -0
  35. pysci/_cli/errors.py +168 -0
  36. pysci/_cli/main.py +229 -0
  37. pysci/_cli/output.py +80 -0
  38. pysci/_cli/retrieval.py +51 -0
  39. pysci/_cli/spec.py +113 -0
  40. pysci/_cli/storage/__init__.py +0 -0
  41. pysci/_cli/storage/catalog.py +371 -0
  42. pysci/_cli/storage/files.py +252 -0
  43. pysci/_cli/storage/inputs.py +205 -0
  44. pysci/_cli/storage/keys.py +132 -0
  45. pysci/_cli/storage/local_publish.py +88 -0
  46. pysci/accounts.py +135 -0
  47. pysci/baseline.py +142 -0
  48. pysci/bundle.py +878 -0
  49. pysci/citations.py +233 -0
  50. pysci/data/freetsa-cacert.pem +45 -0
  51. pysci/data/sci-baseline-LICENSE.txt +457 -0
  52. pysci/data/sci-protocol.md +613 -0
  53. pysci/der.py +146 -0
  54. pysci/discovery.py +461 -0
  55. pysci/draft.py +374 -0
  56. pysci/ed25519.py +153 -0
  57. pysci/errors.py +204 -0
  58. pysci/ids.py +61 -0
  59. pysci/jsonstrict.py +225 -0
  60. pysci/keys.py +247 -0
  61. pysci/manifest.py +302 -0
  62. pysci/metadata.py +406 -0
  63. pysci/paths.py +136 -0
  64. pysci/png.py +217 -0
  65. pysci/py.typed +0 -0
  66. pysci/records.py +384 -0
  67. pysci/rfc3339.py +66 -0
  68. pysci/schemas.py +75 -0
  69. pysci/source.py +166 -0
  70. pysci/specification.py +58 -0
  71. pysci/submission.py +225 -0
  72. pysci/templates.py +129 -0
  73. pysci/text.py +226 -0
  74. pysci/timestamps.py +867 -0
  75. pysci/zipformat.py +400 -0
  76. sci_protocol-0.2.0.dist-info/METADATA +112 -0
  77. sci_protocol-0.2.0.dist-info/RECORD +80 -0
  78. sci_protocol-0.2.0.dist-info/WHEEL +4 -0
  79. sci_protocol-0.2.0.dist-info/entry_points.txt +2 -0
  80. 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,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.33.0
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ sci = pysci._cli.main:main