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/text.py
ADDED
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
"""Text character rules (protocol §4.2) using RFC 9839 Unicode Assignables.
|
|
2
|
+
|
|
3
|
+
Membership is decided by the fixed code-point intervals of RFC 9839 §4.3; no
|
|
4
|
+
runtime Unicode category data is consulted.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import codecs
|
|
10
|
+
import re
|
|
11
|
+
from collections.abc import Iterable
|
|
12
|
+
from typing import Final
|
|
13
|
+
|
|
14
|
+
from .errors import Invalid, Issue, IssueCollector
|
|
15
|
+
|
|
16
|
+
__all__ = [
|
|
17
|
+
"ASSIGNABLE_RANGES",
|
|
18
|
+
"LINE_CONTROLS",
|
|
19
|
+
"TextFileValidator",
|
|
20
|
+
"check_text",
|
|
21
|
+
"check_text_file",
|
|
22
|
+
"decode_text_file",
|
|
23
|
+
"first_disallowed",
|
|
24
|
+
"is_assignable",
|
|
25
|
+
"is_nonblank",
|
|
26
|
+
]
|
|
27
|
+
|
|
28
|
+
ASSIGNABLE_RANGES: Final[tuple[tuple[int, int], ...]] = (
|
|
29
|
+
(0x09, 0x0A),
|
|
30
|
+
(0x0D, 0x0D),
|
|
31
|
+
(0x20, 0x7E),
|
|
32
|
+
(0xA0, 0xD7FF),
|
|
33
|
+
(0xE000, 0xFDCF),
|
|
34
|
+
(0xFDF0, 0xFFFD),
|
|
35
|
+
*((plane << 16, (plane << 16) | 0xFFFD) for plane in range(1, 17)),
|
|
36
|
+
)
|
|
37
|
+
"""Inclusive code-point intervals of RFC 9839 §4.3 Unicode Assignables."""
|
|
38
|
+
|
|
39
|
+
LINE_CONTROLS: Final = frozenset("\t\n\r")
|
|
40
|
+
"""TAB, LF, CR: permitted only in multiline fields and text files."""
|
|
41
|
+
|
|
42
|
+
_BLANK: Final = frozenset(" \t\n\r")
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def _char_class(ranges: Iterable[tuple[int, int]]) -> str:
|
|
46
|
+
parts = []
|
|
47
|
+
for low, high in ranges:
|
|
48
|
+
if low == high:
|
|
49
|
+
parts.append(re.escape(chr(low)))
|
|
50
|
+
else:
|
|
51
|
+
parts.append(f"{re.escape(chr(low))}-{re.escape(chr(high))}")
|
|
52
|
+
return "".join(parts)
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
_SINGLE_LINE_RANGES: Final = tuple(r for r in ASSIGNABLE_RANGES if r[0] > 0x0D)
|
|
56
|
+
_DISALLOWED_MULTILINE: Final = re.compile(f"[^{_char_class(ASSIGNABLE_RANGES)}]")
|
|
57
|
+
_DISALLOWED_SINGLE_LINE: Final = re.compile(f"[^{_char_class(_SINGLE_LINE_RANGES)}]")
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def is_assignable(code_point: int) -> bool:
|
|
61
|
+
"""True if ``code_point`` is an RFC 9839 Unicode Assignable."""
|
|
62
|
+
return any(low <= code_point <= high for low, high in ASSIGNABLE_RANGES)
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def first_disallowed(text: str, *, multiline: bool) -> int | None:
|
|
66
|
+
"""Index of the first character not permitted by §4.2, or None."""
|
|
67
|
+
pattern = _DISALLOWED_MULTILINE if multiline else _DISALLOWED_SINGLE_LINE
|
|
68
|
+
match = pattern.search(text)
|
|
69
|
+
return None if match is None else match.start()
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def is_nonblank(text: str) -> bool:
|
|
73
|
+
"""True if ``text`` has a character other than SPACE, TAB, LF, or CR."""
|
|
74
|
+
return any(ch not in _BLANK for ch in text)
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def check_text(
|
|
78
|
+
value: str,
|
|
79
|
+
*,
|
|
80
|
+
multiline: bool = False,
|
|
81
|
+
nonblank: bool = False,
|
|
82
|
+
max_code_points: int | None = None,
|
|
83
|
+
file: str | None = None,
|
|
84
|
+
field: str | None = None,
|
|
85
|
+
collector: IssueCollector,
|
|
86
|
+
) -> None:
|
|
87
|
+
"""Check a decoded string against §4.2 and an optional length limit.
|
|
88
|
+
|
|
89
|
+
``multiline`` permits TAB, LF, and CR (abstract, citation reason, claim note).
|
|
90
|
+
"""
|
|
91
|
+
index = first_disallowed(value, multiline=multiline)
|
|
92
|
+
if index is not None:
|
|
93
|
+
ch = value[index]
|
|
94
|
+
if ch in LINE_CONTROLS:
|
|
95
|
+
collector.add(
|
|
96
|
+
"text_line_control",
|
|
97
|
+
"TAB, LF, and CR are permitted only in abstract, citation reason, "
|
|
98
|
+
"README.md, and LICENSE",
|
|
99
|
+
section="4.2",
|
|
100
|
+
file=file,
|
|
101
|
+
field=field,
|
|
102
|
+
observed=f"U+{ord(ch):04X} at code point {index}",
|
|
103
|
+
)
|
|
104
|
+
else:
|
|
105
|
+
collector.add(
|
|
106
|
+
"text_not_assignable",
|
|
107
|
+
"text must contain only RFC 9839 Unicode Assignables",
|
|
108
|
+
section="4.2",
|
|
109
|
+
file=file,
|
|
110
|
+
field=field,
|
|
111
|
+
observed=f"U+{ord(ch):04X} at code point {index}",
|
|
112
|
+
)
|
|
113
|
+
if max_code_points is not None and len(value) > max_code_points:
|
|
114
|
+
collector.add(
|
|
115
|
+
"text_too_long",
|
|
116
|
+
f"at most {max_code_points} Unicode code points",
|
|
117
|
+
section="4",
|
|
118
|
+
file=file,
|
|
119
|
+
field=field,
|
|
120
|
+
observed=f"{len(value)} code points",
|
|
121
|
+
)
|
|
122
|
+
if nonblank and not is_nonblank(value):
|
|
123
|
+
collector.add(
|
|
124
|
+
"text_blank",
|
|
125
|
+
"must contain a character other than SPACE, TAB, LF, or CR",
|
|
126
|
+
section="4.2",
|
|
127
|
+
file=file,
|
|
128
|
+
field=field,
|
|
129
|
+
)
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
def decode_text_file(data: bytes, *, file: str, nonblank: bool = False) -> str:
|
|
133
|
+
"""Decode a README.md/LICENSE body and check §4.2. Raises :class:`Invalid`."""
|
|
134
|
+
issues = check_text_file([data], file=file, nonblank=nonblank)
|
|
135
|
+
if issues:
|
|
136
|
+
raise Invalid(issues)
|
|
137
|
+
return data.decode("utf-8")
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
def check_text_file(chunks: Iterable[bytes], *, file: str, nonblank: bool = False) -> list[Issue]:
|
|
141
|
+
"""Stream-check UTF-8 text-file chunks against §2 and §4.2; return the issues."""
|
|
142
|
+
validator = TextFileValidator(file=file, nonblank=nonblank)
|
|
143
|
+
for chunk in chunks:
|
|
144
|
+
if not validator.feed(chunk):
|
|
145
|
+
break
|
|
146
|
+
return validator.finish()
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
class TextFileValidator:
|
|
150
|
+
"""Incremental checker for README.md/LICENSE bodies (§2, §4.2).
|
|
151
|
+
|
|
152
|
+
Feed chunks in order, then call :meth:`finish`. Memory use is bounded by
|
|
153
|
+
the chunk size. At most one encoding/character issue is reported, plus a
|
|
154
|
+
blankness issue for README.md.
|
|
155
|
+
"""
|
|
156
|
+
|
|
157
|
+
def __init__(self, *, file: str, nonblank: bool = False) -> None:
|
|
158
|
+
self.file = file
|
|
159
|
+
self.nonblank = nonblank
|
|
160
|
+
self._decoder = codecs.getincrementaldecoder("utf-8")(errors="strict")
|
|
161
|
+
self._collector = IssueCollector()
|
|
162
|
+
self._seen_nonblank = False
|
|
163
|
+
self._consumed = 0
|
|
164
|
+
self._position = 0
|
|
165
|
+
self._failed = False
|
|
166
|
+
self._finished = False
|
|
167
|
+
|
|
168
|
+
def feed(self, chunk: bytes) -> bool:
|
|
169
|
+
"""Consume a chunk; return False once a violation has been found."""
|
|
170
|
+
if self._failed:
|
|
171
|
+
return False
|
|
172
|
+
try:
|
|
173
|
+
text = self._decoder.decode(chunk)
|
|
174
|
+
except UnicodeDecodeError as exc:
|
|
175
|
+
self._utf8_error(exc)
|
|
176
|
+
return False
|
|
177
|
+
self._consumed += len(chunk)
|
|
178
|
+
self._check(text)
|
|
179
|
+
return not self._failed
|
|
180
|
+
|
|
181
|
+
def finish(self) -> list[Issue]:
|
|
182
|
+
"""Flush the decoder and return all issues."""
|
|
183
|
+
if not self._finished:
|
|
184
|
+
self._finished = True
|
|
185
|
+
if not self._failed:
|
|
186
|
+
try:
|
|
187
|
+
self._check(self._decoder.decode(b"", final=True))
|
|
188
|
+
except UnicodeDecodeError as exc:
|
|
189
|
+
self._utf8_error(exc)
|
|
190
|
+
if not self._failed and self.nonblank and not self._seen_nonblank:
|
|
191
|
+
self._collector.add(
|
|
192
|
+
"text_blank", f"{self.file} must be nonblank", section="2", file=self.file
|
|
193
|
+
)
|
|
194
|
+
return list(self._collector.issues)
|
|
195
|
+
|
|
196
|
+
def _check(self, text: str) -> None:
|
|
197
|
+
if _report_disallowed(text, self._position, self.file, self._collector):
|
|
198
|
+
self._failed = True
|
|
199
|
+
return
|
|
200
|
+
self._position += len(text)
|
|
201
|
+
if not self._seen_nonblank and is_nonblank(text):
|
|
202
|
+
self._seen_nonblank = True
|
|
203
|
+
|
|
204
|
+
def _utf8_error(self, exc: UnicodeDecodeError) -> None:
|
|
205
|
+
self._failed = True
|
|
206
|
+
self._collector.add(
|
|
207
|
+
"text_utf8",
|
|
208
|
+
"text files must be well-formed UTF-8",
|
|
209
|
+
section="2",
|
|
210
|
+
file=self.file,
|
|
211
|
+
observed=f"ill-formed sequence near byte {self._consumed + exc.start}",
|
|
212
|
+
)
|
|
213
|
+
|
|
214
|
+
|
|
215
|
+
def _report_disallowed(text: str, position: int, file: str, collector: IssueCollector) -> bool:
|
|
216
|
+
index = first_disallowed(text, multiline=True)
|
|
217
|
+
if index is None:
|
|
218
|
+
return False
|
|
219
|
+
collector.add(
|
|
220
|
+
"text_not_assignable",
|
|
221
|
+
"text must contain only RFC 9839 Unicode Assignables",
|
|
222
|
+
section="4.2",
|
|
223
|
+
file=file,
|
|
224
|
+
observed=f"U+{ord(text[index]):04X} at code point {position + index}",
|
|
225
|
+
)
|
|
226
|
+
return True
|