chtypes 0.1.2__tar.gz → 0.2.1__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.
Files changed (34) hide show
  1. chtypes-0.2.1/CHANGELOG.md +63 -0
  2. {chtypes-0.1.2 → chtypes-0.2.1}/PKG-INFO +1 -1
  3. {chtypes-0.1.2 → chtypes-0.2.1}/pyproject.toml +3 -3
  4. {chtypes-0.1.2 → chtypes-0.2.1}/src/chtypes/__init__.py +2 -0
  5. {chtypes-0.1.2 → chtypes-0.2.1}/src/chtypes/__main__.py +3 -1
  6. {chtypes-0.1.2 → chtypes-0.2.1}/src/chtypes/_manifest.py +4 -1
  7. {chtypes-0.1.2 → chtypes-0.2.1}/src/chtypes/_native.py +2 -1
  8. {chtypes-0.1.2 → chtypes-0.2.1}/src/chtypes/errors.py +7 -1
  9. {chtypes-0.1.2 → chtypes-0.2.1}/src/chtypes/fetch.py +3 -2
  10. {chtypes-0.1.2 → chtypes-0.2.1}/src/chtypes/registry.py +3 -2
  11. {chtypes-0.1.2 → chtypes-0.2.1}/tests/conftest.py +1 -1
  12. chtypes-0.2.1/tests/test_abi_revision.py +139 -0
  13. {chtypes-0.1.2 → chtypes-0.2.1}/tests/test_cli.py +4 -1
  14. {chtypes-0.1.2 → chtypes-0.2.1}/tests/test_fetch.py +3 -3
  15. {chtypes-0.1.2 → chtypes-0.2.1}/tests/test_parity.py +22 -12
  16. {chtypes-0.1.2 → chtypes-0.2.1}/tests/test_registry.py +23 -0
  17. chtypes-0.2.1/uv.lock +108 -0
  18. chtypes-0.1.2/CHANGELOG.md +0 -35
  19. chtypes-0.1.2/uv.lock +0 -108
  20. {chtypes-0.1.2 → chtypes-0.2.1}/.gitignore +0 -0
  21. {chtypes-0.1.2 → chtypes-0.2.1}/LICENSE +0 -0
  22. {chtypes-0.1.2 → chtypes-0.2.1}/README.md +0 -0
  23. {chtypes-0.1.2 → chtypes-0.2.1}/src/chtypes/_document.py +0 -0
  24. {chtypes-0.1.2 → chtypes-0.2.1}/src/chtypes/_ed25519.py +0 -0
  25. {chtypes-0.1.2 → chtypes-0.2.1}/src/chtypes/_rawjson.py +0 -0
  26. {chtypes-0.1.2 → chtypes-0.2.1}/src/chtypes/discover.py +0 -0
  27. {chtypes-0.1.2 → chtypes-0.2.1}/src/chtypes/py.typed +0 -0
  28. {chtypes-0.1.2 → chtypes-0.2.1}/src/chtypes/results.py +0 -0
  29. {chtypes-0.1.2 → chtypes-0.2.1}/src/chtypes/transform.py +0 -0
  30. {chtypes-0.1.2 → chtypes-0.2.1}/tests/test_ed25519.py +0 -0
  31. {chtypes-0.1.2 → chtypes-0.2.1}/tests/test_errors.py +0 -0
  32. {chtypes-0.1.2 → chtypes-0.2.1}/tests/test_golden.py +0 -0
  33. {chtypes-0.1.2 → chtypes-0.2.1}/tests/test_rawjson.py +0 -0
  34. {chtypes-0.1.2 → chtypes-0.2.1}/tests/test_transform.py +0 -0
