chtypes 0.3.1__tar.gz → 0.3.2__tar.gz
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-0.3.1 → chtypes-0.3.2}/CHANGELOG.md +17 -0
- {chtypes-0.3.1 → chtypes-0.3.2}/PKG-INFO +2 -2
- {chtypes-0.3.1 → chtypes-0.3.2}/README.md +1 -1
- {chtypes-0.3.1 → chtypes-0.3.2}/pyproject.toml +1 -1
- {chtypes-0.3.1 → chtypes-0.3.2}/src/chtypes/__main__.py +13 -1
- {chtypes-0.3.1 → chtypes-0.3.2}/src/chtypes/_manifest.py +9 -4
- {chtypes-0.3.1 → chtypes-0.3.2}/src/chtypes/fetch.py +147 -21
- {chtypes-0.3.1 → chtypes-0.3.2}/src/chtypes/registry.py +4 -4
- {chtypes-0.3.1 → chtypes-0.3.2}/tests/conftest.py +44 -2
- {chtypes-0.3.1 → chtypes-0.3.2}/tests/test_cli.py +36 -4
- {chtypes-0.3.1 → chtypes-0.3.2}/tests/test_fetch.py +116 -6
- chtypes-0.3.2/tests/test_fetch_abi_revision.py +248 -0
- chtypes-0.3.2/tests/test_fetch_sh.py +139 -0
- chtypes-0.3.2/uv.lock +108 -0
- chtypes-0.3.1/uv.lock +0 -108
- {chtypes-0.3.1 → chtypes-0.3.2}/.gitignore +0 -0
- {chtypes-0.3.1 → chtypes-0.3.2}/LICENSE +0 -0
- {chtypes-0.3.1 → chtypes-0.3.2}/src/chtypes/__init__.py +0 -0
- {chtypes-0.3.1 → chtypes-0.3.2}/src/chtypes/_document.py +0 -0
- {chtypes-0.3.1 → chtypes-0.3.2}/src/chtypes/_ed25519.py +0 -0
- {chtypes-0.3.1 → chtypes-0.3.2}/src/chtypes/_native.py +0 -0
- {chtypes-0.3.1 → chtypes-0.3.2}/src/chtypes/_rawjson.py +0 -0
- {chtypes-0.3.1 → chtypes-0.3.2}/src/chtypes/discover.py +0 -0
- {chtypes-0.3.1 → chtypes-0.3.2}/src/chtypes/errors.py +0 -0
- {chtypes-0.3.1 → chtypes-0.3.2}/src/chtypes/py.typed +0 -0
- {chtypes-0.3.1 → chtypes-0.3.2}/src/chtypes/results.py +0 -0
- {chtypes-0.3.1 → chtypes-0.3.2}/src/chtypes/transform.py +0 -0
- {chtypes-0.3.1 → chtypes-0.3.2}/tests/test_abi_revision.py +0 -0
- {chtypes-0.3.1 → chtypes-0.3.2}/tests/test_csv_reader.py +0 -0
- {chtypes-0.3.1 → chtypes-0.3.2}/tests/test_discover_reconstruct.py +0 -0
- {chtypes-0.3.1 → chtypes-0.3.2}/tests/test_ed25519.py +0 -0
- {chtypes-0.3.1 → chtypes-0.3.2}/tests/test_errors.py +0 -0
- {chtypes-0.3.1 → chtypes-0.3.2}/tests/test_formats_withnames.py +0 -0
- {chtypes-0.3.1 → chtypes-0.3.2}/tests/test_golden.py +0 -0
- {chtypes-0.3.1 → chtypes-0.3.2}/tests/test_lazy.py +0 -0
- {chtypes-0.3.1 → chtypes-0.3.2}/tests/test_parity.py +0 -0
- {chtypes-0.3.1 → chtypes-0.3.2}/tests/test_quoting_boundaries.py +0 -0
- {chtypes-0.3.1 → chtypes-0.3.2}/tests/test_rawjson.py +0 -0
- {chtypes-0.3.1 → chtypes-0.3.2}/tests/test_registry.py +0 -0
- {chtypes-0.3.1 → chtypes-0.3.2}/tests/test_results_rules.py +0 -0
- {chtypes-0.3.1 → chtypes-0.3.2}/tests/test_rows_export_with.py +0 -0
- {chtypes-0.3.1 → chtypes-0.3.2}/tests/test_transform.py +0 -0
|
@@ -6,6 +6,23 @@ The four bindings in this repository are released together and give one answer,
|
|
|
6
6
|
|
|
7
7
|
## [Unreleased]
|
|
8
8
|
|
|
9
|
+
## [0.3.2] — 2026-09-28
|
|
10
|
+
|
|
11
|
+
Speaks ABI revision 5, unchanged from 0.3.1: every artifact 0.3.1 loads, this release loads.
|
|
12
|
+
|
|
13
|
+
### Added
|
|
14
|
+
|
|
15
|
+
- **`ReleaseEntry.abi_revision`** (`int | None`): the row's `abi_revision`, `None` when the row declares none or declares something that is not an integer.
|
|
16
|
+
|
|
17
|
+
### Changed
|
|
18
|
+
|
|
19
|
+
- **Installs move to a per-revision directory, so the first fetch after upgrading downloads again.** The per-user cache `default_registry_dir()` is now `${XDG_CACHE_HOME:-~/.cache}/chtypes/artifacts/abi<R>/<os>-<arch>`, `<R>` this binding's own `ABI_REVISION`, where it was `…/chtypes/artifacts/<os>-<arch>`. Two SDK versions that speak different ABI revisions on one machine no longer overwrite each other's artifacts. Nothing is migrated: the old directory is simply no longer read, and can be deleted once nothing older uses it. `CHTYPES_REGISTRY` and an explicit directory are taken exactly as before.
|
|
20
|
+
- **Fetch now selects only artifacts built at this binding's own ABI revision** (`docs/guides/fetch.md` §2). The release's rows are narrowed to those whose `abi_revision` is `ABI_REVISION` before the newest-version, highest-build rule applies; a row that declares no revision never matches. Nothing at that revision for the line and platform is `ArtifactUnpublishedError` (`CHTYPES_ARTIFACT_UNPUBLISHED`, exit 4), naming the revision(s) the release does serve — never a fallback to another revision's build, which the loader would refuse anyway. Across a revision cutover the grace this gives an older binding is temporary: an older-revision build stays fetchable only until that line's next publish evicts it under retention, which keeps two builds per version.
|
|
21
|
+
- **`fetch_lines(..., all_lines=True)` and `python -m chtypes fetch --all` no longer skip a line silently.** A line the release has for the platform only at another ABI revision, or only in rows that record none (built before revisions were recorded), is not installed, and one loud warning line (through `progress` when one is given, else on stderr) names the line, the platform, what the release does serve and this binding's revision; every other line still installs, and the exit status stays 0 when they all do.
|
|
22
|
+
- **`list` shows only the release's rows at this binding's ABI revision** — what a fetch could install — and, when that hides any of the platform's rows, adds one line naming them: `<N> row(s) at ABI revision(s) <S> not shown; this SDK speaks <R>`, or, for rows that record none, `<M> row(s) that record no ABI revision (built before revisions were recorded) not shown; this SDK speaks <R>`. Nothing is added when nothing was hidden.
|
|
23
|
+
- **On darwin-arm64, fetching line 24.8 now fails at fetch time** with `CHTYPES_ARTIFACT_UNPUBLISHED` (exit 4) instead of installing an artifact the loader then refuses: the release's only 24.8 rows for that platform record no ABI revision, because they were built before revisions were recorded, and the message says exactly that rather than that the release serves none.
|
|
24
|
+
- `Release.offered()` now returns only rows at this binding's ABI revision, and `Release.select()` considers only those.
|
|
25
|
+
|
|
9
26
|
## [0.3.1] — 2026-09-26
|
|
10
27
|
|
|
11
28
|
Speaks ABI revision 5, unchanged from 0.3.0: every artifact 0.3.0 loads, this release loads.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: chtypes
|
|
3
|
-
Version: 0.3.
|
|
3
|
+
Version: 0.3.2
|
|
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
|
|
@@ -31,7 +31,7 @@ uv add chtypes # or: pip install chtypes
|
|
|
31
31
|
python -m chtypes fetch 25.8
|
|
32
32
|
```
|
|
33
33
|
|
|
34
|
-
The fetch lands in `~/.cache/chtypes/artifacts
|
|
34
|
+
The fetch lands in `~/.cache/chtypes/artifacts/abi<R>/<os>-<arch>/25.8/` — the per-user cache every chtypes binding reads by default, `<R>` the ABI revision this SDK speaks (fetch installs only artifacts built at it) — after checking an ed25519 signature over the release and the sha256 of every byte. `$CHTYPES_REGISTRY` overrides it. The ed25519 verifier is pure stdlib too.
|
|
35
35
|
|
|
36
36
|
## Quickstart
|
|
37
37
|
|
|
@@ -13,7 +13,7 @@ uv add chtypes # or: pip install chtypes
|
|
|
13
13
|
python -m chtypes fetch 25.8
|
|
14
14
|
```
|
|
15
15
|
|
|
16
|
-
The fetch lands in `~/.cache/chtypes/artifacts
|
|
16
|
+
The fetch lands in `~/.cache/chtypes/artifacts/abi<R>/<os>-<arch>/25.8/` — the per-user cache every chtypes binding reads by default, `<R>` the ABI revision this SDK speaks (fetch installs only artifacts built at it) — after checking an ed25519 signature over the release and the sha256 of every byte. `$CHTYPES_REGISTRY` overrides it. The ed25519 verifier is pure stdlib too.
|
|
17
17
|
|
|
18
18
|
## Quickstart
|
|
19
19
|
|
|
@@ -196,7 +196,14 @@ def _cmd_verify(args: argparse.Namespace) -> int:
|
|
|
196
196
|
|
|
197
197
|
|
|
198
198
|
def _cmd_list(args: argparse.Namespace) -> int:
|
|
199
|
-
from .fetch import
|
|
199
|
+
from .fetch import (
|
|
200
|
+
Fetcher,
|
|
201
|
+
_fetch_abi_revision,
|
|
202
|
+
_not_shown,
|
|
203
|
+
fetch_destination,
|
|
204
|
+
host_platform,
|
|
205
|
+
installed_lines,
|
|
206
|
+
)
|
|
200
207
|
|
|
201
208
|
platform = args.platform or host_platform()
|
|
202
209
|
registry = fetch_destination(args.dest, platform=platform)
|
|
@@ -221,6 +228,11 @@ def _cmd_list(args: argparse.Namespace) -> int:
|
|
|
221
228
|
f" {entry.minor:<8} {entry.clickhouse_version:<18} b{entry.build_number:<3} "
|
|
222
229
|
f"{entry.file} [{state}]\n"
|
|
223
230
|
)
|
|
231
|
+
# Only this SDK's own ABI revision is offered (docs/guides/fetch.md §2), and
|
|
232
|
+
# one line names what that hid.
|
|
233
|
+
note = _not_shown([e for e in release.entries if e.platform == platform], _fetch_abi_revision())
|
|
234
|
+
if note is not None:
|
|
235
|
+
sys.stdout.write(f" {note}\n")
|
|
224
236
|
sys.stdout.flush()
|
|
225
237
|
return EXIT_OK
|
|
226
238
|
|
|
@@ -15,6 +15,7 @@ import platform as _platform
|
|
|
15
15
|
from dataclasses import dataclass, fields
|
|
16
16
|
from pathlib import Path
|
|
17
17
|
|
|
18
|
+
from ._native import ABI_REVISION
|
|
18
19
|
from .errors import RegistryError
|
|
19
20
|
|
|
20
21
|
__all__ = [
|
|
@@ -38,11 +39,15 @@ def host_platform() -> str:
|
|
|
38
39
|
|
|
39
40
|
def cache_registry_dir(platform: str | None = None) -> str:
|
|
40
41
|
"""The per-user artifact cache for a platform — ``${XDG_CACHE_HOME:-~/.cache}/
|
|
41
|
-
chtypes/artifacts
|
|
42
|
-
|
|
43
|
-
(docs/guides/fetch.md §1)
|
|
42
|
+
chtypes/artifacts/abi<R>/<os>-<arch>``, R this binding's `ABI_REVISION`, this
|
|
43
|
+
host's platform by default. Where fetch installs, and slot 3 of the registry
|
|
44
|
+
search path (docs/guides/fetch.md §1); keyed by revision so two SDK versions
|
|
45
|
+
at different revisions never share it. A path, not a promise: it need not
|
|
46
|
+
exist yet."""
|
|
44
47
|
base = os.environ.get("XDG_CACHE_HOME") or os.path.join(os.path.expanduser("~"), ".cache")
|
|
45
|
-
return os.path.join(
|
|
48
|
+
return os.path.join(
|
|
49
|
+
base, "chtypes", "artifacts", f"abi{ABI_REVISION}", platform or host_platform()
|
|
50
|
+
)
|
|
46
51
|
|
|
47
52
|
|
|
48
53
|
@dataclass(frozen=True, slots=True)
|
|
@@ -30,6 +30,7 @@ import logging
|
|
|
30
30
|
import os
|
|
31
31
|
import re
|
|
32
32
|
import shutil
|
|
33
|
+
import sys
|
|
33
34
|
import tarfile
|
|
34
35
|
import tempfile
|
|
35
36
|
import time
|
|
@@ -51,6 +52,7 @@ from ._manifest import (
|
|
|
51
52
|
read_manifest,
|
|
52
53
|
verify_library,
|
|
53
54
|
)
|
|
55
|
+
from ._native import ABI_REVISION
|
|
54
56
|
from .errors import (
|
|
55
57
|
ArtifactCorruptError,
|
|
56
58
|
ArtifactPinnedError,
|
|
@@ -145,6 +147,90 @@ def _env_flag(name: str) -> bool:
|
|
|
145
147
|
return os.environ.get(name, "").strip().lower() in ("1", "true", "yes", "on")
|
|
146
148
|
|
|
147
149
|
|
|
150
|
+
#: TEST-ONLY. When set, the ABI revision fetch selects release rows at, in place
|
|
151
|
+
#: of `ABI_REVISION` (docs/guides/fetch.md §2). Not part of the public contract:
|
|
152
|
+
#: the fetch-fixture suites set it to the revision their fixture release carries
|
|
153
|
+
#: — derived from that release's own ``index.json`` — so a binding whose own
|
|
154
|
+
#: revision has moved ahead of the fixtures still exercises the whole chain.
|
|
155
|
+
#: It changes WHICH rows are eligible and nothing else: the default registry
|
|
156
|
+
#: directory stays ``abi<ABI_REVISION>/``, and the loader still refuses an
|
|
157
|
+
#: artifact of another revision.
|
|
158
|
+
_ABI_REVISION_OVERRIDE: int | None = None
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
def _fetch_abi_revision() -> int:
|
|
162
|
+
"""The one ABI revision whose rows fetch may install: this binding's own."""
|
|
163
|
+
return ABI_REVISION if _ABI_REVISION_OVERRIDE is None else _ABI_REVISION_OVERRIDE
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
def _revision_of(value: object) -> int | None:
|
|
167
|
+
"""A row's ``abi_revision`` when it is a JSON integer, else None. The artifact
|
|
168
|
+
producer writes the field from the revision that introduced it onward, so a
|
|
169
|
+
row without one is an older revision; a value that is not an integer is
|
|
170
|
+
read the same way rather than failing the whole listing."""
|
|
171
|
+
return value if isinstance(value, int) and not isinstance(value, bool) else None
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
#: Rows that carry no ``abi_revision``, named with the reason: the artifact
|
|
175
|
+
#: producer records the field from the revision that introduced it onward. Every
|
|
176
|
+
#: unpublished message, ``--all`` warning and ``list`` note says this in these
|
|
177
|
+
#: words rather than that the release serves "none".
|
|
178
|
+
_BUILT_BEFORE: Final = "(built before revisions were recorded)"
|
|
179
|
+
_NO_RECORDED_REVISION: Final = f"rows that record no ABI revision {_BUILT_BEFORE}"
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
def _served(entries: Sequence[ReleaseEntry], noun: str) -> str:
|
|
183
|
+
"""What the release DOES have for ``noun``, for an unpublished message: the
|
|
184
|
+
ABI revision(s) its rows carry, or that it has none at any revision."""
|
|
185
|
+
if not entries:
|
|
186
|
+
return f"the release does not have {noun} at any ABI revision"
|
|
187
|
+
revs = sorted({e.abi_revision for e in entries if e.abi_revision is not None})
|
|
188
|
+
if not revs:
|
|
189
|
+
return f"the release has {noun} only in {_NO_RECORDED_REVISION}"
|
|
190
|
+
said = (
|
|
191
|
+
f"ABI revision {revs[0]}"
|
|
192
|
+
if len(revs) == 1
|
|
193
|
+
else "ABI revisions " + ", ".join(str(r) for r in revs)
|
|
194
|
+
)
|
|
195
|
+
if any(e.abi_revision is None for e in entries):
|
|
196
|
+
said += f" and in {_NO_RECORDED_REVISION}"
|
|
197
|
+
return f"the release has {noun} only at {said}"
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
def _skipped_lines(on_platform: Sequence[ReleaseEntry], platform: str, rev: int) -> list[str]:
|
|
201
|
+
"""One message per line the release has for ``platform`` only at another ABI
|
|
202
|
+
revision, or only in rows that record none — the lines ``--all`` installs
|
|
203
|
+
nothing for — in numeric line order."""
|
|
204
|
+
by_line: dict[str, list[ReleaseEntry]] = {}
|
|
205
|
+
for e in on_platform:
|
|
206
|
+
by_line.setdefault(e.minor, []).append(e)
|
|
207
|
+
return [
|
|
208
|
+
f"ClickHouse line {line} on {platform} is not installed: "
|
|
209
|
+
f"{_served(rows, f'that line for {platform}')}, and this SDK speaks ABI revision {rev}"
|
|
210
|
+
for line, rows in sorted(by_line.items(), key=lambda kv: _minor_key(kv[0]))
|
|
211
|
+
if not any(e.abi_revision == rev for e in rows)
|
|
212
|
+
]
|
|
213
|
+
|
|
214
|
+
|
|
215
|
+
def _not_shown(on_platform: Sequence[ReleaseEntry], rev: int) -> str | None:
|
|
216
|
+
"""``list``'s one line naming the platform's rows at another ABI revision,
|
|
217
|
+
which it does not show (docs/guides/fetch.md §6); None when none were
|
|
218
|
+
hidden. Rows that carry no ``abi_revision`` are named for what they are —
|
|
219
|
+
built before revisions were recorded — never as a revision called "none"."""
|
|
220
|
+
hidden = [e for e in on_platform if e.abi_revision != rev]
|
|
221
|
+
if not hidden:
|
|
222
|
+
return None
|
|
223
|
+
declared = [e.abi_revision for e in hidden if e.abi_revision is not None]
|
|
224
|
+
undeclared = len(hidden) - len(declared)
|
|
225
|
+
parts = []
|
|
226
|
+
if declared:
|
|
227
|
+
revs = ", ".join(str(r) for r in sorted(set(declared)))
|
|
228
|
+
parts.append(f"{len(declared)} row(s) at ABI revision(s) {revs}")
|
|
229
|
+
if undeclared:
|
|
230
|
+
parts.append(f"{undeclared} row(s) that record no ABI revision " + _BUILT_BEFORE)
|
|
231
|
+
return f"{' and '.join(parts)} not shown; this SDK speaks {rev}"
|
|
232
|
+
|
|
233
|
+
|
|
148
234
|
# ------------------------------------------------------------------ §1 paths
|
|
149
235
|
|
|
150
236
|
|
|
@@ -372,6 +458,11 @@ class ReleaseEntry:
|
|
|
372
458
|
build: int = 0
|
|
373
459
|
#: The core commit the wrapper was built from; ``""`` on an old row.
|
|
374
460
|
core_commit: str = ""
|
|
461
|
+
#: The chs_* ABI revision the artifact was built from, or None when the row
|
|
462
|
+
#: declares none (or declares something that is not an integer). Fetch
|
|
463
|
+
#: installs only a row at this binding's own `ABI_REVISION`
|
|
464
|
+
#: (docs/guides/fetch.md §2): any other would be refused at load.
|
|
465
|
+
abi_revision: int | None = None
|
|
375
466
|
|
|
376
467
|
@property
|
|
377
468
|
def platform(self) -> str:
|
|
@@ -411,10 +502,14 @@ class Release:
|
|
|
411
502
|
license_url: str = ""
|
|
412
503
|
|
|
413
504
|
def offered(self, platform: str) -> list[ReleaseEntry]:
|
|
414
|
-
"""Every line published for a platform
|
|
505
|
+
"""Every line published for a platform at this binding's ABI revision,
|
|
506
|
+
newest patch per line (then highest build), release order. Rows of any
|
|
507
|
+
other revision, and rows that record none, are not offered: fetch never
|
|
508
|
+
installs them (docs/guides/fetch.md §2)."""
|
|
509
|
+
rev = _fetch_abi_revision()
|
|
415
510
|
best: dict[str, ReleaseEntry] = {}
|
|
416
511
|
for e in self.entries:
|
|
417
|
-
if e.platform != platform:
|
|
512
|
+
if e.platform != platform or e.abi_revision != rev:
|
|
418
513
|
continue
|
|
419
514
|
cur = best.get(e.minor)
|
|
420
515
|
if cur is None or e.rank > cur.rank:
|
|
@@ -426,42 +521,49 @@ class Release:
|
|
|
426
521
|
|
|
427
522
|
def select(self, spelling: str, platform: str) -> ReleaseEntry:
|
|
428
523
|
"""A line (``25.8``) resolves to the one patch published for it; an
|
|
429
|
-
exact patch (``25.8.28.1-lts``) is a hard requirement (§2).
|
|
524
|
+
exact patch (``25.8.28.1-lts``) is a hard requirement (§2). Either way,
|
|
525
|
+
only rows at this binding's ABI revision are considered, FIRST: nothing
|
|
526
|
+
at that revision is `ArtifactUnpublishedError`, never another
|
|
527
|
+
revision's row."""
|
|
430
528
|
line, exact = parse_spelling(spelling)
|
|
431
|
-
|
|
432
|
-
if
|
|
529
|
+
rev = _fetch_abi_revision()
|
|
530
|
+
on_platform = [e for e in self.entries if e.platform == platform]
|
|
531
|
+
if not on_platform:
|
|
433
532
|
raise ArtifactUnpublishedError(
|
|
434
533
|
f"chtypes: {self.source} publishes nothing for {platform} "
|
|
435
534
|
f"(it has: {', '.join(self.platforms()) or 'nothing'})"
|
|
436
535
|
)
|
|
536
|
+
have = ", ".join(e.clickhouse_version for e in on_platform if e.abi_revision == rev)
|
|
437
537
|
if exact is not None:
|
|
438
538
|
bare = exact.split("-", 1)[0]
|
|
439
539
|
# A rebuild publishes the same clickhouse_version twice, so an exact
|
|
440
540
|
# request can match more than one row: take the highest build, never
|
|
441
541
|
# whichever the index happens to list first.
|
|
442
|
-
|
|
542
|
+
any_revision = [
|
|
443
543
|
e
|
|
444
|
-
for e in
|
|
445
|
-
if e.
|
|
446
|
-
and (
|
|
447
|
-
e.clickhouse_version == exact
|
|
448
|
-
or ("-" not in exact and e.clickhouse_version.split("-", 1)[0] == bare)
|
|
449
|
-
)
|
|
544
|
+
for e in on_platform
|
|
545
|
+
if e.clickhouse_version == exact
|
|
546
|
+
or ("-" not in exact and e.clickhouse_version.split("-", 1)[0] == bare)
|
|
450
547
|
]
|
|
548
|
+
hits = [e for e in any_revision if e.abi_revision == rev]
|
|
451
549
|
if hits:
|
|
452
550
|
return max(hits, key=lambda e: e.rank)
|
|
453
551
|
raise ArtifactUnpublishedError(
|
|
454
|
-
f"chtypes: you asked for exactly ClickHouse {exact} on {platform}
|
|
455
|
-
f"{self.source} does not publish it
|
|
456
|
-
f"{
|
|
552
|
+
f"chtypes: you asked for exactly ClickHouse {exact} on {platform} at ABI "
|
|
553
|
+
f"revision {rev} (this SDK's) and {self.source} does not publish it at that "
|
|
554
|
+
f"revision: {_served(any_revision, f'that patch for {platform}')}; at ABI "
|
|
555
|
+
f"revision {rev} the release has: {have or 'nothing'}. "
|
|
457
556
|
f"Ask for the line ({line}) to take what was published."
|
|
458
557
|
)
|
|
459
|
-
for e in offered:
|
|
558
|
+
for e in self.offered(platform):
|
|
460
559
|
if e.minor == line:
|
|
461
560
|
return e
|
|
561
|
+
any_revision = [e for e in on_platform if e.minor == line]
|
|
462
562
|
raise ArtifactUnpublishedError(
|
|
463
|
-
f"chtypes: no artifact for ClickHouse line {line} on {platform} at
|
|
464
|
-
f"
|
|
563
|
+
f"chtypes: no artifact for ClickHouse line {line} on {platform} at ABI revision "
|
|
564
|
+
f"{rev} (this SDK's) at {self.source}: "
|
|
565
|
+
f"{_served(any_revision, f'that line for {platform}')}"
|
|
566
|
+
f"; at ABI revision {rev} the release has: {have or 'nothing'}"
|
|
465
567
|
)
|
|
466
568
|
|
|
467
569
|
|
|
@@ -509,6 +611,7 @@ def _parse_index(raw: bytes, source: str) -> tuple[list[ReleaseEntry], str, str,
|
|
|
509
611
|
arch=str(row["arch"]),
|
|
510
612
|
build=int(row.get("build") or 0),
|
|
511
613
|
core_commit=str(row.get("core_commit") or ""),
|
|
614
|
+
abi_revision=_revision_of(row.get("abi_revision")),
|
|
512
615
|
)
|
|
513
616
|
)
|
|
514
617
|
except (KeyError, TypeError, ValueError) as exc:
|
|
@@ -725,6 +828,7 @@ class Fetcher:
|
|
|
725
828
|
host = (os.environ.get(ENV_ARTIFACTS_URL) or DEFAULT_ARTIFACTS_URL).rstrip("/")
|
|
726
829
|
base = url or f"{host}/{self.tag}"
|
|
727
830
|
self.progress: Progress = progress or (lambda line: None)
|
|
831
|
+
self._progress_given = progress is not None
|
|
728
832
|
self.source = _Source(base, self._say)
|
|
729
833
|
self.lock = Path(lock) if lock is not None else None
|
|
730
834
|
self.frozen = frozen
|
|
@@ -743,6 +847,19 @@ class Fetcher:
|
|
|
743
847
|
log.info("%s", line)
|
|
744
848
|
self.progress(line)
|
|
745
849
|
|
|
850
|
+
def _warn(self, message: str) -> None:
|
|
851
|
+
"""A loud line, never silent: through ``progress`` when the caller gave
|
|
852
|
+
one (the CLI does), else straight to stderr."""
|
|
853
|
+
line = f"chtypes: WARNING: {message}"
|
|
854
|
+
# Not log.warning: logging's last-resort handler would print it a
|
|
855
|
+
# second time on stderr for a process that configured no logging.
|
|
856
|
+
log.info("%s", line)
|
|
857
|
+
if self._progress_given:
|
|
858
|
+
self.progress(line)
|
|
859
|
+
else:
|
|
860
|
+
sys.stderr.write(line + "\n")
|
|
861
|
+
sys.stderr.flush()
|
|
862
|
+
|
|
746
863
|
def _lock_pins(self) -> dict[str, dict[str, str]]:
|
|
747
864
|
if self._pins is None:
|
|
748
865
|
if self.lock is None:
|
|
@@ -926,12 +1043,21 @@ class Fetcher:
|
|
|
926
1043
|
return installed
|
|
927
1044
|
|
|
928
1045
|
def ensure_all(self) -> list[Path]:
|
|
929
|
-
"""Every line the release publishes for the platform
|
|
930
|
-
|
|
1046
|
+
"""Every line the release publishes for the platform at this binding's
|
|
1047
|
+
ABI revision. A line it has only at another revision is not installed,
|
|
1048
|
+
and is named in one loud warning line; the rest go on."""
|
|
1049
|
+
release = self.release()
|
|
1050
|
+
offered = release.offered(self.platform)
|
|
1051
|
+
rev = _fetch_abi_revision()
|
|
1052
|
+
on_platform = [e for e in release.entries if e.platform == self.platform]
|
|
931
1053
|
if not offered:
|
|
932
1054
|
raise ArtifactUnpublishedError(
|
|
933
|
-
f"chtypes: {
|
|
1055
|
+
f"chtypes: {release.source} publishes nothing for {self.platform} at ABI "
|
|
1056
|
+
f"revision {rev} (this SDK's): "
|
|
1057
|
+
f"{_served(on_platform, f'rows for {self.platform}')}"
|
|
934
1058
|
)
|
|
1059
|
+
for message in _skipped_lines(on_platform, self.platform, rev):
|
|
1060
|
+
self._warn(message)
|
|
935
1061
|
out = [self.install(entry) for entry in offered]
|
|
936
1062
|
self.install_goldens()
|
|
937
1063
|
return out
|
|
@@ -76,10 +76,10 @@ ENV_REGISTRY: Final = "CHTYPES_REGISTRY"
|
|
|
76
76
|
|
|
77
77
|
def default_registry_dir() -> str:
|
|
78
78
|
"""The per-user artifact cache for this host — ``${XDG_CACHE_HOME:-~/.cache}/
|
|
79
|
-
chtypes/artifacts
|
|
80
|
-
(``amd64``/``arm64``). Where
|
|
81
|
-
|
|
82
|
-
(docs/guides/fetch.md §1). A path, not a promise: it need not exist yet."""
|
|
79
|
+
chtypes/artifacts/abi<R>/<os>-<arch>`` with R this binding's `ABI_REVISION`
|
|
80
|
+
and ``<arch>`` spelled the artifact way (``amd64``/``arm64``). Where
|
|
81
|
+
``chtypes fetch`` installs, and slot 3 of the registry search path every SDK
|
|
82
|
+
walks (docs/guides/fetch.md §1). A path, not a promise: it need not exist yet."""
|
|
83
83
|
return cache_registry_dir()
|
|
84
84
|
|
|
85
85
|
|
|
@@ -6,14 +6,15 @@ server-truth suites against this same tree.
|
|
|
6
6
|
|
|
7
7
|
The registry comes from the search path (docs/guides/fetch.md §1): `$CHTYPES_REGISTRY`,
|
|
8
8
|
else the per-user artifact cache for this host (`chtypes.default_registry_dir()`:
|
|
9
|
-
`~/.cache/chtypes/artifacts
|
|
10
|
-
|
|
9
|
+
`~/.cache/chtypes/artifacts/abi<R>/<os>-<arch>`, R the binding's ABI revision,
|
|
10
|
+
where `chtypes fetch` installs), else the system locations. Without a single line on it
|
|
11
11
|
the suite skips with that message: a green run that never touched a ClickHouse
|
|
12
12
|
build would be worse than no run.
|
|
13
13
|
"""
|
|
14
14
|
|
|
15
15
|
from __future__ import annotations
|
|
16
16
|
|
|
17
|
+
import json
|
|
17
18
|
import os
|
|
18
19
|
from collections.abc import Callable
|
|
19
20
|
from pathlib import Path
|
|
@@ -23,6 +24,47 @@ import pytest
|
|
|
23
24
|
import chtypes
|
|
24
25
|
|
|
25
26
|
FALLBACK_REGISTRY = Path(chtypes.default_registry_dir())
|
|
27
|
+
FETCH_FIXTURES = Path(__file__).resolve().parents[2] / "tests" / "fixtures" / "fetch"
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def _fixture_abi_revision(fixtures: Path) -> int:
|
|
31
|
+
"""The ABI revision the fixture set under ``fixtures`` DECLARES, as
|
|
32
|
+
``expected.json``'s ``fixtures_abi_revision``: the revision every fixture's
|
|
33
|
+
rows carry except ``two-revisions/``'s, which deliberately spans two.
|
|
34
|
+
|
|
35
|
+
Read from the fixtures' own declaration, never typed into a test: a suite
|
|
36
|
+
that hand-set it would be testing its author's belief about the fixtures,
|
|
37
|
+
not the fetch. A missing or non-integer field raises."""
|
|
38
|
+
expected = fixtures / "expected.json"
|
|
39
|
+
doc = json.loads(expected.read_text())
|
|
40
|
+
if "fixtures_abi_revision" not in doc:
|
|
41
|
+
raise AssertionError(
|
|
42
|
+
f"{expected} declares no fixtures_abi_revision — regenerate the fixtures from the "
|
|
43
|
+
f"artifact producer's current set"
|
|
44
|
+
)
|
|
45
|
+
rev = doc["fixtures_abi_revision"]
|
|
46
|
+
if not isinstance(rev, int) or isinstance(rev, bool):
|
|
47
|
+
raise AssertionError(f"{expected}: fixtures_abi_revision is {rev!r}, not an integer")
|
|
48
|
+
return rev
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
@pytest.fixture
|
|
52
|
+
def derive_fixture_abi_revision() -> Callable[[Path], int]:
|
|
53
|
+
"""The declared-field reader itself, for the test that pins it."""
|
|
54
|
+
return _fixture_abi_revision
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
@pytest.fixture
|
|
58
|
+
def at_fixture_revision(monkeypatch: pytest.MonkeyPatch) -> int:
|
|
59
|
+
"""Fetch selects only rows at the binding's own ABI revision
|
|
60
|
+
(docs/guides/fetch.md §2); the fixtures carry whatever revision they were
|
|
61
|
+
generated at, and declare it. Point the test-only override at THAT revision —
|
|
62
|
+
read from expected.json's ``fixtures_abi_revision`` — for this test, and return it."""
|
|
63
|
+
from chtypes import fetch as fetch_module
|
|
64
|
+
|
|
65
|
+
rev = _fixture_abi_revision(FETCH_FIXTURES)
|
|
66
|
+
monkeypatch.setattr(fetch_module, "_ABI_REVISION_OVERRIDE", rev)
|
|
67
|
+
return rev
|
|
26
68
|
|
|
27
69
|
|
|
28
70
|
def _candidate() -> tuple[Path, str]:
|
|
@@ -35,12 +35,26 @@ def _keys() -> str:
|
|
|
35
35
|
|
|
36
36
|
|
|
37
37
|
@pytest.fixture
|
|
38
|
-
def env(
|
|
38
|
+
def env(
|
|
39
|
+
isolated_search_path: Path, monkeypatch: pytest.MonkeyPatch, request: pytest.FixtureRequest
|
|
40
|
+
) -> Path:
|
|
39
41
|
if EXPECTED_FILE.is_file():
|
|
40
42
|
monkeypatch.setenv("CHTYPES_TRUSTED_KEYS", _keys())
|
|
43
|
+
# Fetch at the ABI revision the fixtures carry, read off their own rows.
|
|
44
|
+
request.getfixturevalue("at_fixture_revision")
|
|
41
45
|
return isolated_search_path
|
|
42
46
|
|
|
43
47
|
|
|
48
|
+
# `python -m chtypes`, exactly — runpy runs chtypes/__main__.py as __main__,
|
|
49
|
+
# which is all `-m` does — with fetch's test-only revision override set first
|
|
50
|
+
# (the child cannot inherit the parent's monkeypatch). argv[1] is the revision.
|
|
51
|
+
_MODULE_AT_REVISION = (
|
|
52
|
+
"import runpy, sys; import chtypes.fetch as f; "
|
|
53
|
+
"f._ABI_REVISION_OVERRIDE = int(sys.argv.pop(1)); "
|
|
54
|
+
"runpy.run_module('chtypes', run_name='__main__', alter_sys=True)"
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
|
|
44
58
|
def test_where_prints_the_write_directory(env: Path, capsys: pytest.CaptureFixture[str]) -> None:
|
|
45
59
|
assert main(["where"]) == 0
|
|
46
60
|
out = capsys.readouterr().out
|
|
@@ -80,7 +94,11 @@ def test_usage_errors_exit_2(env: Path, capsys: pytest.CaptureFixture[str]) -> N
|
|
|
80
94
|
),
|
|
81
95
|
)
|
|
82
96
|
def test_fetch_exit_codes_and_streams(
|
|
83
|
-
verdict: dict,
|
|
97
|
+
verdict: dict,
|
|
98
|
+
env: Path,
|
|
99
|
+
tmp_path: Path,
|
|
100
|
+
monkeypatch: pytest.MonkeyPatch,
|
|
101
|
+
at_fixture_revision: int,
|
|
84
102
|
) -> None:
|
|
85
103
|
"""Every fixture verdict through the real entry point, in a subprocess:
|
|
86
104
|
the spec's exit code, the code on stderr, and stdout empty or exactly
|
|
@@ -99,8 +117,9 @@ def test_fetch_exit_codes_and_streams(
|
|
|
99
117
|
proc = subprocess.run(
|
|
100
118
|
[
|
|
101
119
|
sys.executable,
|
|
102
|
-
"-
|
|
103
|
-
|
|
120
|
+
"-c",
|
|
121
|
+
_MODULE_AT_REVISION,
|
|
122
|
+
str(at_fixture_revision),
|
|
104
123
|
"fetch",
|
|
105
124
|
"25.8",
|
|
106
125
|
"--platform",
|
|
@@ -174,6 +193,19 @@ def test_verify_and_list(env: Path, tmp_path: Path, capsys: pytest.CaptureFixtur
|
|
|
174
193
|
out = capsys.readouterr().out
|
|
175
194
|
assert "installed (" in out and "[installed]" in out and "signed by key" in out
|
|
176
195
|
assert "[not installed]" not in out
|
|
196
|
+
assert "not shown" not in out # every row is at the revision fetch selects at
|
|
197
|
+
|
|
198
|
+
# One revision past the fixtures' own, list shows none of their rows and
|
|
199
|
+
# says so in one line naming what it hid (docs/guides/fetch.md §6).
|
|
200
|
+
from chtypes import fetch as fetch_module
|
|
201
|
+
|
|
202
|
+
rev = fetch_module._fetch_abi_revision()
|
|
203
|
+
with pytest.MonkeyPatch.context() as mp:
|
|
204
|
+
mp.setattr(fetch_module, "_ABI_REVISION_OVERRIDE", rev + 1)
|
|
205
|
+
assert main(["list", "--platform", PLATFORM, "--url", signed, "--dest", str(dest)]) == 0
|
|
206
|
+
out = capsys.readouterr().out
|
|
207
|
+
assert f" 2 row(s) at ABI revision(s) {rev} not shown; this SDK speaks {rev + 1}\n" in out
|
|
208
|
+
assert "[installed]" not in out
|
|
177
209
|
|
|
178
210
|
# Corrupt one library: verify says so, exits 1, and names the line.
|
|
179
211
|
(dest / "26.7" / "libchtypes.so").write_bytes(b"corrupt")
|