ref-id 0.7.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.
- ref_id/__init__.py +69 -0
- ref_id/build.py +146 -0
- ref_id/canonical.py +171 -0
- ref_id/digest.py +45 -0
- ref_id/encoding.py +78 -0
- ref_id/envelope.py +64 -0
- ref_id/errors.py +65 -0
- ref_id/grammar.py +92 -0
- ref_id/parse.py +236 -0
- ref_id/py.typed +0 -0
- ref_id/relations.py +464 -0
- ref_id/serialise.py +109 -0
- ref_id/spec/ref-id.json +5258 -0
- ref_id/spec/ref-id.json.sha256 +1 -0
- ref_id/spec.py +192 -0
- ref_id/types.py +264 -0
- ref_id/validators.py +258 -0
- ref_id-0.7.0.dist-info/METADATA +70 -0
- ref_id-0.7.0.dist-info/RECORD +21 -0
- ref_id-0.7.0.dist-info/WHEEL +4 -0
- ref_id-0.7.0.dist-info/licenses/LICENSE +202 -0
ref_id/grammar.py
ADDED
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
"""Compiles every pattern the specification declares through this dialect's declared adaptations, and
|
|
3
|
+
holds the only structural literals this package writes.
|
|
4
|
+
|
|
5
|
+
Ported from `crates/ref-id/src/grammar.rs`.
|
|
6
|
+
"""
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import re
|
|
10
|
+
from typing import TYPE_CHECKING
|
|
11
|
+
|
|
12
|
+
from .errors import SpecVersionError
|
|
13
|
+
|
|
14
|
+
if TYPE_CHECKING:
|
|
15
|
+
from .spec import Spec
|
|
16
|
+
|
|
17
|
+
__all__: list[str] = []
|
|
18
|
+
|
|
19
|
+
# The dialect this port compiles for. Its adaptation list, when the spec declares one, is applied to
|
|
20
|
+
# every pattern.
|
|
21
|
+
_DIALECT = "python-re"
|
|
22
|
+
|
|
23
|
+
# The structural literals of the scheme, each written exactly once, here. Their sources are
|
|
24
|
+
# `spec.grammar.expression` (the field separator, the fragment introducer, the line breaks the character
|
|
25
|
+
# classes exclude) and the two pair grammars (the key/value separator).
|
|
26
|
+
FIELD = ":"
|
|
27
|
+
FRAGMENT_INTRODUCER = "#"
|
|
28
|
+
PAIR = "="
|
|
29
|
+
LINE_BREAKS = ("\r", "\n")
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def scheme_prefix(spec: Spec) -> str:
|
|
33
|
+
return f"{spec.scheme()}{FIELD}"
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def _assert_structure(spec: Spec) -> None:
|
|
37
|
+
expression = spec.grammar_expression()
|
|
38
|
+
for literal in (f"^{scheme_prefix(spec)}", FRAGMENT_INTRODUCER, "\\r", "\\n"):
|
|
39
|
+
if literal not in expression:
|
|
40
|
+
raise SpecVersionError(f"spec.grammar.expression does not carry {literal!r}; this package's structural literals do not match")
|
|
41
|
+
for pair in (spec.get_str("grammar", "state", "pair"), spec.get_str("grammar", "fragment", "pair")):
|
|
42
|
+
if f"){PAIR}(" not in pair:
|
|
43
|
+
raise SpecVersionError(f"a pair grammar does not separate key and value with {PAIR!r}; this package's structural literals do not match")
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def compile_pattern(spec: Spec, pattern: str) -> re.Pattern[str]:
|
|
47
|
+
"""Applies this dialect's declared adaptations — the replace pairs in order, then an `anchor` in place of
|
|
48
|
+
the `$` that ends the pattern — then compiles."""
|
|
49
|
+
adapted = pattern
|
|
50
|
+
replacements = spec.value("grammar", "adaptations", _DIALECT, "replace") or []
|
|
51
|
+
for pair in replacements:
|
|
52
|
+
if not (isinstance(pair, list) and len(pair) == 2):
|
|
53
|
+
continue
|
|
54
|
+
source, target = pair
|
|
55
|
+
if isinstance(source, str) and isinstance(target, str):
|
|
56
|
+
adapted = adapted.replace(source, target)
|
|
57
|
+
anchor = spec.value("grammar", "adaptations", _DIALECT, "anchor")
|
|
58
|
+
if isinstance(anchor, str) and adapted.endswith("$"):
|
|
59
|
+
adapted = adapted[:-1] + anchor
|
|
60
|
+
try:
|
|
61
|
+
return re.compile(adapted)
|
|
62
|
+
except re.error as error:
|
|
63
|
+
raise SpecVersionError(f"pattern {pattern} does not compile in {_DIALECT}: {error}") from error
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
class Grammar:
|
|
67
|
+
"""The compiled patterns every parse and build step needs, plus a lazily-compiled cache for every
|
|
68
|
+
other pattern the specification declares (a form, a dispatch entry, a refinement)."""
|
|
69
|
+
|
|
70
|
+
def __init__(self, top: re.Pattern[str], state_pair: re.Pattern[str], fragment_pair: re.Pattern[str]) -> None:
|
|
71
|
+
self.top = top
|
|
72
|
+
self.state_pair = state_pair
|
|
73
|
+
self.fragment_pair = fragment_pair
|
|
74
|
+
self._others: dict[str, re.Pattern[str]] = {}
|
|
75
|
+
|
|
76
|
+
@classmethod
|
|
77
|
+
def compile_for(cls, spec: Spec) -> Grammar:
|
|
78
|
+
_assert_structure(spec)
|
|
79
|
+
return cls(
|
|
80
|
+
top=compile_pattern(spec, spec.grammar_expression()),
|
|
81
|
+
state_pair=compile_pattern(spec, spec.get_str("grammar", "state", "pair")),
|
|
82
|
+
fragment_pair=compile_pattern(spec, spec.get_str("grammar", "fragment", "pair")),
|
|
83
|
+
)
|
|
84
|
+
|
|
85
|
+
def matches(self, spec: Spec, source: str, value: str) -> bool:
|
|
86
|
+
"""Whether any other pattern the spec declares (a form, a dispatch entry, a refinement) matches
|
|
87
|
+
the whole value."""
|
|
88
|
+
compiled = self._others.get(source)
|
|
89
|
+
if compiled is None:
|
|
90
|
+
compiled = compile_pattern(spec, source)
|
|
91
|
+
self._others[source] = compiled
|
|
92
|
+
return compiled.fullmatch(value) is not None
|
ref_id/parse.py
ADDED
|
@@ -0,0 +1,236 @@
|
|
|
1
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
"""Decomposes a `ref:` identifier string against the loaded spec. Order, and the precedence it
|
|
3
|
+
produces: grammar -> version -> state and fragment decomposition, including the repeated-key policy ->
|
|
4
|
+
(unsupported version stops here, decomposed and unvalidated) -> the two sides must not trade keys ->
|
|
5
|
+
qualifier values against their forms -> refinement values against their patterns and range constraints ->
|
|
6
|
+
dispatch: unknown type is `uncovered`, else the locator goes to its validator.
|
|
7
|
+
|
|
8
|
+
Ported from `crates/ref-id/src/parse.rs`.
|
|
9
|
+
"""
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
from dataclasses import replace
|
|
13
|
+
from typing import Any
|
|
14
|
+
|
|
15
|
+
from .encoding import decode, strictly_encoded, table_for
|
|
16
|
+
from .grammar import Grammar, scheme_prefix
|
|
17
|
+
from .spec import Spec, load_spec
|
|
18
|
+
from .types import Fragment, ParseResult
|
|
19
|
+
from .validators import (
|
|
20
|
+
assert_implemented,
|
|
21
|
+
delegated_string,
|
|
22
|
+
range_holds,
|
|
23
|
+
validate_locator,
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
__all__ = ["parse"]
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def _bounded_int(text: str, maximum: int) -> int:
|
|
30
|
+
"""A digit-only literal capped at `maximum`, without ever handing CPython's `int()` a literal longer
|
|
31
|
+
than `maximum` itself can be — which keeps this independent of `sys.get_int_max_str_digits()`. Leading
|
|
32
|
+
zeros are stripped first (`"007"` and `"7"` must compare the same); a literal whose stripped digit
|
|
33
|
+
count exceeds `str(maximum)`'s is above `maximum` on digit count alone, so it is never converted.
|
|
34
|
+
Whatever survives to `int()` is no longer than `maximum`'s own digit count, which is always small."""
|
|
35
|
+
stripped = text.lstrip("0") or "0"
|
|
36
|
+
if len(stripped) > len(str(maximum)):
|
|
37
|
+
return maximum
|
|
38
|
+
return min(int(stripped), maximum)
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
class _Parser:
|
|
42
|
+
def __init__(self, spec: Spec, grammar: Grammar) -> None:
|
|
43
|
+
self.spec = spec
|
|
44
|
+
self.grammar = grammar
|
|
45
|
+
|
|
46
|
+
def _malformed(self, input_: str, part: str, head: ParseResult | None) -> ParseResult:
|
|
47
|
+
return self._malformed_at(input_, self.spec.part(part), head)
|
|
48
|
+
|
|
49
|
+
def _malformed_at(self, input_: str, part: str, head: ParseResult | None) -> ParseResult:
|
|
50
|
+
"""Malformed at a key the identifier itself carries — reported verbatim, not checked against the
|
|
51
|
+
vocabulary this package names, because an undeclared key is carried through by policy."""
|
|
52
|
+
base = head if head is not None else ParseResult(
|
|
53
|
+
input="",
|
|
54
|
+
status="",
|
|
55
|
+
version=self.spec.get_int("version", "default"),
|
|
56
|
+
explicit_version=False,
|
|
57
|
+
type="",
|
|
58
|
+
locator="",
|
|
59
|
+
)
|
|
60
|
+
return replace(
|
|
61
|
+
base,
|
|
62
|
+
input=input_,
|
|
63
|
+
status=self.spec.status("malformed"),
|
|
64
|
+
delegated=None,
|
|
65
|
+
canonical=None,
|
|
66
|
+
nested=None,
|
|
67
|
+
part=part,
|
|
68
|
+
)
|
|
69
|
+
|
|
70
|
+
def _pairs(self, pattern: Any, segments: list[str], failed_part: str) -> tuple[str, Any]:
|
|
71
|
+
"""Splits `key=value` segments with the given pair grammar; a repeated key is malformed at that
|
|
72
|
+
key. Returns `("ok", tuple[Pair, ...])`, `("failed", part)` or `("repeated", key)`."""
|
|
73
|
+
out: list[tuple[str, str]] = []
|
|
74
|
+
seen: set[str] = set()
|
|
75
|
+
for segment in segments:
|
|
76
|
+
match = pattern.fullmatch(segment)
|
|
77
|
+
if match is None:
|
|
78
|
+
return "failed", failed_part
|
|
79
|
+
key = match.group("key") or ""
|
|
80
|
+
if key in seen:
|
|
81
|
+
return "repeated", key
|
|
82
|
+
seen.add(key)
|
|
83
|
+
out.append((key, match.group("value") or ""))
|
|
84
|
+
return "ok", tuple(out)
|
|
85
|
+
|
|
86
|
+
def _state(self, raw: str) -> tuple[str, Any]:
|
|
87
|
+
segments = raw.split(self.spec.get_str("grammar", "state", "separator"))
|
|
88
|
+
return self._pairs(self.grammar.state_pair, segments, "state")
|
|
89
|
+
|
|
90
|
+
def _fragment(self, raw: str) -> tuple[str, Any]:
|
|
91
|
+
segments = raw.split(self.spec.get_str("grammar", "fragment", "separator"))
|
|
92
|
+
path = segments[0] if segments else ""
|
|
93
|
+
if not path:
|
|
94
|
+
return "failed", "fragment"
|
|
95
|
+
kind, value = self._pairs(self.grammar.fragment_pair, segments[1:], "fragment")
|
|
96
|
+
if kind == "ok":
|
|
97
|
+
return "ok", Fragment(path=path, refinements=value)
|
|
98
|
+
return kind, value
|
|
99
|
+
|
|
100
|
+
def _try_nested(self, form: dict[str, Any], value: str, depth: int) -> str | None:
|
|
101
|
+
"""A qualifier value that nests an identifier: strictly encoded, decoded with the form's table,
|
|
102
|
+
parsed one level down."""
|
|
103
|
+
max_depth = form.get("depth", 0)
|
|
104
|
+
if not isinstance(max_depth, int) or depth >= max_depth or not value.startswith(scheme_prefix(self.spec)):
|
|
105
|
+
return None
|
|
106
|
+
table = table_for(self.spec, form)
|
|
107
|
+
if not strictly_encoded(value, table):
|
|
108
|
+
return None
|
|
109
|
+
decoded = decode(value, table)
|
|
110
|
+
inner = self.parse(decoded, depth + 1)
|
|
111
|
+
return None if inner.status == self.spec.status("malformed") else decoded
|
|
112
|
+
|
|
113
|
+
def _match_forms(self, names: list[str], value: str, depth: int) -> tuple[bool, str | None]:
|
|
114
|
+
forms = self.spec.get_object("forms") or {}
|
|
115
|
+
for name in names:
|
|
116
|
+
form = forms.get(name)
|
|
117
|
+
if not isinstance(form, dict):
|
|
118
|
+
continue
|
|
119
|
+
if form.get("nested") is True:
|
|
120
|
+
nested = self._try_nested(form, value, depth)
|
|
121
|
+
if nested is not None:
|
|
122
|
+
return True, nested
|
|
123
|
+
continue
|
|
124
|
+
pattern = form.get("pattern")
|
|
125
|
+
if isinstance(pattern, str) and self.grammar.matches(self.spec, pattern, value):
|
|
126
|
+
return True, None
|
|
127
|
+
return False, None
|
|
128
|
+
|
|
129
|
+
def parse(self, input_: str, depth: int) -> ParseResult:
|
|
130
|
+
match = self.grammar.top.fullmatch(input_)
|
|
131
|
+
if match is None:
|
|
132
|
+
return self._malformed(input_, "grammar", None)
|
|
133
|
+
|
|
134
|
+
def opt(name: str) -> str | None:
|
|
135
|
+
return match.groupdict().get(name)
|
|
136
|
+
|
|
137
|
+
version_text = opt("version")
|
|
138
|
+
type_ = opt("type") or ""
|
|
139
|
+
locator = opt("locator") or ""
|
|
140
|
+
explicit_version = version_text is not None
|
|
141
|
+
# `[0-9]+` guarantees a digit-only literal, but `int()` on one long enough hits CPython's
|
|
142
|
+
# int-conversion digit-count guard (`sys.get_int_max_str_digits()`, 4300 by default and settable
|
|
143
|
+
# as low as 640) — a literal that long is already far above `version.maximum`, so `_bounded_int`
|
|
144
|
+
# decides by digit count before ever converting; `versionText` below keeps the literal exactly as
|
|
145
|
+
# written regardless.
|
|
146
|
+
version = self.spec.get_int("version", "default")
|
|
147
|
+
if version_text is not None:
|
|
148
|
+
maximum = self.spec.get_int("version", "maximum")
|
|
149
|
+
version = _bounded_int(version_text, maximum)
|
|
150
|
+
|
|
151
|
+
head = ParseResult(
|
|
152
|
+
input=input_,
|
|
153
|
+
status="",
|
|
154
|
+
version=version,
|
|
155
|
+
explicit_version=explicit_version,
|
|
156
|
+
type=type_,
|
|
157
|
+
locator=locator,
|
|
158
|
+
version_text=version_text,
|
|
159
|
+
)
|
|
160
|
+
|
|
161
|
+
state_raw = opt("state")
|
|
162
|
+
if state_raw is not None:
|
|
163
|
+
kind, value = self._state(state_raw)
|
|
164
|
+
if kind == "failed":
|
|
165
|
+
return self._malformed(input_, value, head)
|
|
166
|
+
if kind == "repeated":
|
|
167
|
+
return self._malformed_at(input_, value, head)
|
|
168
|
+
head = replace(head, qualifiers=value)
|
|
169
|
+
|
|
170
|
+
fragment_raw = opt("fragment")
|
|
171
|
+
if fragment_raw is not None:
|
|
172
|
+
kind, value = self._fragment(fragment_raw)
|
|
173
|
+
if kind == "failed":
|
|
174
|
+
return self._malformed(input_, value, head)
|
|
175
|
+
if kind == "repeated":
|
|
176
|
+
return self._malformed_at(input_, value, head)
|
|
177
|
+
head = replace(head, fragment=value)
|
|
178
|
+
|
|
179
|
+
base = replace(head, status=self.spec.status("ok"), delegated=delegated_string(self.spec, type_, locator))
|
|
180
|
+
|
|
181
|
+
if version not in self.spec.get_ints("version", "supported"):
|
|
182
|
+
return replace(base, status=self.spec.status("unsupported"))
|
|
183
|
+
|
|
184
|
+
refinements_table = self.spec.get_object("refinements") or {}
|
|
185
|
+
qualifiers_table = self.spec.get_object("qualifiers") or {}
|
|
186
|
+
for key, _value in head.qualifiers:
|
|
187
|
+
if key in refinements_table:
|
|
188
|
+
return self._malformed(input_, key, head)
|
|
189
|
+
if head.fragment is not None:
|
|
190
|
+
for key, _value in head.fragment.refinements:
|
|
191
|
+
if key in qualifiers_table:
|
|
192
|
+
return self._malformed(input_, key, head)
|
|
193
|
+
|
|
194
|
+
nested: list[tuple[str, str]] = []
|
|
195
|
+
for key, value in head.qualifiers:
|
|
196
|
+
declared = qualifiers_table.get(key)
|
|
197
|
+
if not isinstance(declared, dict):
|
|
198
|
+
continue # unknownKey: carry-through
|
|
199
|
+
forms = [name for name in declared.get("forms", []) if isinstance(name, str)]
|
|
200
|
+
matches, inner = self._match_forms(forms, value, depth)
|
|
201
|
+
if not matches:
|
|
202
|
+
return self._malformed(input_, key, head)
|
|
203
|
+
if inner is not None:
|
|
204
|
+
nested.append((key, inner))
|
|
205
|
+
if nested:
|
|
206
|
+
base = replace(base, nested=tuple(nested))
|
|
207
|
+
|
|
208
|
+
if head.fragment is not None:
|
|
209
|
+
for key, value in head.fragment.refinements:
|
|
210
|
+
declared = refinements_table.get(key)
|
|
211
|
+
if not isinstance(declared, dict):
|
|
212
|
+
continue # unknownRefinement: carry-through
|
|
213
|
+
pattern = declared.get("pattern", "")
|
|
214
|
+
if not self.grammar.matches(self.spec, pattern, value) or not range_holds(self.spec, declared, value):
|
|
215
|
+
return self._malformed(input_, key, head)
|
|
216
|
+
|
|
217
|
+
dispatch = self.spec.get_object("dispatch") or {}
|
|
218
|
+
entry = dispatch.get(type_)
|
|
219
|
+
if not isinstance(entry, dict) or base.delegated is None:
|
|
220
|
+
return replace(base, status=self.spec.unknown_type())
|
|
221
|
+
validation = validate_locator(self.spec, entry, base.delegated)
|
|
222
|
+
if validation is None:
|
|
223
|
+
return replace(base, status=self.spec.unknown_type())
|
|
224
|
+
if not validation.ok:
|
|
225
|
+
return self._malformed(input_, "locator", head)
|
|
226
|
+
return replace(base, canonical=validation.canonical)
|
|
227
|
+
|
|
228
|
+
|
|
229
|
+
def parse(input_: str, /) -> ParseResult:
|
|
230
|
+
"""Parses a `ref:` identifier string against the embedded spec. Never raises for an identifier
|
|
231
|
+
problem; it raises only when the embedded specification itself cannot be honoured."""
|
|
232
|
+
if not isinstance(input_, str):
|
|
233
|
+
raise TypeError("parse() takes a string")
|
|
234
|
+
spec = load_spec()
|
|
235
|
+
assert_implemented(spec)
|
|
236
|
+
return _Parser(spec, spec.grammar()).parse(input_, 0)
|
ref_id/py.typed
ADDED
|
File without changes
|