chtypes 2.0.0.dev1__tar.gz → 2.0.0.dev3__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-2.0.0.dev1 → chtypes-2.0.0.dev3}/CHANGELOG.md +4 -0
- {chtypes-2.0.0.dev1 → chtypes-2.0.0.dev3}/PKG-INFO +9 -7
- {chtypes-2.0.0.dev1 → chtypes-2.0.0.dev3}/README.md +8 -6
- {chtypes-2.0.0.dev1 → chtypes-2.0.0.dev3}/pyproject.toml +1 -1
- {chtypes-2.0.0.dev1 → chtypes-2.0.0.dev3}/src/chtypes/__init__.py +15 -3
- {chtypes-2.0.0.dev1 → chtypes-2.0.0.dev3}/src/chtypes/__main__.py +23 -5
- {chtypes-2.0.0.dev1 → chtypes-2.0.0.dev3}/src/chtypes/_abi2/_decls.py +2 -2
- {chtypes-2.0.0.dev1 → chtypes-2.0.0.dev3}/src/chtypes/_abi2/_errmap.py +1 -1
- {chtypes-2.0.0.dev1 → chtypes-2.0.0.dev3}/src/chtypes/_abi2/_vocab.py +30 -5
- {chtypes-2.0.0.dev1 → chtypes-2.0.0.dev3}/src/chtypes/_decode.py +75 -2
- {chtypes-2.0.0.dev1 → chtypes-2.0.0.dev3}/src/chtypes/_input.py +42 -1
- {chtypes-2.0.0.dev1 → chtypes-2.0.0.dev3}/src/chtypes/_ocifetch/_channel.py +76 -0
- {chtypes-2.0.0.dev1 → chtypes-2.0.0.dev3}/src/chtypes/_ocifetch/_ensure.py +26 -3
- {chtypes-2.0.0.dev1 → chtypes-2.0.0.dev3}/src/chtypes/_ocifetch/_http.py +18 -0
- {chtypes-2.0.0.dev1 → chtypes-2.0.0.dev3}/src/chtypes/_ocifetch/_layout.py +17 -7
- {chtypes-2.0.0.dev1 → chtypes-2.0.0.dev3}/src/chtypes/_ocifetch/_oci.py +30 -2
- {chtypes-2.0.0.dev1 → chtypes-2.0.0.dev3}/src/chtypes/library.py +47 -5
- {chtypes-2.0.0.dev1 → chtypes-2.0.0.dev3}/src/chtypes/registry.py +25 -1
- {chtypes-2.0.0.dev1 → chtypes-2.0.0.dev3}/src/chtypes/results.py +67 -1
- chtypes-2.0.0.dev3/tests/abi2/test_public_server.py +268 -0
- {chtypes-2.0.0.dev1 → chtypes-2.0.0.dev3}/tests/abi2/test_public_setup_cases.py +50 -1
- {chtypes-2.0.0.dev1 → chtypes-2.0.0.dev3}/tests/abi2/test_reader_rules.py +13 -0
- {chtypes-2.0.0.dev1 → chtypes-2.0.0.dev3}/tests/api/test_decode.py +89 -1
- {chtypes-2.0.0.dev1 → chtypes-2.0.0.dev3}/tests/cli/test_devchannel.py +22 -0
- {chtypes-2.0.0.dev1 → chtypes-2.0.0.dev3}/tests/ocifetch/test_cache_roots.py +38 -1
- {chtypes-2.0.0.dev1 → chtypes-2.0.0.dev3}/tests/ocifetch/test_conformance.py +74 -0
- {chtypes-2.0.0.dev1 → chtypes-2.0.0.dev3}/tests/ocifetch/test_devchannel.py +151 -2
- chtypes-2.0.0.dev3/tests/test_cache_roots_api.py +61 -0
- {chtypes-2.0.0.dev1 → chtypes-2.0.0.dev3}/tests/test_examples_tour.py +1 -1
- {chtypes-2.0.0.dev1 → chtypes-2.0.0.dev3}/uv.lock +1 -1
- {chtypes-2.0.0.dev1 → chtypes-2.0.0.dev3}/.gitignore +0 -0
- {chtypes-2.0.0.dev1 → chtypes-2.0.0.dev3}/LICENSE +0 -0
- {chtypes-2.0.0.dev1 → chtypes-2.0.0.dev3}/src/chtypes/_abi2/__init__.py +0 -0
- {chtypes-2.0.0.dev1 → chtypes-2.0.0.dev3}/src/chtypes/_abi2/_errors.py +0 -0
- {chtypes-2.0.0.dev1 → chtypes-2.0.0.dev3}/src/chtypes/_abi2/_loader.py +0 -0
- {chtypes-2.0.0.dev1 → chtypes-2.0.0.dev3}/src/chtypes/_ed25519.py +0 -0
- {chtypes-2.0.0.dev1 → chtypes-2.0.0.dev3}/src/chtypes/_guard.py +0 -0
- {chtypes-2.0.0.dev1 → chtypes-2.0.0.dev3}/src/chtypes/_ocifetch/__init__.py +0 -0
- {chtypes-2.0.0.dev1 → chtypes-2.0.0.dev3}/src/chtypes/_ocifetch/_constants.py +0 -0
- {chtypes-2.0.0.dev1 → chtypes-2.0.0.dev3}/src/chtypes/_ocifetch/_dsse.py +0 -0
- {chtypes-2.0.0.dev1 → chtypes-2.0.0.dev3}/src/chtypes/_ocifetch/_errors.py +0 -0
- {chtypes-2.0.0.dev1 → chtypes-2.0.0.dev3}/src/chtypes/_ocifetch/_faults.py +0 -0
- {chtypes-2.0.0.dev1 → chtypes-2.0.0.dev3}/src/chtypes/_ocifetch/_goldens.py +0 -0
- {chtypes-2.0.0.dev1 → chtypes-2.0.0.dev3}/src/chtypes/_ocifetch/_lock.py +0 -0
- {chtypes-2.0.0.dev1 → chtypes-2.0.0.dev3}/src/chtypes/_ocifetch/_referrers.py +0 -0
- {chtypes-2.0.0.dev1 → chtypes-2.0.0.dev3}/src/chtypes/_ocifetch/_unpack.py +0 -0
- {chtypes-2.0.0.dev1 → chtypes-2.0.0.dev3}/src/chtypes/_setup.py +0 -0
- {chtypes-2.0.0.dev1 → chtypes-2.0.0.dev3}/src/chtypes/errors.py +0 -0
- {chtypes-2.0.0.dev1 → chtypes-2.0.0.dev3}/src/chtypes/py.typed +0 -0
- {chtypes-2.0.0.dev1 → chtypes-2.0.0.dev3}/tests/abi2/__init__.py +0 -0
- {chtypes-2.0.0.dev1 → chtypes-2.0.0.dev3}/tests/abi2/_stub_tree.py +0 -0
- {chtypes-2.0.0.dev1 → chtypes-2.0.0.dev3}/tests/abi2/conftest.py +0 -0
- {chtypes-2.0.0.dev1 → chtypes-2.0.0.dev3}/tests/abi2/test_conformance.py +0 -0
- {chtypes-2.0.0.dev1 → chtypes-2.0.0.dev3}/tests/abi2/test_dev_fingerprint.py +0 -0
- {chtypes-2.0.0.dev1 → chtypes-2.0.0.dev3}/tests/abi2/test_goldens_runner_stub.py +0 -0
- {chtypes-2.0.0.dev1 → chtypes-2.0.0.dev3}/tests/abi2/test_loader.py +0 -0
- {chtypes-2.0.0.dev1 → chtypes-2.0.0.dev3}/tests/abi2/test_public_library.py +0 -0
- {chtypes-2.0.0.dev1 → chtypes-2.0.0.dev3}/tests/abi2/test_public_open.py +0 -0
- {chtypes-2.0.0.dev1 → chtypes-2.0.0.dev3}/tests/abi2/test_public_registry.py +0 -0
- {chtypes-2.0.0.dev1 → chtypes-2.0.0.dev3}/tests/api/__init__.py +0 -0
- {chtypes-2.0.0.dev1 → chtypes-2.0.0.dev3}/tests/api/test_input.py +0 -0
- {chtypes-2.0.0.dev1 → chtypes-2.0.0.dev3}/tests/api/test_vocab.py +0 -0
- {chtypes-2.0.0.dev1 → chtypes-2.0.0.dev3}/tests/cli/__init__.py +0 -0
- {chtypes-2.0.0.dev1 → chtypes-2.0.0.dev3}/tests/cli/conftest.py +0 -0
- {chtypes-2.0.0.dev1 → chtypes-2.0.0.dev3}/tests/cli/test_main.py +0 -0
- {chtypes-2.0.0.dev1 → chtypes-2.0.0.dev3}/tests/conftest.py +0 -0
- {chtypes-2.0.0.dev1 → chtypes-2.0.0.dev3}/tests/goldens_v1/__init__.py +0 -0
- {chtypes-2.0.0.dev1 → chtypes-2.0.0.dev3}/tests/goldens_v1/data/stub-goldens.json +0 -0
- {chtypes-2.0.0.dev1 → chtypes-2.0.0.dev3}/tests/goldens_v1/test_runner.py +0 -0
- {chtypes-2.0.0.dev1 → chtypes-2.0.0.dev3}/tests/ocifetch/__init__.py +0 -0
- {chtypes-2.0.0.dev1 → chtypes-2.0.0.dev3}/tests/ocifetch/_bundle_support.py +0 -0
- {chtypes-2.0.0.dev1 → chtypes-2.0.0.dev3}/tests/ocifetch/_registry_support.py +0 -0
- {chtypes-2.0.0.dev1 → chtypes-2.0.0.dev3}/tests/ocifetch/_sign_support.py +0 -0
- {chtypes-2.0.0.dev1 → chtypes-2.0.0.dev3}/tests/ocifetch/conftest.py +0 -0
- {chtypes-2.0.0.dev1 → chtypes-2.0.0.dev3}/tests/ocifetch/test_cache_strict.py +0 -0
- {chtypes-2.0.0.dev1 → chtypes-2.0.0.dev3}/tests/ocifetch/test_dsse.py +0 -0
- {chtypes-2.0.0.dev1 → chtypes-2.0.0.dev3}/tests/ocifetch/test_ensure.py +0 -0
- {chtypes-2.0.0.dev1 → chtypes-2.0.0.dev3}/tests/ocifetch/test_goldens.py +0 -0
- {chtypes-2.0.0.dev1 → chtypes-2.0.0.dev3}/tests/ocifetch/test_http.py +0 -0
- {chtypes-2.0.0.dev1 → chtypes-2.0.0.dev3}/tests/ocifetch/test_layout.py +0 -0
- {chtypes-2.0.0.dev1 → chtypes-2.0.0.dev3}/tests/ocifetch/test_lock.py +0 -0
- {chtypes-2.0.0.dev1 → chtypes-2.0.0.dev3}/tests/ocifetch/test_oci.py +0 -0
- {chtypes-2.0.0.dev1 → chtypes-2.0.0.dev3}/tests/ocifetch/test_seam_gate.py +0 -0
- {chtypes-2.0.0.dev1 → chtypes-2.0.0.dev3}/tests/ocifetch/test_unpack.py +0 -0
- {chtypes-2.0.0.dev1 → chtypes-2.0.0.dev3}/tests/test_abi_identity.py +0 -0
- {chtypes-2.0.0.dev1 → chtypes-2.0.0.dev3}/tests/test_devchannel_registry.py +0 -0
- {chtypes-2.0.0.dev1 → chtypes-2.0.0.dev3}/tests/test_ed25519.py +0 -0
- {chtypes-2.0.0.dev1 → chtypes-2.0.0.dev3}/tests/test_fetch_options.py +0 -0
|
@@ -6,6 +6,10 @@ The four bindings in this repository are released together and give one answer,
|
|
|
6
6
|
|
|
7
7
|
## [Unreleased]
|
|
8
8
|
|
|
9
|
+
### Added
|
|
10
|
+
|
|
11
|
+
- A 2.0.0-dev SDK prefers the newest build of its own fingerprint (#511). A version request resolves `<tag>--fp-<fingerprint>` first, the alias the staging dev registry serves beside every dev tag, with the binding's own generated ABI fingerprint, and the tag itself only when every base answers the alias 404; any other failure of the alias request is that failure, with no fallback, and an alias whose index lacks the platform is `CHTYPES_ARTIFACT_UNPUBLISHED`. The dev channel's cache lookups (the installed-build lookup a registry open makes first, the offline answer, and the monotonic rule of a fetch) now see only records whose signed `abi_fingerprint` is the SDK's own, so a newer build of another fingerprint, installed by another dev SDK in the shared cache, is never returned or kept over this one's own, and is never removed or rewritten. So a dev build of a newer fingerprint no longer strands an older dev SDK until its consumers upgrade. Trust is unchanged (the signed predicate and the load checks decide), a listing never shows an alias, nothing records one, `list` and `where` are unchanged, and there is no option: both steps are the dev channel's alone, and the v1 contract and every production channel never take them. The rule is `docs/guides/fetch-v1.md` section 3, and the `dev-alias-*`, `resolve-installed-dev-alias-*` and `list-tags-dev-alias` conformance cases hold all four bindings to it.
|
|
12
|
+
|
|
9
13
|
## [1.1.0] — 2026-10-06
|
|
10
14
|
|
|
11
15
|
### Added
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: chtypes
|
|
3
|
-
Version: 2.0.0.
|
|
3
|
+
Version: 2.0.0.dev3
|
|
4
4
|
Summary: Python binding for the chtypes artifacts: ClickHouse's own type system, per version, behind the frozen chs_* C ABI
|
|
5
5
|
Project-URL: Homepage, https://github.com/wave-rf/chtypes
|
|
6
6
|
Project-URL: Repository, https://github.com/wave-rf/chtypes
|
|
@@ -19,7 +19,9 @@ Description-Content-Type: text/markdown
|
|
|
19
19
|
|
|
20
20
|
# chtypes — Python SDK
|
|
21
21
|
|
|
22
|
-
> **2.0.0-dev: UNSTABLE, staging only, not for production.** This is the ABI v2 development binding (public issue #511). It speaks ABI v2's unstable description, pins its dev fingerprint and refuses a library with any other ("update your dev SDK"). It fetches only from the staging dev channel (`https://registry-staging.wavehouse.dev/chtypes/v2-dev`) and trusts only the staging key; `CHTYPES_ARTIFACTS_URL`, `CHTYPES_TRUSTED_KEYS` and `CHTYPES_ALLOW_UNSIGNED`, and the `bases`, `trusted_keys` and `allow_unsigned` fetch options, are ignored, each with one warning; `--lock`, `--frozen` and `--update`, and the `frozen`, `lock_path`, `lock_write` and `update` options, are refused, because a dev build is replaceable and a superseded one expires after 14 days. Its cache is `${XDG_CACHE_HOME:-~/.cache}/chtypes/v2-dev`, or `<CHTYPES_CACHE>/v2-dev` under an explicit cache, which no 1.x SDK reads. Every vocabulary keeps a value it does not list as its `unknown(n)` member (`Outcome("x")`, whose `known` is `False`) instead of failing the document. The rules are r1 to r6 of [`docs/reference/abi-v2.md`](../docs/reference/abi-v2.md). For production, use the 1.x package: `pip install "chtypes<2"`.
|
|
22
|
+
> **2.0.0-dev: UNSTABLE, staging only, not for production.** This is the ABI v2 development binding (public issue #511). It speaks ABI v2's unstable description, pins its dev fingerprint and refuses a library with any other ("update your dev SDK"). It fetches only from the staging dev channel (`https://registry-staging.wavehouse.dev/chtypes/v2-dev`) and trusts only the staging key; `CHTYPES_ARTIFACTS_URL`, `CHTYPES_TRUSTED_KEYS` and `CHTYPES_ALLOW_UNSIGNED`, and the `bases`, `trusted_keys` and `allow_unsigned` fetch options, are ignored, each with one warning; `--lock`, `--frozen` and `--update`, and the `frozen`, `lock_path`, `lock_write` and `update` options, are refused, because a dev build is replaceable and a superseded one expires after 14 days. Its cache is `${XDG_CACHE_HOME:-~/.cache}/chtypes/v2-dev`, or `<CHTYPES_CACHE>/v2-dev` under an explicit cache, which no 1.x SDK reads. A dev SDK prefers the newest build of its own fingerprint: it resolves `<tag>--fp-<its fingerprint>` first and the tag only when no such build exists, and its cache lookups ignore another dev SDK's builds in a shared cache, so a newer dev fingerprint never strands it (§3 of [`docs/guides/fetch-v1.md`](../docs/guides/fetch-v1.md)). Every vocabulary keeps a value it does not list as its `unknown(n)` member (`Outcome("x")`, whose `known` is `False`) instead of failing the document. The rules are r1 to r6 of [`docs/reference/abi-v2.md`](../docs/reference/abi-v2.md). For production, use the 1.x package: `pip install "chtypes<2"`.
|
|
23
|
+
>
|
|
24
|
+
> An explicit cache directory gets the `v2-dev` subroot whether it comes from `CHTYPES_CACHE` or from `FetchOptions(cache_dir=...)`: the layout is under `<cache_dir>/v2-dev`, never in the directory itself, so code that inspects or pre-populates a cache looks there; `chtypes.cache_root` and `chtypes.search_dirs` return the resolved paths. `CHTYPES_OFFLINE=1` is the environment twin of `FetchOptions(offline=True)` and `--offline`: the cache only, no request.
|
|
23
25
|
|
|
24
26
|
**If this row were inserted into this table on this ClickHouse version, what would happen?** chtypes answers with ClickHouse's own code: the real C++ type machinery, vendored per release into a native library behind the frozen `chs_*` C ABI and reached here through stdlib `ctypes`. Nothing semantic is reimplemented, so _"what does ClickHouse do with `256` into a `UInt8`?"_ is answered by ClickHouse rather than by a model of it. One peer binding among `{go, python, ts, rust}` — no language is privileged, and all four give one answer.
|
|
25
27
|
|
|
@@ -41,18 +43,18 @@ uv add "chtypes>=2.0.0.dev0" # or: pip install "chtypes>=2.0.0.dev0"
|
|
|
41
43
|
import chtypes
|
|
42
44
|
from chtypes import Format, Registry
|
|
43
45
|
|
|
44
|
-
chtypes.setup(timezone="UTC")
|
|
46
|
+
chtypes.setup(timezone="UTC") # once, before the first open (optional)
|
|
45
47
|
registry = Registry(autofetch=True)
|
|
46
|
-
library = registry.for_version("26.8")
|
|
48
|
+
library = registry.for_version("26.8") # a release line or an exact version
|
|
47
49
|
|
|
48
50
|
ddl = b"CREATE TABLE t (x UInt8, ts DateTime DEFAULT now()) ENGINE = Memory"
|
|
49
51
|
with library.compile_table(ddl) as schema:
|
|
50
52
|
batch = schema.rows(Format.JSON_EACH_ROW, b'{"x":256}\n')
|
|
51
53
|
|
|
52
54
|
row = batch.rows[0]
|
|
53
|
-
print(batch.outcome)
|
|
54
|
-
print(row.values[0].text)
|
|
55
|
-
print(row.transformed[0].reason)
|
|
55
|
+
print(batch.outcome) # accepted
|
|
56
|
+
print(row.values[0].text) # b'0' what would actually be stored
|
|
57
|
+
print(row.transformed[0].reason) # overflow_wrap which is the product
|
|
56
58
|
```
|
|
57
59
|
|
|
58
60
|
The row is **accepted** and `256` is silently stored as `0`. That report — `transformed` — is why chtypes exists. Names, SQL, messages and renderings come back as `bytes`, never decoded for you: a column name that is not valid UTF-8 round-trips exactly.
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
# chtypes — Python SDK
|
|
2
2
|
|
|
3
|
-
> **2.0.0-dev: UNSTABLE, staging only, not for production.** This is the ABI v2 development binding (public issue #511). It speaks ABI v2's unstable description, pins its dev fingerprint and refuses a library with any other ("update your dev SDK"). It fetches only from the staging dev channel (`https://registry-staging.wavehouse.dev/chtypes/v2-dev`) and trusts only the staging key; `CHTYPES_ARTIFACTS_URL`, `CHTYPES_TRUSTED_KEYS` and `CHTYPES_ALLOW_UNSIGNED`, and the `bases`, `trusted_keys` and `allow_unsigned` fetch options, are ignored, each with one warning; `--lock`, `--frozen` and `--update`, and the `frozen`, `lock_path`, `lock_write` and `update` options, are refused, because a dev build is replaceable and a superseded one expires after 14 days. Its cache is `${XDG_CACHE_HOME:-~/.cache}/chtypes/v2-dev`, or `<CHTYPES_CACHE>/v2-dev` under an explicit cache, which no 1.x SDK reads. Every vocabulary keeps a value it does not list as its `unknown(n)` member (`Outcome("x")`, whose `known` is `False`) instead of failing the document. The rules are r1 to r6 of [`docs/reference/abi-v2.md`](../docs/reference/abi-v2.md). For production, use the 1.x package: `pip install "chtypes<2"`.
|
|
3
|
+
> **2.0.0-dev: UNSTABLE, staging only, not for production.** This is the ABI v2 development binding (public issue #511). It speaks ABI v2's unstable description, pins its dev fingerprint and refuses a library with any other ("update your dev SDK"). It fetches only from the staging dev channel (`https://registry-staging.wavehouse.dev/chtypes/v2-dev`) and trusts only the staging key; `CHTYPES_ARTIFACTS_URL`, `CHTYPES_TRUSTED_KEYS` and `CHTYPES_ALLOW_UNSIGNED`, and the `bases`, `trusted_keys` and `allow_unsigned` fetch options, are ignored, each with one warning; `--lock`, `--frozen` and `--update`, and the `frozen`, `lock_path`, `lock_write` and `update` options, are refused, because a dev build is replaceable and a superseded one expires after 14 days. Its cache is `${XDG_CACHE_HOME:-~/.cache}/chtypes/v2-dev`, or `<CHTYPES_CACHE>/v2-dev` under an explicit cache, which no 1.x SDK reads. A dev SDK prefers the newest build of its own fingerprint: it resolves `<tag>--fp-<its fingerprint>` first and the tag only when no such build exists, and its cache lookups ignore another dev SDK's builds in a shared cache, so a newer dev fingerprint never strands it (§3 of [`docs/guides/fetch-v1.md`](../docs/guides/fetch-v1.md)). Every vocabulary keeps a value it does not list as its `unknown(n)` member (`Outcome("x")`, whose `known` is `False`) instead of failing the document. The rules are r1 to r6 of [`docs/reference/abi-v2.md`](../docs/reference/abi-v2.md). For production, use the 1.x package: `pip install "chtypes<2"`.
|
|
4
|
+
>
|
|
5
|
+
> An explicit cache directory gets the `v2-dev` subroot whether it comes from `CHTYPES_CACHE` or from `FetchOptions(cache_dir=...)`: the layout is under `<cache_dir>/v2-dev`, never in the directory itself, so code that inspects or pre-populates a cache looks there; `chtypes.cache_root` and `chtypes.search_dirs` return the resolved paths. `CHTYPES_OFFLINE=1` is the environment twin of `FetchOptions(offline=True)` and `--offline`: the cache only, no request.
|
|
4
6
|
|
|
5
7
|
**If this row were inserted into this table on this ClickHouse version, what would happen?** chtypes answers with ClickHouse's own code: the real C++ type machinery, vendored per release into a native library behind the frozen `chs_*` C ABI and reached here through stdlib `ctypes`. Nothing semantic is reimplemented, so _"what does ClickHouse do with `256` into a `UInt8`?"_ is answered by ClickHouse rather than by a model of it. One peer binding among `{go, python, ts, rust}` — no language is privileged, and all four give one answer.
|
|
6
8
|
|
|
@@ -22,18 +24,18 @@ uv add "chtypes>=2.0.0.dev0" # or: pip install "chtypes>=2.0.0.dev0"
|
|
|
22
24
|
import chtypes
|
|
23
25
|
from chtypes import Format, Registry
|
|
24
26
|
|
|
25
|
-
chtypes.setup(timezone="UTC")
|
|
27
|
+
chtypes.setup(timezone="UTC") # once, before the first open (optional)
|
|
26
28
|
registry = Registry(autofetch=True)
|
|
27
|
-
library = registry.for_version("26.8")
|
|
29
|
+
library = registry.for_version("26.8") # a release line or an exact version
|
|
28
30
|
|
|
29
31
|
ddl = b"CREATE TABLE t (x UInt8, ts DateTime DEFAULT now()) ENGINE = Memory"
|
|
30
32
|
with library.compile_table(ddl) as schema:
|
|
31
33
|
batch = schema.rows(Format.JSON_EACH_ROW, b'{"x":256}\n')
|
|
32
34
|
|
|
33
35
|
row = batch.rows[0]
|
|
34
|
-
print(batch.outcome)
|
|
35
|
-
print(row.values[0].text)
|
|
36
|
-
print(row.transformed[0].reason)
|
|
36
|
+
print(batch.outcome) # accepted
|
|
37
|
+
print(row.values[0].text) # b'0' what would actually be stored
|
|
38
|
+
print(row.transformed[0].reason) # overflow_wrap which is the product
|
|
37
39
|
```
|
|
38
40
|
|
|
39
41
|
The row is **accepted** and `256` is silently stored as `0`. That report — `transformed` — is why chtypes exists. Names, SQL, messages and renderings come back as `bytes`, never decoded for you: a column name that is not valid UTF-8 round-trips exactly.
|
|
@@ -53,13 +53,14 @@ from ._abi2._vocab import (
|
|
|
53
53
|
DocFlags,
|
|
54
54
|
FilterOutcome,
|
|
55
55
|
Format,
|
|
56
|
+
MergeReason,
|
|
56
57
|
Outcome,
|
|
57
58
|
Reason,
|
|
58
59
|
Source,
|
|
59
60
|
Status,
|
|
60
61
|
Verdict,
|
|
61
62
|
)
|
|
62
|
-
from ._input import BytesIn, Settings
|
|
63
|
+
from ._input import BytesIn, ServerProfile, Settings
|
|
63
64
|
from ._setup import setup
|
|
64
65
|
from .errors import (
|
|
65
66
|
CODE_ARTIFACT_CORRUPT,
|
|
@@ -92,9 +93,10 @@ from .errors import (
|
|
|
92
93
|
UnsupportedError,
|
|
93
94
|
UsageError,
|
|
94
95
|
)
|
|
95
|
-
from .library import Block, Filter, Library, Schema, open_unverified
|
|
96
|
-
from .registry import FetchOptions, Registry, Resolved, TrustedKey
|
|
96
|
+
from .library import Block, Filter, Library, Schema, Server, open_unverified
|
|
97
|
+
from .registry import FetchOptions, Registry, Resolved, TrustedKey, cache_root, search_dirs
|
|
97
98
|
from .results import (
|
|
99
|
+
AtMergeEntry,
|
|
98
100
|
BatchResult,
|
|
99
101
|
BuildInfo,
|
|
100
102
|
Capabilities,
|
|
@@ -111,6 +113,8 @@ from .results import (
|
|
|
111
113
|
Header,
|
|
112
114
|
RowResult,
|
|
113
115
|
SchemaDescription,
|
|
116
|
+
SchemaReplicated,
|
|
117
|
+
SchemaServer,
|
|
114
118
|
Span,
|
|
115
119
|
Transform,
|
|
116
120
|
Value,
|
|
@@ -136,6 +140,7 @@ __all__ = [
|
|
|
136
140
|
"ArtifactPinnedError",
|
|
137
141
|
"ArtifactUnpublishedError",
|
|
138
142
|
"ArtifactUntrustedError",
|
|
143
|
+
"AtMergeEntry",
|
|
139
144
|
"BatchResult",
|
|
140
145
|
"Block",
|
|
141
146
|
"BuildInfo",
|
|
@@ -164,6 +169,7 @@ __all__ = [
|
|
|
164
169
|
"Header",
|
|
165
170
|
"InternalError",
|
|
166
171
|
"Library",
|
|
172
|
+
"MergeReason",
|
|
167
173
|
"Outcome",
|
|
168
174
|
"Reason",
|
|
169
175
|
"Registry",
|
|
@@ -172,6 +178,10 @@ __all__ = [
|
|
|
172
178
|
"Schema",
|
|
173
179
|
"SchemaDescription",
|
|
174
180
|
"SchemaError",
|
|
181
|
+
"SchemaReplicated",
|
|
182
|
+
"SchemaServer",
|
|
183
|
+
"Server",
|
|
184
|
+
"ServerProfile",
|
|
175
185
|
"Settings",
|
|
176
186
|
"Source",
|
|
177
187
|
"SourceForbiddenError",
|
|
@@ -186,7 +196,9 @@ __all__ = [
|
|
|
186
196
|
"UsageError",
|
|
187
197
|
"Value",
|
|
188
198
|
"Verdict",
|
|
199
|
+
"cache_root",
|
|
189
200
|
"open_unverified",
|
|
201
|
+
"search_dirs",
|
|
190
202
|
"setup",
|
|
191
203
|
]
|
|
192
204
|
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
[--frozen] [--offline] [--update] [--strict]
|
|
6
6
|
chtypes verify [--cache <dir>] [--strict] re-verify every installed build
|
|
7
7
|
chtypes list [--cache <dir>] [--offline] [--strict]
|
|
8
|
-
chtypes where [--cache <dir>] [--strict] the cache root
|
|
8
|
+
chtypes where [--cache <dir>] [--strict] [--all] the cache root; --all: every search dir
|
|
9
9
|
|
|
10
10
|
A spelling is `26.8`, `26.8.15` or `26.8.15.10`. Exit statuses come from the
|
|
11
11
|
`errors` table of spec/fetch-v1/constants.json (generated into the fetch layer
|
|
@@ -137,7 +137,10 @@ def build_parser() -> argparse.ArgumentParser:
|
|
|
137
137
|
fetch.add_argument(
|
|
138
138
|
"--offline",
|
|
139
139
|
action="store_true",
|
|
140
|
-
help=
|
|
140
|
+
help=(
|
|
141
|
+
"never touch a source: installed and verified, or CHTYPES_ARTIFACT_MISSING "
|
|
142
|
+
"(or set CHTYPES_OFFLINE=1)"
|
|
143
|
+
),
|
|
141
144
|
)
|
|
142
145
|
|
|
143
146
|
verify = sub.add_parser(
|
|
@@ -154,11 +157,22 @@ def build_parser() -> argparse.ArgumentParser:
|
|
|
154
157
|
)
|
|
155
158
|
cache_option(listing)
|
|
156
159
|
listing.add_argument(
|
|
157
|
-
"--offline",
|
|
160
|
+
"--offline",
|
|
161
|
+
action="store_true",
|
|
162
|
+
help="list only what is installed; no network (or set CHTYPES_OFFLINE=1)",
|
|
158
163
|
)
|
|
159
164
|
|
|
160
|
-
where = sub.add_parser(
|
|
165
|
+
where = sub.add_parser(
|
|
166
|
+
"where",
|
|
167
|
+
help="the cache root",
|
|
168
|
+
description="Print the cache root; with --all, every directory searched.",
|
|
169
|
+
)
|
|
161
170
|
cache_option(where)
|
|
171
|
+
where.add_argument(
|
|
172
|
+
"--all",
|
|
173
|
+
action="store_true",
|
|
174
|
+
help="every directory searched, in order, the cache root first (one per line)",
|
|
175
|
+
)
|
|
162
176
|
return parser
|
|
163
177
|
|
|
164
178
|
|
|
@@ -294,7 +308,7 @@ def _cmd_list(args: argparse.Namespace) -> int:
|
|
|
294
308
|
sys.stdout.write(f"installed {r.version} {r.platform} {r.dir}\n")
|
|
295
309
|
sys.stdout.flush()
|
|
296
310
|
_say_notes(options)
|
|
297
|
-
if not
|
|
311
|
+
if not options.resolved_offline():
|
|
298
312
|
for tag in _published_tags(options):
|
|
299
313
|
sys.stdout.write(f"published {tag} support unknown\n")
|
|
300
314
|
sys.stdout.flush()
|
|
@@ -306,6 +320,10 @@ def _cmd_where(args: argparse.Namespace) -> int:
|
|
|
306
320
|
if options.resolved_strict():
|
|
307
321
|
# Strict mode checks the root before naming it.
|
|
308
322
|
probe_roots(search_roots(options.cache_dir, options.system_dirs), strict=True)
|
|
323
|
+
if args.all:
|
|
324
|
+
for directory in search_roots(options.cache_dir, options.system_dirs):
|
|
325
|
+
sys.stdout.write(f"{directory}\n")
|
|
326
|
+
return EXIT_OK
|
|
309
327
|
sys.stdout.write(f"{resolve_cache_root(args.cache)}\n")
|
|
310
328
|
return EXIT_OK
|
|
311
329
|
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
# GENERATED by scripts/abi-v1/gen.py --major 2 from spec/abi-v2/abi.json (CHS_ABI_FINGERPRINT sha256:
|
|
1
|
+
# GENERATED by scripts/abi-v1/gen.py --major 2 from spec/abi-v2/abi.json (CHS_ABI_FINGERPRINT sha256:4129dccc3f4e698dedb0e99cae413f5240085f633c5a1244e2c55bf7a7bb6c3a) — DO NOT EDIT # noqa: E501
|
|
2
2
|
"""The Python binding's generated ABI v2 layer: ctypes signatures, the handle
|
|
3
3
|
classes, the two-phase resolve functions and invoke_by_name(), the test-only
|
|
4
4
|
dispatcher the conformance suite drives from tests/fixtures/abi-v2/cases.json.
|
|
@@ -19,7 +19,7 @@ from dataclasses import dataclass, field
|
|
|
19
19
|
from . import _errmap
|
|
20
20
|
|
|
21
21
|
CHS_ABI_VERSION = 2
|
|
22
|
-
CHS_ABI_FINGERPRINT = "sha256:
|
|
22
|
+
CHS_ABI_FINGERPRINT = "sha256:4129dccc3f4e698dedb0e99cae413f5240085f633c5a1244e2c55bf7a7bb6c3a"
|
|
23
23
|
|
|
24
24
|
# The description's stability: "unstable" while this generation is being
|
|
25
25
|
# designed, so CHS_ABI_FINGERPRINT moves with every change, and "locked" after
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
# GENERATED by scripts/abi-v1/gen.py --major 2 from spec/abi-v2/abi.json (CHS_ABI_FINGERPRINT sha256:
|
|
1
|
+
# GENERATED by scripts/abi-v1/gen.py --major 2 from spec/abi-v2/abi.json (CHS_ABI_FINGERPRINT sha256:4129dccc3f4e698dedb0e99cae413f5240085f633c5a1244e2c55bf7a7bb6c3a) — DO NOT EDIT # noqa: E501
|
|
2
2
|
"""spec/abi-v2/sdk.json's error-mapping tables, as references to the
|
|
3
3
|
public classes (python/src/chtypes/errors.py, re-exported by
|
|
4
4
|
python/src/chtypes/_abi2/_errors.py). Only the MAPPING is generated: the
|
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
# GENERATED by scripts/abi-v1/gen.py --major 2 from spec/abi-v2/abi.json (CHS_ABI_FINGERPRINT sha256:
|
|
1
|
+
# GENERATED by scripts/abi-v1/gen.py --major 2 from spec/abi-v2/abi.json (CHS_ABI_FINGERPRINT sha256:4129dccc3f4e698dedb0e99cae413f5240085f633c5a1244e2c55bf7a7bb6c3a) — DO NOT EDIT # noqa: E501
|
|
2
2
|
"""The Python binding's generated vocabularies, from spec/abi-v2/abi.json: the C
|
|
3
3
|
enums (`Format`, `Status`), the document vocabularies (`Outcome`, `FilterOutcome`,
|
|
4
|
-
`Verdict`, `DefaultKind`, `Reason`, `Source`, `DiscoverQueryParam`) with the
|
|
5
|
-
description attaches to each value (`Format.ch_name`, `Reason` lossy,
|
|
6
|
-
is_stored, `Verdict.answered`), and the `DocFlags` groups. No other file in
|
|
7
|
-
package keeps a copy of any of it.
|
|
4
|
+
`Verdict`, `DefaultKind`, `MergeReason`, `Reason`, `Source`, `DiscoverQueryParam`) with the
|
|
5
|
+
facts the description attaches to each value (`Format.ch_name`, `Reason` lossy,
|
|
6
|
+
`Source` is_stored, `Verdict.answered`), and the `DocFlags` groups. No other file in
|
|
7
|
+
this package keeps a copy of any of it.
|
|
8
8
|
|
|
9
9
|
Rule r3 (spec/abi-v2/docs.md): every vocabulary has an unknown(n) member, and a
|
|
10
10
|
value the description does not list reads as it, carrying the raw value, for that
|
|
@@ -225,6 +225,30 @@ class DefaultKind(StrEnum):
|
|
|
225
225
|
return cls(text)
|
|
226
226
|
|
|
227
227
|
|
|
228
|
+
class MergeReason(StrEnum):
|
|
229
|
+
"""What a merge would do to one row of the part an INSERT writes, as a batch's
|
|
230
|
+
`at_merge` reports it."""
|
|
231
|
+
|
|
232
|
+
TTL_DELETE = "ttl_delete"
|
|
233
|
+
TTL_COLUMN_RESET = "ttl_column_reset"
|
|
234
|
+
|
|
235
|
+
@classmethod
|
|
236
|
+
def _missing_(cls, value: object) -> MergeReason | None:
|
|
237
|
+
return _unknown_str(cls, value)
|
|
238
|
+
|
|
239
|
+
@property
|
|
240
|
+
def known(self) -> bool:
|
|
241
|
+
"""Whether the description lists this value: False for exactly the unknown(n)
|
|
242
|
+
member an unlisted value reads as (rule r3)."""
|
|
243
|
+
return type(self)._value2member_map_.get(self._value_) is self
|
|
244
|
+
|
|
245
|
+
@classmethod
|
|
246
|
+
def of(cls, text: str) -> MergeReason:
|
|
247
|
+
"""Map a document's spelling. One the description does not list is kept as
|
|
248
|
+
this vocabulary's unknown(n) member, carrying it (rule r3)."""
|
|
249
|
+
return cls(text)
|
|
250
|
+
|
|
251
|
+
|
|
228
252
|
class DiscoverQueryParam(StrEnum):
|
|
229
253
|
"""A query parameter of the SQL `discover_query` returns, which the caller binds
|
|
230
254
|
when it runs the query."""
|
|
@@ -383,6 +407,7 @@ DESCRIBED_VOCABULARIES: Final[dict[str, type]] = {
|
|
|
383
407
|
"chs_status": Status,
|
|
384
408
|
"chs_format": Format,
|
|
385
409
|
"transform_reason": Reason,
|
|
410
|
+
"merge_reason": MergeReason,
|
|
386
411
|
"value_src": Source,
|
|
387
412
|
"row_outcome": Outcome,
|
|
388
413
|
"batch_outcome": Outcome,
|
|
@@ -28,9 +28,10 @@ import json
|
|
|
28
28
|
from collections.abc import Callable, Mapping
|
|
29
29
|
from typing import Any
|
|
30
30
|
|
|
31
|
-
from ._abi2._vocab import DefaultKind, FilterOutcome, Outcome, Reason, Source, Verdict
|
|
31
|
+
from ._abi2._vocab import DefaultKind, FilterOutcome, MergeReason, Outcome, Reason, Source, Verdict
|
|
32
32
|
from .errors import ArtifactCorruptError, internal
|
|
33
33
|
from .results import (
|
|
34
|
+
AtMergeEntry,
|
|
34
35
|
BatchResult,
|
|
35
36
|
BuildInfo,
|
|
36
37
|
Capabilities,
|
|
@@ -47,6 +48,8 @@ from .results import (
|
|
|
47
48
|
Header,
|
|
48
49
|
RowResult,
|
|
49
50
|
SchemaDescription,
|
|
51
|
+
SchemaReplicated,
|
|
52
|
+
SchemaServer,
|
|
50
53
|
Span,
|
|
51
54
|
Transform,
|
|
52
55
|
Value,
|
|
@@ -323,6 +326,29 @@ def _engine_cell(obj: dict[str, Any], where: str) -> EngineCell:
|
|
|
323
326
|
)
|
|
324
327
|
|
|
325
328
|
|
|
329
|
+
def _int_list(obj: Mapping[str, Any], key: str, where: str) -> tuple[int, ...] | None:
|
|
330
|
+
"""A list of non-negative integers: None when absent."""
|
|
331
|
+
if obj.get(key) is None:
|
|
332
|
+
return None
|
|
333
|
+
out = []
|
|
334
|
+
for item in _list(obj, key, where):
|
|
335
|
+
if isinstance(item, bool) or not isinstance(item, int) or item < 0:
|
|
336
|
+
raise _wrong(where, key, "an array of non-negative integers", item)
|
|
337
|
+
out.append(item)
|
|
338
|
+
return tuple(out)
|
|
339
|
+
|
|
340
|
+
|
|
341
|
+
def _at_merge(obj: dict[str, Any], where: str) -> AtMergeEntry:
|
|
342
|
+
# An unlisted reason is its unknown(n): kept, never a failure (rule r3).
|
|
343
|
+
return AtMergeEntry(
|
|
344
|
+
row=_int(obj, "row", where),
|
|
345
|
+
reason=MergeReason.of(_text(obj, "reason", where)),
|
|
346
|
+
column=_bytes_or_none(obj, "column", where),
|
|
347
|
+
stored=_bytes_or_none(obj, "stored", where),
|
|
348
|
+
input_rows=_int_list(obj, "input_rows", where),
|
|
349
|
+
)
|
|
350
|
+
|
|
351
|
+
|
|
326
352
|
def _engine_rows(obj: Mapping[str, Any], where: str) -> tuple[tuple[EngineCell, ...], ...]:
|
|
327
353
|
rows = []
|
|
328
354
|
for i, row in enumerate(_list(obj, "engine_rows", where)):
|
|
@@ -409,6 +435,8 @@ def decode_batch(raw: bytes, export: bytes | None) -> BatchResult:
|
|
|
409
435
|
framing=None
|
|
410
436
|
if framing is None
|
|
411
437
|
else _framing(_obj(framing, where, "framing"), f"{where}.framing"),
|
|
438
|
+
unsupported_settings=_name_list(obj, "unsupported_settings", where),
|
|
439
|
+
at_merge=_objects(obj, "at_merge", where, _at_merge),
|
|
412
440
|
)
|
|
413
441
|
|
|
414
442
|
|
|
@@ -449,10 +477,55 @@ def _column(obj: dict[str, Any], where: str) -> Column:
|
|
|
449
477
|
)
|
|
450
478
|
|
|
451
479
|
|
|
480
|
+
def _string_map(obj: Mapping[str, Any], key: str, where: str) -> dict[str, str] | None:
|
|
481
|
+
"""An object of plain JSON strings (a server's settings or macros, the
|
|
482
|
+
caller's own values given back): None when absent, a dict, even an empty
|
|
483
|
+
one, when present, so absent and `{}` stay apart."""
|
|
484
|
+
value = obj.get(key)
|
|
485
|
+
if value is None:
|
|
486
|
+
return None
|
|
487
|
+
inner = _obj(value, where, key)
|
|
488
|
+
out: dict[str, str] = {}
|
|
489
|
+
for name, item in inner.items():
|
|
490
|
+
if not isinstance(item, str):
|
|
491
|
+
raise _wrong(f"{where}.{key}", name, "a string", item)
|
|
492
|
+
out[name] = item
|
|
493
|
+
return out
|
|
494
|
+
|
|
495
|
+
|
|
496
|
+
def _server(obj: dict[str, Any], where: str) -> SchemaServer:
|
|
497
|
+
return SchemaServer(
|
|
498
|
+
timezone=_text(obj, "timezone", where),
|
|
499
|
+
settings=_string_map(obj, "settings", where) or {},
|
|
500
|
+
macros=_string_map(obj, "macros", where),
|
|
501
|
+
)
|
|
502
|
+
|
|
503
|
+
|
|
504
|
+
def _replicated(obj: dict[str, Any], where: str) -> SchemaReplicated:
|
|
505
|
+
return SchemaReplicated(
|
|
506
|
+
zookeeper_path=_bytes(obj, "zookeeper_path", where),
|
|
507
|
+
replica_name=_bytes(obj, "replica_name", where),
|
|
508
|
+
)
|
|
509
|
+
|
|
510
|
+
|
|
452
511
|
def decode_schema_description(raw: bytes) -> SchemaDescription:
|
|
453
512
|
where = "schema_description"
|
|
454
513
|
obj = _obj(strict_loads(raw, where), where)
|
|
455
|
-
|
|
514
|
+
# Both are absent on a schema compiled without a server, so that document
|
|
515
|
+
# decodes exactly as it did before servers existed.
|
|
516
|
+
server = obj.get("server")
|
|
517
|
+
replicated = obj.get("replicated")
|
|
518
|
+
return SchemaDescription(
|
|
519
|
+
columns=_objects(obj, "columns", where, _column),
|
|
520
|
+
server=None
|
|
521
|
+
if server is None
|
|
522
|
+
else _server(_obj(server, where, "server"), f"{where}.server"),
|
|
523
|
+
replicated=(
|
|
524
|
+
None
|
|
525
|
+
if replicated is None
|
|
526
|
+
else _replicated(_obj(replicated, where, "replicated"), f"{where}.replicated")
|
|
527
|
+
),
|
|
528
|
+
)
|
|
456
529
|
|
|
457
530
|
|
|
458
531
|
def _discovered(obj: dict[str, Any], where: str) -> DiscoveredColumn:
|
|
@@ -12,10 +12,11 @@ from __future__ import annotations
|
|
|
12
12
|
import base64
|
|
13
13
|
import json
|
|
14
14
|
from collections.abc import Mapping, Sequence
|
|
15
|
+
from dataclasses import dataclass
|
|
15
16
|
|
|
16
17
|
from .errors import misuse
|
|
17
18
|
|
|
18
|
-
__all__ = ["BytesIn", "Settings"]
|
|
19
|
+
__all__ = ["BytesIn", "ServerProfile", "Settings"]
|
|
19
20
|
|
|
20
21
|
BytesIn = bytes | str
|
|
21
22
|
Settings = Mapping[str, str]
|
|
@@ -107,3 +108,43 @@ def columns_json(columns: Sequence[BytesIn] | None) -> bytes | None:
|
|
|
107
108
|
except UnicodeDecodeError:
|
|
108
109
|
names.append({"name_b64": base64.b64encode(raw).decode("ascii")})
|
|
109
110
|
return _dump(names)
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
@dataclass(frozen=True, slots=True)
|
|
114
|
+
class ServerProfile:
|
|
115
|
+
"""One ClickHouse server, as the library's `input:server_profile` describes
|
|
116
|
+
it. Every field is optional and passed through as given: the library
|
|
117
|
+
validates the zone with DateLUT, each setting with the server's own SET
|
|
118
|
+
check, and the macros with ClickHouse's own reader. The binding validates
|
|
119
|
+
nothing in it.
|
|
120
|
+
|
|
121
|
+
`timezone` is the server's zone, the one its `timezone()` returns; None or
|
|
122
|
+
"" leaves it undescribed (omitted) and the image zone applies. `settings` is
|
|
123
|
+
what the server's profile applies to every query, layered under a schema's
|
|
124
|
+
and a call's own; None omits it, and `{}` is sent as `{}`. `macros` is the
|
|
125
|
+
server's `<macros>`: None omits it, and the server's macros are UNKNOWN (a
|
|
126
|
+
schema whose engine reads one is declined); a mapping, even an empty one, is
|
|
127
|
+
sent (`{}` when empty) and is the server's COMPLETE set."""
|
|
128
|
+
|
|
129
|
+
timezone: str | None = None
|
|
130
|
+
settings: Settings | None = None
|
|
131
|
+
macros: Settings | None = None
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
def server_profile_json(profile: ServerProfile) -> bytes:
|
|
135
|
+
"""The profile document: an empty or absent zone and an absent map are
|
|
136
|
+
omitted; a present map is sent as given, `{}` when empty. Absent `macros` is
|
|
137
|
+
not empty `macros`."""
|
|
138
|
+
if not isinstance(profile, ServerProfile):
|
|
139
|
+
raise TypeError(f"profile must be a ServerProfile, not {type(profile).__name__}")
|
|
140
|
+
doc: dict[str, object] = {}
|
|
141
|
+
if profile.timezone is not None:
|
|
142
|
+
if not isinstance(profile.timezone, str):
|
|
143
|
+
raise TypeError(f"timezone must be str, not {type(profile.timezone).__name__}")
|
|
144
|
+
if profile.timezone != "":
|
|
145
|
+
doc["timezone"] = profile.timezone
|
|
146
|
+
if profile.settings is not None:
|
|
147
|
+
doc["settings"] = _string_map(profile.settings, "settings")
|
|
148
|
+
if profile.macros is not None:
|
|
149
|
+
doc["macros"] = _string_map(profile.macros, "macros")
|
|
150
|
+
return _dump(doc)
|
|
@@ -18,6 +18,19 @@ five ways:
|
|
|
18
18
|
used through its subroot `<cache>/v2-dev`, never as a whole layout;
|
|
19
19
|
- a signed predicate must say abi 2.
|
|
20
20
|
|
|
21
|
+
It also resolves one tag the v1 contract never asks for: a version request
|
|
22
|
+
fetches `<tag>--fp-<its own fingerprint>` first, the newest dev build of the ABI
|
|
23
|
+
this binding speaks, and falls back to `<tag>` only when no base has that alias
|
|
24
|
+
(docs/guides/fetch-v1.md §3, "The dev channel's alias step"), so a dev build of a
|
|
25
|
+
newer fingerprint never strands this one. Its cache lookups (`resolve_installed`,
|
|
26
|
+
and `ensure`'s offline answer and its monotonic rule) see only the records whose
|
|
27
|
+
signed `abi_fingerprint` is its own: a build of another fingerprint in a shared
|
|
28
|
+
cache is never returned and never kept over this one's own, and it is never
|
|
29
|
+
removed or rewritten either. It is automatic, with no override; the trust checks
|
|
30
|
+
are unchanged, and a listing never shows an alias. The fingerprint
|
|
31
|
+
is the generated `CHS_ABI_FINGERPRINT` of the ABI layer (`chtypes._abi2._decls`),
|
|
32
|
+
never a hand-written copy.
|
|
33
|
+
|
|
21
34
|
The v1 contract itself stays in this package, unchanged, because the fetch-v1
|
|
22
35
|
conformance cases (tests/fixtures/fetch-v1) are its specification and the dev
|
|
23
36
|
channel shares every other rule with it. Only a test run reaches it:
|
|
@@ -29,16 +42,19 @@ override a user can reach.
|
|
|
29
42
|
from __future__ import annotations
|
|
30
43
|
|
|
31
44
|
import os
|
|
45
|
+
import re
|
|
32
46
|
import sys
|
|
33
47
|
import threading
|
|
34
48
|
from collections.abc import Callable, Sequence
|
|
35
49
|
from dataclasses import dataclass, replace
|
|
36
50
|
from typing import Final, Protocol
|
|
37
51
|
|
|
52
|
+
from chtypes._abi2._decls import CHS_ABI_FINGERPRINT
|
|
38
53
|
from chtypes._ocifetch import _constants as C
|
|
39
54
|
from chtypes._ocifetch._dsse import TrustedKey
|
|
40
55
|
|
|
41
56
|
__all__ = [
|
|
57
|
+
"ALIAS_SEPARATOR",
|
|
42
58
|
"DEV_ABI_GENERATION",
|
|
43
59
|
"DEV_CACHE_DIR",
|
|
44
60
|
"DEV_CHANNEL_BASE",
|
|
@@ -50,12 +66,15 @@ __all__ = [
|
|
|
50
66
|
"PinningRefusedError",
|
|
51
67
|
"abi",
|
|
52
68
|
"active",
|
|
69
|
+
"alias_tag",
|
|
53
70
|
"allow_overrides_for_tests",
|
|
54
71
|
"channel_name",
|
|
55
72
|
"enforce",
|
|
56
73
|
"ignored_settings_warned",
|
|
57
74
|
"pinning_requested",
|
|
58
75
|
"trusted_keys",
|
|
76
|
+
"use_own_fingerprint_for_tests",
|
|
77
|
+
"visible",
|
|
59
78
|
"use_dev_channel_for_tests",
|
|
60
79
|
"use_fetch_v1_for_tests",
|
|
61
80
|
]
|
|
@@ -69,12 +88,20 @@ DEV_KEY_ID: Final = "824345f9bcf8e5bf"
|
|
|
69
88
|
DEV_KEY_HEX: Final = "5cd30c53c65a1ebc2d85836a41deb06661bb0ae7b658adb9eb116ec2db8e9b1c"
|
|
70
89
|
# The dev channel's cache directory name: the default root's last element and
|
|
71
90
|
# an explicit cache's subroot (rule r5).
|
|
91
|
+
# The environment twin of the offline option (public issue #528): CHTYPES_OFFLINE=1
|
|
92
|
+
# reads the cache only and makes no request. It names no source, so rule r6 holds.
|
|
93
|
+
# Not a generated constant: the generated set is the v1 fetch contract's.
|
|
94
|
+
ENV_OFFLINE_NAME: Final = "CHTYPES_OFFLINE"
|
|
72
95
|
DEV_CACHE_DIR: Final = "v2-dev"
|
|
73
96
|
# The verified.json schema the dev channel writes, and the only one it reads (rule r5).
|
|
74
97
|
DEV_RECORD_SCHEMA: Final = 2
|
|
75
98
|
# The abi a dev predicate must carry.
|
|
76
99
|
DEV_ABI_GENERATION: Final = 2
|
|
77
100
|
|
|
101
|
+
# Joins a tag and a fingerprint into the dev channel's alias tag:
|
|
102
|
+
# <tag>--fp-<64 lowercase hex> (docs/guides/fetch-v1.md §3).
|
|
103
|
+
ALIAS_SEPARATOR: Final = "--fp-"
|
|
104
|
+
|
|
78
105
|
# What every lock, frozen or update request gets from a 2.0.0-dev SDK, before
|
|
79
106
|
# any network call (rule r6).
|
|
80
107
|
PINNING_REFUSED: Final = (
|
|
@@ -98,6 +125,11 @@ class Channel:
|
|
|
98
125
|
keys: tuple[dict[str, str], ...] # {"keyid", "ed25519_hex"}: the default trust
|
|
99
126
|
overridable: bool # the base, trust and unsigned overrides are honored
|
|
100
127
|
pinnable: bool # lock, frozen and update are honored
|
|
128
|
+
# The fingerprint, 64 lowercase hex, this contract's SDK speaks: a version
|
|
129
|
+
# request resolves its alias tag before the tag itself, and the cache
|
|
130
|
+
# lookups see only records signed with it. "" does neither (the v1 contract
|
|
131
|
+
# and every production channel).
|
|
132
|
+
own_fingerprint: str = ""
|
|
101
133
|
|
|
102
134
|
|
|
103
135
|
# What a 2.0.0-dev SDK speaks, and what every process speaks unless a test
|
|
@@ -113,6 +145,7 @@ DEV_CHANNEL: Final = Channel(
|
|
|
113
145
|
keys=({"keyid": DEV_KEY_ID, "ed25519_hex": DEV_KEY_HEX},),
|
|
114
146
|
overridable=False,
|
|
115
147
|
pinnable=False,
|
|
148
|
+
own_fingerprint=CHS_ABI_FINGERPRINT.removeprefix("sha256:"),
|
|
116
149
|
)
|
|
117
150
|
|
|
118
151
|
# The v1 contract the fetch-v1 conformance cases specify, from the generated
|
|
@@ -198,6 +231,49 @@ def allow_overrides_for_tests() -> Callable[[], None]:
|
|
|
198
231
|
return _use(replace(DEV_CHANNEL, overridable=True))
|
|
199
232
|
|
|
200
233
|
|
|
234
|
+
_FINGERPRINT_HEX = re.compile(r"[0-9a-f]{64}")
|
|
235
|
+
|
|
236
|
+
|
|
237
|
+
def use_own_fingerprint_for_tests(fingerprint: str) -> Callable[[], None]:
|
|
238
|
+
"""Make this TEST run's active contract speak `fingerprint` (64 lowercase
|
|
239
|
+
hex) as the dev channel speaks its own: a version request resolves that
|
|
240
|
+
fingerprint's alias tag first, and the cache lookups see only records
|
|
241
|
+
signed with it. The fetch-v1 conformance cases that carry
|
|
242
|
+
`request.own_fingerprint` run under it, against fixtures that name a
|
|
243
|
+
fixture fingerprint. Everything else stays the active contract's. Returns
|
|
244
|
+
the restore function; raises outside a test run or on a fingerprint that is
|
|
245
|
+
not 64 lowercase hex."""
|
|
246
|
+
_test_only("use_own_fingerprint_for_tests")
|
|
247
|
+
if not _FINGERPRINT_HEX.fullmatch(fingerprint):
|
|
248
|
+
raise ValueError(f"use_own_fingerprint_for_tests: {fingerprint!r} is not 64 lowercase hex")
|
|
249
|
+
return _use(replace(active(), own_fingerprint=fingerprint))
|
|
250
|
+
|
|
251
|
+
|
|
252
|
+
def alias_tag(tag: str, channel: Channel | None = None) -> str | None:
|
|
253
|
+
"""The alias a version request for `tag` resolves first under a contract
|
|
254
|
+
with an own fingerprint, or None when it resolves `tag` alone: under the
|
|
255
|
+
v1 contract and every production channel, and for a request that is not a
|
|
256
|
+
version spelling (an arbitrary tag has no alias)."""
|
|
257
|
+
channel = channel or active()
|
|
258
|
+
if not channel.own_fingerprint or not re.fullmatch(C.SPELLING_REGEX, tag):
|
|
259
|
+
return None
|
|
260
|
+
return f"{tag}{ALIAS_SEPARATOR}{channel.own_fingerprint}"
|
|
261
|
+
|
|
262
|
+
|
|
263
|
+
def visible(predicate: object, channel: Channel | None = None) -> bool:
|
|
264
|
+
"""Whether a cache record (or a pre-seeded entry) whose SIGNED predicate is
|
|
265
|
+
`predicate` may answer a request under the active contract: under one with
|
|
266
|
+
an own fingerprint (the dev channel), only when the predicate's
|
|
267
|
+
`abi_fingerprint` is that fingerprint; under every other, always. A record
|
|
268
|
+
it cannot see is never returned and never kept by the monotonic rule, and
|
|
269
|
+
nothing removes or rewrites it: another SDK of another fingerprint owns it."""
|
|
270
|
+
channel = channel or active()
|
|
271
|
+
if not channel.own_fingerprint:
|
|
272
|
+
return True
|
|
273
|
+
fingerprint = predicate.get("abi_fingerprint") if isinstance(predicate, dict) else None
|
|
274
|
+
return fingerprint == f"sha256:{channel.own_fingerprint}"
|
|
275
|
+
|
|
276
|
+
|
|
201
277
|
def use_dev_channel_for_tests() -> Callable[[], None]:
|
|
202
278
|
"""Make this TEST run speak the dev channel exactly as a user's process does
|
|
203
279
|
(undoing either function above, for a test of the dev channel itself).
|