chtypes 2.0.0.dev2__py3-none-any.whl → 2.0.0.dev4__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.
- chtypes/__init__.py +12 -0
- chtypes/_abi2/_decls.py +2 -2
- chtypes/_abi2/_errmap.py +1 -1
- chtypes/_abi2/_vocab.py +56 -5
- chtypes/_decode.py +48 -1
- chtypes/_ocifetch/_channel.py +111 -0
- chtypes/_ocifetch/_constants.py +5 -1
- chtypes/_ocifetch/_ensure.py +44 -6
- chtypes/_ocifetch/_errors.py +15 -1
- chtypes/_ocifetch/_http.py +46 -1
- chtypes/_ocifetch/_oci.py +36 -4
- chtypes/_ocifetch/_retired.py +77 -0
- chtypes/errors.py +12 -0
- chtypes/registry.py +1 -0
- chtypes/results.py +59 -4
- {chtypes-2.0.0.dev2.dist-info → chtypes-2.0.0.dev4.dist-info}/METADATA +4 -2
- {chtypes-2.0.0.dev2.dist-info → chtypes-2.0.0.dev4.dist-info}/RECORD +20 -19
- {chtypes-2.0.0.dev2.dist-info → chtypes-2.0.0.dev4.dist-info}/WHEEL +0 -0
- {chtypes-2.0.0.dev2.dist-info → chtypes-2.0.0.dev4.dist-info}/entry_points.txt +0 -0
- {chtypes-2.0.0.dev2.dist-info → chtypes-2.0.0.dev4.dist-info}/licenses/LICENSE +0 -0
chtypes/__init__.py
CHANGED
|
@@ -48,11 +48,13 @@ from importlib.metadata import version as _package_version
|
|
|
48
48
|
|
|
49
49
|
from ._abi2._vocab import (
|
|
50
50
|
EXPORT_NONE,
|
|
51
|
+
DeclinedTier,
|
|
51
52
|
DefaultKind,
|
|
52
53
|
DiscoverQueryParam,
|
|
53
54
|
DocFlags,
|
|
54
55
|
FilterOutcome,
|
|
55
56
|
Format,
|
|
57
|
+
MergeReason,
|
|
56
58
|
Outcome,
|
|
57
59
|
Reason,
|
|
58
60
|
Source,
|
|
@@ -71,6 +73,7 @@ from .errors import (
|
|
|
71
73
|
CODE_CACHE_UNUSABLE,
|
|
72
74
|
CODE_SOURCE_FORBIDDEN,
|
|
73
75
|
CODE_SOURCE_INCOMPATIBLE,
|
|
76
|
+
CODE_SOURCE_RETIRED,
|
|
74
77
|
CODE_SOURCE_UNAUTHORIZED,
|
|
75
78
|
CODE_SOURCE_UNREACHABLE,
|
|
76
79
|
ArtifactCorruptError,
|
|
@@ -87,6 +90,7 @@ from .errors import (
|
|
|
87
90
|
SchemaError,
|
|
88
91
|
SourceForbiddenError,
|
|
89
92
|
SourceIncompatibleError,
|
|
93
|
+
SourceRetiredError,
|
|
90
94
|
SourceUnauthorizedError,
|
|
91
95
|
SourceUnreachableError,
|
|
92
96
|
UnsupportedError,
|
|
@@ -95,11 +99,13 @@ from .errors import (
|
|
|
95
99
|
from .library import Block, Filter, Library, Schema, Server, open_unverified
|
|
96
100
|
from .registry import FetchOptions, Registry, Resolved, TrustedKey, cache_root, search_dirs
|
|
97
101
|
from .results import (
|
|
102
|
+
AtMergeEntry,
|
|
98
103
|
BatchResult,
|
|
99
104
|
BuildInfo,
|
|
100
105
|
Capabilities,
|
|
101
106
|
Column,
|
|
102
107
|
Computed,
|
|
108
|
+
DeclinedSetting,
|
|
103
109
|
DiscoveredColumn,
|
|
104
110
|
Discovery,
|
|
105
111
|
EngineCell,
|
|
@@ -128,6 +134,7 @@ __all__ = [
|
|
|
128
134
|
"CODE_CACHE_UNUSABLE",
|
|
129
135
|
"CODE_SOURCE_FORBIDDEN",
|
|
130
136
|
"CODE_SOURCE_INCOMPATIBLE",
|
|
137
|
+
"CODE_SOURCE_RETIRED",
|
|
131
138
|
"CODE_SOURCE_UNAUTHORIZED",
|
|
132
139
|
"CODE_SOURCE_UNREACHABLE",
|
|
133
140
|
"EXPORT_NONE",
|
|
@@ -138,6 +145,7 @@ __all__ = [
|
|
|
138
145
|
"ArtifactPinnedError",
|
|
139
146
|
"ArtifactUnpublishedError",
|
|
140
147
|
"ArtifactUntrustedError",
|
|
148
|
+
"AtMergeEntry",
|
|
141
149
|
"BatchResult",
|
|
142
150
|
"Block",
|
|
143
151
|
"BuildInfo",
|
|
@@ -148,6 +156,8 @@ __all__ = [
|
|
|
148
156
|
"ChtypesError",
|
|
149
157
|
"Column",
|
|
150
158
|
"Computed",
|
|
159
|
+
"DeclinedSetting",
|
|
160
|
+
"DeclinedTier",
|
|
151
161
|
"DefaultKind",
|
|
152
162
|
"DiscoverQueryParam",
|
|
153
163
|
"DiscoveredColumn",
|
|
@@ -166,6 +176,7 @@ __all__ = [
|
|
|
166
176
|
"Header",
|
|
167
177
|
"InternalError",
|
|
168
178
|
"Library",
|
|
179
|
+
"MergeReason",
|
|
169
180
|
"Outcome",
|
|
170
181
|
"Reason",
|
|
171
182
|
"Registry",
|
|
@@ -182,6 +193,7 @@ __all__ = [
|
|
|
182
193
|
"Source",
|
|
183
194
|
"SourceForbiddenError",
|
|
184
195
|
"SourceIncompatibleError",
|
|
196
|
+
"SourceRetiredError",
|
|
185
197
|
"SourceUnauthorizedError",
|
|
186
198
|
"SourceUnreachableError",
|
|
187
199
|
"Span",
|
chtypes/_abi2/_decls.py
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
# GENERATED by scripts/abi-v1/gen.py --major 2 from spec/abi-v2/abi.json (CHS_ABI_FINGERPRINT sha256:
|
|
1
|
+
# GENERATED by scripts/abi-v1/gen.py --major 2 from spec/abi-v2/abi.json (CHS_ABI_FINGERPRINT sha256:28c7445ce6b5c6d0ae33177fd60eb7b3b5654a966c842b8d812aea622824dc72) — DO NOT EDIT # noqa: E501
|
|
2
2
|
"""The Python binding's generated ABI v2 layer: ctypes signatures, the handle
|
|
3
3
|
classes, the two-phase resolve functions and invoke_by_name(), the test-only
|
|
4
4
|
dispatcher the conformance suite drives from tests/fixtures/abi-v2/cases.json.
|
|
@@ -19,7 +19,7 @@ from dataclasses import dataclass, field
|
|
|
19
19
|
from . import _errmap
|
|
20
20
|
|
|
21
21
|
CHS_ABI_VERSION = 2
|
|
22
|
-
CHS_ABI_FINGERPRINT = "sha256:
|
|
22
|
+
CHS_ABI_FINGERPRINT = "sha256:28c7445ce6b5c6d0ae33177fd60eb7b3b5654a966c842b8d812aea622824dc72"
|
|
23
23
|
|
|
24
24
|
# The description's stability: "unstable" while this generation is being
|
|
25
25
|
# designed, so CHS_ABI_FINGERPRINT moves with every change, and "locked" after
|
chtypes/_abi2/_errmap.py
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
# GENERATED by scripts/abi-v1/gen.py --major 2 from spec/abi-v2/abi.json (CHS_ABI_FINGERPRINT sha256:
|
|
1
|
+
# GENERATED by scripts/abi-v1/gen.py --major 2 from spec/abi-v2/abi.json (CHS_ABI_FINGERPRINT sha256:28c7445ce6b5c6d0ae33177fd60eb7b3b5654a966c842b8d812aea622824dc72) — DO NOT EDIT # noqa: E501
|
|
2
2
|
"""spec/abi-v2/sdk.json's error-mapping tables, as references to the
|
|
3
3
|
public classes (python/src/chtypes/errors.py, re-exported by
|
|
4
4
|
python/src/chtypes/_abi2/_errors.py). Only the MAPPING is generated: the
|
chtypes/_abi2/_vocab.py
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
# GENERATED by scripts/abi-v1/gen.py --major 2 from spec/abi-v2/abi.json (CHS_ABI_FINGERPRINT sha256:
|
|
1
|
+
# GENERATED by scripts/abi-v1/gen.py --major 2 from spec/abi-v2/abi.json (CHS_ABI_FINGERPRINT sha256:28c7445ce6b5c6d0ae33177fd60eb7b3b5654a966c842b8d812aea622824dc72) — DO NOT EDIT # noqa: E501
|
|
2
2
|
"""The Python binding's generated vocabularies, from spec/abi-v2/abi.json: the C
|
|
3
3
|
enums (`Format`, `Status`), the document vocabularies (`Outcome`, `FilterOutcome`,
|
|
4
|
-
`Verdict`, `DefaultKind`, `
|
|
5
|
-
description attaches to each value
|
|
6
|
-
is_stored, `Verdict.answered`), and the
|
|
7
|
-
package keeps a copy of any of it.
|
|
4
|
+
`Verdict`, `DefaultKind`, `MergeReason`, `DeclinedTier`, `Reason`, `Source`,
|
|
5
|
+
`DiscoverQueryParam`) with the facts the description attaches to each value
|
|
6
|
+
(`Format.ch_name`, `Reason` lossy, `Source` is_stored, `Verdict.answered`), and the
|
|
7
|
+
`DocFlags` groups. No other file in this package keeps a copy of any of it.
|
|
8
8
|
|
|
9
9
|
Rule r3 (spec/abi-v2/docs.md): every vocabulary has an unknown(n) member, and a
|
|
10
10
|
value the description does not list reads as it, carrying the raw value, for that
|
|
@@ -225,6 +225,55 @@ class DefaultKind(StrEnum):
|
|
|
225
225
|
return cls(text)
|
|
226
226
|
|
|
227
227
|
|
|
228
|
+
class MergeReason(StrEnum):
|
|
229
|
+
"""What a merge would do to one row of the part an INSERT writes, as a batch's
|
|
230
|
+
`at_merge` reports it."""
|
|
231
|
+
|
|
232
|
+
TTL_DELETE = "ttl_delete"
|
|
233
|
+
TTL_COLUMN_RESET = "ttl_column_reset"
|
|
234
|
+
|
|
235
|
+
@classmethod
|
|
236
|
+
def _missing_(cls, value: object) -> MergeReason | None:
|
|
237
|
+
return _unknown_str(cls, value)
|
|
238
|
+
|
|
239
|
+
@property
|
|
240
|
+
def known(self) -> bool:
|
|
241
|
+
"""Whether the description lists this value: False for exactly the unknown(n)
|
|
242
|
+
member an unlisted value reads as (rule r3)."""
|
|
243
|
+
return type(self)._value2member_map_.get(self._value_) is self
|
|
244
|
+
|
|
245
|
+
@classmethod
|
|
246
|
+
def of(cls, text: str) -> MergeReason:
|
|
247
|
+
"""Map a document's spelling. One the description does not list is kept as
|
|
248
|
+
this vocabulary's unknown(n) member, carrying it (rule r3)."""
|
|
249
|
+
return cls(text)
|
|
250
|
+
|
|
251
|
+
|
|
252
|
+
class DeclinedTier(StrEnum):
|
|
253
|
+
"""The WHERE-settings tier of a setting a server profile sets and this build's
|
|
254
|
+
filters do not honor in a WHERE, as a `DeclinedSetting` reports it."""
|
|
255
|
+
|
|
256
|
+
PREDICATE = "predicate"
|
|
257
|
+
PREDICATE_UNFLIPPED = "predicate-unflipped"
|
|
258
|
+
RESULT_CONTENT = "result-content"
|
|
259
|
+
|
|
260
|
+
@classmethod
|
|
261
|
+
def _missing_(cls, value: object) -> DeclinedTier | None:
|
|
262
|
+
return _unknown_str(cls, value)
|
|
263
|
+
|
|
264
|
+
@property
|
|
265
|
+
def known(self) -> bool:
|
|
266
|
+
"""Whether the description lists this value: False for exactly the unknown(n)
|
|
267
|
+
member an unlisted value reads as (rule r3)."""
|
|
268
|
+
return type(self)._value2member_map_.get(self._value_) is self
|
|
269
|
+
|
|
270
|
+
@classmethod
|
|
271
|
+
def of(cls, text: str) -> DeclinedTier:
|
|
272
|
+
"""Map a document's spelling. One the description does not list is kept as
|
|
273
|
+
this vocabulary's unknown(n) member, carrying it (rule r3)."""
|
|
274
|
+
return cls(text)
|
|
275
|
+
|
|
276
|
+
|
|
228
277
|
class DiscoverQueryParam(StrEnum):
|
|
229
278
|
"""A query parameter of the SQL `discover_query` returns, which the caller binds
|
|
230
279
|
when it runs the query."""
|
|
@@ -383,6 +432,7 @@ DESCRIBED_VOCABULARIES: Final[dict[str, type]] = {
|
|
|
383
432
|
"chs_status": Status,
|
|
384
433
|
"chs_format": Format,
|
|
385
434
|
"transform_reason": Reason,
|
|
435
|
+
"merge_reason": MergeReason,
|
|
386
436
|
"value_src": Source,
|
|
387
437
|
"row_outcome": Outcome,
|
|
388
438
|
"batch_outcome": Outcome,
|
|
@@ -390,6 +440,7 @@ DESCRIBED_VOCABULARIES: Final[dict[str, type]] = {
|
|
|
390
440
|
"filter_verdict": Verdict,
|
|
391
441
|
"discover_query_param": DiscoverQueryParam,
|
|
392
442
|
"default_kind": DefaultKind,
|
|
443
|
+
"declined_tier": DeclinedTier,
|
|
393
444
|
}
|
|
394
445
|
|
|
395
446
|
|
chtypes/_decode.py
CHANGED
|
@@ -28,14 +28,25 @@ import json
|
|
|
28
28
|
from collections.abc import Callable, Mapping
|
|
29
29
|
from typing import Any
|
|
30
30
|
|
|
31
|
-
from ._abi2._vocab import
|
|
31
|
+
from ._abi2._vocab import (
|
|
32
|
+
DeclinedTier,
|
|
33
|
+
DefaultKind,
|
|
34
|
+
FilterOutcome,
|
|
35
|
+
MergeReason,
|
|
36
|
+
Outcome,
|
|
37
|
+
Reason,
|
|
38
|
+
Source,
|
|
39
|
+
Verdict,
|
|
40
|
+
)
|
|
32
41
|
from .errors import ArtifactCorruptError, internal
|
|
33
42
|
from .results import (
|
|
43
|
+
AtMergeEntry,
|
|
34
44
|
BatchResult,
|
|
35
45
|
BuildInfo,
|
|
36
46
|
Capabilities,
|
|
37
47
|
Column,
|
|
38
48
|
Computed,
|
|
49
|
+
DeclinedSetting,
|
|
39
50
|
DiscoveredColumn,
|
|
40
51
|
Discovery,
|
|
41
52
|
EngineCell,
|
|
@@ -325,6 +336,29 @@ def _engine_cell(obj: dict[str, Any], where: str) -> EngineCell:
|
|
|
325
336
|
)
|
|
326
337
|
|
|
327
338
|
|
|
339
|
+
def _int_list(obj: Mapping[str, Any], key: str, where: str) -> tuple[int, ...] | None:
|
|
340
|
+
"""A list of non-negative integers: None when absent."""
|
|
341
|
+
if obj.get(key) is None:
|
|
342
|
+
return None
|
|
343
|
+
out = []
|
|
344
|
+
for item in _list(obj, key, where):
|
|
345
|
+
if isinstance(item, bool) or not isinstance(item, int) or item < 0:
|
|
346
|
+
raise _wrong(where, key, "an array of non-negative integers", item)
|
|
347
|
+
out.append(item)
|
|
348
|
+
return tuple(out)
|
|
349
|
+
|
|
350
|
+
|
|
351
|
+
def _at_merge(obj: dict[str, Any], where: str) -> AtMergeEntry:
|
|
352
|
+
# An unlisted reason is its unknown(n): kept, never a failure (rule r3).
|
|
353
|
+
return AtMergeEntry(
|
|
354
|
+
row=_int(obj, "row", where),
|
|
355
|
+
reason=MergeReason.of(_text(obj, "reason", where)),
|
|
356
|
+
column=_bytes_or_none(obj, "column", where),
|
|
357
|
+
stored=_bytes_or_none(obj, "stored", where),
|
|
358
|
+
input_rows=_int_list(obj, "input_rows", where),
|
|
359
|
+
)
|
|
360
|
+
|
|
361
|
+
|
|
328
362
|
def _engine_rows(obj: Mapping[str, Any], where: str) -> tuple[tuple[EngineCell, ...], ...]:
|
|
329
363
|
rows = []
|
|
330
364
|
for i, row in enumerate(_list(obj, "engine_rows", where)):
|
|
@@ -411,6 +445,8 @@ def decode_batch(raw: bytes, export: bytes | None) -> BatchResult:
|
|
|
411
445
|
framing=None
|
|
412
446
|
if framing is None
|
|
413
447
|
else _framing(_obj(framing, where, "framing"), f"{where}.framing"),
|
|
448
|
+
unsupported_settings=_name_list(obj, "unsupported_settings", where),
|
|
449
|
+
at_merge=_objects(obj, "at_merge", where, _at_merge),
|
|
414
450
|
)
|
|
415
451
|
|
|
416
452
|
|
|
@@ -467,11 +503,22 @@ def _string_map(obj: Mapping[str, Any], key: str, where: str) -> dict[str, str]
|
|
|
467
503
|
return out
|
|
468
504
|
|
|
469
505
|
|
|
506
|
+
def _declined_setting(obj: dict[str, Any], where: str) -> DeclinedSetting:
|
|
507
|
+
# An unlisted tier is its unknown(n): kept, never a failure (rule r3).
|
|
508
|
+
return DeclinedSetting(
|
|
509
|
+
name=_name(obj, "name", where),
|
|
510
|
+
tier=DeclinedTier.of(_text(obj, "tier", where)),
|
|
511
|
+
)
|
|
512
|
+
|
|
513
|
+
|
|
470
514
|
def _server(obj: dict[str, Any], where: str) -> SchemaServer:
|
|
471
515
|
return SchemaServer(
|
|
472
516
|
timezone=_text(obj, "timezone", where),
|
|
473
517
|
settings=_string_map(obj, "settings", where) or {},
|
|
474
518
|
macros=_string_map(obj, "macros", where),
|
|
519
|
+
filter_declined_settings=_objects(
|
|
520
|
+
obj, "filter_declined_settings", where, _declined_setting
|
|
521
|
+
),
|
|
475
522
|
)
|
|
476
523
|
|
|
477
524
|
|
chtypes/_ocifetch/_channel.py
CHANGED
|
@@ -18,6 +18,19 @@ five ways:
|
|
|
18
18
|
used through its subroot `<cache>/v2-dev`, never as a whole layout;
|
|
19
19
|
- a signed predicate must say abi 2.
|
|
20
20
|
|
|
21
|
+
It also resolves one tag the v1 contract never asks for: a version request
|
|
22
|
+
fetches `<tag>--fp-<its own fingerprint>` first, the newest dev build of the ABI
|
|
23
|
+
this binding speaks, and falls back to `<tag>` only when no base has that alias
|
|
24
|
+
(docs/guides/fetch-v1.md §3, "The dev channel's alias step"), so a dev build of a
|
|
25
|
+
newer fingerprint never strands this one. Its cache lookups (`resolve_installed`,
|
|
26
|
+
and `ensure`'s offline answer and its monotonic rule) see only the records whose
|
|
27
|
+
signed `abi_fingerprint` is its own: a build of another fingerprint in a shared
|
|
28
|
+
cache is never returned and never kept over this one's own, and it is never
|
|
29
|
+
removed or rewritten either. It is automatic, with no override; the trust checks
|
|
30
|
+
are unchanged, and a listing never shows an alias. The fingerprint
|
|
31
|
+
is the generated `CHS_ABI_FINGERPRINT` of the ABI layer (`chtypes._abi2._decls`),
|
|
32
|
+
never a hand-written copy.
|
|
33
|
+
|
|
21
34
|
The v1 contract itself stays in this package, unchanged, because the fetch-v1
|
|
22
35
|
conformance cases (tests/fixtures/fetch-v1) are its specification and the dev
|
|
23
36
|
channel shares every other rule with it. Only a test run reaches it:
|
|
@@ -29,16 +42,20 @@ override a user can reach.
|
|
|
29
42
|
from __future__ import annotations
|
|
30
43
|
|
|
31
44
|
import os
|
|
45
|
+
import re
|
|
32
46
|
import sys
|
|
33
47
|
import threading
|
|
34
48
|
from collections.abc import Callable, Sequence
|
|
35
49
|
from dataclasses import dataclass, replace
|
|
36
50
|
from typing import Final, Protocol
|
|
37
51
|
|
|
52
|
+
from chtypes._abi2._decls import CHS_ABI_FINGERPRINT
|
|
38
53
|
from chtypes._ocifetch import _constants as C
|
|
39
54
|
from chtypes._ocifetch._dsse import TrustedKey
|
|
55
|
+
from chtypes._ocifetch._errors import ArtifactUnpublishedError
|
|
40
56
|
|
|
41
57
|
__all__ = [
|
|
58
|
+
"ALIAS_SEPARATOR",
|
|
42
59
|
"DEV_ABI_GENERATION",
|
|
43
60
|
"DEV_CACHE_DIR",
|
|
44
61
|
"DEV_CHANNEL_BASE",
|
|
@@ -50,12 +67,16 @@ __all__ = [
|
|
|
50
67
|
"PinningRefusedError",
|
|
51
68
|
"abi",
|
|
52
69
|
"active",
|
|
70
|
+
"ahead_of_registry",
|
|
71
|
+
"alias_tag",
|
|
53
72
|
"allow_overrides_for_tests",
|
|
54
73
|
"channel_name",
|
|
55
74
|
"enforce",
|
|
56
75
|
"ignored_settings_warned",
|
|
57
76
|
"pinning_requested",
|
|
58
77
|
"trusted_keys",
|
|
78
|
+
"use_own_fingerprint_for_tests",
|
|
79
|
+
"visible",
|
|
59
80
|
"use_dev_channel_for_tests",
|
|
60
81
|
"use_fetch_v1_for_tests",
|
|
61
82
|
]
|
|
@@ -79,6 +100,10 @@ DEV_RECORD_SCHEMA: Final = 2
|
|
|
79
100
|
# The abi a dev predicate must carry.
|
|
80
101
|
DEV_ABI_GENERATION: Final = 2
|
|
81
102
|
|
|
103
|
+
# Joins a tag and a fingerprint into the dev channel's alias tag:
|
|
104
|
+
# <tag>--fp-<64 lowercase hex> (docs/guides/fetch-v1.md §3).
|
|
105
|
+
ALIAS_SEPARATOR: Final = "--fp-"
|
|
106
|
+
|
|
82
107
|
# What every lock, frozen or update request gets from a 2.0.0-dev SDK, before
|
|
83
108
|
# any network call (rule r6).
|
|
84
109
|
PINNING_REFUSED: Final = (
|
|
@@ -102,6 +127,11 @@ class Channel:
|
|
|
102
127
|
keys: tuple[dict[str, str], ...] # {"keyid", "ed25519_hex"}: the default trust
|
|
103
128
|
overridable: bool # the base, trust and unsigned overrides are honored
|
|
104
129
|
pinnable: bool # lock, frozen and update are honored
|
|
130
|
+
# The fingerprint, 64 lowercase hex, this contract's SDK speaks: a version
|
|
131
|
+
# request resolves its alias tag before the tag itself, and the cache
|
|
132
|
+
# lookups see only records signed with it. "" does neither (the v1 contract
|
|
133
|
+
# and every production channel).
|
|
134
|
+
own_fingerprint: str = ""
|
|
105
135
|
|
|
106
136
|
|
|
107
137
|
# What a 2.0.0-dev SDK speaks, and what every process speaks unless a test
|
|
@@ -117,6 +147,7 @@ DEV_CHANNEL: Final = Channel(
|
|
|
117
147
|
keys=({"keyid": DEV_KEY_ID, "ed25519_hex": DEV_KEY_HEX},),
|
|
118
148
|
overridable=False,
|
|
119
149
|
pinnable=False,
|
|
150
|
+
own_fingerprint=CHS_ABI_FINGERPRINT.removeprefix("sha256:"),
|
|
120
151
|
)
|
|
121
152
|
|
|
122
153
|
# The v1 contract the fetch-v1 conformance cases specify, from the generated
|
|
@@ -202,6 +233,86 @@ def allow_overrides_for_tests() -> Callable[[], None]:
|
|
|
202
233
|
return _use(replace(DEV_CHANNEL, overridable=True))
|
|
203
234
|
|
|
204
235
|
|
|
236
|
+
_FINGERPRINT_HEX = re.compile(r"[0-9a-f]{64}")
|
|
237
|
+
|
|
238
|
+
|
|
239
|
+
def use_own_fingerprint_for_tests(fingerprint: str) -> Callable[[], None]:
|
|
240
|
+
"""Make this TEST run's active contract speak `fingerprint` (64 lowercase
|
|
241
|
+
hex) as the dev channel speaks its own: a version request resolves that
|
|
242
|
+
fingerprint's alias tag first, and the cache lookups see only records
|
|
243
|
+
signed with it. The fetch-v1 conformance cases that carry
|
|
244
|
+
`request.own_fingerprint` run under it, against fixtures that name a
|
|
245
|
+
fixture fingerprint. Everything else stays the active contract's. Returns
|
|
246
|
+
the restore function; raises outside a test run or on a fingerprint that is
|
|
247
|
+
not 64 lowercase hex."""
|
|
248
|
+
_test_only("use_own_fingerprint_for_tests")
|
|
249
|
+
if not _FINGERPRINT_HEX.fullmatch(fingerprint):
|
|
250
|
+
raise ValueError(f"use_own_fingerprint_for_tests: {fingerprint!r} is not 64 lowercase hex")
|
|
251
|
+
return _use(replace(active(), own_fingerprint=fingerprint))
|
|
252
|
+
|
|
253
|
+
|
|
254
|
+
def alias_tag(tag: str, channel: Channel | None = None) -> str | None:
|
|
255
|
+
"""The alias a version request for `tag` resolves first under a contract
|
|
256
|
+
with an own fingerprint, or None when it resolves `tag` alone: under the
|
|
257
|
+
v1 contract and every production channel, and for a request that is not a
|
|
258
|
+
version spelling (an arbitrary tag has no alias)."""
|
|
259
|
+
channel = channel or active()
|
|
260
|
+
if not channel.own_fingerprint or not re.fullmatch(C.SPELLING_REGEX, tag):
|
|
261
|
+
return None
|
|
262
|
+
return f"{tag}{ALIAS_SEPARATOR}{channel.own_fingerprint}"
|
|
263
|
+
|
|
264
|
+
|
|
265
|
+
def visible(predicate: object, channel: Channel | None = None) -> bool:
|
|
266
|
+
"""Whether a cache record (or a pre-seeded entry) whose SIGNED predicate is
|
|
267
|
+
`predicate` may answer a request under the active contract: under one with
|
|
268
|
+
an own fingerprint (the dev channel), only when the predicate's
|
|
269
|
+
`abi_fingerprint` is that fingerprint; under every other, always. A record
|
|
270
|
+
it cannot see is never returned and never kept by the monotonic rule, and
|
|
271
|
+
nothing removes or rewrites it: another SDK of another fingerprint owns it."""
|
|
272
|
+
channel = channel or active()
|
|
273
|
+
if not channel.own_fingerprint:
|
|
274
|
+
return True
|
|
275
|
+
fingerprint = predicate.get("abi_fingerprint") if isinstance(predicate, dict) else None
|
|
276
|
+
return fingerprint == f"sha256:{channel.own_fingerprint}"
|
|
277
|
+
|
|
278
|
+
|
|
279
|
+
def ahead_of_registry(
|
|
280
|
+
alias_absent: bool, predicate: object, channel: Channel | None = None
|
|
281
|
+
) -> None:
|
|
282
|
+
"""The dev channel's answer for an SDK whose fingerprint no published build
|
|
283
|
+
carries yet (docs/guides/fetch-v1.md §3; public issue #578): when every base
|
|
284
|
+
answered its own alias 404 (`alias_absent`) and the build the tag names is
|
|
285
|
+
signed for another fingerprint (`predicate`, the SIGNED predicate, the field
|
|
286
|
+
`visible` keys on), raise `ArtifactUnpublishedError`, the code a request no
|
|
287
|
+
build answers gets, naming both fingerprints. Nothing is installed and an
|
|
288
|
+
install of that build is never reported, because the lookup this SDK opens
|
|
289
|
+
through would refuse it. Returns in every other case: the alias answered,
|
|
290
|
+
the tag's build is this SDK's own, or the contract has no own fingerprint."""
|
|
291
|
+
channel = channel or active()
|
|
292
|
+
if not alias_absent or visible(predicate, channel):
|
|
293
|
+
return
|
|
294
|
+
raise ArtifactUnpublishedError(f"chtypes: {_ahead_message(channel.own_fingerprint, predicate)}")
|
|
295
|
+
|
|
296
|
+
|
|
297
|
+
def _ahead_message(own: str, predicate: object) -> str:
|
|
298
|
+
"""`ahead_of_registry`'s message: "no published build for this SDK's
|
|
299
|
+
fingerprint <own>; newest published on this channel is <fp> (build <id>)",
|
|
300
|
+
each fingerprint 64 lowercase hex without its `sha256:` prefix, the
|
|
301
|
+
parenthesis dropped when the predicate names no build, and "unnamed" for a
|
|
302
|
+
predicate that names no fingerprint."""
|
|
303
|
+
fields = predicate if isinstance(predicate, dict) else {}
|
|
304
|
+
theirs = fields.get("abi_fingerprint")
|
|
305
|
+
theirs = theirs.removeprefix("sha256:") if isinstance(theirs, str) else ""
|
|
306
|
+
message = (
|
|
307
|
+
f"no published build for this SDK's fingerprint {own}; "
|
|
308
|
+
f"newest published on this channel is {theirs or 'unnamed'}"
|
|
309
|
+
)
|
|
310
|
+
build = fields.get("build")
|
|
311
|
+
if isinstance(build, str) and build:
|
|
312
|
+
message += f" (build {build})"
|
|
313
|
+
return message
|
|
314
|
+
|
|
315
|
+
|
|
205
316
|
def use_dev_channel_for_tests() -> Callable[[], None]:
|
|
206
317
|
"""Make this TEST run speak the dev channel exactly as a user's process does
|
|
207
318
|
(undoing either function above, for a test of the dev channel itself).
|
chtypes/_ocifetch/_constants.py
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Code generated by scripts/fetch-v1/gen-constants.py from
|
|
2
2
|
# spec/fetch-v1/constants.json. DO NOT EDIT.
|
|
3
|
-
# Source sha256:
|
|
3
|
+
# Source sha256: 4554956faeb154fc324a6120b7b35976222c6bc8ecbce7fd0147502e3073611d
|
|
4
4
|
"""The v1 fetch constants, generated: see docs/guides/fetch-v1.md."""
|
|
5
5
|
|
|
6
6
|
from __future__ import annotations
|
|
@@ -83,6 +83,7 @@ RETRY_AFTER_STATUSES: Final[tuple[int, ...]] = (429, 503)
|
|
|
83
83
|
RETRY_AFTER_OVER_BUDGET: Final[str] = "refuse"
|
|
84
84
|
DIGEST_404_POLICY: Final[str] = "next_base_then_retry_on_last"
|
|
85
85
|
TAG_404_POLICY: Final[str] = "next_base_then_unpublished"
|
|
86
|
+
RETIRED_STATUSES: Final[tuple[int, ...]] = (410,)
|
|
86
87
|
|
|
87
88
|
CONNECT_TIMEOUT_S: Final[float] = 30.0
|
|
88
89
|
IDLE_READ_TIMEOUT_S: Final[float] = 60.0
|
|
@@ -95,6 +96,8 @@ MAX_REFERRERS: Final[int] = 16
|
|
|
95
96
|
MAX_REDIRECTS: Final[int] = 5
|
|
96
97
|
MAX_UNPACKED_BYTES: Final[int] = 4294967296
|
|
97
98
|
ZSTD_WINDOW_LOG_MAX: Final[int] = 27
|
|
99
|
+
RETIRED_BODY_MAX_BYTES: Final[int] = 65536
|
|
100
|
+
RETIRED_MESSAGE_MAX_CODE_POINTS: Final[int] = 256
|
|
98
101
|
|
|
99
102
|
CACHE_ROOT_TEMPLATE: Final[str] = "${XDG_CACHE_HOME:-$HOME/.cache}/chtypes/v1"
|
|
100
103
|
SYSTEM_CACHE_DIRS: Final[tuple[str, ...]] = ("/usr/local/share/chtypes/v1", "/opt/chtypes/v1")
|
|
@@ -122,4 +125,5 @@ ERROR_EXIT_CODES: Final[dict[str, int]] = {
|
|
|
122
125
|
"CHTYPES_SOURCE_INCOMPATIBLE": 7,
|
|
123
126
|
"CHTYPES_ARTIFACT_INCOMPATIBLE": 8,
|
|
124
127
|
"CHTYPES_CACHE_UNUSABLE": 9,
|
|
128
|
+
"CHTYPES_SOURCE_RETIRED": 10,
|
|
125
129
|
}
|
chtypes/_ocifetch/_ensure.py
CHANGED
|
@@ -38,6 +38,7 @@ from chtypes._ocifetch._errors import (
|
|
|
38
38
|
ArtifactUntrustedError,
|
|
39
39
|
FetchError,
|
|
40
40
|
SourceForbiddenError,
|
|
41
|
+
SourceRetiredError,
|
|
41
42
|
SourceUnauthorizedError,
|
|
42
43
|
SourceUnreachableError,
|
|
43
44
|
)
|
|
@@ -49,6 +50,7 @@ from chtypes._ocifetch._http import (
|
|
|
49
50
|
ForbiddenHttpError,
|
|
50
51
|
NotFoundHttpError,
|
|
51
52
|
OversizeHttpError,
|
|
53
|
+
RetiredHttpError,
|
|
52
54
|
RetryPolicy,
|
|
53
55
|
TransportError,
|
|
54
56
|
UnauthorizedHttpError,
|
|
@@ -355,6 +357,10 @@ def _newer_installed(
|
|
|
355
357
|
(`26.3.4.1`) keeps only a newer BUILD of that exact version. A request
|
|
356
358
|
that is not a version spelling (an arbitrary tag) names no range, so
|
|
357
359
|
nothing is kept for it.
|
|
360
|
+
|
|
361
|
+
Under the dev channel only an install of its own fingerprint counts
|
|
362
|
+
(`_channel.visible`): a newer build of another fingerprint, installed by
|
|
363
|
+
another dev SDK, is never kept over this one's own.
|
|
358
364
|
"""
|
|
359
365
|
if not re.match(C.SPELLING_REGEX, request_spelling):
|
|
360
366
|
return None
|
|
@@ -368,6 +374,8 @@ def _newer_installed(
|
|
|
368
374
|
for dir_, record in list_verified_records(roots):
|
|
369
375
|
if record.platform != platform_key:
|
|
370
376
|
continue
|
|
377
|
+
if not _channel.visible(record.predicate):
|
|
378
|
+
continue # another fingerprint's build (_channel.visible): never kept over this one's
|
|
371
379
|
if not version_within_request(record.version, request_spelling):
|
|
372
380
|
continue
|
|
373
381
|
version_key = _version_key(record.version)
|
|
@@ -434,7 +442,7 @@ def _find_verified_signature(
|
|
|
434
442
|
bases, ref.digest, policy=policy, retry=retry
|
|
435
443
|
)
|
|
436
444
|
blob_desc = manifest_single_layer(referrer_doc, expected_media_type=C.MEDIA_TYPE_BUNDLE)
|
|
437
|
-
except ArtifactCorruptError:
|
|
445
|
+
except (ArtifactCorruptError, RetiredHttpError):
|
|
438
446
|
raise
|
|
439
447
|
except (TransportError, FetchError):
|
|
440
448
|
continue
|
|
@@ -452,7 +460,7 @@ def _find_verified_signature(
|
|
|
452
460
|
with open(bundle_path, "rb") as f:
|
|
453
461
|
bundle_json = json.loads(f.read())
|
|
454
462
|
verified = verify_bundle(bundle_json, trusted_keys)
|
|
455
|
-
except ArtifactCorruptError:
|
|
463
|
+
except (ArtifactCorruptError, RetiredHttpError):
|
|
456
464
|
raise
|
|
457
465
|
except (TransportError, FetchError, json.JSONDecodeError):
|
|
458
466
|
continue
|
|
@@ -692,8 +700,12 @@ def _ensure_floating(
|
|
|
692
700
|
cache_root_path = roots[0]
|
|
693
701
|
scratch_root = _scratch_root(cache_root_path)
|
|
694
702
|
|
|
695
|
-
index_doc, index_bytes, index_digest = fetch_manifest_by_tag(
|
|
696
|
-
bases,
|
|
703
|
+
index_doc, index_bytes, index_digest, alias_absent = fetch_manifest_by_tag(
|
|
704
|
+
bases,
|
|
705
|
+
request.spelling,
|
|
706
|
+
policy=policy,
|
|
707
|
+
retry=retry,
|
|
708
|
+
alias=_channel.alias_tag(request.spelling),
|
|
697
709
|
)
|
|
698
710
|
platform_desc = resolve_platform_manifest(index_doc, platform_key)
|
|
699
711
|
manifest_hex = parse_digest(platform_desc.digest)
|
|
@@ -701,6 +713,10 @@ def _ensure_floating(
|
|
|
701
713
|
existing_dir = unpacked_dir_for(cache_root_path, manifest_hex)
|
|
702
714
|
existing_record = read_verified_record(existing_dir)
|
|
703
715
|
if existing_record is not None and existing_record.platform == platform_key:
|
|
716
|
+
# An install of another fingerprint's build, reached through the tag
|
|
717
|
+
# because the registry has none at this SDK's own, is never reported
|
|
718
|
+
# installed (public issue #578): refused before the cache is touched.
|
|
719
|
+
_channel.ahead_of_registry(alias_absent, existing_record.predicate)
|
|
704
720
|
# A NEWER build within the request, installed beside it, is still the
|
|
705
721
|
# answer ("monotonic-warning", docs/guides/fetch-v1.md §9).
|
|
706
722
|
newer = _newer_installed(
|
|
@@ -791,6 +807,12 @@ def _ensure_floating(
|
|
|
791
807
|
signed_by = verified.signed_by
|
|
792
808
|
|
|
793
809
|
_check_predicate_matches_request(predicate, platform_key, request.spelling)
|
|
810
|
+
if found is not None:
|
|
811
|
+
# An SDK ahead of the registry (public issue #578): the alias for its
|
|
812
|
+
# own fingerprint answered 404 on every base, and the tag's build is
|
|
813
|
+
# signed for another. Refused from the signed statement, before the
|
|
814
|
+
# layer is requested, so nothing is downloaded or installed.
|
|
815
|
+
_channel.ahead_of_registry(alias_absent, predicate)
|
|
794
816
|
newer = _newer_installed(roots, platform_key, predicate, request.spelling)
|
|
795
817
|
if newer is not None:
|
|
796
818
|
newer_dir, newer_record, warning = newer
|
|
@@ -973,6 +995,8 @@ def _translate_transport_error(e: TransportError) -> FetchError:
|
|
|
973
995
|
return SourceForbiddenError(f"chtypes: {e}")
|
|
974
996
|
if isinstance(e, OversizeHttpError):
|
|
975
997
|
return ArtifactCorruptError(f"chtypes: {e}")
|
|
998
|
+
if isinstance(e, RetiredHttpError):
|
|
999
|
+
return SourceRetiredError(f"chtypes: {e}")
|
|
976
1000
|
if isinstance(e, UnreachableHttpError):
|
|
977
1001
|
return SourceUnreachableError(f"chtypes: {e}", retryable=getattr(e, "retryable", True))
|
|
978
1002
|
return SourceUnreachableError(f"chtypes: {e}")
|
|
@@ -1050,6 +1074,7 @@ def _verify_and_install_from_local_blobs(
|
|
|
1050
1074
|
manifest_digest: str,
|
|
1051
1075
|
cache_write_root: Path,
|
|
1052
1076
|
trusted_keys: tuple[TrustedKey, ...],
|
|
1077
|
+
visible_only: bool = False,
|
|
1053
1078
|
) -> VerifiedRecord | None:
|
|
1054
1079
|
"""docs/guides/fetch-v1.md §1/§6: "A pre-seeded layout has entries in
|
|
1055
1080
|
`index.json` with no corresponding `unpacked/` directory yet; the first
|
|
@@ -1103,6 +1128,9 @@ def _verify_and_install_from_local_blobs(
|
|
|
1103
1128
|
return None
|
|
1104
1129
|
|
|
1105
1130
|
predicate = verified.statement.predicate
|
|
1131
|
+
if visible_only and not _channel.visible(predicate):
|
|
1132
|
+
# A lookup never installs another fingerprint's pre-seeded build.
|
|
1133
|
+
return None
|
|
1106
1134
|
layer_hex = parse_digest(layer_desc.digest)
|
|
1107
1135
|
if layer_hex not in verified.statement.subject_sha256:
|
|
1108
1136
|
return None
|
|
@@ -1180,6 +1208,7 @@ def _verify_preseeded_entries(roots: Sequence[Path], trusted_keys: tuple[Trusted
|
|
|
1180
1208
|
manifest_digest=digest,
|
|
1181
1209
|
cache_write_root=cache_write_root,
|
|
1182
1210
|
trusted_keys=trusted_keys,
|
|
1211
|
+
visible_only=True,
|
|
1183
1212
|
)
|
|
1184
1213
|
if record is not None:
|
|
1185
1214
|
already_verified.add(digest)
|
|
@@ -1224,7 +1253,10 @@ def resolve_installed(request: Request, platform: str, options: Options) -> Reso
|
|
|
1224
1253
|
candidates = [
|
|
1225
1254
|
(dir_path, record)
|
|
1226
1255
|
for dir_path, record in list_verified_records(roots)
|
|
1227
|
-
if record.platform == platform
|
|
1256
|
+
if record.platform == platform
|
|
1257
|
+
and version_within_request(record.version, request.spelling)
|
|
1258
|
+
# Under the dev channel, only its own fingerprint's builds (_channel.visible).
|
|
1259
|
+
and _channel.visible(record.predicate)
|
|
1228
1260
|
]
|
|
1229
1261
|
if not candidates:
|
|
1230
1262
|
return None
|
|
@@ -1350,7 +1382,9 @@ def _fetch_signed_impl(repository: str, ref: str, predicate_type: str, options:
|
|
|
1350
1382
|
doc, _raw = fetch_manifest_by_digest(bases, ref, policy=policy, retry=retry)
|
|
1351
1383
|
manifest_digest = ref
|
|
1352
1384
|
else:
|
|
1353
|
-
doc, _raw, manifest_digest = fetch_manifest_by_tag(
|
|
1385
|
+
doc, _raw, manifest_digest, _alias_absent = fetch_manifest_by_tag(
|
|
1386
|
+
bases, ref, policy=policy, retry=retry
|
|
1387
|
+
)
|
|
1354
1388
|
|
|
1355
1389
|
layer_desc = manifest_single_layer(doc)
|
|
1356
1390
|
found = _find_verified_signature(
|
|
@@ -1448,6 +1482,8 @@ def _fetch_goldens(
|
|
|
1448
1482
|
retry=retry,
|
|
1449
1483
|
max_bytes=C.MAX_UNPACKED_BYTES,
|
|
1450
1484
|
)
|
|
1485
|
+
except RetiredHttpError:
|
|
1486
|
+
raise
|
|
1451
1487
|
except (TransportError, FetchError) as e:
|
|
1452
1488
|
first_failure = first_failure or e
|
|
1453
1489
|
continue
|
|
@@ -1562,6 +1598,8 @@ def _verify_goldens_signature(
|
|
|
1562
1598
|
except ArtifactCorruptError as e:
|
|
1563
1599
|
failure = failure or e
|
|
1564
1600
|
continue
|
|
1601
|
+
except RetiredHttpError:
|
|
1602
|
+
raise
|
|
1565
1603
|
except (TransportError, FetchError, json.JSONDecodeError):
|
|
1566
1604
|
continue
|
|
1567
1605
|
finally:
|
chtypes/_ocifetch/_errors.py
CHANGED
|
@@ -5,7 +5,7 @@ Scoped to `chtypes._ocifetch`: the stable, public exception types live in
|
|
|
5
5
|
imports from or edits. A future switch lane decides how (or whether) these
|
|
6
6
|
become public types; until then every name here is private.
|
|
7
7
|
|
|
8
|
-
Every exception carries a `.code` string from one of the
|
|
8
|
+
Every exception carries a `.code` string from one of the twelve codes
|
|
9
9
|
`spec/fetch-v1/constants.json`'s `errors` map assigns an exit status to
|
|
10
10
|
(the generated `ERROR_EXIT_CODES` table), so a CLI added later needs no
|
|
11
11
|
second mapping — it is the same lookup v0's `__main__.py` does today.
|
|
@@ -30,6 +30,7 @@ __all__ = [
|
|
|
30
30
|
"CODE_CACHE_UNUSABLE",
|
|
31
31
|
"CODE_SOURCE_FORBIDDEN",
|
|
32
32
|
"CODE_SOURCE_INCOMPATIBLE",
|
|
33
|
+
"CODE_SOURCE_RETIRED",
|
|
33
34
|
"CODE_SOURCE_UNAUTHORIZED",
|
|
34
35
|
"CODE_SOURCE_UNREACHABLE",
|
|
35
36
|
"ArtifactCorruptError",
|
|
@@ -41,6 +42,7 @@ __all__ = [
|
|
|
41
42
|
"FetchError",
|
|
42
43
|
"SourceForbiddenError",
|
|
43
44
|
"SourceIncompatibleError",
|
|
45
|
+
"SourceRetiredError",
|
|
44
46
|
"SourceUnauthorizedError",
|
|
45
47
|
"SourceUnreachableError",
|
|
46
48
|
]
|
|
@@ -55,6 +57,7 @@ CODE_SOURCE_UNAUTHORIZED = "CHTYPES_SOURCE_UNAUTHORIZED"
|
|
|
55
57
|
CODE_SOURCE_FORBIDDEN = "CHTYPES_SOURCE_FORBIDDEN"
|
|
56
58
|
CODE_SOURCE_INCOMPATIBLE = "CHTYPES_SOURCE_INCOMPATIBLE"
|
|
57
59
|
CODE_CACHE_UNUSABLE = "CHTYPES_CACHE_UNUSABLE"
|
|
60
|
+
CODE_SOURCE_RETIRED = "CHTYPES_SOURCE_RETIRED"
|
|
58
61
|
|
|
59
62
|
_CODES_USED_HERE = (
|
|
60
63
|
CODE_ARTIFACT_MISSING,
|
|
@@ -67,6 +70,7 @@ _CODES_USED_HERE = (
|
|
|
67
70
|
CODE_SOURCE_FORBIDDEN,
|
|
68
71
|
CODE_SOURCE_INCOMPATIBLE,
|
|
69
72
|
CODE_CACHE_UNUSABLE,
|
|
73
|
+
CODE_SOURCE_RETIRED,
|
|
70
74
|
)
|
|
71
75
|
|
|
72
76
|
# A drift guard, cheaper than a test: every code this module raises must be
|
|
@@ -187,3 +191,13 @@ class CacheUnusableError(FetchError):
|
|
|
187
191
|
self.path = path
|
|
188
192
|
self.reason = reason
|
|
189
193
|
self.os_error = os_error
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
class SourceRetiredError(FetchError):
|
|
197
|
+
"""The source answered 410 Gone: a retired repository, which is permanent,
|
|
198
|
+
so the request was never retried and never sent to the next base
|
|
199
|
+
(docs/guides/fetch-v1.md §2, "A retired repository"; public issue #571).
|
|
200
|
+
The message names the URL that answered and carries the registry's own
|
|
201
|
+
message, made safe to print."""
|
|
202
|
+
|
|
203
|
+
code = CODE_SOURCE_RETIRED
|
chtypes/_ocifetch/_http.py
CHANGED
|
@@ -29,6 +29,7 @@ from email.utils import parsedate_to_datetime
|
|
|
29
29
|
from urllib.parse import urljoin, urlsplit, urlunsplit
|
|
30
30
|
|
|
31
31
|
from chtypes._ocifetch import _constants as C
|
|
32
|
+
from chtypes._ocifetch._retired import retired_text
|
|
32
33
|
|
|
33
34
|
__all__ = [
|
|
34
35
|
"USER_AGENT",
|
|
@@ -38,6 +39,7 @@ __all__ = [
|
|
|
38
39
|
"HttpResponse",
|
|
39
40
|
"NotFoundHttpError",
|
|
40
41
|
"OversizeHttpError",
|
|
42
|
+
"RetiredHttpError",
|
|
41
43
|
"RetryPolicy",
|
|
42
44
|
"TransportError",
|
|
43
45
|
"UnauthorizedHttpError",
|
|
@@ -88,6 +90,12 @@ class NotFoundHttpError(TransportError):
|
|
|
88
90
|
"""A 404 (or, for a `file://` base, a missing path)."""
|
|
89
91
|
|
|
90
92
|
|
|
93
|
+
class AliasNotFoundError(TransportError):
|
|
94
|
+
"""`mode="alias"`: every configured base answered the alias tag 404 (or,
|
|
95
|
+
for a `file://` base, has no such path). The one alias outcome the caller
|
|
96
|
+
answers with the tag itself (docs/guides/fetch-v1.md §3)."""
|
|
97
|
+
|
|
98
|
+
|
|
91
99
|
class UnauthorizedHttpError(TransportError):
|
|
92
100
|
"""A 401 that is not (or is no longer) resolvable by the anonymous token flow."""
|
|
93
101
|
|
|
@@ -114,6 +122,13 @@ class OversizeHttpError(TransportError):
|
|
|
114
122
|
"""A response body exceeded the caller's declared size cap."""
|
|
115
123
|
|
|
116
124
|
|
|
125
|
+
class RetiredHttpError(TransportError):
|
|
126
|
+
"""A retired repository (`RETIRED_STATUSES`, a 410; docs/guides/fetch-v1.md
|
|
127
|
+
§2): permanent, so never retried and never a reason to try the next base.
|
|
128
|
+
Its text names the URL that answered and carries the registry's message,
|
|
129
|
+
made safe to print (`_retired.retired_message`)."""
|
|
130
|
+
|
|
131
|
+
|
|
117
132
|
class Clock:
|
|
118
133
|
"""The real wall clock. Tests inject a fake with the same interface so
|
|
119
134
|
retry cases run instantly and sleeps are asserted exactly."""
|
|
@@ -222,7 +237,16 @@ def _single_request(
|
|
|
222
237
|
)
|
|
223
238
|
except urllib.error.HTTPError as e:
|
|
224
239
|
try:
|
|
225
|
-
|
|
240
|
+
if e.code in C.RETIRED_STATUSES:
|
|
241
|
+
# Only the registry's message is wanted from a retired
|
|
242
|
+
# repository's answer: at most RETIRED_BODY_MAX_BYTES of it,
|
|
243
|
+
# and a body that fails part way is no body.
|
|
244
|
+
try:
|
|
245
|
+
body = e.read(C.RETIRED_BODY_MAX_BYTES)
|
|
246
|
+
except (OSError, ValueError, AttributeError):
|
|
247
|
+
body = b""
|
|
248
|
+
else:
|
|
249
|
+
body = _read_capped(e, max_bytes) if e.code not in _REDIRECT_CODES else b""
|
|
226
250
|
finally:
|
|
227
251
|
e.close()
|
|
228
252
|
return HttpResponse(status=e.code, headers=dict(e.headers or {}), body=body, url=url)
|
|
@@ -374,6 +398,11 @@ def _http_get_once(
|
|
|
374
398
|
headers.pop("Authorization", None)
|
|
375
399
|
continue
|
|
376
400
|
|
|
401
|
+
if resp.status in C.RETIRED_STATUSES:
|
|
402
|
+
# A retired repository: permanent, never retried, and the
|
|
403
|
+
# exception ends fetch_from_bases's loop over bases.
|
|
404
|
+
raise RetiredHttpError(retired_text(current_url, resp.status, resp.body))
|
|
405
|
+
|
|
377
406
|
if resp.status == 401:
|
|
378
407
|
if not tried_anon_flow and not policy.token:
|
|
379
408
|
challenge = resp.headers.get("WWW-Authenticate") or resp.headers.get(
|
|
@@ -506,16 +535,27 @@ def fetch_from_bases(
|
|
|
506
535
|
where it is retried within the shared budget before giving up — a
|
|
507
536
|
listed digest that 404s is a host fault, never "unpublished", so the
|
|
508
537
|
final exception for this mode is always `UnreachableHttpError`.
|
|
538
|
+
- `alias`: the dev channel's alias tag (docs/guides/fetch-v1.md §3). A 404
|
|
539
|
+
moves to the next base, as for a tag, but only a 404 on EVERY base is
|
|
540
|
+
"not found" (`AliasNotFoundError`, which the caller answers with the tag
|
|
541
|
+
itself). Any other failure on any base is raised as that failure, as the
|
|
542
|
+
tag would get it, so a transient error can never route a request to the
|
|
543
|
+
tag, which may name a build of another fingerprint.
|
|
509
544
|
|
|
510
545
|
`decided-here` (the spec does not state it): when bases disagree — some
|
|
511
546
|
404, some unreachable — the outcome follows the LAST base tried, since
|
|
512
547
|
bases are an explicit preference order and the last one is the final
|
|
513
548
|
word a caller configured.
|
|
549
|
+
|
|
550
|
+
In every mode a retired repository (`RetiredHttpError`, a 410) ends the
|
|
551
|
+
loop on the base that answered it: it is permanent, never a reason to try
|
|
552
|
+
the next base, and in `alias` mode never a reason to fall back to the tag.
|
|
514
553
|
"""
|
|
515
554
|
if not bases:
|
|
516
555
|
raise UnreachableHttpError("no base URLs configured")
|
|
517
556
|
last_index = len(bases) - 1
|
|
518
557
|
last_error: TransportError | None = None
|
|
558
|
+
alias_failure: TransportError | None = None # mode="alias": a base's non-404 failure
|
|
519
559
|
for i, base in enumerate(bases):
|
|
520
560
|
is_last = i == last_index
|
|
521
561
|
scheme = urlsplit(base).scheme
|
|
@@ -538,8 +578,13 @@ def fetch_from_bases(
|
|
|
538
578
|
continue
|
|
539
579
|
except UnreachableHttpError as e:
|
|
540
580
|
last_error = e
|
|
581
|
+
alias_failure = e
|
|
541
582
|
continue
|
|
542
583
|
assert last_error is not None
|
|
584
|
+
if mode == "alias":
|
|
585
|
+
if alias_failure is None:
|
|
586
|
+
raise AliasNotFoundError(f"{path}: not found on any configured base")
|
|
587
|
+
last_error = alias_failure
|
|
543
588
|
if mode == "tag" and isinstance(last_error, NotFoundHttpError):
|
|
544
589
|
raise last_error
|
|
545
590
|
if isinstance(last_error, (UnauthorizedHttpError, ForbiddenHttpError)):
|
chtypes/_ocifetch/_oci.py
CHANGED
|
@@ -16,7 +16,13 @@ from chtypes._ocifetch._errors import (
|
|
|
16
16
|
ArtifactUnpublishedError,
|
|
17
17
|
SourceIncompatibleError,
|
|
18
18
|
)
|
|
19
|
-
from chtypes._ocifetch._http import
|
|
19
|
+
from chtypes._ocifetch._http import (
|
|
20
|
+
AliasNotFoundError,
|
|
21
|
+
FetchPolicy,
|
|
22
|
+
RetryPolicy,
|
|
23
|
+
fetch_from_bases,
|
|
24
|
+
get_json,
|
|
25
|
+
)
|
|
20
26
|
|
|
21
27
|
__all__ = [
|
|
22
28
|
"Descriptor",
|
|
@@ -242,9 +248,35 @@ def fetch_manifest_by_tag(
|
|
|
242
248
|
policy: FetchPolicy,
|
|
243
249
|
retry: RetryPolicy,
|
|
244
250
|
max_bytes: int = C.MANIFEST_MAX_BYTES,
|
|
245
|
-
|
|
246
|
-
|
|
251
|
+
alias: str | None = None,
|
|
252
|
+
) -> tuple[dict, bytes, str, bool]:
|
|
253
|
+
"""GET manifests/<tag>; returns (doc, raw_bytes, resolved_digest, alias_absent).
|
|
254
|
+
|
|
255
|
+
With `alias` (the dev channel's `<tag>--fp-<fingerprint>`, from
|
|
256
|
+
`_channel.alias_tag`), GET manifests/<alias> first, and the tag only when
|
|
257
|
+
every base answered the alias 404 (docs/guides/fetch-v1.md §3); any other
|
|
258
|
+
failure of the alias request is raised as it is. What is returned is then
|
|
259
|
+
trusted exactly as the tag's own answer would be. `alias_absent` reports
|
|
260
|
+
that fallback: the alias was asked for and every base answered it 404, so
|
|
261
|
+
the registry has no build at this SDK's fingerprint, and the build the tag
|
|
262
|
+
names is checked against it (`_channel.ahead_of_registry`)."""
|
|
247
263
|
accept = (C.MEDIA_TYPE_INDEX, C.MEDIA_TYPE_MANIFEST)
|
|
264
|
+
alias_absent = False
|
|
265
|
+
if alias is not None:
|
|
266
|
+
try:
|
|
267
|
+
doc, resp = get_json(
|
|
268
|
+
bases,
|
|
269
|
+
f"/manifests/{alias}",
|
|
270
|
+
mode="alias",
|
|
271
|
+
accept=accept,
|
|
272
|
+
max_bytes=max_bytes,
|
|
273
|
+
policy=policy,
|
|
274
|
+
retry=retry,
|
|
275
|
+
)
|
|
276
|
+
except AliasNotFoundError:
|
|
277
|
+
alias_absent = True
|
|
278
|
+
else:
|
|
279
|
+
return doc, resp.body, f"sha256:{hashlib.sha256(resp.body).hexdigest()}", False
|
|
248
280
|
doc, resp = get_json(
|
|
249
281
|
bases,
|
|
250
282
|
f"/manifests/{tag}",
|
|
@@ -255,7 +287,7 @@ def fetch_manifest_by_tag(
|
|
|
255
287
|
retry=retry,
|
|
256
288
|
)
|
|
257
289
|
digest = f"sha256:{hashlib.sha256(resp.body).hexdigest()}"
|
|
258
|
-
return doc, resp.body, digest
|
|
290
|
+
return doc, resp.body, digest, alias_absent
|
|
259
291
|
|
|
260
292
|
|
|
261
293
|
def fetch_blob_to_path(
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
"""A retired repository (docs/guides/fetch-v1.md §2, "A retired repository";
|
|
2
|
+
public issue #571).
|
|
3
|
+
|
|
4
|
+
The registry's operator answers every route of a retired repository with 410
|
|
5
|
+
Gone (`RETIRED_STATUSES`) and a short error document. That is permanent: the
|
|
6
|
+
transport never retries it and never asks the next base, so the caller sees
|
|
7
|
+
`CHTYPES_SOURCE_RETIRED` with the URL that answered and the registry's own
|
|
8
|
+
message, made safe to print by `retired_message`, the one function every
|
|
9
|
+
binding has (tests/fixtures/retired-message/cases.json is the table all four
|
|
10
|
+
answer alike).
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
import json
|
|
16
|
+
|
|
17
|
+
from chtypes._ocifetch import _constants as C
|
|
18
|
+
|
|
19
|
+
__all__ = ["retired_message", "retired_text"]
|
|
20
|
+
|
|
21
|
+
_ELLIPSIS = "…"
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def _removed(code_point: int) -> bool:
|
|
25
|
+
return (
|
|
26
|
+
code_point <= 0x1F
|
|
27
|
+
or 0x7F <= code_point <= 0x9F
|
|
28
|
+
or code_point in (0x200E, 0x200F)
|
|
29
|
+
or 0x202A <= code_point <= 0x202E
|
|
30
|
+
or 0x2066 <= code_point <= 0x2069
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def _not_json(name: str) -> None:
|
|
35
|
+
# `json.loads` accepts NaN, Infinity and -Infinity, which are not JSON;
|
|
36
|
+
# the other three bindings' readers refuse them.
|
|
37
|
+
raise ValueError(f"{name} is not JSON")
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def retired_message(body: bytes) -> str | None:
|
|
41
|
+
"""The registry's own message in a retired repository's response body,
|
|
42
|
+
made safe to print, or None when it sent none.
|
|
43
|
+
|
|
44
|
+
`body` is what was read of it (at most `RETIRED_BODY_MAX_BYTES`). When it is
|
|
45
|
+
UTF-8 JSON whose `errors[0].message` is a string, that string is the
|
|
46
|
+
message: every code point in U+0000-U+001F, U+007F-U+009F, U+200E, U+200F,
|
|
47
|
+
U+202A-U+202E and U+2066-U+2069 is removed, and the rest is cut to
|
|
48
|
+
`RETIRED_MESSAGE_MAX_CODE_POINTS` code points, with U+2026 appended when it
|
|
49
|
+
was cut. Nothing else is changed, and an empty result is no message."""
|
|
50
|
+
try:
|
|
51
|
+
text = body.decode("utf-8")
|
|
52
|
+
doc = json.loads(text, parse_constant=_not_json)
|
|
53
|
+
except (UnicodeDecodeError, ValueError, RecursionError):
|
|
54
|
+
return None
|
|
55
|
+
if not isinstance(doc, dict):
|
|
56
|
+
return None
|
|
57
|
+
errors = doc.get("errors")
|
|
58
|
+
if not isinstance(errors, list) or not errors or not isinstance(errors[0], dict):
|
|
59
|
+
return None
|
|
60
|
+
message = errors[0].get("message")
|
|
61
|
+
# A lone surrogate escape is not a Unicode string (and could not be printed).
|
|
62
|
+
if not isinstance(message, str) or any("\ud800" <= ch <= "\udfff" for ch in message):
|
|
63
|
+
return None
|
|
64
|
+
kept = [ch for ch in message if not _removed(ord(ch))]
|
|
65
|
+
if len(kept) > C.RETIRED_MESSAGE_MAX_CODE_POINTS:
|
|
66
|
+
kept = [*kept[: C.RETIRED_MESSAGE_MAX_CODE_POINTS], _ELLIPSIS]
|
|
67
|
+
return "".join(kept) or None
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def retired_text(url: str, status: int, body: bytes) -> str:
|
|
71
|
+
"""The error text for a retired repository's answer: the URL and status
|
|
72
|
+
that answered, and the registry's message when it sent one."""
|
|
73
|
+
text = f"{url} answered {status} Gone: the repository is retired"
|
|
74
|
+
message = retired_message(body)
|
|
75
|
+
if message is not None:
|
|
76
|
+
text += f"; the registry says: {message}"
|
|
77
|
+
return text
|
chtypes/errors.py
CHANGED
|
@@ -31,6 +31,7 @@ __all__ = [
|
|
|
31
31
|
"CODE_CACHE_UNUSABLE",
|
|
32
32
|
"CODE_SOURCE_FORBIDDEN",
|
|
33
33
|
"CODE_SOURCE_INCOMPATIBLE",
|
|
34
|
+
"CODE_SOURCE_RETIRED",
|
|
34
35
|
"CODE_SOURCE_UNAUTHORIZED",
|
|
35
36
|
"CODE_SOURCE_UNREACHABLE",
|
|
36
37
|
"ArtifactCorruptError",
|
|
@@ -47,6 +48,7 @@ __all__ = [
|
|
|
47
48
|
"SchemaError",
|
|
48
49
|
"SourceForbiddenError",
|
|
49
50
|
"SourceIncompatibleError",
|
|
51
|
+
"SourceRetiredError",
|
|
50
52
|
"SourceUnauthorizedError",
|
|
51
53
|
"SourceUnreachableError",
|
|
52
54
|
"UnsupportedError",
|
|
@@ -66,6 +68,7 @@ CODE_SOURCE_UNAUTHORIZED = "CHTYPES_SOURCE_UNAUTHORIZED"
|
|
|
66
68
|
CODE_SOURCE_FORBIDDEN = "CHTYPES_SOURCE_FORBIDDEN"
|
|
67
69
|
CODE_SOURCE_INCOMPATIBLE = "CHTYPES_SOURCE_INCOMPATIBLE"
|
|
68
70
|
CODE_CACHE_UNUSABLE = "CHTYPES_CACHE_UNUSABLE"
|
|
71
|
+
CODE_SOURCE_RETIRED = "CHTYPES_SOURCE_RETIRED"
|
|
69
72
|
|
|
70
73
|
# `chs_status` value of CHS_INVALID_ARGUMENT. The binding's own misuse errors
|
|
71
74
|
# carry it so a handler sees one shape whichever side caught the misuse. It is
|
|
@@ -258,6 +261,15 @@ class SourceIncompatibleError(ArtifactError):
|
|
|
258
261
|
code = CODE_SOURCE_INCOMPATIBLE
|
|
259
262
|
|
|
260
263
|
|
|
264
|
+
class SourceRetiredError(ArtifactError):
|
|
265
|
+
"""The source answered 410 Gone: a retired repository, which is permanent,
|
|
266
|
+
so the request was never retried and never sent to the next base. The
|
|
267
|
+
message names the URL that answered and carries the registry's own
|
|
268
|
+
message, made safe to print."""
|
|
269
|
+
|
|
270
|
+
code = CODE_SOURCE_RETIRED
|
|
271
|
+
|
|
272
|
+
|
|
261
273
|
class CacheUnusableError(ArtifactError):
|
|
262
274
|
"""A cache directory or entry the fetch layer could not read or write, or
|
|
263
275
|
one strict mode refuses: never "not installed", never the network's
|
chtypes/registry.py
CHANGED
|
@@ -51,6 +51,7 @@ _FETCH_CLASSES: dict[str, type[errors.ArtifactError]] = {
|
|
|
51
51
|
errors.CODE_SOURCE_FORBIDDEN: errors.SourceForbiddenError,
|
|
52
52
|
errors.CODE_SOURCE_INCOMPATIBLE: errors.SourceIncompatibleError,
|
|
53
53
|
errors.CODE_CACHE_UNUSABLE: errors.CacheUnusableError,
|
|
54
|
+
errors.CODE_SOURCE_RETIRED: errors.SourceRetiredError,
|
|
54
55
|
}
|
|
55
56
|
|
|
56
57
|
|
chtypes/results.py
CHANGED
|
@@ -14,14 +14,16 @@ from __future__ import annotations
|
|
|
14
14
|
from collections.abc import Mapping
|
|
15
15
|
from dataclasses import dataclass
|
|
16
16
|
|
|
17
|
-
from ._abi2._vocab import DefaultKind, FilterOutcome, Outcome, Verdict
|
|
17
|
+
from ._abi2._vocab import DeclinedTier, DefaultKind, FilterOutcome, MergeReason, Outcome, Verdict
|
|
18
18
|
|
|
19
19
|
__all__ = [
|
|
20
|
+
"AtMergeEntry",
|
|
20
21
|
"BatchResult",
|
|
21
22
|
"BuildInfo",
|
|
22
23
|
"Capabilities",
|
|
23
24
|
"Column",
|
|
24
25
|
"Computed",
|
|
26
|
+
"DeclinedSetting",
|
|
25
27
|
"DiscoveredColumn",
|
|
26
28
|
"EngineCell",
|
|
27
29
|
"Discovery",
|
|
@@ -108,6 +110,31 @@ class EngineCell:
|
|
|
108
110
|
value: bytes | None = None
|
|
109
111
|
|
|
110
112
|
|
|
113
|
+
@dataclass(frozen=True, slots=True)
|
|
114
|
+
class AtMergeEntry:
|
|
115
|
+
"""One thing an `OPTIMIZE TABLE ... FINAL` of the part the INSERT writes would do
|
|
116
|
+
to one of its rows, at the call's clock instant: an entry of the batch's
|
|
117
|
+
`at_merge` list (ABI v2). `engine_rows` stays exactly what the INSERT writer
|
|
118
|
+
produces, so the two never contradict each other.
|
|
119
|
+
|
|
120
|
+
`row` indexes `engine_rows`: the part's row, not the input body's. `reason` is
|
|
121
|
+
a `MergeReason`; one the description does not list is its unknown(n) member
|
|
122
|
+
(rule r3). `column` is the column a `ttl_column_reset` resets (from `column`
|
|
123
|
+
or `column_b64`), None when the entry names none. `stored` is the column's
|
|
124
|
+
value after the reset (from `stored` or `stored_b64`), None when the document
|
|
125
|
+
carries none, which it omits when the column's DEFAULT reads the clock or a
|
|
126
|
+
generator and the value is decided at the merge. `input_rows` is the input
|
|
127
|
+
rows, each by its index in the body, that formed the part's row, None when
|
|
128
|
+
the document carries none.
|
|
129
|
+
"""
|
|
130
|
+
|
|
131
|
+
row: int
|
|
132
|
+
reason: MergeReason
|
|
133
|
+
column: bytes | None = None
|
|
134
|
+
stored: bytes | None = None
|
|
135
|
+
input_rows: tuple[int, ...] | None = None
|
|
136
|
+
|
|
137
|
+
|
|
111
138
|
@dataclass(frozen=True, slots=True)
|
|
112
139
|
class RowResult:
|
|
113
140
|
"""The `row` document. `columns` is every `cols` entry in document order;
|
|
@@ -160,6 +187,12 @@ class BatchResult:
|
|
|
160
187
|
can be fooled too. A caller that needs every record accounted for declines
|
|
161
188
|
a body with any skipped row or any `unconsumed` range
|
|
162
189
|
(docs/guides/batches.md).
|
|
190
|
+
|
|
191
|
+
`unsupported_settings` is the call's own settings the library declined, from
|
|
192
|
+
the batch's top-level list (ABI v2). `at_merge` is what an `OPTIMIZE TABLE ...
|
|
193
|
+
FINAL` of the part would do to its rows, at least: a later background merge
|
|
194
|
+
can remove more as more rows expire, and a row both removed and reset lists
|
|
195
|
+
only `ttl_delete`. Each is empty when the document carries none.
|
|
163
196
|
"""
|
|
164
197
|
|
|
165
198
|
outcome: Outcome
|
|
@@ -178,6 +211,8 @@ class BatchResult:
|
|
|
178
211
|
partition_count: int | None
|
|
179
212
|
unconsumed: tuple[Span, ...]
|
|
180
213
|
framing: Framing | None
|
|
214
|
+
unsupported_settings: tuple[bytes, ...] = ()
|
|
215
|
+
at_merge: tuple[AtMergeEntry, ...] = ()
|
|
181
216
|
|
|
182
217
|
|
|
183
218
|
@dataclass(frozen=True, slots=True)
|
|
@@ -189,8 +224,10 @@ class FilterRowError:
|
|
|
189
224
|
|
|
190
225
|
@dataclass(frozen=True, slots=True)
|
|
191
226
|
class FilterResult:
|
|
192
|
-
"""The `filter_result` document.
|
|
193
|
-
|
|
227
|
+
"""The `filter_result` document. A verdict counts only when `outcome` is `ok`;
|
|
228
|
+
otherwise treat every verdict as `d`, whatever the verdict string holds. Under
|
|
229
|
+
`ok`, `e` and `d` are never answers, and a caller enforcing visibility fails
|
|
230
|
+
closed on both; `Verdict.answered` says which."""
|
|
194
231
|
|
|
195
232
|
outcome: FilterOutcome
|
|
196
233
|
err_code: int
|
|
@@ -213,6 +250,18 @@ class Column:
|
|
|
213
250
|
default_expr: bytes
|
|
214
251
|
|
|
215
252
|
|
|
253
|
+
@dataclass(frozen=True, slots=True)
|
|
254
|
+
class DeclinedSetting:
|
|
255
|
+
"""One setting a server's profile sets that this build's filters do not honor
|
|
256
|
+
in a WHERE: an entry of the server's `filter_declined_settings` (ABI v2).
|
|
257
|
+
`name` is bytes (from `name` or `name_b64`). `tier` is the tier this build
|
|
258
|
+
line's WHERE-settings list gives it, a `DeclinedTier`; one the description
|
|
259
|
+
does not list is its unknown(n) member (rule r3)."""
|
|
260
|
+
|
|
261
|
+
name: bytes
|
|
262
|
+
tier: DeclinedTier
|
|
263
|
+
|
|
264
|
+
|
|
216
265
|
@dataclass(frozen=True, slots=True)
|
|
217
266
|
class SchemaServer:
|
|
218
267
|
"""The server a schema was compiled on, as the library holds it. Its strings
|
|
@@ -220,11 +269,17 @@ class SchemaServer:
|
|
|
220
269
|
data-derived. `timezone` is the zone the schema's types bind: the profile's,
|
|
221
270
|
or else the image zone. `settings` is the profile's settings (`{}` when it
|
|
222
271
|
gave none). `macros` is None exactly when the profile carried no macros
|
|
223
|
-
(unknown), and a mapping, even an empty one, when it did (the complete set).
|
|
272
|
+
(unknown), and a mapping, even an empty one, when it did (the complete set).
|
|
273
|
+
`filter_declined_settings` is the settings the profile sets that this
|
|
274
|
+
build's filters do not honor in a WHERE, in the profile's own order, empty
|
|
275
|
+
when it lists none: a schema on a server that lists any is one
|
|
276
|
+
`compile_filter` declines (`UnsupportedError`), and a consumer that streams
|
|
277
|
+
rows refuses such a tenant (spec/abi-v2/where-settings/README.md)."""
|
|
224
278
|
|
|
225
279
|
timezone: str
|
|
226
280
|
settings: Mapping[str, str]
|
|
227
281
|
macros: Mapping[str, str] | None
|
|
282
|
+
filter_declined_settings: tuple[DeclinedSetting, ...] = ()
|
|
228
283
|
|
|
229
284
|
|
|
230
285
|
@dataclass(frozen=True, slots=True)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: chtypes
|
|
3
|
-
Version: 2.0.0.
|
|
3
|
+
Version: 2.0.0.dev4
|
|
4
4
|
Summary: Python binding for the chtypes artifacts: ClickHouse's own type system, per version, behind the frozen chs_* C ABI
|
|
5
5
|
Project-URL: Homepage, https://github.com/wave-rf/chtypes
|
|
6
6
|
Project-URL: Repository, https://github.com/wave-rf/chtypes
|
|
@@ -19,9 +19,11 @@ Description-Content-Type: text/markdown
|
|
|
19
19
|
|
|
20
20
|
# chtypes — Python SDK
|
|
21
21
|
|
|
22
|
-
> **2.0.0-dev: UNSTABLE, staging only, not for production.** This is the ABI v2 development binding (public issue #511). It speaks ABI v2's unstable description, pins its dev fingerprint and refuses a library with any other ("update your dev SDK"). It fetches only from the staging dev channel (`https://registry-staging.wavehouse.dev/chtypes/v2-dev`) and trusts only the staging key; `CHTYPES_ARTIFACTS_URL`, `CHTYPES_TRUSTED_KEYS` and `CHTYPES_ALLOW_UNSIGNED`, and the `bases`, `trusted_keys` and `allow_unsigned` fetch options, are ignored, each with one warning; `--lock`, `--frozen` and `--update`, and the `frozen`, `lock_path`, `lock_write` and `update` options, are refused, because a dev build is replaceable and a superseded one expires after 14 days. Its cache is `${XDG_CACHE_HOME:-~/.cache}/chtypes/v2-dev`, or `<CHTYPES_CACHE>/v2-dev` under an explicit cache, which no 1.x SDK reads. Every vocabulary keeps a value it does not list as its `unknown(n)` member (`Outcome("x")`, whose `known` is `False`) instead of failing the document. The rules are r1 to r6 of [`docs/reference/abi-v2.md`](../docs/reference/abi-v2.md). For production, use the 1.x package: `pip install "chtypes<2"`.
|
|
22
|
+
> **2.0.0-dev: UNSTABLE, staging only, not for production.** This is the ABI v2 development binding (public issue #511). It speaks ABI v2's unstable description, pins its dev fingerprint and refuses a library with any other ("update your dev SDK"). It fetches only from the staging dev channel (`https://registry-staging.wavehouse.dev/chtypes/v2-dev`) and trusts only the staging key; `CHTYPES_ARTIFACTS_URL`, `CHTYPES_TRUSTED_KEYS` and `CHTYPES_ALLOW_UNSIGNED`, and the `bases`, `trusted_keys` and `allow_unsigned` fetch options, are ignored, each with one warning; `--lock`, `--frozen` and `--update`, and the `frozen`, `lock_path`, `lock_write` and `update` options, are refused, because a dev build is replaceable and a superseded one expires after 14 days. Its cache is `${XDG_CACHE_HOME:-~/.cache}/chtypes/v2-dev`, or `<CHTYPES_CACHE>/v2-dev` under an explicit cache, which no 1.x SDK reads. A dev SDK prefers the newest build of its own fingerprint: it resolves `<tag>--fp-<its fingerprint>` first and the tag only when no such build exists, and its cache lookups ignore another dev SDK's builds in a shared cache, so a newer dev fingerprint never strands it (§3 of [`docs/guides/fetch-v1.md`](../docs/guides/fetch-v1.md)). Every vocabulary keeps a value it does not list as its `unknown(n)` member (`Outcome("x")`, whose `known` is `False`) instead of failing the document. The rules are r1 to r6 of [`docs/reference/abi-v2.md`](../docs/reference/abi-v2.md). For production, use the 1.x package: `pip install "chtypes<2"`.
|
|
23
23
|
>
|
|
24
24
|
> An explicit cache directory gets the `v2-dev` subroot whether it comes from `CHTYPES_CACHE` or from `FetchOptions(cache_dir=...)`: the layout is under `<cache_dir>/v2-dev`, never in the directory itself, so code that inspects or pre-populates a cache looks there; `chtypes.cache_root` and `chtypes.search_dirs` return the resolved paths. `CHTYPES_OFFLINE=1` is the environment twin of `FetchOptions(offline=True)` and `--offline`: the cache only, no request.
|
|
25
|
+
>
|
|
26
|
+
> <!-- remove-at-v2-lock -->v2-dev builds before the one that carries this contract (its release notice names it) can answer `ok` with `t` verdicts after a refused row. On those builds, when a body or block holds more than one row, treat every verdict as `d` if `errors` is non-empty.<!-- /remove-at-v2-lock -->
|
|
25
27
|
|
|
26
28
|
**If this row were inserted into this table on this ClickHouse version, what would happen?** chtypes answers with ClickHouse's own code: the real C++ type machinery, vendored per release into a native library behind the frozen `chs_*` C ABI and reached here through stdlib `ctypes`. Nothing semantic is reimplemented, so _"what does ClickHouse do with `256` into a `UInt8`?"_ is answered by ClickHouse rather than by a model of it. One peer binding among `{go, python, ts, rust}` — no language is privileged, and all four give one answer.
|
|
27
29
|
|
|
@@ -1,37 +1,38 @@
|
|
|
1
|
-
chtypes/__init__.py,sha256=
|
|
1
|
+
chtypes/__init__.py,sha256=9xXEUgqnxJmAjUbz8X3coym4UyM01HShX7Lrds751VM,6177
|
|
2
2
|
chtypes/__main__.py,sha256=XYGNTpTLXI1AUp7srvuNVv2SZAcfwLaXpoXRVInyhF4,14030
|
|
3
|
-
chtypes/_decode.py,sha256=
|
|
3
|
+
chtypes/_decode.py,sha256=v5pdwphwAldVzgHg6saWsxqUFjMHqcMO-XOuDyuRVZg,22686
|
|
4
4
|
chtypes/_ed25519.py,sha256=uwaAKWvgoc936e1ZfgLrtR47W87WWCL541kFaqBXrB0,4141
|
|
5
5
|
chtypes/_guard.py,sha256=zTtwprCS0XvGWY7GDozo-IP6GwjMAWl-o-AjShdCSVI,1985
|
|
6
6
|
chtypes/_input.py,sha256=i4OUmgmHxx618g0DVFYy8LmHhggfBT03NyMxH3ptZBI,6020
|
|
7
7
|
chtypes/_setup.py,sha256=n84T0hFmXu_fdnXQIFhk3dMVL0_-yhfdRZsqCXTLzzg,6114
|
|
8
|
-
chtypes/errors.py,sha256=
|
|
8
|
+
chtypes/errors.py,sha256=7gDi9Ij8yptrr9E0LZyA2XkeaFFVyZoQMVQjQj6mn0s,9936
|
|
9
9
|
chtypes/library.py,sha256=G6DJFiRuv8--JARxfiSNEqeFLkafaMs_n9nG-IwoC74,17878
|
|
10
10
|
chtypes/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
11
|
-
chtypes/registry.py,sha256=
|
|
12
|
-
chtypes/results.py,sha256=
|
|
11
|
+
chtypes/registry.py,sha256=Q3LR7wBs7Ggy_5xG_-0bZrTRgvRWJO5OJbD92ecUlt4,13786
|
|
12
|
+
chtypes/results.py,sha256=OnxFUNyhlNeA2jwsMVAqYjKjmqguvKtWzrCwi7F6CFM,12123
|
|
13
13
|
chtypes/_abi2/__init__.py,sha256=d3i7_iUDMFT1jg49vRQz1SMH6oseYZHUR0oPt6aSTiY,1042
|
|
14
|
-
chtypes/_abi2/_decls.py,sha256=
|
|
15
|
-
chtypes/_abi2/_errmap.py,sha256=
|
|
14
|
+
chtypes/_abi2/_decls.py,sha256=X88Ylb33Sw5iJjikVTfVzdgCfjGvKpn4F3mT0qAnD7I,38454
|
|
15
|
+
chtypes/_abi2/_errmap.py,sha256=vNPgHFV742Px2aTyYqJez7I3qXRq80AOUcMqDvyPGFI,2886
|
|
16
16
|
chtypes/_abi2/_errors.py,sha256=wRE9M0oYtJz_9izQqpEBh5571M_1QzGWBoJ3pJTykWw,761
|
|
17
17
|
chtypes/_abi2/_loader.py,sha256=VMWrbWMp9Uq3K-7a219lfqAmSmBlD2hcB6WDJgG8XG0,12514
|
|
18
|
-
chtypes/_abi2/_vocab.py,sha256=
|
|
18
|
+
chtypes/_abi2/_vocab.py,sha256=rzbenqXqDoWZkvpjRw0UUBEkgWoOtE462QUGyCE3cTw,16516
|
|
19
19
|
chtypes/_ocifetch/__init__.py,sha256=enfPOKkqP4kqAKqckBGWXN1OQsiYo9ngDP-wzG0zn2I,1484
|
|
20
|
-
chtypes/_ocifetch/_channel.py,sha256=
|
|
21
|
-
chtypes/_ocifetch/_constants.py,sha256=
|
|
20
|
+
chtypes/_ocifetch/_channel.py,sha256=2L-sSetXv4iF0mpsIX4-iTAhNbu9eY1IsGnb4WTptwM,17980
|
|
21
|
+
chtypes/_ocifetch/_constants.py,sha256=IZtAK3oxnCELZysvrzRozJkntxKSQllGjxod5No-Kco,5644
|
|
22
22
|
chtypes/_ocifetch/_dsse.py,sha256=0RaFJaG1jhXr1uPfuHxm7yAn6viNzYhfCRUf-sW5SvQ,9333
|
|
23
|
-
chtypes/_ocifetch/_ensure.py,sha256=
|
|
24
|
-
chtypes/_ocifetch/_errors.py,sha256=
|
|
23
|
+
chtypes/_ocifetch/_ensure.py,sha256=GtuYJJXhL0TbUypx94OPr3BhnkrQoHuWjZdPYrvpLgw,62597
|
|
24
|
+
chtypes/_ocifetch/_errors.py,sha256=TTYpwhvZ7MyD9Ssg5js8_YB8Fl30rYBPlENyAetqEbQ,7129
|
|
25
25
|
chtypes/_ocifetch/_faults.py,sha256=HpoB9tJW7aLRiJdCFnspzkwhpFATZ6f6hGi49Mbfnhs,7609
|
|
26
26
|
chtypes/_ocifetch/_goldens.py,sha256=8MicgeJSRd4WZkaEsdujRnOa7twQvbT-wfSMPZaD-ms,4191
|
|
27
|
-
chtypes/_ocifetch/_http.py,sha256=
|
|
27
|
+
chtypes/_ocifetch/_http.py,sha256=e1zAWu-2gvBe3FNE5bZ7jmz2OPn1bzImPfHWvA10O_4,25179
|
|
28
28
|
chtypes/_ocifetch/_layout.py,sha256=ezEoVrsSMhvDhQ4U04hsGdu1-_ISZQ4V6zg4gGIChTQ,21300
|
|
29
29
|
chtypes/_ocifetch/_lock.py,sha256=14_uex9qXdOht-5FSrSvFvd99-gB7DmI3bDuxXs4YU8,5530
|
|
30
|
-
chtypes/_ocifetch/_oci.py,sha256=
|
|
30
|
+
chtypes/_ocifetch/_oci.py,sha256=UdA8QGTh1O27DBYRf5UxsevjgI52IecQlEf9IO6Y0A4,13920
|
|
31
31
|
chtypes/_ocifetch/_referrers.py,sha256=J2b4gN6vryz_IzYHxrd2Tp-tQO9Cz766rZtKgCVcKqA,6008
|
|
32
|
+
chtypes/_ocifetch/_retired.py,sha256=MQ6nbWLJw50IZYY1VfM6AiCZ4BySK0GVkGVOtAXaq3Y,3060
|
|
32
33
|
chtypes/_ocifetch/_unpack.py,sha256=Cn4-up63KL63L8pnkS5zwJ6VbbHPSigpCnmOdcHJ4RI,5609
|
|
33
|
-
chtypes-2.0.0.
|
|
34
|
-
chtypes-2.0.0.
|
|
35
|
-
chtypes-2.0.0.
|
|
36
|
-
chtypes-2.0.0.
|
|
37
|
-
chtypes-2.0.0.
|
|
34
|
+
chtypes-2.0.0.dev4.dist-info/METADATA,sha256=xGAOsOpaNNPKh1cxhmBitwUrzSsCgiwBOqNltkBmWO0,11280
|
|
35
|
+
chtypes-2.0.0.dev4.dist-info/WHEEL,sha256=W3fkpkm7-wf9vBI5Z-7s0eWkeM-spu78I8Neb98DeEg,87
|
|
36
|
+
chtypes-2.0.0.dev4.dist-info/entry_points.txt,sha256=TdNcluWBR0VOt5mVFROsp-NiVPAdR3EHo0UsHlMC0r0,50
|
|
37
|
+
chtypes-2.0.0.dev4.dist-info/licenses/LICENSE,sha256=z8d0m5b2O9McPEK1xHG_dWgUBT6EfBDz6wA0F7xSPTA,11358
|
|
38
|
+
chtypes-2.0.0.dev4.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|