chtypes 2.0.0.dev3__py3-none-any.whl → 2.0.0.dev5__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 +10 -0
- chtypes/_abi2/_decls.py +2 -2
- chtypes/_abi2/_errmap.py +1 -1
- chtypes/_abi2/_vocab.py +57 -5
- chtypes/_decode.py +25 -1
- chtypes/_ocifetch/_channel.py +65 -0
- chtypes/_ocifetch/_constants.py +5 -1
- chtypes/_ocifetch/_ensure.py +51 -4
- chtypes/_ocifetch/_errors.py +15 -1
- chtypes/_ocifetch/_http.py +28 -1
- chtypes/_ocifetch/_oci.py +10 -6
- chtypes/_ocifetch/_retired.py +77 -0
- chtypes/errors.py +12 -0
- chtypes/registry.py +1 -0
- chtypes/results.py +41 -6
- {chtypes-2.0.0.dev3.dist-info → chtypes-2.0.0.dev5.dist-info}/METADATA +3 -1
- {chtypes-2.0.0.dev3.dist-info → chtypes-2.0.0.dev5.dist-info}/RECORD +20 -19
- {chtypes-2.0.0.dev3.dist-info → chtypes-2.0.0.dev5.dist-info}/WHEEL +0 -0
- {chtypes-2.0.0.dev3.dist-info → chtypes-2.0.0.dev5.dist-info}/entry_points.txt +0 -0
- {chtypes-2.0.0.dev3.dist-info → chtypes-2.0.0.dev5.dist-info}/licenses/LICENSE +0 -0
chtypes/__init__.py
CHANGED
|
@@ -48,6 +48,8 @@ from importlib.metadata import version as _package_version
|
|
|
48
48
|
|
|
49
49
|
from ._abi2._vocab import (
|
|
50
50
|
EXPORT_NONE,
|
|
51
|
+
DeclinedLayer,
|
|
52
|
+
DeclinedTier,
|
|
51
53
|
DefaultKind,
|
|
52
54
|
DiscoverQueryParam,
|
|
53
55
|
DocFlags,
|
|
@@ -72,6 +74,7 @@ from .errors import (
|
|
|
72
74
|
CODE_CACHE_UNUSABLE,
|
|
73
75
|
CODE_SOURCE_FORBIDDEN,
|
|
74
76
|
CODE_SOURCE_INCOMPATIBLE,
|
|
77
|
+
CODE_SOURCE_RETIRED,
|
|
75
78
|
CODE_SOURCE_UNAUTHORIZED,
|
|
76
79
|
CODE_SOURCE_UNREACHABLE,
|
|
77
80
|
ArtifactCorruptError,
|
|
@@ -88,6 +91,7 @@ from .errors import (
|
|
|
88
91
|
SchemaError,
|
|
89
92
|
SourceForbiddenError,
|
|
90
93
|
SourceIncompatibleError,
|
|
94
|
+
SourceRetiredError,
|
|
91
95
|
SourceUnauthorizedError,
|
|
92
96
|
SourceUnreachableError,
|
|
93
97
|
UnsupportedError,
|
|
@@ -102,6 +106,7 @@ from .results import (
|
|
|
102
106
|
Capabilities,
|
|
103
107
|
Column,
|
|
104
108
|
Computed,
|
|
109
|
+
DeclinedSetting,
|
|
105
110
|
DiscoveredColumn,
|
|
106
111
|
Discovery,
|
|
107
112
|
EngineCell,
|
|
@@ -130,6 +135,7 @@ __all__ = [
|
|
|
130
135
|
"CODE_CACHE_UNUSABLE",
|
|
131
136
|
"CODE_SOURCE_FORBIDDEN",
|
|
132
137
|
"CODE_SOURCE_INCOMPATIBLE",
|
|
138
|
+
"CODE_SOURCE_RETIRED",
|
|
133
139
|
"CODE_SOURCE_UNAUTHORIZED",
|
|
134
140
|
"CODE_SOURCE_UNREACHABLE",
|
|
135
141
|
"EXPORT_NONE",
|
|
@@ -151,6 +157,9 @@ __all__ = [
|
|
|
151
157
|
"ChtypesError",
|
|
152
158
|
"Column",
|
|
153
159
|
"Computed",
|
|
160
|
+
"DeclinedLayer",
|
|
161
|
+
"DeclinedSetting",
|
|
162
|
+
"DeclinedTier",
|
|
154
163
|
"DefaultKind",
|
|
155
164
|
"DiscoverQueryParam",
|
|
156
165
|
"DiscoveredColumn",
|
|
@@ -186,6 +195,7 @@ __all__ = [
|
|
|
186
195
|
"Source",
|
|
187
196
|
"SourceForbiddenError",
|
|
188
197
|
"SourceIncompatibleError",
|
|
198
|
+
"SourceRetiredError",
|
|
189
199
|
"SourceUnauthorizedError",
|
|
190
200
|
"SourceUnreachableError",
|
|
191
201
|
"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:d60a681ea0e249ab9e2c83b50640ad9cef040261ddf61805106c93e2c68774e4) — 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:d60a681ea0e249ab9e2c83b50640ad9cef040261ddf61805106c93e2c68774e4"
|
|
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:d60a681ea0e249ab9e2c83b50640ad9cef040261ddf61805106c93e2c68774e4) — 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:d60a681ea0e249ab9e2c83b50640ad9cef040261ddf61805106c93e2c68774e4) — 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`, `MergeReason`, `
|
|
5
|
-
facts the description attaches to each value
|
|
6
|
-
`Source` is_stored, `Verdict.answered`), and the
|
|
7
|
-
this package keeps a copy of any of it.
|
|
4
|
+
`Verdict`, `DefaultKind`, `MergeReason`, `DeclinedTier`, `DeclinedLayer`, `Reason`,
|
|
5
|
+
`Source`, `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
|
|
@@ -249,6 +249,56 @@ class MergeReason(StrEnum):
|
|
|
249
249
|
return cls(text)
|
|
250
250
|
|
|
251
251
|
|
|
252
|
+
class DeclinedTier(StrEnum):
|
|
253
|
+
"""The WHERE-settings tier of a setting a settings layer 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
|
+
|
|
277
|
+
class DeclinedLayer(StrEnum):
|
|
278
|
+
"""The settings layer a `DeclinedSetting` comes from, the highest one that sets
|
|
279
|
+
the name: `defaults`, `server` or `schema`, lowest first."""
|
|
280
|
+
|
|
281
|
+
DEFAULTS = "defaults"
|
|
282
|
+
SERVER = "server"
|
|
283
|
+
SCHEMA = "schema"
|
|
284
|
+
|
|
285
|
+
@classmethod
|
|
286
|
+
def _missing_(cls, value: object) -> DeclinedLayer | None:
|
|
287
|
+
return _unknown_str(cls, value)
|
|
288
|
+
|
|
289
|
+
@property
|
|
290
|
+
def known(self) -> bool:
|
|
291
|
+
"""Whether the description lists this value: False for exactly the unknown(n)
|
|
292
|
+
member an unlisted value reads as (rule r3)."""
|
|
293
|
+
return type(self)._value2member_map_.get(self._value_) is self
|
|
294
|
+
|
|
295
|
+
@classmethod
|
|
296
|
+
def of(cls, text: str) -> DeclinedLayer:
|
|
297
|
+
"""Map a document's spelling. One the description does not list is kept as
|
|
298
|
+
this vocabulary's unknown(n) member, carrying it (rule r3)."""
|
|
299
|
+
return cls(text)
|
|
300
|
+
|
|
301
|
+
|
|
252
302
|
class DiscoverQueryParam(StrEnum):
|
|
253
303
|
"""A query parameter of the SQL `discover_query` returns, which the caller binds
|
|
254
304
|
when it runs the query."""
|
|
@@ -415,6 +465,8 @@ DESCRIBED_VOCABULARIES: Final[dict[str, type]] = {
|
|
|
415
465
|
"filter_verdict": Verdict,
|
|
416
466
|
"discover_query_param": DiscoverQueryParam,
|
|
417
467
|
"default_kind": DefaultKind,
|
|
468
|
+
"declined_tier": DeclinedTier,
|
|
469
|
+
"declined_layer": DeclinedLayer,
|
|
418
470
|
}
|
|
419
471
|
|
|
420
472
|
|
chtypes/_decode.py
CHANGED
|
@@ -28,7 +28,17 @@ 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
|
+
DeclinedLayer,
|
|
33
|
+
DeclinedTier,
|
|
34
|
+
DefaultKind,
|
|
35
|
+
FilterOutcome,
|
|
36
|
+
MergeReason,
|
|
37
|
+
Outcome,
|
|
38
|
+
Reason,
|
|
39
|
+
Source,
|
|
40
|
+
Verdict,
|
|
41
|
+
)
|
|
32
42
|
from .errors import ArtifactCorruptError, internal
|
|
33
43
|
from .results import (
|
|
34
44
|
AtMergeEntry,
|
|
@@ -37,6 +47,7 @@ from .results import (
|
|
|
37
47
|
Capabilities,
|
|
38
48
|
Column,
|
|
39
49
|
Computed,
|
|
50
|
+
DeclinedSetting,
|
|
40
51
|
DiscoveredColumn,
|
|
41
52
|
Discovery,
|
|
42
53
|
EngineCell,
|
|
@@ -493,6 +504,15 @@ def _string_map(obj: Mapping[str, Any], key: str, where: str) -> dict[str, str]
|
|
|
493
504
|
return out
|
|
494
505
|
|
|
495
506
|
|
|
507
|
+
def _declined_setting(obj: dict[str, Any], where: str) -> DeclinedSetting:
|
|
508
|
+
# An unlisted tier or layer is its unknown(n): kept, never a failure (rule r3).
|
|
509
|
+
return DeclinedSetting(
|
|
510
|
+
name=_name(obj, "name", where),
|
|
511
|
+
tier=DeclinedTier.of(_text(obj, "tier", where)),
|
|
512
|
+
layer=DeclinedLayer.of(_text(obj, "layer", where)),
|
|
513
|
+
)
|
|
514
|
+
|
|
515
|
+
|
|
496
516
|
def _server(obj: dict[str, Any], where: str) -> SchemaServer:
|
|
497
517
|
return SchemaServer(
|
|
498
518
|
timezone=_text(obj, "timezone", where),
|
|
@@ -525,6 +545,10 @@ def decode_schema_description(raw: bytes) -> SchemaDescription:
|
|
|
525
545
|
if replicated is None
|
|
526
546
|
else _replicated(_obj(replicated, where, "replicated"), f"{where}.replicated")
|
|
527
547
|
),
|
|
548
|
+
# Over every layer known at schema compile, with or without a server.
|
|
549
|
+
filter_declined_settings=_objects(
|
|
550
|
+
obj, "filter_declined_settings", where, _declined_setting
|
|
551
|
+
),
|
|
528
552
|
)
|
|
529
553
|
|
|
530
554
|
|
chtypes/_ocifetch/_channel.py
CHANGED
|
@@ -52,6 +52,7 @@ from typing import Final, Protocol
|
|
|
52
52
|
from chtypes._abi2._decls import CHS_ABI_FINGERPRINT
|
|
53
53
|
from chtypes._ocifetch import _constants as C
|
|
54
54
|
from chtypes._ocifetch._dsse import TrustedKey
|
|
55
|
+
from chtypes._ocifetch._errors import ArtifactUnpublishedError
|
|
55
56
|
|
|
56
57
|
__all__ = [
|
|
57
58
|
"ALIAS_SEPARATOR",
|
|
@@ -66,6 +67,7 @@ __all__ = [
|
|
|
66
67
|
"PinningRefusedError",
|
|
67
68
|
"abi",
|
|
68
69
|
"active",
|
|
70
|
+
"ahead_of_registry",
|
|
69
71
|
"alias_tag",
|
|
70
72
|
"allow_overrides_for_tests",
|
|
71
73
|
"channel_name",
|
|
@@ -274,6 +276,69 @@ def visible(predicate: object, channel: Channel | None = None) -> bool:
|
|
|
274
276
|
return fingerprint == f"sha256:{channel.own_fingerprint}"
|
|
275
277
|
|
|
276
278
|
|
|
279
|
+
def ahead_of_registry(
|
|
280
|
+
alias_absent: bool,
|
|
281
|
+
predicate: object,
|
|
282
|
+
channel: Channel | None = None,
|
|
283
|
+
*,
|
|
284
|
+
cached_own_build: Callable[[], str | None] | None = None,
|
|
285
|
+
) -> None:
|
|
286
|
+
"""The dev channel's answer for an SDK whose fingerprint no published build
|
|
287
|
+
carries yet (docs/guides/fetch-v1.md §3; public issue #578): when every base
|
|
288
|
+
answered its own alias 404 (`alias_absent`) and the build the tag names is
|
|
289
|
+
signed for another fingerprint (`predicate`, the SIGNED predicate, the field
|
|
290
|
+
`visible` keys on), raise `ArtifactUnpublishedError`, the code a request no
|
|
291
|
+
build answers gets, naming both fingerprints. Nothing is installed and an
|
|
292
|
+
install of that build is never reported, because the lookup this SDK opens
|
|
293
|
+
through would refuse it. Returns in every other case: the alias answered,
|
|
294
|
+
the tag's build is this SDK's own, or the contract has no own fingerprint.
|
|
295
|
+
|
|
296
|
+
The expired alias with a usable cache (public issue #581): `cached_own_build`
|
|
297
|
+
looks up, through the filter open uses, the cache's verified record of this
|
|
298
|
+
SDK's OWN fingerprint for the request, and returns its build id ("" for a
|
|
299
|
+
record that names none) or None for no record. When there is one the
|
|
300
|
+
message grows by `_cached_own_suffix`; the code does not change."""
|
|
301
|
+
channel = channel or active()
|
|
302
|
+
if not alias_absent or visible(predicate, channel):
|
|
303
|
+
return
|
|
304
|
+
message = _ahead_message(channel.own_fingerprint, predicate)
|
|
305
|
+
if cached_own_build is not None:
|
|
306
|
+
build = cached_own_build()
|
|
307
|
+
if build is not None:
|
|
308
|
+
message += _cached_own_suffix(build)
|
|
309
|
+
raise ArtifactUnpublishedError(f"chtypes: {message}")
|
|
310
|
+
|
|
311
|
+
|
|
312
|
+
def _cached_own_suffix(build: str) -> str:
|
|
313
|
+
"""The clause appended when the cache holds an own-fingerprint build: "; a
|
|
314
|
+
cached build for this fingerprint (build <id>) is still usable by open;
|
|
315
|
+
upgrade the SDK to get newer builds", the parenthesis dropped when the
|
|
316
|
+
record names no build."""
|
|
317
|
+
suffix = "; a cached build for this fingerprint"
|
|
318
|
+
if build:
|
|
319
|
+
suffix += f" (build {build})"
|
|
320
|
+
return suffix + " is still usable by open; upgrade the SDK to get newer builds"
|
|
321
|
+
|
|
322
|
+
|
|
323
|
+
def _ahead_message(own: str, predicate: object) -> str:
|
|
324
|
+
"""`ahead_of_registry`'s message: "no published build for this SDK's
|
|
325
|
+
fingerprint <own>; newest published on this channel is <fp> (build <id>)",
|
|
326
|
+
each fingerprint 64 lowercase hex without its `sha256:` prefix, the
|
|
327
|
+
parenthesis dropped when the predicate names no build, and "unnamed" for a
|
|
328
|
+
predicate that names no fingerprint."""
|
|
329
|
+
fields = predicate if isinstance(predicate, dict) else {}
|
|
330
|
+
theirs = fields.get("abi_fingerprint")
|
|
331
|
+
theirs = theirs.removeprefix("sha256:") if isinstance(theirs, str) else ""
|
|
332
|
+
message = (
|
|
333
|
+
f"no published build for this SDK's fingerprint {own}; "
|
|
334
|
+
f"newest published on this channel is {theirs or 'unnamed'}"
|
|
335
|
+
)
|
|
336
|
+
build = fields.get("build")
|
|
337
|
+
if isinstance(build, str) and build:
|
|
338
|
+
message += f" (build {build})"
|
|
339
|
+
return message
|
|
340
|
+
|
|
341
|
+
|
|
277
342
|
def use_dev_channel_for_tests() -> Callable[[], None]:
|
|
278
343
|
"""Make this TEST run speak the dev channel exactly as a user's process does
|
|
279
344
|
(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,
|
|
@@ -440,7 +442,7 @@ def _find_verified_signature(
|
|
|
440
442
|
bases, ref.digest, policy=policy, retry=retry
|
|
441
443
|
)
|
|
442
444
|
blob_desc = manifest_single_layer(referrer_doc, expected_media_type=C.MEDIA_TYPE_BUNDLE)
|
|
443
|
-
except ArtifactCorruptError:
|
|
445
|
+
except (ArtifactCorruptError, RetiredHttpError):
|
|
444
446
|
raise
|
|
445
447
|
except (TransportError, FetchError):
|
|
446
448
|
continue
|
|
@@ -458,7 +460,7 @@ def _find_verified_signature(
|
|
|
458
460
|
with open(bundle_path, "rb") as f:
|
|
459
461
|
bundle_json = json.loads(f.read())
|
|
460
462
|
verified = verify_bundle(bundle_json, trusted_keys)
|
|
461
|
-
except ArtifactCorruptError:
|
|
463
|
+
except (ArtifactCorruptError, RetiredHttpError):
|
|
462
464
|
raise
|
|
463
465
|
except (TransportError, FetchError, json.JSONDecodeError):
|
|
464
466
|
continue
|
|
@@ -698,7 +700,7 @@ def _ensure_floating(
|
|
|
698
700
|
cache_root_path = roots[0]
|
|
699
701
|
scratch_root = _scratch_root(cache_root_path)
|
|
700
702
|
|
|
701
|
-
index_doc, index_bytes, index_digest = fetch_manifest_by_tag(
|
|
703
|
+
index_doc, index_bytes, index_digest, alias_absent = fetch_manifest_by_tag(
|
|
702
704
|
bases,
|
|
703
705
|
request.spelling,
|
|
704
706
|
policy=policy,
|
|
@@ -711,6 +713,14 @@ def _ensure_floating(
|
|
|
711
713
|
existing_dir = unpacked_dir_for(cache_root_path, manifest_hex)
|
|
712
714
|
existing_record = read_verified_record(existing_dir)
|
|
713
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(
|
|
720
|
+
alias_absent,
|
|
721
|
+
existing_record.predicate,
|
|
722
|
+
cached_own_build=lambda: _cached_own_build(roots, platform_key, request.spelling),
|
|
723
|
+
)
|
|
714
724
|
# A NEWER build within the request, installed beside it, is still the
|
|
715
725
|
# answer ("monotonic-warning", docs/guides/fetch-v1.md §9).
|
|
716
726
|
newer = _newer_installed(
|
|
@@ -801,6 +811,16 @@ def _ensure_floating(
|
|
|
801
811
|
signed_by = verified.signed_by
|
|
802
812
|
|
|
803
813
|
_check_predicate_matches_request(predicate, platform_key, request.spelling)
|
|
814
|
+
if found is not None:
|
|
815
|
+
# An SDK ahead of the registry (public issue #578): the alias for its
|
|
816
|
+
# own fingerprint answered 404 on every base, and the tag's build is
|
|
817
|
+
# signed for another. Refused from the signed statement, before the
|
|
818
|
+
# layer is requested, so nothing is downloaded or installed.
|
|
819
|
+
_channel.ahead_of_registry(
|
|
820
|
+
alias_absent,
|
|
821
|
+
predicate,
|
|
822
|
+
cached_own_build=lambda: _cached_own_build(roots, platform_key, request.spelling),
|
|
823
|
+
)
|
|
804
824
|
newer = _newer_installed(roots, platform_key, predicate, request.spelling)
|
|
805
825
|
if newer is not None:
|
|
806
826
|
newer_dir, newer_record, warning = newer
|
|
@@ -983,6 +1003,8 @@ def _translate_transport_error(e: TransportError) -> FetchError:
|
|
|
983
1003
|
return SourceForbiddenError(f"chtypes: {e}")
|
|
984
1004
|
if isinstance(e, OversizeHttpError):
|
|
985
1005
|
return ArtifactCorruptError(f"chtypes: {e}")
|
|
1006
|
+
if isinstance(e, RetiredHttpError):
|
|
1007
|
+
return SourceRetiredError(f"chtypes: {e}")
|
|
986
1008
|
if isinstance(e, UnreachableHttpError):
|
|
987
1009
|
return SourceUnreachableError(f"chtypes: {e}", retryable=getattr(e, "retryable", True))
|
|
988
1010
|
return SourceUnreachableError(f"chtypes: {e}")
|
|
@@ -1221,6 +1243,25 @@ def with_notes(message: str, notes: Sequence[str]) -> str:
|
|
|
1221
1243
|
return f"{message}. {' '.join(notes)}"
|
|
1222
1244
|
|
|
1223
1245
|
|
|
1246
|
+
def _cached_own_build(
|
|
1247
|
+
roots: Sequence[Path], platform_key: str, request_spelling: str
|
|
1248
|
+
) -> str | None:
|
|
1249
|
+
"""The build id of the newest cached record of this SDK's OWN fingerprint
|
|
1250
|
+
that answers the request (`resolve_installed`'s filter), "" when it names
|
|
1251
|
+
none, or None when the cache holds no such record (public issue #581)."""
|
|
1252
|
+
candidates = [
|
|
1253
|
+
record
|
|
1254
|
+
for _dir, record in list_verified_records(roots)
|
|
1255
|
+
if record.platform == platform_key
|
|
1256
|
+
and version_within_request(record.version, request_spelling)
|
|
1257
|
+
and _channel.visible(record.predicate)
|
|
1258
|
+
]
|
|
1259
|
+
if not candidates:
|
|
1260
|
+
return None
|
|
1261
|
+
best = max(candidates, key=lambda r: (spelling_components(r.version), r.build))
|
|
1262
|
+
return best.build or ""
|
|
1263
|
+
|
|
1264
|
+
|
|
1224
1265
|
def resolve_installed(request: Request, platform: str, options: Options) -> Resolved | None:
|
|
1225
1266
|
"""Cache-only: never touches the network (docs/guides/fetch-v1.md "The seam").
|
|
1226
1267
|
|
|
@@ -1368,7 +1409,9 @@ def _fetch_signed_impl(repository: str, ref: str, predicate_type: str, options:
|
|
|
1368
1409
|
doc, _raw = fetch_manifest_by_digest(bases, ref, policy=policy, retry=retry)
|
|
1369
1410
|
manifest_digest = ref
|
|
1370
1411
|
else:
|
|
1371
|
-
doc, _raw, manifest_digest = fetch_manifest_by_tag(
|
|
1412
|
+
doc, _raw, manifest_digest, _alias_absent = fetch_manifest_by_tag(
|
|
1413
|
+
bases, ref, policy=policy, retry=retry
|
|
1414
|
+
)
|
|
1372
1415
|
|
|
1373
1416
|
layer_desc = manifest_single_layer(doc)
|
|
1374
1417
|
found = _find_verified_signature(
|
|
@@ -1466,6 +1509,8 @@ def _fetch_goldens(
|
|
|
1466
1509
|
retry=retry,
|
|
1467
1510
|
max_bytes=C.MAX_UNPACKED_BYTES,
|
|
1468
1511
|
)
|
|
1512
|
+
except RetiredHttpError:
|
|
1513
|
+
raise
|
|
1469
1514
|
except (TransportError, FetchError) as e:
|
|
1470
1515
|
first_failure = first_failure or e
|
|
1471
1516
|
continue
|
|
@@ -1580,6 +1625,8 @@ def _verify_goldens_signature(
|
|
|
1580
1625
|
except ArtifactCorruptError as e:
|
|
1581
1626
|
failure = failure or e
|
|
1582
1627
|
continue
|
|
1628
|
+
except RetiredHttpError:
|
|
1629
|
+
raise
|
|
1583
1630
|
except (TransportError, FetchError, json.JSONDecodeError):
|
|
1584
1631
|
continue
|
|
1585
1632
|
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",
|
|
@@ -120,6 +122,13 @@ class OversizeHttpError(TransportError):
|
|
|
120
122
|
"""A response body exceeded the caller's declared size cap."""
|
|
121
123
|
|
|
122
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
|
+
|
|
123
132
|
class Clock:
|
|
124
133
|
"""The real wall clock. Tests inject a fake with the same interface so
|
|
125
134
|
retry cases run instantly and sleeps are asserted exactly."""
|
|
@@ -228,7 +237,16 @@ def _single_request(
|
|
|
228
237
|
)
|
|
229
238
|
except urllib.error.HTTPError as e:
|
|
230
239
|
try:
|
|
231
|
-
|
|
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""
|
|
232
250
|
finally:
|
|
233
251
|
e.close()
|
|
234
252
|
return HttpResponse(status=e.code, headers=dict(e.headers or {}), body=body, url=url)
|
|
@@ -380,6 +398,11 @@ def _http_get_once(
|
|
|
380
398
|
headers.pop("Authorization", None)
|
|
381
399
|
continue
|
|
382
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
|
+
|
|
383
406
|
if resp.status == 401:
|
|
384
407
|
if not tried_anon_flow and not policy.token:
|
|
385
408
|
challenge = resp.headers.get("WWW-Authenticate") or resp.headers.get(
|
|
@@ -523,6 +546,10 @@ def fetch_from_bases(
|
|
|
523
546
|
404, some unreachable — the outcome follows the LAST base tried, since
|
|
524
547
|
bases are an explicit preference order and the last one is the final
|
|
525
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.
|
|
526
553
|
"""
|
|
527
554
|
if not bases:
|
|
528
555
|
raise UnreachableHttpError("no base URLs configured")
|
chtypes/_ocifetch/_oci.py
CHANGED
|
@@ -249,15 +249,19 @@ def fetch_manifest_by_tag(
|
|
|
249
249
|
retry: RetryPolicy,
|
|
250
250
|
max_bytes: int = C.MANIFEST_MAX_BYTES,
|
|
251
251
|
alias: str | None = None,
|
|
252
|
-
) -> tuple[dict, bytes, str]:
|
|
253
|
-
"""GET manifests/<tag>; returns (doc, raw_bytes, resolved_digest).
|
|
252
|
+
) -> tuple[dict, bytes, str, bool]:
|
|
253
|
+
"""GET manifests/<tag>; returns (doc, raw_bytes, resolved_digest, alias_absent).
|
|
254
254
|
|
|
255
255
|
With `alias` (the dev channel's `<tag>--fp-<fingerprint>`, from
|
|
256
256
|
`_channel.alias_tag`), GET manifests/<alias> first, and the tag only when
|
|
257
257
|
every base answered the alias 404 (docs/guides/fetch-v1.md §3); any other
|
|
258
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.
|
|
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`)."""
|
|
260
263
|
accept = (C.MEDIA_TYPE_INDEX, C.MEDIA_TYPE_MANIFEST)
|
|
264
|
+
alias_absent = False
|
|
261
265
|
if alias is not None:
|
|
262
266
|
try:
|
|
263
267
|
doc, resp = get_json(
|
|
@@ -270,9 +274,9 @@ def fetch_manifest_by_tag(
|
|
|
270
274
|
retry=retry,
|
|
271
275
|
)
|
|
272
276
|
except AliasNotFoundError:
|
|
273
|
-
|
|
277
|
+
alias_absent = True
|
|
274
278
|
else:
|
|
275
|
-
return doc, resp.body, f"sha256:{hashlib.sha256(resp.body).hexdigest()}"
|
|
279
|
+
return doc, resp.body, f"sha256:{hashlib.sha256(resp.body).hexdigest()}", False
|
|
276
280
|
doc, resp = get_json(
|
|
277
281
|
bases,
|
|
278
282
|
f"/manifests/{tag}",
|
|
@@ -283,7 +287,7 @@ def fetch_manifest_by_tag(
|
|
|
283
287
|
retry=retry,
|
|
284
288
|
)
|
|
285
289
|
digest = f"sha256:{hashlib.sha256(resp.body).hexdigest()}"
|
|
286
|
-
return doc, resp.body, digest
|
|
290
|
+
return doc, resp.body, digest, alias_absent
|
|
287
291
|
|
|
288
292
|
|
|
289
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,7 +14,15 @@ from __future__ import annotations
|
|
|
14
14
|
from collections.abc import Mapping
|
|
15
15
|
from dataclasses import dataclass
|
|
16
16
|
|
|
17
|
-
from ._abi2._vocab import
|
|
17
|
+
from ._abi2._vocab import (
|
|
18
|
+
DeclinedLayer,
|
|
19
|
+
DeclinedTier,
|
|
20
|
+
DefaultKind,
|
|
21
|
+
FilterOutcome,
|
|
22
|
+
MergeReason,
|
|
23
|
+
Outcome,
|
|
24
|
+
Verdict,
|
|
25
|
+
)
|
|
18
26
|
|
|
19
27
|
__all__ = [
|
|
20
28
|
"AtMergeEntry",
|
|
@@ -23,6 +31,7 @@ __all__ = [
|
|
|
23
31
|
"Capabilities",
|
|
24
32
|
"Column",
|
|
25
33
|
"Computed",
|
|
34
|
+
"DeclinedSetting",
|
|
26
35
|
"DiscoveredColumn",
|
|
27
36
|
"EngineCell",
|
|
28
37
|
"Discovery",
|
|
@@ -223,8 +232,10 @@ class FilterRowError:
|
|
|
223
232
|
|
|
224
233
|
@dataclass(frozen=True, slots=True)
|
|
225
234
|
class FilterResult:
|
|
226
|
-
"""The `filter_result` document.
|
|
227
|
-
|
|
235
|
+
"""The `filter_result` document. A verdict counts only when `outcome` is `ok`;
|
|
236
|
+
otherwise treat every verdict as `d`, whatever the verdict string holds. Under
|
|
237
|
+
`ok`, `e` and `d` are never answers, and a caller enforcing visibility fails
|
|
238
|
+
closed on both; `Verdict.answered` says which."""
|
|
228
239
|
|
|
229
240
|
outcome: FilterOutcome
|
|
230
241
|
err_code: int
|
|
@@ -247,6 +258,21 @@ class Column:
|
|
|
247
258
|
default_expr: bytes
|
|
248
259
|
|
|
249
260
|
|
|
261
|
+
@dataclass(frozen=True, slots=True)
|
|
262
|
+
class DeclinedSetting:
|
|
263
|
+
"""One setting the schema compiles under that this build's filters do not
|
|
264
|
+
honor in a WHERE: an entry of the schema description's
|
|
265
|
+
`filter_declined_settings` (ABI v2). `name` is bytes (from `name` or
|
|
266
|
+
`name_b64`). `tier` is the tier this build line's WHERE-settings list gives
|
|
267
|
+
it, a `DeclinedTier`, and `layer` the settings layer it comes from, the
|
|
268
|
+
highest one that sets the name, a `DeclinedLayer`; a value the description
|
|
269
|
+
does not list is its vocabulary's unknown(n) member (rule r3)."""
|
|
270
|
+
|
|
271
|
+
name: bytes
|
|
272
|
+
tier: DeclinedTier
|
|
273
|
+
layer: DeclinedLayer
|
|
274
|
+
|
|
275
|
+
|
|
250
276
|
@dataclass(frozen=True, slots=True)
|
|
251
277
|
class SchemaServer:
|
|
252
278
|
"""The server a schema was compiled on, as the library holds it. Its strings
|
|
@@ -272,13 +298,22 @@ class SchemaReplicated:
|
|
|
272
298
|
|
|
273
299
|
@dataclass(frozen=True, slots=True)
|
|
274
300
|
class SchemaDescription:
|
|
275
|
-
"""A schema's columns, in declared order,
|
|
276
|
-
`server` is None exactly when it was
|
|
277
|
-
None when the document carries none.
|
|
301
|
+
"""A schema's columns, in declared order, the server it was compiled on, and
|
|
302
|
+
the settings its filters decline. `server` is None exactly when it was
|
|
303
|
+
compiled without one; `replicated` is None when the document carries none.
|
|
304
|
+
`filter_declined_settings` is the settings the schema compiles under that
|
|
305
|
+
this build's filters do not honor in a WHERE, over every layer known at
|
|
306
|
+
schema compile (the defaults `setup` set, the server profile and the
|
|
307
|
+
schema's own settings), empty when it lists none: one entry per name, at
|
|
308
|
+
the highest layer that sets it, ordered by layer and then by name bytes, as
|
|
309
|
+
the library wrote it. A schema that lists any is one `compile_filter`
|
|
310
|
+
declines (`UnsupportedError`), and a consumer that streams rows refuses
|
|
311
|
+
such a tenant (spec/abi-v2/where-settings/README.md)."""
|
|
278
312
|
|
|
279
313
|
columns: tuple[Column, ...]
|
|
280
314
|
server: SchemaServer | None = None
|
|
281
315
|
replicated: SchemaReplicated | None = None
|
|
316
|
+
filter_declined_settings: tuple[DeclinedSetting, ...] = ()
|
|
282
317
|
|
|
283
318
|
|
|
284
319
|
@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.dev5
|
|
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
|
|
@@ -22,6 +22,8 @@ Description-Content-Type: text/markdown
|
|
|
22
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=SGE4VfnmW5NbOQSExtplVwFPhddBQHVJ38Yv-CUWJ0o,6217
|
|
2
2
|
chtypes/__main__.py,sha256=XYGNTpTLXI1AUp7srvuNVv2SZAcfwLaXpoXRVInyhF4,14030
|
|
3
|
-
chtypes/_decode.py,sha256=
|
|
3
|
+
chtypes/_decode.py,sha256=owtRPXVPtNOGzQzgb37Rki7iceMoua_k_VWY38RkaIo,22852
|
|
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=gue7f4pqnpvXlSteYqhmqU1ZuAYCD7p4AgTiuYWpgYY,12591
|
|
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=tGj_gOkMsw9fv8lWBXHUMn6dfYqpkta_nq1BhT6p4Dk,38454
|
|
15
|
+
chtypes/_abi2/_errmap.py,sha256=BQ2Wgj8H3ukUQwCAKgUyAhs6xy5fDoYYLX1YZGdWEcA,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=6Ly15gQ1Vf3SP8hDSWCg1jnwVEURCBWSj9LZgv23xfA,17442
|
|
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=8GqSNwuWaO3f4550N_DEsswqJu-0mWFAXbCB3ccFF9M,19108
|
|
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=rI_xj0dccct7wMz0DwHoJTAB-yGVYFkLZ2Z0qRXuR30,63612
|
|
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.dev5.dist-info/METADATA,sha256=42l-skEiDCQQ3VA7jXzokD7woNnMpYzZm-qVMw2vivc,11280
|
|
35
|
+
chtypes-2.0.0.dev5.dist-info/WHEEL,sha256=W3fkpkm7-wf9vBI5Z-7s0eWkeM-spu78I8Neb98DeEg,87
|
|
36
|
+
chtypes-2.0.0.dev5.dist-info/entry_points.txt,sha256=TdNcluWBR0VOt5mVFROsp-NiVPAdR3EHo0UsHlMC0r0,50
|
|
37
|
+
chtypes-2.0.0.dev5.dist-info/licenses/LICENSE,sha256=z8d0m5b2O9McPEK1xHG_dWgUBT6EfBDz6wA0F7xSPTA,11358
|
|
38
|
+
chtypes-2.0.0.dev5.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|