@@ -0,0 +1,63 @@
1
+ # Changelog — `chtypes`
2
+
3
+ All notable changes to the python binding. The format is [Keep a Changelog](https://keepachangelog.com/en/1.1.0/); this package follows [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
4
+
5
+ The four bindings in this repository are released together and give one answer, so an entry here has a counterpart in the other three.
6
+
7
+ ## [0.2.1] — 2026-09-15
8
+
9
+ Released in step with the Rust crate, which gains `Registry::open` and `Error::LibraryRead`; this binding's public API is unchanged.
10
+
11
+ ### Notes
12
+
13
+ - Speaks **ABI revision 4**, unchanged since 0.1.0, so no artifact needs relinking. The golden set this release was tested against is the one core serves, generated on the ClickHouse lines 24.8, 25.3, 25.8, 25.10, 26.2, 26.3, 26.4, 26.5, 26.6, 26.7 and 26.8.
14
+ - **The ABI-revision refusal is now run, not assumed.** This binding's suite points a registry at a generated artifact answering the wrong revision and asserts the load is refused naming both numbers, beside a matching-revision control that must load; CI fails the build if either case did not run (#36).
15
+
16
+ ## [0.2.0] — 2026-09-15
17
+
18
+ ### Changed — BREAKING
19
+
20
+ - **The TypeScript filter verdicts are the wire characters.** `Verdict.True` / `False` / `Error` / `Decline` are now `'t'` / `'f'` / `'e'` / `'d'`, which Go, Python and Rust have always rendered. TypeScript alone spelled them `'true'` / `'false'` / `'error'` / `'decline'`, so two SDKs could not share a log line, a fixture or a test. The four states and the fail-closed rule are unchanged: `'e'` and `'d'` are NOT answers, and a caller enforcing visibility must hide the row or fail the request on both — collapsing either into `false` inverts fail-closed into fail-open, the measured leak class. Only the rendered characters moved. **The parity suites now ENFORCE the shared value in all four bindings**; until this release two of the four merely declared it.
21
+
22
+ - **TypeScript `FetchError` now extends `ArtifactError` (and so `RegistryError`), where it previously extended `ChtypesError` directly.** A `try`/`catch` chain that tests `RegistryError` before `FetchError` now takes the `RegistryError` arm for the five fetch verdicts, which it did not before. Ordering a catch chain most-specific-first is unaffected.
23
+
24
+ ### Added
25
+
26
+ - **Load-time checksum verification in Go and Rust**, matching what Python and TypeScript have always offered: re-hash the library against `manifest.json`'s `library_sha256` **before `dlopen`**, and refuse the load on a mismatch. Off by default in all four. Whether an artifact was re-hashed before being mapped used to depend on which binding a consumer picked, which is a security posture and not an API spelling. A manifest carrying no `library_sha256` is refused by Go, TypeScript and Rust — verification asked for and not possible is not verification — while Python's returns silently, which is what remains of issue #13's item A2.
27
+ - **One catchable artifact error in TypeScript.** `ArtifactError` is a new exported base: all six artifact conditions now extend it, where `ArtifactMissingError` previously sat under `RegistryError` and the other five under `FetchError`, so a caller had to catch two unrelated types. Every existing `code` value and message is unchanged, and `ArtifactMissingError instanceof RegistryError` still holds.
28
+ - **Eight constants that three bindings exported and the fourth did not**, so a caller no longer retypes a literal the other SDKs hand them: `Registry.SearchPath()` (go), `parse_signature_file` and `FETCH_COMMAND` (python), `RELEASE_KEY_ID` and `DEFAULT_LOCK_FILE` (ts), `fetch::LOCK_SCHEMA` (rust), and `CHTYPES_REGISTRY` / `CHTYPES_AUTOFETCH` as `EnvRegistry` / `EnvAutoFetch` (go) and `ENV_REGISTRY` / `ENV_AUTOFETCH` (ts). Each new name replaces the inline literal in its own binding's code path, so there is one definition rather than two.
29
+
30
+ ### Notes
31
+
32
+ - Speaks **ABI revision 4**, unchanged since 0.1.0, so no artifact needs relinking. The golden set this release was tested against is the one core serves, generated on the ClickHouse lines 24.8, 25.3, 25.8, 25.10, 26.2, 26.3, 26.4, 26.5, 26.6, 26.7 and 26.8.
33
+ - `Library` still has no scope-based release in Python or TypeScript, while `Registry`, `Schema`, `Filter` and `Block` do. That asymmetry is deliberate — a `with library:` or `using library` closes at the end of a block, which is the mid-lifecycle teardown measured to segfault on the next open — and it is now recorded in the parity manifest so no future audit reopens it as a gap.
34
+
35
+ ## [0.1.2] — 2026-09-11
36
+
37
+ ### Changed — BREAKING
38
+
39
+ - **The transform reason for a materialized DEFAULT is now `default_materialized`** — previously the same word spelled with an `s` — and the exported constant naming it is now `Reason.DEFAULT_MATERIALIZED`. ClickHouse's own keyword is `MATERIALIZED`; its parser rejects the `s` spelling outright with a syntax error, so the reason naming that concept now matches the system it describes. Code comparing against the old string or the old constant name must be updated.
40
+ - This is an SDK-only change and **the ABI is untouched: it remains revision 4, and no artifact needs relinking.** The reason is derived in the binding, not received from the artifact — the library emits `default_substituted`, which each binding translates. That wire value is unchanged.
41
+
42
+ ### Changed
43
+
44
+ - **The per-language README is now an orientation page, not a manual.** It carries install, a quickstart that runs, the three outcomes and the error model, and then points at `docs/` for the rest — roughly 450 lines of reference prose moved to `docs/guides/` and `docs/reference/` rather than being repeated four times and drifting four ways.
45
+ - **The published packages no longer cite a repository you cannot open.** 148 source comments — in every binding and in `include/chtypes.h`, all of which ship inside the crate, the sdist and the module — attributed the contract they implement to a private repository's specification. They now cite **the C ABI contract**, with `include/chtypes.h` as the public authority for it. Alongside those, 14 dead paths into that repository, the examples' instruction to build an artifact there, and two descriptions of the signing infrastructure are gone. What remains is every place where the two-repository split is itself the subject — describing it in prose is honest and stays legal; naming something a reader cannot open is not, and `scripts/lint-public.sh` is what enforces the difference. (An earlier draft of this entry enumerated three files. That count was never checked and was wrong by eighteen, which is the same mistake the entry above is about: the rule is checkable, a file list is not.)
46
+ - `scripts/lint-public.sh` is new and blocking in CI, because the sweep this replaces reported itself complete while 148 of its targets survived in a different shape. It refuses both the private repository's name and that exact prose pointer, and its `--selftest` proves each rule fires rather than assuming it.
47
+
48
+ ## [0.1.1] — 2026-09-11
49
+
50
+ - **The golden set is served, not tracked.** `goldens/cases.json` no longer exists in this repository. Core publishes `sdk-goldens.json` in the rolling release as a row in the signed `SHA256SUMS`; `scripts/fetch.sh` installs it at `<registry>/sdk-goldens.json` and the golden test reads it offline from there (`CHTYPES_GOLDENS` overrides). A case runs only against the exact ClickHouse version `generated.exact` names for its line and skips loudly otherwise, so the set shrinks as lines are added rather than recording an answer twice.
51
+ - **Artifact identity gained a wrapper build.** Assets are `chtypes-<version>-<os>-<arch>-b<N>.tar.gz`, where `<N>` is the core commit's UNIX timestamp; an absent suffix means build 0 and stays valid. Line resolution ranks by `(version, build)`, so a rebuild of a line supersedes the older build.
52
+ - Fetching now retries the publish window, so a fetch that races a release no longer fails on sums naming a file the host has not finished serving.
53
+ - README install instructions name the published package rather than a path dependency.
54
+ - Two errors in the published README, both found by running the snippets rather than reading them: Python's `substituted` is on `RowResult`, not `BatchResult`, and TypeScript's `using schema = …` is a syntax error on Node 22, this package's own `engines` floor (`schema.close()` is portable).
55
+ - `docs/support.md` is new: which language versions, platforms and ClickHouse lines are supported, generated from the manifests and the release's own index.
56
+
57
+ ## [0.1.0] — 2026-09-10
58
+
59
+ First release, published to PyPI from the tag `python/v0.1.0`.
60
+
61
+ - Speaks **ABI revision 4** of the frozen `chs_*` C ABI (`include/chtypes.h`, 28 functions). The binding `dlopen`s a per-version artifact and reimplements no ClickHouse semantics; `Transformed` is the one derived result.
62
+ - Verified against the public golden set (`goldens/cases.json`, schema 1, 31 cases), whose expectations were generated on ClickHouse 24.8, 25.3, 25.8, 25.10, 26.5, 26.6 and 26.7 — every case answered identically on all seven lines, and any case that did not was refused by the generator.
63
+ - Pre-1.0: the artifact name `libchtypes`, the `chs_` prefix and the `enum chs_format` numbers are frozen. Function signatures freeze at this tag.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: chtypes
3
- Version: 0.1.2
3
+ Version: 0.2.1
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
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "chtypes"
3
- version = "0.1.2"
3
+ version = "0.2.1"
4
4
  description = "Python binding for the chtypes artifacts: ClickHouse's own type system, per version, behind the frozen chs_* C ABI"
5
5
  readme = "README.md"
6
6
  license = "Apache-2.0"
@@ -54,7 +54,7 @@ src = ["src", "tests"]
54
54
  # the classes that have actually cost time here — unused imports and names (F),
55
55
  # style errors that hide real ones (E), import order drift across 20 files (I),
56
56
  # syntax this project's floor already allows (UP), and the mutable-default /
57
- # loop-variable-capture family (B). `just lint` and ci/steps/static-checks.sh
58
- # both run this, so a finding is a gate failure, not advice.
57
+ # loop-variable-capture family (B). `just lint` and the core repository's
58
+ # static checks both run this, so a finding is a gate failure, not advice.
59
59
  [tool.ruff.lint]
60
60
  select = ["E", "F", "I", "UP", "B"]
@@ -64,6 +64,7 @@ from .errors import (
64
64
  CODE_ARTIFACT_UNTRUSTED,
65
65
  CODE_SOURCE_UNREACHABLE,
66
66
  CODE_UNSUPPORTED,
67
+ FETCH_COMMAND,
67
68
  ArtifactCorruptError,
68
69
  ArtifactError,
69
70
  ArtifactMissingError,
@@ -142,6 +143,7 @@ __all__ = [
142
143
  "DOC_VALUES",
143
144
  "ENV_AUTOFETCH",
144
145
  "ENV_REGISTRY",
146
+ "FETCH_COMMAND",
145
147
  "default_registry_dir",
146
148
  "EXPORT_NONE",
147
149
  "RELEASE_KEY_ID",
@@ -76,7 +76,9 @@ def build_parser() -> argparse.ArgumentParser:
76
76
  fetch = sub.add_parser(
77
77
  "fetch",
78
78
  help="install one or more ClickHouse lines, verified",
79
- description="Install ClickHouse lines through the verification chain (docs/guides/fetch.md §3).",
79
+ description=(
80
+ "Install ClickHouse lines through the verification chain (docs/guides/fetch.md §3)."
81
+ ),
80
82
  )
81
83
  fetch.add_argument(
82
84
  "lines",
@@ -111,7 +111,10 @@ def verify_library(version_dir: str | os.PathLike[str]) -> None:
111
111
  with path.open("rb") as handle:
112
112
  for chunk in iter(lambda: handle.read(1 << 20), b""):
113
113
  digest.update(chunk)
114
- if digest.hexdigest() != manifest.library_sha256:
114
+ # Case-insensitive: a hex digest is the same digest in either case, and Go
115
+ # and Rust already lower-case before comparing. Comparing raw here made an
116
+ # uppercase manifest digest pass in two bindings and fail in two.
117
+ if digest.hexdigest() != manifest.library_sha256.lower():
115
118
  raise RegistryError(
116
119
  f"chtypes: {path} sha256 {digest.hexdigest()} != manifest {manifest.library_sha256}"
117
120
  )
@@ -179,7 +179,8 @@ class _RWLock:
179
179
 
180
180
  **Readers** are every call that reaches `chs_row`, `chs_rows`,
181
181
  `chs_schema_compile`, `chs_schema_engine`, `chs_schema_ttl`,
182
- `chs_validate_type` and the column accessors. The C ABI contract §Thread-safety declares those safe together **on distinct handles**, so
182
+ `chs_validate_type` and the column accessors. The C ABI contract
183
+ §Thread-safety declares those safe together **on distinct handles**, so
183
184
  per-handle serialization is `Schema`'s own lock and not this one.
184
185
 
185
186
  **The writer** is the pair that mutates per-library PROCESS state —
@@ -14,6 +14,7 @@ __all__ = [
14
14
  "CODE_ARTIFACT_UNTRUSTED",
15
15
  "CODE_SOURCE_UNREACHABLE",
16
16
  "CODE_UNSUPPORTED",
17
+ "FETCH_COMMAND",
17
18
  "ArtifactCorruptError",
18
19
  "ArtifactError",
19
20
  "ArtifactMissingError",
@@ -70,6 +71,11 @@ CODE_ARTIFACT_PINNED: Final = "CHTYPES_ARTIFACT_PINNED"
70
71
  CODE_ARTIFACT_UNPUBLISHED: Final = "CHTYPES_ARTIFACT_UNPUBLISHED"
71
72
  CODE_SOURCE_UNREACHABLE: Final = "CHTYPES_SOURCE_UNREACHABLE"
72
73
 
74
+ # This SDK's own fetch command, as the docs/guides/fetch.md §7 "Install it" message
75
+ # spells it. The VALUE is deliberately per-language — it is the command a
76
+ # caller of THIS SDK runs — go/ts/rust each carry their own equivalent.
77
+ FETCH_COMMAND: Final = "python -m chtypes fetch"
78
+
73
79
 
74
80
  class ArtifactError(RegistryError):
75
81
  """An artifact is missing, could not be obtained, or failed verification.
@@ -123,7 +129,7 @@ class ArtifactMissingError(ArtifactError):
123
129
  super().__init__(
124
130
  f"chtypes: no artifact for ClickHouse {line} ({platform}). "
125
131
  f"Looked in: {', '.join(self.looked_in)}.\n"
126
- f"Install it: python -m chtypes fetch {line}\n"
132
+ f"Install it: {FETCH_COMMAND} {line}\n"
127
133
  f"or set CHTYPES_AUTOFETCH=1 to fetch on first use."
128
134
  )
129
135
 
@@ -79,6 +79,7 @@ __all__ = [
79
79
  "fetch_lines",
80
80
  "fetch_destination",
81
81
  "installed_lines",
82
+ "parse_signature_file",
82
83
  "read_lock",
83
84
  "registry_search_path",
84
85
  "trusted_keys",
@@ -258,7 +259,7 @@ def key_id(raw_public_key: bytes) -> str:
258
259
  return hashlib.sha256(raw_public_key).hexdigest()[:16]
259
260
 
260
261
 
261
- def _parse_signature(sig: bytes) -> tuple[str, bytes]:
262
+ def parse_signature_file(sig: bytes) -> tuple[str, bytes]:
262
263
  """``SHA256SUMS.sig`` -> (the comment line, the 64 signature bytes)."""
263
264
  comment = ""
264
265
  body: list[str] = []
@@ -285,7 +286,7 @@ def verify_sums_signature(sums: bytes, sig: bytes, keys: Sequence[bytes], source
285
286
  """Step 0 of the chain: the signature over the EXACT bytes of ``SHA256SUMS``
286
287
  with one of the trusted keys. Returns the id of the key that verified;
287
288
  raises `ArtifactUntrustedError` otherwise."""
288
- comment, signature = _parse_signature(sig)
289
+ comment, signature = parse_signature_file(sig)
289
290
  for raw in keys:
290
291
  if _ed25519_verify(raw, sums, signature):
291
292
  return key_id(raw)
@@ -370,8 +370,9 @@ class Schema:
370
370
  `BatchResult.rows` as `Outcome.SKIPPED`, carrying the server's own
371
371
  caught error verbatim (2026-08-27).
372
372
 
373
- **The revision-3 export channel** (the C ABI contract §Rows;
374
- docs/proposals/rows-export.md) — still ONE `chs_rows` call, never a
373
+ **The revision-3 export channel** (the C ABI contract §Rows, for
374
+ which `include/chtypes.h` is the public authority) — still ONE
375
+ `chs_rows` call, never a
375
376
  second, never re-parsing:
376
377
 
377
378
  `export` is None (no bytes — today's path, byte-identical documents)
@@ -2,7 +2,7 @@
2
2
  not run at all — it skips, loudly, by name. The fetch, CLI and pure-Python
3
3
  tests need none and always run; that is what this repository's CI proves on
4
4
  hosted runners, and the artifact-backed proof is the core repository's
5
- `certify` workflow against this same tree.
5
+ 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()`:
@@ -0,0 +1,139 @@
1
+ """The ABI-revision handshake, run against a wrong-revision fixture (#36).
2
+
3
+ `docs/reference/artifact.md` step 5 is normative: an artifact whose
4
+ `chs_abi_revision()` answers a value that is neither 0 nor the revision this
5
+ binding was written against MUST be refused, **naming both numbers**
6
+ (`chtypes/_native.py`, `NativeLibrary.__init__`: "refusing to call through
7
+ mismatched declarations"). This suite is the first thing in this repository
8
+ that actually RUNS that rule instead of assuming it.
9
+
10
+ The fixture is a pair of stub shared libraries generated from `include/chtypes.h`
11
+ itself, by the served ABI-revision fixture generator (`abi-revision/gen.py`):
12
+ one registry whose artifact answers `abi_revision + 1` (`wrong-revision/`), one
13
+ answering `abi_revision`
14
+ exactly (`at-revision/`), both on ClickHouse minor line "0.0". **Both halves
15
+ matter** — without the `at-revision` control, a binding whose own
16
+ `ABI_REVISION` had drifted would refuse everything and pass the refusal case
17
+ for entirely the wrong reason.
18
+
19
+ Every expected number comes from `fixture.json` — never from
20
+ `chtypes.ABI_REVISION` — because a case that read the constant under test
21
+ could not catch that constant being wrong.
22
+
23
+ `isolated_search_path` (`tests/conftest.py`) is used to keep the per-user
24
+ artifact cache and any ambient `$CHTYPES_REGISTRY`/system roots out of the
25
+ search path: `Registry(root)` puts `root` first regardless, but this removes
26
+ any chance that the fixture's odd "0.0" minor line is ever resolved from
27
+ somewhere other than the fixture itself.
28
+
29
+ Reopening a library after `close()` in the same process is measured to
30
+ segfault (`docs/reference/bindings.md` §Teardown), so nothing here closes a
31
+ loaded library: the refusal case never gets far enough to open one, the
32
+ control case leaves its `Library` for process teardown (`chs_shutdown` runs
33
+ via `atexit`), and neither path is ever loaded a second time.
34
+ """
35
+
36
+ from __future__ import annotations
37
+
38
+ import json
39
+ import os
40
+ import re
41
+ from pathlib import Path
42
+ from typing import Final
43
+
44
+ import pytest
45
+
46
+ import chtypes
47
+
48
+ #: Where the fixture root lives. Built by the served abi-revision generator
49
+ #: (`abi-revision/gen.py build`, core repo); this suite never builds one itself.
50
+ ENV_ABI_FIXTURES: Final = "CHTYPES_ABI_FIXTURES"
51
+
52
+ #: The one ClickHouse minor line every fixture artifact answers to.
53
+ FIXTURE_MINOR: Final = "0.0"
54
+
55
+
56
+ @pytest.fixture(scope="session")
57
+ def abi_fixture() -> dict:
58
+ """Parse and validate `fixture.json`. Missing `$CHTYPES_ABI_FIXTURES` is a
59
+ SKIP (no fallback build); a fixture that exists but is malformed is a
60
+ FAILURE naming the path — a bad fixture must never read as "nothing to
61
+ test here"."""
62
+ env = os.environ.get(ENV_ABI_FIXTURES)
63
+ if not env:
64
+ pytest.skip(
65
+ f"no ABI revision fixture: ${ENV_ABI_FIXTURES} is unset (build one with "
66
+ "abi-revision/gen.py build --out DIR --header include/chtypes.h)"
67
+ )
68
+ root = Path(env)
69
+ fixture_path = root / "fixture.json"
70
+ try:
71
+ raw = fixture_path.read_text()
72
+ except OSError as exc:
73
+ pytest.fail(f"{fixture_path}: cannot read the ABI revision fixture: {exc}")
74
+ try:
75
+ doc = json.loads(raw)
76
+ except json.JSONDecodeError as exc:
77
+ pytest.fail(f"{fixture_path}: not valid JSON: {exc}")
78
+ abi_revision = doc.get("abi_revision")
79
+ valid = isinstance(abi_revision, int) and not isinstance(abi_revision, bool)
80
+ if not valid or abi_revision <= 0:
81
+ pytest.fail(
82
+ f"{fixture_path}: abi_revision must be a positive integer, got {abi_revision!r}"
83
+ )
84
+ mismatch_revision = doc.get("mismatch_revision")
85
+ if mismatch_revision != abi_revision + 1:
86
+ pytest.fail(
87
+ f"{fixture_path}: mismatch_revision is {mismatch_revision!r}, expected "
88
+ f"abi_revision ({abi_revision}) + 1"
89
+ )
90
+ doc["root"] = str(root)
91
+ return doc
92
+
93
+
94
+ def _names_number(message: str, n: int) -> bool:
95
+ """ "The message says N", not "the message contains the digits of N": a
96
+ refusal naming revision 4 must not be satisfied by the 4 in a path like
97
+ `.../24.8/`, and one naming 5 must not be satisfied by 15."""
98
+ return re.search(rf"(^|[^0-9]){n}([^0-9]|$)", message) is not None
99
+
100
+
101
+ def _without_fixture_path(message: str, root: Path) -> str:
102
+ """Strip every occurrence of the fixture root from a message before
103
+ `_names_number` checks it. The refusal message embeds the artifact's
104
+ PATH, and a path segment can itself hold a standalone digit (a scratchpad
105
+ session id such as `4d9fd784-...`), which would let the path alone
106
+ satisfy "the message names 4" for reasons that have nothing to do with
107
+ the refusal text itself. Both the given root and its resolved form are
108
+ stripped (macOS's `/tmp` vs `/private/tmp` differ), longest first so one
109
+ is never a partial match of the other.
110
+ """
111
+ candidates = sorted({str(root), os.path.realpath(root)}, key=len, reverse=True)
112
+ for candidate in candidates:
113
+ message = message.replace(candidate, "<fixture>")
114
+ return message
115
+
116
+
117
+ def test_wrong_revision_is_refused(
118
+ abi_fixture: dict,
119
+ isolated_search_path: Path,
120
+ ) -> None:
121
+ root = Path(abi_fixture["root"], "wrong-revision")
122
+ registry = chtypes.Registry(root)
123
+ with pytest.raises(chtypes.RegistryError) as excinfo:
124
+ registry.for_version(FIXTURE_MINOR)
125
+ message = str(excinfo.value)
126
+ stripped = _without_fixture_path(message, root)
127
+ for n in (abi_fixture["mismatch_revision"], abi_fixture["abi_revision"]):
128
+ assert _names_number(stripped, n), f"the refusal does not name {n}: {message!r}"
129
+
130
+
131
+ def test_matching_revision_loads(
132
+ abi_fixture: dict,
133
+ isolated_search_path: Path,
134
+ ) -> None:
135
+ root = Path(abi_fixture["root"], "at-revision")
136
+ registry = chtypes.Registry(root)
137
+ library = registry.for_version(FIXTURE_MINOR)
138
+ assert library.abi_revision == abi_fixture["abi_revision"]
139
+ assert library.version == abi_fixture["clickhouse_version"]
@@ -23,7 +23,10 @@ PLATFORM = "linux-arm64"
23
23
 
24
24
  needs_fixtures = pytest.mark.skipif(
25
25
  not EXPECTED_FILE.is_file(),
26
- reason=f"no fetch fixtures at {FIXTURES} (generated in the core repository, docs/guides/fetch.md §9)",
26
+ reason=(
27
+ f"no fetch fixtures at {FIXTURES} "
28
+ "(generated in the core repository, docs/guides/fetch.md §9)"
29
+ ),
27
30
  )
28
31
 
29
32
 
@@ -504,7 +504,7 @@ def test_trusted_keys_policy(monkeypatch: pytest.MonkeyPatch) -> None:
504
504
 
505
505
  def test_signature_file_shape() -> None:
506
506
  sig = (FIXTURES / "signed" / "SHA256SUMS.sig").read_bytes()
507
- comment, raw = fetch_module._parse_signature(sig)
507
+ comment, raw = fetch_module.parse_signature_file(sig)
508
508
  assert comment == f"chtypes artifacts, ed25519 key {_expected()['key_id']}"
509
509
  assert len(raw) == 64
510
510
  sums = (FIXTURES / "signed" / "SHA256SUMS").read_bytes()
@@ -517,9 +517,9 @@ def test_signature_file_shape() -> None:
517
517
  sums + b"\n", sig, fetch_module.trusted_keys(_test_keys()), "x"
518
518
  )
519
519
  with pytest.raises(chtypes.ArtifactUntrustedError, match="base64"):
520
- fetch_module._parse_signature(b"untrusted comment: x\n!!!\n")
520
+ fetch_module.parse_signature_file(b"untrusted comment: x\n!!!\n")
521
521
  with pytest.raises(chtypes.ArtifactUntrustedError, match="64"):
522
- fetch_module._parse_signature(b"untrusted comment: x\nAAAA\n")
522
+ fetch_module.parse_signature_file(b"untrusted comment: x\nAAAA\n")
523
523
 
524
524
 
525
525
  # ------------------------------------------------------------- tar safety
@@ -127,7 +127,9 @@ def test_every_capability_is_fully_declared() -> None:
127
127
  problems.append(f"{cid}: duplicate id")
128
128
  seen.add(cid)
129
129
  if not cap.get("what", "").strip():
130
- problems.append(f"{cid}: no `what` — a capability with no description is a name, not a contract")
130
+ problems.append(
131
+ f"{cid}: no `what` — a capability with no description is a name, not a contract"
132
+ )
131
133
  if cap.get("group") not in MANIFEST_DOC["groups"]:
132
134
  problems.append(f"{cid}: unknown group {cap.get('group')!r}")
133
135
 
@@ -135,7 +137,9 @@ def test_every_capability_is_fully_declared() -> None:
135
137
  for lang in MANIFEST_DOC["languages"]:
136
138
  col = column(cap, lang)
137
139
  if not col:
138
- problems.append(f"{cid}: no {lang} column at all — declare a spelling or an absence")
140
+ problems.append(
141
+ f"{cid}: no {lang} column at all — declare a spelling or an absence"
142
+ )
139
143
  continue
140
144
  if "absent" in col:
141
145
  absent += 1
@@ -148,7 +152,9 @@ def test_every_capability_is_fully_declared() -> None:
148
152
  problems.append(f"{cid}: {lang} column has neither `symbol` nor `absent`")
149
153
  if absent == len(MANIFEST_DOC["languages"]):
150
154
  problems.append(f"{cid}: absent in every binding — that is a note, not a contract")
151
- assert not problems, "the parity manifest is not internally consistent:\n " + "\n ".join(problems)
155
+ assert not problems, "the parity manifest is not internally consistent:\n " + "\n ".join(
156
+ problems
157
+ )
152
158
 
153
159
 
154
160
  def test_the_go_copy_of_the_manifest_is_byte_identical() -> None:
@@ -162,7 +168,8 @@ def test_the_go_copy_of_the_manifest_is_byte_identical() -> None:
162
168
  if GO_COPY.read_bytes() != MANIFEST.read_bytes():
163
169
  pytest.fail(
164
170
  f"{GO_COPY} has drifted from {MANIFEST}. The manifest is one file; this is its "
165
- f"embedded copy. Re-sync it:\n\n cp tests/parity/manifest.json go/chtypes/testdata/parity.json\n"
171
+ f"embedded copy. Re-sync it:\n\n "
172
+ f"cp tests/parity/manifest.json go/chtypes/testdata/parity.json\n"
166
173
  )
167
174
 
168
175
 
@@ -285,7 +292,8 @@ def test_the_cli_offers_every_contract_subcommand() -> None:
285
292
  """`chtypes fetch|verify|list|where` is a surface too, and it drifted once."""
286
293
  source = (SRC / "__main__.py").read_text(encoding="utf-8")
287
294
  missing = [
288
- f"{cap['id']}: the python CLI has no `{cap['value']}` subcommand, which {also_in(cap)} offer"
295
+ f"{cap['id']}: the python CLI has no `{cap['value']}` subcommand, "
296
+ f"which {also_in(cap)} offer"
289
297
  for cap in CAPABILITIES
290
298
  if cap["kind"] == "cli" and spelling(cap, LANG) and f'"{cap["value"]}"' not in source
291
299
  ]
@@ -334,8 +342,8 @@ def test_no_public_name_escapes_the_contract() -> None:
334
342
  assert not undeclared, (
335
343
  f"python exports {len(undeclared)} public name(s) the parity contract has never heard "
336
344
  f"of:\n " + "\n ".join(undeclared) + "\n\nEither give each one a capability in "
337
- f"tests/parity/manifest.json (if the other bindings should have it too) or list it under "
338
- f'`unlisted.python` with a reason (if it is deliberately ours alone).'
345
+ "tests/parity/manifest.json (if the other bindings should have it too) or list it under "
346
+ "`unlisted.python` with a reason (if it is deliberately ours alone)."
339
347
  )
340
348
 
341
349
 
@@ -397,8 +405,9 @@ def test_the_manifest_and_the_spec_table_agree() -> None:
397
405
  # assertion is about which COLUMNS exist, not about their alignment.
398
406
  squashed = re.sub(r"[ \t]+", " ", full)
399
407
  assert "| Concept | Go | Python | TypeScript | Rust |" in squashed, (
400
- "docs/reference/bindings.md's object-model table has lost a language column. Every language is a peer "
401
- "SDK over the same C ABI and the document says so in its own second paragraph; a table with "
408
+ "docs/reference/bindings.md's object-model table has lost a language "
409
+ "column. Every language is a peer SDK over the same C ABI and the "
410
+ "document says so in its own second paragraph; a table with "
402
411
  "three of the four columns is how Rust came to be described only in a Notes cell."
403
412
  )
404
413
 
@@ -420,10 +429,11 @@ def test_the_manifest_and_the_spec_table_agree() -> None:
420
429
  undocumented.append(f"{cap['id']}: {lang} `{spelled}` — {cap['what']}")
421
430
  assert checked > 0, "no object-model spelling was compared against docs/reference/bindings.md"
422
431
  assert not undocumented, (
423
- f"the parity manifest requires {len(undocumented)} spelling(s) that docs/reference/bindings.md never "
432
+ f"the parity manifest requires {len(undocumented)} spelling(s) that "
433
+ f"docs/reference/bindings.md never "
424
434
  f"names:\n " + "\n ".join(undocumented) + "\n\nEither document them there, or — if the "
425
- f"capability should not be required at all — remove it from the manifest. The two are one "
426
- f"contract stated twice."
435
+ "capability should not be required at all — remove it from the manifest. The two are one "
436
+ "contract stated twice."
427
437
  )
428
438
 
429
439
 
@@ -178,6 +178,29 @@ def test_manifest_ignores_unknown_fields(tmp_path: Path) -> None:
178
178
  assert manifest.library == "libchtypes.so"
179
179
 
180
180
 
181
+ def test_verify_library_accepts_an_uppercase_digest(tmp_path: Path) -> None:
182
+ """A hex digest is the same digest in either case.
183
+
184
+ Go lower-cases (`strings.ToLower`) and Rust lower-cases
185
+ (`.to_ascii_lowercase()`) before comparing; Python and TypeScript once
186
+ compared raw, so an uppercase `library_sha256` was accepted by two
187
+ bindings and refused by two. docs/reference/artifact.md asserts all four
188
+ behave alike, so this pins it.
189
+ """
190
+ payload = b"not really a library"
191
+ (tmp_path / "fake.so").write_bytes(payload)
192
+ (tmp_path / "manifest.json").write_text(
193
+ json.dumps(
194
+ {
195
+ "library": "fake.so",
196
+ "library_bytes": len(payload),
197
+ "library_sha256": hashlib.sha256(payload).hexdigest().upper(),
198
+ }
199
+ )
200
+ )
201
+ chtypes.verify_library(tmp_path) # must NOT raise
202
+
203
+
181
204
  def test_verify_library_checks_the_bytes(tmp_path: Path) -> None:
182
205
  payload = b"not really a library"
183
206
  (tmp_path / "fake.so").write_bytes(payload)
chtypes-0.2.1/uv.lock ADDED
@@ -0,0 +1,108 @@
1
+ version = 1
2
+ revision = 3
3
+ requires-python = ">=3.11"
4
+
5
+ [[package]]
6
+ name = "chtypes"
7
+ version = "0.2.1"
8
+ source = { editable = "." }
9
+
10
+ [package.dev-dependencies]
11
+ dev = [
12
+ { name = "pytest" },
13
+ { name = "ruff" },
14
+ ]
15
+
16
+ [package.metadata]
17
+
18
+ [package.metadata.requires-dev]
19
+ dev = [
20
+ { name = "pytest", specifier = ">=8.0" },
21
+ { name = "ruff", specifier = ">=0.14" },
22
+ ]
23
+
24
+ [[package]]
25
+ name = "colorama"
26
+ version = "0.4.6"
27
+ source = { registry = "https://pypi.org/simple" }
28
+ sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697, upload-time = "2022-10-25T02:36:22.414Z" }
29
+ wheels = [
30
+ { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" },
31
+ ]
32
+
33
+ [[package]]
34
+ name = "iniconfig"
35
+ version = "2.3.0"
36
+ source = { registry = "https://pypi.org/simple" }
37
+ sdist = { url = "https://files.pythonhosted.org/packages/72/34/14ca021ce8e5dfedc35312d08ba8bf51fdd999c576889fc2c24cb97f4f10/iniconfig-2.3.0.tar.gz", hash = "sha256:c76315c77db068650d49c5b56314774a7804df16fee4402c1f19d6d15d8c4730", size = 20503, upload-time = "2025-10-18T21:55:43.219Z" }
38
+ wheels = [
39
+ { url = "https://files.pythonhosted.org/packages/cb/b1/3846dd7f199d53cb17f49cba7e651e9ce294d8497c8c150530ed11865bb8/iniconfig-2.3.0-py3-none-any.whl", hash = "sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12", size = 7484, upload-time = "2025-10-18T21:55:41.639Z" },
40
+ ]
41
+
42
+ [[package]]
43
+ name = "packaging"
44
+ version = "26.3"
45
+ source = { registry = "https://pypi.org/simple" }
46
+ sdist = { url = "https://files.pythonhosted.org/packages/7d/fa/3944b40b07da9ce895c0e6303a5ab7d53da063554f534556b134a54d6093/packaging-26.3.tar.gz", hash = "sha256:94edc256424af38762eb31306eed28beb9f0efc50a8837492c9d6fd6004aed79", size = 313412, upload-time = "2026-08-04T18:15:28.737Z" }
47
+ wheels = [
48
+ { url = "https://files.pythonhosted.org/packages/63/34/ba1c580383c9eada3711951fef0795c80b829a078d72188184bcab9dd527/packaging-26.3-py3-none-any.whl", hash = "sha256:d7193f7c8e4e93f444fde0262bf90af30e16fa0ad0ad44cb553c87339b23cd1c", size = 129956, upload-time = "2026-08-04T18:15:27.159Z" },
49
+ ]
50
+
51
+ [[package]]
52
+ name = "pluggy"
53
+ version = "1.6.0"
54
+ source = { registry = "https://pypi.org/simple" }
55
+ sdist = { url = "https://files.pythonhosted.org/packages/f9/e2/3e91f31a7d2b083fe6ef3fa267035b518369d9511ffab804f839851d2779/pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3", size = 69412, upload-time = "2025-05-15T12:30:07.975Z" }
56
+ wheels = [
57
+ { url = "https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746", size = 20538, upload-time = "2025-05-15T12:30:06.134Z" },
58
+ ]
59
+
60
+ [[package]]
61
+ name = "pygments"
62
+ version = "2.21.0"
63
+ source = { registry = "https://pypi.org/simple" }
64
+ sdist = { url = "https://files.pythonhosted.org/packages/49/2e/ced460408999b33da6b31b0021b0f37d329e202d4169aeb164493778f25b/pygments-2.21.0.tar.gz", hash = "sha256:610ca751c9bc2492b38eb9a38a7fbc93edbbb2d7182edaf34e66ae493dee5c8c", size = 5005329, upload-time = "2026-08-17T08:02:48.824Z" }
65
+ wheels = [
66
+ { url = "https://files.pythonhosted.org/packages/71/46/17f022dd3e953bf20a04a028a21ec746d942f8d2af30fa0f124fa0e6a684/pygments-2.21.0-py3-none-any.whl", hash = "sha256:2363c69b61c4a97c838da3b130dcd6468f4848992b21a82f2a63ec34377137d9", size = 1250147, upload-time = "2026-08-17T08:02:44.912Z" },
67
+ ]
68
+
69
+ [[package]]
70
+ name = "pytest"
71
+ version = "9.1.1"
72
+ source = { registry = "https://pypi.org/simple" }
73
+ dependencies = [
74
+ { name = "colorama", marker = "sys_platform == 'win32'" },
75
+ { name = "iniconfig" },
76
+ { name = "packaging" },
77
+ { name = "pluggy" },
78
+ { name = "pygments" },
79
+ ]
80
+ sdist = { url = "https://files.pythonhosted.org/packages/e4/47/b9efed96c114afcfa3c9d3fe98a76a1d14c74a9e266d397cf6eb64be5e01/pytest-9.1.1.tar.gz", hash = "sha256:1088fbde8f2b49d95a549a195707afa7a76a3ce9bcadc26b6d71f0ffda5fe313", size = 1636369, upload-time = "2026-06-19T10:58:32.857Z" }
81
+ wheels = [
82
+ { url = "https://files.pythonhosted.org/packages/24/25/1de2678b631f5a49215c6c96fff41ba892b0a34df68d6d80292b1b48aa7f/pytest-9.1.1-py3-none-any.whl", hash = "sha256:37a86b45efb9a47a61a36449063e8e18d0cab3161329fc099eb21783169c4f0c", size = 386536, upload-time = "2026-06-19T10:58:31.347Z" },
83
+ ]
84
+
85
+ [[package]]
86
+ name = "ruff"
87
+ version = "0.16.7"
88
+ source = { registry = "https://pypi.org/simple" }
89
+ sdist = { url = "https://files.pythonhosted.org/packages/82/bb/5a449b9162e49b139d72f61672bd3ac1d790221f796d3304e2241fff4c58/ruff-0.16.7.tar.gz", hash = "sha256:5f71d004ac1263b22fa39462ac5ae618a4b77d58981af2cc79bf79a29c12b1a6", size = 4924184, upload-time = "2026-09-10T18:04:06.336Z" }
90
+ wheels = [
91
+ { url = "https://files.pythonhosted.org/packages/e3/b2/c80aeeb7f9e469c0d63a85d2f1ab6e1ebfbe10ea7a8d2438b7e09e3ff09e/ruff-0.16.7-py3-none-linux_armv6l.whl", hash = "sha256:727307773e7c7f9181d3ed3a2484186e56c1fa1874255911c74585eb2c7c19f9", size = 10048917, upload-time = "2026-09-10T18:03:30.28Z" },
92
+ { url = "https://files.pythonhosted.org/packages/7b/96/20bb7bcae008004df52afcb7ac83432d4a467f2c17b672fe46d26be231c5/ruff-0.16.7-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:9d61c258deabf58f34c67bd4bb4d939c7f2e6b5f0e59c1cdd1cf771b11cde929", size = 10242929, upload-time = "2026-09-10T18:03:32.706Z" },
93
+ { url = "https://files.pythonhosted.org/packages/90/b2/f184b0d5abec02db69cfd7e49b688ae0237554528ca777136c613bf36bee/ruff-0.16.7-py3-none-macosx_11_0_arm64.whl", hash = "sha256:7ab81118df8945e0193d0240712aa4496573595b75185c3636ed825592a0f728", size = 9847245, upload-time = "2026-09-10T18:03:34.509Z" },
94
+ { url = "https://files.pythonhosted.org/packages/eb/2d/db1633a641866ed801e34cc6b60ef236c5e16f9b2124ab1d49cc24a5fe4f/ruff-0.16.7-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4c196c968874fc8019da8e7163de7a1a370f111e2309b4b7dfea0fce950198d0", size = 9961780, upload-time = "2026-09-10T18:03:36.618Z" },
95
+ { url = "https://files.pythonhosted.org/packages/4d/98/edea21e1a3e38dbbc3bf6bb068b863b3b06184cf8533a4c7dbbe208a89d5/ruff-0.16.7-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ac8c3bd0a7e10ad31e6ce51e7a99f3cb772e69aecdd6b9ea7e99b362f62a62c0", size = 9866337, upload-time = "2026-09-10T18:03:38.805Z" },
96
+ { url = "https://files.pythonhosted.org/packages/0b/11/a15e60d4c87b214646f116ca9d204475bf993ee1047459bc9a360fd4d6d1/ruff-0.16.7-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:398d3988edde000b5c75dc1b3f584708da9bc990de069c18909142580fec1af9", size = 10562512, upload-time = "2026-09-10T18:03:40.71Z" },
97
+ { url = "https://files.pythonhosted.org/packages/29/42/eaff4c9b6d0c7cdf56df313a17e89ae854f5bbc0b0c8f9cce19be0ab7a8f/ruff-0.16.7-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ce05b62b770a8217c4646a9c4139fca00efe8fe5d71f87df2b243ff20d4584d1", size = 11302938, upload-time = "2026-09-10T18:03:42.607Z" },
98
+ { url = "https://files.pythonhosted.org/packages/5d/43/c75aa59a4ec181fe2ec06cab30e198c1c6d107229a9f008ae3a7c16cabd8/ruff-0.16.7-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:af1b576fddb9d9ef2ececfb5fadcd6a624b25070ed85e3cfcfe449fc3ff6a7b9", size = 10840857, upload-time = "2026-09-10T18:03:44.604Z" },
99
+ { url = "https://files.pythonhosted.org/packages/21/33/81f3da371942ea031105ba679d8d6e28ec1660ccd690a45f42d381161356/ruff-0.16.7-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9ce7f8f22df67c93ed96c717f9128eadb797144ac2bad475cf536f31d6100c55", size = 10370001, upload-time = "2026-09-10T18:03:46.706Z" },
100
+ { url = "https://files.pythonhosted.org/packages/fa/0b/6345fb4dbf6dd0ed1cfe5d18391dc9c3f59cc81622a7b0a65b84b3e730ba/ruff-0.16.7-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:06d0e93d04f392996435ebd600c153f65b47d73fbec2415aa99c5ee5756b3a5f", size = 10548735, upload-time = "2026-09-10T18:03:48.658Z" },
101
+ { url = "https://files.pythonhosted.org/packages/3f/4d/c5576adf511f92a328e5569dda190ecdd430da51f1a649f3a4a2fd73e21e/ruff-0.16.7-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:142151a5e7b93c1b11111337142f89dd2fbfee92161225c99a97222f22e32656", size = 10108496, upload-time = "2026-09-10T18:03:50.563Z" },
102
+ { url = "https://files.pythonhosted.org/packages/ff/8c/667d83c16199a17a56adc6b0bd4c3beb5b767a2babcd16a56f76f9be7fd6/ruff-0.16.7-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:e6651f97a342d8b35d54d8991544ca22169b86dc54111cb604666940c431b750", size = 9860136, upload-time = "2026-09-10T18:03:52.621Z" },
103
+ { url = "https://files.pythonhosted.org/packages/99/75/78d401106731999a1dd20cc5a6961e37e1eb9397a3b589f73f3a5ce146a3/ruff-0.16.7-py3-none-musllinux_1_2_i686.whl", hash = "sha256:ef140c6eb935fa9a84c9c607dfb2cb1b85843c192e79265b0c54f35f557ea8e5", size = 10286290, upload-time = "2026-09-10T18:03:55.207Z" },
104
+ { url = "https://files.pythonhosted.org/packages/68/49/56f9c3a8b755df93a0ad318b2147bf4ef5dae9a7e5ec61c460109c67957f/ruff-0.16.7-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:53e39506a730fadeee0d998ed5946f30671f0db240c6c7c73bdabbe33604bb6f", size = 10745048, upload-time = "2026-09-10T18:03:57.299Z" },
105
+ { url = "https://files.pythonhosted.org/packages/5f/ea/7f9b938a63ece4bec677ad7f9f7fa02df3383db1949ed93a382441c09a87/ruff-0.16.7-py3-none-win32.whl", hash = "sha256:2ea3470fcebcbc5df2fb0c6f3b90333fa9084c534e0111c038fa4a6ab9f1c4b7", size = 10059082, upload-time = "2026-09-10T18:03:59.632Z" },
106
+ { url = "https://files.pythonhosted.org/packages/39/11/480a6973a927aa653e1cead6a6416008640e03a99d05b34c0434b8c6c366/ruff-0.16.7-py3-none-win_amd64.whl", hash = "sha256:7ac26aca826e9e21d0f1cb25b54ac660760a9fdd094d3e4df9848232be98cfc6", size = 10593368, upload-time = "2026-09-10T18:04:01.999Z" },
107
+ { url = "https://files.pythonhosted.org/packages/8b/4b/51327018d056f0dad2c2238f26d1fb0f53707a9d91b75dea6d1b3039f136/ruff-0.16.7-py3-none-win_arm64.whl", hash = "sha256:aab7f39e2c9df6c596216070f98eef1207b94f8516cca20c808826974971855b", size = 10412401, upload-time = "2026-09-10T18:04:04.098Z" },
108
+ ]
@@ -1,35 +0,0 @@
1
- # Changelog — `chtypes`
2
-
3
- All notable changes to the python binding. The format is [Keep a Changelog](https://keepachangelog.com/en/1.1.0/); this package follows [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
4
-
5
- The four bindings in this repository are released together and give one answer, so an entry here has a counterpart in the other three.
6
-
7
- ## [0.1.2] — 2026-09-11
8
-
9
- ### Changed — BREAKING
10
-
11
- - **The transform reason for a materialized DEFAULT is now `default_materialized`** — previously the same word spelled with an `s` — and the exported constant naming it is now `Reason.DEFAULT_MATERIALIZED`. ClickHouse's own keyword is `MATERIALIZED`; its parser rejects the `s` spelling outright with a syntax error, so the reason naming that concept now matches the system it describes. Code comparing against the old string or the old constant name must be updated.
12
- - This is an SDK-only change and **the ABI is untouched: it remains revision 4, and no artifact needs relinking.** The reason is derived in the binding, not received from the artifact — the library emits `default_substituted`, which each binding translates. That wire value is unchanged.
13
-
14
- ### Changed
15
-
16
- - **The per-language README is now an orientation page, not a manual.** It carries install, a quickstart that runs, the three outcomes and the error model, and then points at `docs/` for the rest — roughly 450 lines of reference prose moved to `docs/guides/` and `docs/reference/` rather than being repeated four times and drifting four ways.
17
- - **The published packages no longer cite a repository you cannot open.** 148 source comments — in every binding and in `include/chtypes.h`, all of which ship inside the crate, the sdist and the module — attributed the contract they implement to a private repository's specification. They now cite **the C ABI contract**, with `include/chtypes.h` as the public authority for it. Alongside those, 14 dead paths into that repository, the examples' instruction to build an artifact there, and two descriptions of the signing infrastructure are gone. What remains naming it is the handful of places where the two-repository split is the actual subject — the README, `SECURITY.md`, and `docs/reference/README.md`, which says plainly that the full normative specification is not public.
18
- - `scripts/lint-public.sh` is new and blocking in CI, because the sweep this replaces reported itself complete while 148 of its targets survived in a different shape. It refuses both the private repository's name and that exact prose pointer, and its `--selftest` proves each rule fires rather than assuming it.
19
-
20
- ## [0.1.1] — 2026-09-11
21
-
22
- - **The golden set is served, not tracked.** `goldens/cases.json` no longer exists in this repository. Core publishes `sdk-goldens.json` in the rolling release as a row in the signed `SHA256SUMS`; `scripts/fetch.sh` installs it at `<registry>/sdk-goldens.json` and the golden test reads it offline from there (`CHTYPES_GOLDENS` overrides). A case runs only against the exact ClickHouse version `generated.exact` names for its line and skips loudly otherwise, so the set shrinks as lines are added rather than recording an answer twice.
23
- - **Artifact identity gained a wrapper build.** Assets are `chtypes-<version>-<os>-<arch>-b<N>.tar.gz`, where `<N>` is the core commit's UNIX timestamp; an absent suffix means build 0 and stays valid. Line resolution ranks by `(version, build)`, so a rebuild of a line supersedes the older build.
24
- - Fetching now retries the publish window, so a fetch that races a release no longer fails on sums naming a file the host has not finished serving.
25
- - README install instructions name the published package rather than a path dependency.
26
- - Two errors in the published README, both found by running the snippets rather than reading them: Python's `substituted` is on `RowResult`, not `BatchResult`, and TypeScript's `using schema = …` is a syntax error on Node 22, this package's own `engines` floor (`schema.close()` is portable).
27
- - `docs/support.md` is new: which language versions, platforms and ClickHouse lines are supported, generated from the manifests and the release's own index.
28
-
29
- ## [0.1.0] — 2026-09-10
30
-
31
- First release, published to PyPI from the tag `python/v0.1.0`.
32
-
33
- - Speaks **ABI revision 4** of the frozen `chs_*` C ABI (`include/chtypes.h`, 28 functions). The binding `dlopen`s a per-version artifact and reimplements no ClickHouse semantics; `Transformed` is the one derived result.
34
- - Verified against the public golden set (`goldens/cases.json`, schema 1, 31 cases), whose expectations were generated on ClickHouse 24.8, 25.3, 25.8, 25.10, 26.5, 26.6 and 26.7 — every case answered identically on all seven lines, and any case that did not was refused by the generator.
35
- - Pre-1.0: the artifact name `libchtypes`, the `chs_` prefix and the `enum chs_format` numbers are frozen. Function signatures freeze at this tag.
chtypes-0.1.2/uv.lock DELETED
@@ -1,108 +0,0 @@
1
- version = 1
2
- revision = 3
3
- requires-python = ">=3.11"
4
-
5
- [[package]]
6
- name = "chtypes"
7
- version = "0.1.2"
8
- source = { editable = "." }
9
-
10
- [package.dev-dependencies]
11
- dev = [
12
- { name = "pytest" },
13
- { name = "ruff" },
14
- ]
15
-
16
- [package.metadata]
17
-
18
- [package.metadata.requires-dev]
19
- dev = [
20
- { name = "pytest", specifier = ">=8.0" },
21
- { name = "ruff", specifier = ">=0.14" },
22
- ]
23
-
24
- [[package]]
25
- name = "colorama"
26
- version = "0.4.6"
27
- source = { registry = "https://pypi.org/simple" }
28
- sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697, upload-time = "2022-10-25T02:36:22.414Z" }
29
- wheels = [
30
- { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" },
31
- ]
32
-
33
- [[package]]
34
- name = "iniconfig"
35
- version = "2.3.0"
36
- source = { registry = "https://pypi.org/simple" }
37
- sdist = { url = "https://files.pythonhosted.org/packages/72/34/14ca021ce8e5dfedc35312d08ba8bf51fdd999c576889fc2c24cb97f4f10/iniconfig-2.3.0.tar.gz", hash = "sha256:c76315c77db068650d49c5b56314774a7804df16fee4402c1f19d6d15d8c4730", size = 20503, upload-time = "2025-10-18T21:55:43.219Z" }
38
- wheels = [
39
- { url = "https://files.pythonhosted.org/packages/cb/b1/3846dd7f199d53cb17f49cba7e651e9ce294d8497c8c150530ed11865bb8/iniconfig-2.3.0-py3-none-any.whl", hash = "sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12", size = 7484, upload-time = "2025-10-18T21:55:41.639Z" },
40
- ]
41
-
42
- [[package]]
43
- name = "packaging"
44
- version = "26.3"
45
- source = { registry = "https://pypi.org/simple" }
46
- sdist = { url = "https://files.pythonhosted.org/packages/7d/fa/3944b40b07da9ce895c0e6303a5ab7d53da063554f534556b134a54d6093/packaging-26.3.tar.gz", hash = "sha256:94edc256424af38762eb31306eed28beb9f0efc50a8837492c9d6fd6004aed79", size = 313412, upload-time = "2026-08-04T18:15:28.737Z" }
47
- wheels = [
48
- { url = "https://files.pythonhosted.org/packages/63/34/ba1c580383c9eada3711951fef0795c80b829a078d72188184bcab9dd527/packaging-26.3-py3-none-any.whl", hash = "sha256:d7193f7c8e4e93f444fde0262bf90af30e16fa0ad0ad44cb553c87339b23cd1c", size = 129956, upload-time = "2026-08-04T18:15:27.159Z" },
49
- ]
50
-
51
- [[package]]
52
- name = "pluggy"
53
- version = "1.6.0"
54
- source = { registry = "https://pypi.org/simple" }
55
- sdist = { url = "https://files.pythonhosted.org/packages/f9/e2/3e91f31a7d2b083fe6ef3fa267035b518369d9511ffab804f839851d2779/pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3", size = 69412, upload-time = "2025-05-15T12:30:07.975Z" }
56
- wheels = [
57
- { url = "https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746", size = 20538, upload-time = "2025-05-15T12:30:06.134Z" },
58
- ]
59
-
60
- [[package]]
61
- name = "pygments"
62
- version = "2.21.0"
63
- source = { registry = "https://pypi.org/simple" }
64
- sdist = { url = "https://files.pythonhosted.org/packages/49/2e/ced460408999b33da6b31b0021b0f37d329e202d4169aeb164493778f25b/pygments-2.21.0.tar.gz", hash = "sha256:610ca751c9bc2492b38eb9a38a7fbc93edbbb2d7182edaf34e66ae493dee5c8c", size = 5005329, upload-time = "2026-08-17T08:02:48.824Z" }
65
- wheels = [
66
- { url = "https://files.pythonhosted.org/packages/71/46/17f022dd3e953bf20a04a028a21ec746d942f8d2af30fa0f124fa0e6a684/pygments-2.21.0-py3-none-any.whl", hash = "sha256:2363c69b61c4a97c838da3b130dcd6468f4848992b21a82f2a63ec34377137d9", size = 1250147, upload-time = "2026-08-17T08:02:44.912Z" },
67
- ]
68
-
69
- [[package]]
70
- name = "pytest"
71
- version = "9.1.1"
72
- source = { registry = "https://pypi.org/simple" }
73
- dependencies = [
74
- { name = "colorama", marker = "sys_platform == 'win32'" },
75
- { name = "iniconfig" },
76
- { name = "packaging" },
77
- { name = "pluggy" },
78
- { name = "pygments" },
79
- ]
80
- sdist = { url = "https://files.pythonhosted.org/packages/e4/47/b9efed96c114afcfa3c9d3fe98a76a1d14c74a9e266d397cf6eb64be5e01/pytest-9.1.1.tar.gz", hash = "sha256:1088fbde8f2b49d95a549a195707afa7a76a3ce9bcadc26b6d71f0ffda5fe313", size = 1636369, upload-time = "2026-06-19T10:58:32.857Z" }
81
- wheels = [
82
- { url = "https://files.pythonhosted.org/packages/24/25/1de2678b631f5a49215c6c96fff41ba892b0a34df68d6d80292b1b48aa7f/pytest-9.1.1-py3-none-any.whl", hash = "sha256:37a86b45efb9a47a61a36449063e8e18d0cab3161329fc099eb21783169c4f0c", size = 386536, upload-time = "2026-06-19T10:58:31.347Z" },
83
- ]
84
-
85
- [[package]]
86
- name = "ruff"
87
- version = "0.16.6"
88
- source = { registry = "https://pypi.org/simple" }
89
- sdist = { url = "https://files.pythonhosted.org/packages/a4/7c/6adb35d70e7c027e308274557901c7e00fb3407750faf3620c184ae058cb/ruff-0.16.6.tar.gz", hash = "sha256:dcf8a73d2ff77e99dde91244b4da16feba7f14e6beeb4015dee7c5a909e99050", size = 4921251, upload-time = "2026-09-03T16:57:29.037Z" }
90
- wheels = [
91
- { url = "https://files.pythonhosted.org/packages/a4/28/9cc1b79639e284ec103f43c88c644db4eb58cbd0ea1ca11f1193435369ac/ruff-0.16.6-py3-none-linux_armv6l.whl", hash = "sha256:61c368c26bf8e973e5ab14a2772de587bc068ea3f9a277f673380749b4898fb8", size = 10015638, upload-time = "2026-09-03T16:56:40.986Z" },
92
- { url = "https://files.pythonhosted.org/packages/71/11/627d342ef727ea7794edf74fe23d60a074b02c3acc2e9436684e782286ca/ruff-0.16.6-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:ecf4f068e2e123e43a26e9db4e19524cc56563912404e83bbfca375757e45a32", size = 10220762, upload-time = "2026-09-03T16:56:44.681Z" },
93
- { url = "https://files.pythonhosted.org/packages/43/d9/b75668ce41e4c8d073d18d6d08672ba6906ce45d5c06ea4fdb2e84ce3853/ruff-0.16.6-py3-none-macosx_11_0_arm64.whl", hash = "sha256:99b62ea33baf130f50368798d841f0d95527b6d817bf31817b65dd058f1d314c", size = 9835082, upload-time = "2026-09-03T16:56:47.142Z" },
94
- { url = "https://files.pythonhosted.org/packages/99/97/123ab10b05cde889c107c20f5a9774955104b5552796a2a8584b089ae8eb/ruff-0.16.6-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7fbf89013f2bb3f6835a6038ff658dc8a1b38c98dc8e724b964168ad4e881876", size = 9949304, upload-time = "2026-09-03T16:56:49.813Z" },
95
- { url = "https://files.pythonhosted.org/packages/3e/58/a4a2c59dd2e5b85929c912d9cac3056eb9ee8c7e75e9b9fe3e109174966b/ruff-0.16.6-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:56a67065e22efa6bc4d498299d3bb06c0c90aace8fac2068b5a12f9dc4d8d51d", size = 9840612, upload-time = "2026-09-03T16:56:52.368Z" },
96
- { url = "https://files.pythonhosted.org/packages/61/6a/ff8c8626a786c4f49d48ced4a752dadbca65f5263005f9c2416578194694/ruff-0.16.6-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e25cc89174874b176a157e4428d66761c2c0c006654419bf384f967f361ff1b1", size = 10543465, upload-time = "2026-09-03T16:56:55.089Z" },
97
- { url = "https://files.pythonhosted.org/packages/ad/bb/c47535923365f337b82e28192e4e9eef2176511007cfd99a62fc22df5dad/ruff-0.16.6-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0700580ed5303723cb3c11c2f1d2a8913ce77b7ea86646dddb887f5417a9ba70", size = 11267576, upload-time = "2026-09-03T16:56:57.791Z" },
98
- { url = "https://files.pythonhosted.org/packages/ba/50/e5119a5212b5cd63b51e1f4b25e7bd636a6668fc069a3160b108ad7e3c16/ruff-0.16.6-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:15f1d0b6e165a6e56567befb6629f8209271311d990bae0f37e6d065035ef5f3", size = 10781993, upload-time = "2026-09-03T16:57:00.666Z" },
99
- { url = "https://files.pythonhosted.org/packages/8b/98/083d8b4ef3c51a0d19db84367791cbe9f44e4b53343d19dfa83556e1cd9a/ruff-0.16.6-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d72c591a96986ee4268860e2b7235082129ca5e4cb9cbba653a4b57c11893757", size = 10317748, upload-time = "2026-09-03T16:57:03.428Z" },
100
- { url = "https://files.pythonhosted.org/packages/9a/29/68f7ff2c5ad95f19f00627ac2de95644e25fe47371ea60b2db1fd952315e/ruff-0.16.6-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:65a006baa18f33324325814c864daef03541d51564b98c517610ea756ab7003e", size = 10540096, upload-time = "2026-09-03T16:57:06.182Z" },
101
- { url = "https://files.pythonhosted.org/packages/c4/f9/79a8f6de85968641d68a7863aeec577551924ef066a990a48ff93167beab/ruff-0.16.6-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:cd02a7bf1a21a8735228a3e8c95a9dc5cf86bd2a52194f4aaae2a5755b4de0f4", size = 10100494, upload-time = "2026-09-03T16:57:09.194Z" },
102
- { url = "https://files.pythonhosted.org/packages/d9/e8/b81a22d9b90c00b892ccf2fa2ac36fa95de4c13ab85aea3e73795cfe4651/ruff-0.16.6-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:31b36f1e5ad85e0737f09d2be4e512e2e283583c14015da3b9dc07359ac0fc88", size = 9843663, upload-time = "2026-09-03T16:57:12.168Z" },
103
- { url = "https://files.pythonhosted.org/packages/39/aa/54f516ec5e5a11c4afdceb1c454ebb054ffb96e4f4a1705580b4346abd35/ruff-0.16.6-py3-none-musllinux_1_2_i686.whl", hash = "sha256:61029b4ab4aa723fd3064fab96b1d814492596bf0c792679fffcbde1e1679953", size = 10282461, upload-time = "2026-09-03T16:57:15.077Z" },
104
- { url = "https://files.pythonhosted.org/packages/52/0b/38d0aa8aa32372b96dc44f97b22e576c4147808271aab7b2cb1e353d4445/ruff-0.16.6-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:9ac8998457832c2061709d900856b7ad271dace0cb41f346588d540162bfa718", size = 10728808, upload-time = "2026-09-03T16:57:17.797Z" },
105
- { url = "https://files.pythonhosted.org/packages/5e/e5/9e274e24eeb027640ffc7442f21239f16d17f47acec15ae34f32e03a5c79/ruff-0.16.6-py3-none-win32.whl", hash = "sha256:0b87d9d16fcb63e8018423ca1d50b7260f15cb2da33e30db4baad4183a948c25", size = 10049212, upload-time = "2026-09-03T16:57:20.55Z" },
106
- { url = "https://files.pythonhosted.org/packages/22/31/72472449414223ed1a2da236b992adbb1a2ae59e34794574810f60ce068e/ruff-0.16.6-py3-none-win_amd64.whl", hash = "sha256:10d21c51c3495d8eaea7b703a16592117ea6eb1d649e36335aa965ff1173eb39", size = 10556402, upload-time = "2026-09-03T16:57:23.501Z" },
107
- { url = "https://files.pythonhosted.org/packages/fc/07/d781f8f8e1ac24bef9f3269cf62ffb1407ca24c3a8f12e5e22874f90528c/ruff-0.16.6-py3-none-win_arm64.whl", hash = "sha256:7a976c79b958f94e50a022a19f0f8c87387448020935ec14fc74331bd0a7f2c5", size = 10412850, upload-time = "2026-09-03T16:57:26.416Z" },
108
- ]
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes