chtypes 2.0.0.dev6__tar.gz → 2.0.0.dev8__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.dev6 → chtypes-2.0.0.dev8}/CHANGELOG.md +24 -0
- {chtypes-2.0.0.dev6 → chtypes-2.0.0.dev8}/PKG-INFO +2 -2
- {chtypes-2.0.0.dev6 → chtypes-2.0.0.dev8}/README.md +1 -1
- {chtypes-2.0.0.dev6 → chtypes-2.0.0.dev8}/pyproject.toml +1 -1
- {chtypes-2.0.0.dev6 → chtypes-2.0.0.dev8}/src/chtypes/__init__.py +1 -2
- {chtypes-2.0.0.dev6 → chtypes-2.0.0.dev8}/src/chtypes/__main__.py +115 -10
- {chtypes-2.0.0.dev6 → chtypes-2.0.0.dev8}/src/chtypes/_abi2/_errors.py +1 -1
- chtypes-2.0.0.dev8/src/chtypes/_codes.py +23 -0
- {chtypes-2.0.0.dev6 → chtypes-2.0.0.dev8}/src/chtypes/_decode.py +9 -2
- {chtypes-2.0.0.dev6 → chtypes-2.0.0.dev8}/src/chtypes/_guard.py +1 -1
- {chtypes-2.0.0.dev6 → chtypes-2.0.0.dev8}/src/chtypes/_input.py +1 -1
- {chtypes-2.0.0.dev6 → chtypes-2.0.0.dev8}/src/chtypes/_ocifetch/__init__.py +12 -1
- {chtypes-2.0.0.dev6 → chtypes-2.0.0.dev8}/src/chtypes/_ocifetch/_dsse.py +12 -2
- {chtypes-2.0.0.dev6 → chtypes-2.0.0.dev8}/src/chtypes/_ocifetch/_ensure.py +89 -12
- {chtypes-2.0.0.dev6 → chtypes-2.0.0.dev8}/src/chtypes/_ocifetch/_errors.py +13 -11
- chtypes-2.0.0.dev8/src/chtypes/_ocifetch/_hold.py +133 -0
- chtypes-2.0.0.dev8/src/chtypes/_ocifetch/_prune.py +282 -0
- chtypes-2.0.0.dev8/src/chtypes/_ocifetch/_resolve.py +309 -0
- {chtypes-2.0.0.dev6 → chtypes-2.0.0.dev8}/src/chtypes/_setup.py +1 -1
- {chtypes-2.0.0.dev6 → chtypes-2.0.0.dev8}/src/chtypes/errors.py +20 -34
- {chtypes-2.0.0.dev6 → chtypes-2.0.0.dev8}/src/chtypes/registry.py +132 -15
- {chtypes-2.0.0.dev6 → chtypes-2.0.0.dev8}/tests/abi2/test_public_registry.py +1 -1
- {chtypes-2.0.0.dev6 → chtypes-2.0.0.dev8}/tests/abi2/test_reader_rules.py +4 -0
- chtypes-2.0.0.dev8/tests/abi2/test_registry_fetch.py +157 -0
- {chtypes-2.0.0.dev6 → chtypes-2.0.0.dev8}/tests/abi2/test_registry_flight.py +3 -3
- {chtypes-2.0.0.dev6 → chtypes-2.0.0.dev8}/tests/api/test_decode.py +21 -0
- chtypes-2.0.0.dev8/tests/api/test_trusted_keys.py +49 -0
- {chtypes-2.0.0.dev6 → chtypes-2.0.0.dev8}/tests/api/test_vocab.py +4 -2
- {chtypes-2.0.0.dev6 → chtypes-2.0.0.dev8}/tests/cli/test_main.py +2 -2
- chtypes-2.0.0.dev8/tests/cli/test_resolve_prune.py +125 -0
- {chtypes-2.0.0.dev6 → chtypes-2.0.0.dev8}/tests/ocifetch/test_conformance.py +76 -1
- {chtypes-2.0.0.dev6 → chtypes-2.0.0.dev8}/tests/ocifetch/test_ensure.py +23 -0
- {chtypes-2.0.0.dev6 → chtypes-2.0.0.dev8}/tests/test_devchannel_registry.py +1 -1
- chtypes-2.0.0.dev8/tests/test_support_v2_gen.py +81 -0
- {chtypes-2.0.0.dev6 → chtypes-2.0.0.dev8}/uv.lock +1 -1
- {chtypes-2.0.0.dev6 → chtypes-2.0.0.dev8}/.gitignore +0 -0
- {chtypes-2.0.0.dev6 → chtypes-2.0.0.dev8}/LICENSE +0 -0
- {chtypes-2.0.0.dev6 → chtypes-2.0.0.dev8}/src/chtypes/_abi2/__init__.py +0 -0
- {chtypes-2.0.0.dev6 → chtypes-2.0.0.dev8}/src/chtypes/_abi2/_decls.py +0 -0
- {chtypes-2.0.0.dev6 → chtypes-2.0.0.dev8}/src/chtypes/_abi2/_errmap.py +0 -0
- {chtypes-2.0.0.dev6 → chtypes-2.0.0.dev8}/src/chtypes/_abi2/_loader.py +0 -0
- {chtypes-2.0.0.dev6 → chtypes-2.0.0.dev8}/src/chtypes/_abi2/_vocab.py +0 -0
- {chtypes-2.0.0.dev6 → chtypes-2.0.0.dev8}/src/chtypes/_ed25519.py +0 -0
- {chtypes-2.0.0.dev6 → chtypes-2.0.0.dev8}/src/chtypes/_ocifetch/_channel.py +0 -0
- {chtypes-2.0.0.dev6 → chtypes-2.0.0.dev8}/src/chtypes/_ocifetch/_constants.py +0 -0
- {chtypes-2.0.0.dev6 → chtypes-2.0.0.dev8}/src/chtypes/_ocifetch/_faults.py +0 -0
- {chtypes-2.0.0.dev6 → chtypes-2.0.0.dev8}/src/chtypes/_ocifetch/_goldens.py +0 -0
- {chtypes-2.0.0.dev6 → chtypes-2.0.0.dev8}/src/chtypes/_ocifetch/_http.py +0 -0
- {chtypes-2.0.0.dev6 → chtypes-2.0.0.dev8}/src/chtypes/_ocifetch/_layout.py +0 -0
- {chtypes-2.0.0.dev6 → chtypes-2.0.0.dev8}/src/chtypes/_ocifetch/_lock.py +0 -0
- {chtypes-2.0.0.dev6 → chtypes-2.0.0.dev8}/src/chtypes/_ocifetch/_oci.py +0 -0
- {chtypes-2.0.0.dev6 → chtypes-2.0.0.dev8}/src/chtypes/_ocifetch/_referrers.py +0 -0
- {chtypes-2.0.0.dev6 → chtypes-2.0.0.dev8}/src/chtypes/_ocifetch/_retired.py +0 -0
- {chtypes-2.0.0.dev6 → chtypes-2.0.0.dev8}/src/chtypes/_ocifetch/_unpack.py +0 -0
- {chtypes-2.0.0.dev6 → chtypes-2.0.0.dev8}/src/chtypes/library.py +0 -0
- {chtypes-2.0.0.dev6 → chtypes-2.0.0.dev8}/src/chtypes/py.typed +0 -0
- {chtypes-2.0.0.dev6 → chtypes-2.0.0.dev8}/src/chtypes/results.py +0 -0
- {chtypes-2.0.0.dev6 → chtypes-2.0.0.dev8}/tests/abi2/__init__.py +0 -0
- {chtypes-2.0.0.dev6 → chtypes-2.0.0.dev8}/tests/abi2/_stub_tree.py +0 -0
- {chtypes-2.0.0.dev6 → chtypes-2.0.0.dev8}/tests/abi2/conftest.py +0 -0
- {chtypes-2.0.0.dev6 → chtypes-2.0.0.dev8}/tests/abi2/test_conformance.py +0 -0
- {chtypes-2.0.0.dev6 → chtypes-2.0.0.dev8}/tests/abi2/test_dev_fingerprint.py +0 -0
- {chtypes-2.0.0.dev6 → chtypes-2.0.0.dev8}/tests/abi2/test_goldens_runner_stub.py +0 -0
- {chtypes-2.0.0.dev6 → chtypes-2.0.0.dev8}/tests/abi2/test_loader.py +0 -0
- {chtypes-2.0.0.dev6 → chtypes-2.0.0.dev8}/tests/abi2/test_public_library.py +0 -0
- {chtypes-2.0.0.dev6 → chtypes-2.0.0.dev8}/tests/abi2/test_public_open.py +0 -0
- {chtypes-2.0.0.dev6 → chtypes-2.0.0.dev8}/tests/abi2/test_public_server.py +0 -0
- {chtypes-2.0.0.dev6 → chtypes-2.0.0.dev8}/tests/abi2/test_public_setup_cases.py +0 -0
- {chtypes-2.0.0.dev6 → chtypes-2.0.0.dev8}/tests/api/__init__.py +0 -0
- {chtypes-2.0.0.dev6 → chtypes-2.0.0.dev8}/tests/api/test_input.py +0 -0
- {chtypes-2.0.0.dev6 → chtypes-2.0.0.dev8}/tests/cli/__init__.py +0 -0
- {chtypes-2.0.0.dev6 → chtypes-2.0.0.dev8}/tests/cli/conftest.py +0 -0
- {chtypes-2.0.0.dev6 → chtypes-2.0.0.dev8}/tests/cli/test_devchannel.py +0 -0
- {chtypes-2.0.0.dev6 → chtypes-2.0.0.dev8}/tests/conftest.py +0 -0
- {chtypes-2.0.0.dev6 → chtypes-2.0.0.dev8}/tests/goldens_v1/__init__.py +0 -0
- {chtypes-2.0.0.dev6 → chtypes-2.0.0.dev8}/tests/goldens_v1/data/stub-goldens.json +0 -0
- {chtypes-2.0.0.dev6 → chtypes-2.0.0.dev8}/tests/goldens_v1/test_runner.py +0 -0
- {chtypes-2.0.0.dev6 → chtypes-2.0.0.dev8}/tests/ocifetch/__init__.py +0 -0
- {chtypes-2.0.0.dev6 → chtypes-2.0.0.dev8}/tests/ocifetch/_bundle_support.py +0 -0
- {chtypes-2.0.0.dev6 → chtypes-2.0.0.dev8}/tests/ocifetch/_registry_support.py +0 -0
- {chtypes-2.0.0.dev6 → chtypes-2.0.0.dev8}/tests/ocifetch/_sign_support.py +0 -0
- {chtypes-2.0.0.dev6 → chtypes-2.0.0.dev8}/tests/ocifetch/conftest.py +0 -0
- {chtypes-2.0.0.dev6 → chtypes-2.0.0.dev8}/tests/ocifetch/test_cache_roots.py +0 -0
- {chtypes-2.0.0.dev6 → chtypes-2.0.0.dev8}/tests/ocifetch/test_cache_strict.py +0 -0
- {chtypes-2.0.0.dev6 → chtypes-2.0.0.dev8}/tests/ocifetch/test_devchannel.py +0 -0
- {chtypes-2.0.0.dev6 → chtypes-2.0.0.dev8}/tests/ocifetch/test_dsse.py +0 -0
- {chtypes-2.0.0.dev6 → chtypes-2.0.0.dev8}/tests/ocifetch/test_goldens.py +0 -0
- {chtypes-2.0.0.dev6 → chtypes-2.0.0.dev8}/tests/ocifetch/test_http.py +0 -0
- {chtypes-2.0.0.dev6 → chtypes-2.0.0.dev8}/tests/ocifetch/test_layout.py +0 -0
- {chtypes-2.0.0.dev6 → chtypes-2.0.0.dev8}/tests/ocifetch/test_lock.py +0 -0
- {chtypes-2.0.0.dev6 → chtypes-2.0.0.dev8}/tests/ocifetch/test_oci.py +0 -0
- {chtypes-2.0.0.dev6 → chtypes-2.0.0.dev8}/tests/ocifetch/test_prodv2_smoke.py +0 -0
- {chtypes-2.0.0.dev6 → chtypes-2.0.0.dev8}/tests/ocifetch/test_prodv2channel.py +0 -0
- {chtypes-2.0.0.dev6 → chtypes-2.0.0.dev8}/tests/ocifetch/test_retired.py +0 -0
- {chtypes-2.0.0.dev6 → chtypes-2.0.0.dev8}/tests/ocifetch/test_seam_gate.py +0 -0
- {chtypes-2.0.0.dev6 → chtypes-2.0.0.dev8}/tests/ocifetch/test_unpack.py +0 -0
- {chtypes-2.0.0.dev6 → chtypes-2.0.0.dev8}/tests/test_abi_identity.py +0 -0
- {chtypes-2.0.0.dev6 → chtypes-2.0.0.dev8}/tests/test_cache_roots_api.py +0 -0
- {chtypes-2.0.0.dev6 → chtypes-2.0.0.dev8}/tests/test_ed25519.py +0 -0
- {chtypes-2.0.0.dev6 → chtypes-2.0.0.dev8}/tests/test_examples_tour.py +0 -0
- {chtypes-2.0.0.dev6 → chtypes-2.0.0.dev8}/tests/test_fetch_options.py +0 -0
|
@@ -8,6 +8,9 @@ The four bindings in this repository are released together and give one answer,
|
|
|
8
8
|
|
|
9
9
|
### Added
|
|
10
10
|
|
|
11
|
+
- A fetch-only call (#492): `Registry.fetch(request) -> Resolved` resolves, fetches, verifies and installs the build a request names into the cache, as `chtypes fetch` does, and opens nothing: no library is loaded and the process setup is untouched, so it needs no `setup`. It honors the registry's fetch options (offline, the cache, and under the production channel the bases, the trust and the lock), and a concurrent `fetch` and open of one request share one fetch. The `Resolved` names the installed version, build, digests and library path; errors are the fetch codes' classes.
|
|
12
|
+
- `chtypes resolve <spelling> [--json]` (#493): what a line or version resolves to on every platform the registry offers, one `resolved <version> <platform> <build> <manifest>` line each, or with `--json` one JSON array of `{platform, version, build, manifest}` objects, each signed statement verified exactly as a fetch verifies it and no layer downloaded; `--offline` answers from the cache. The four CLIs print the same bytes, so one parser reads them (a CI cache key, for example).
|
|
13
|
+
- `chtypes prune [--line <line>] [--keep <n>] [--dry-run]` (#494): removes the installed builds of each line and platform that at least n newer installed builds supersede (n defaults to 1), each with its record, its blobs and its `index.json` entry; `--dry-run` prints exactly what would go. It never removes a build a running process holds: a registry now holds every build it opens or fetches, with a shared `flock` on the entry's `verified.json` until the process exits, and prune removes an entry only after taking the exclusive lock without waiting, reporting it `in-use` otherwise. It never touches a system directory, nor, on the dev channel, another fingerprint's builds. The rules are `docs/guides/fetch-v1.md` sections 1 and 3, and the `resolve-build-*` and `prune-*` conformance cases hold all four bindings to them.
|
|
11
14
|
- Every settings layer's filter-declined settings, in one list (ABI v2; moves the dev fingerprint; #588): `SchemaDescription.filter_declined_settings`, a tuple of `DeclinedSetting`, decoded from `schema_description`'s new required top-level `filter_declined_settings`, over every layer known at schema compile (`setup`'s defaults as the compile captured them, the server profile, and the schema's own settings). `DeclinedSetting` gains `layer`, the highest layer that sets the name, from the new `DeclinedLayer` vocabulary (`DEFAULTS`, `SERVER`, `SCHEMA`, and its unknown(n)). One entry per name, ordered by layer and then by name bytes, as the library writes it; the binding sorts nothing.
|
|
12
15
|
- `Schema.describe` raises the library's `CHS_REJECTED` with ch_code 306 (TOO_DEEP_RECURSION), a `SchemaError`, for a deep input the server's stack check refuses (ABI v2: `chs_schema_describe` now lists `CHS_REJECTED`).
|
|
13
16
|
- The server's filter-declined settings (ABI v2; moves the dev fingerprint): `SchemaServer.filter_declined_settings`, a tuple of `DeclinedSetting` (`name`, `tier`), decoded from `schema_description`'s new required `server.filter_declined_settings`, with the new `DeclinedTier` vocabulary (`PREDICATE`, `PREDICATE_UNFLIPPED`, `RESULT_CONTENT`, and its unknown(n)). `compile_filter` on a schema whose server lists any is the library's `CHS_DECLINED`, an `UnsupportedError` carrying its message; a filter's own settings other than `session_timezone` are no longer declined at compile, and a declined one is named at evaluation instead.
|
|
@@ -16,11 +19,18 @@ The four bindings in this repository are released together and give one answer,
|
|
|
16
19
|
- The v2 spec publishes the filter decline-reasons catalog (`spec/abi-v2/declines/`).
|
|
17
20
|
- The v2 spec's decline catalog has 49 entries, and it publishes the settings values a server refuses outright (`spec/abi-v2/where-settings/documented-pathological.tsv`).
|
|
18
21
|
- The v2 spec's decline catalog follows the producer's batch 11: its `server-profile-setting` entry now covers every settings layer the schema compiles under (the id is unchanged), the evaluation-time setting entries name only the filter's own settings, and its `stack-depth` entry lists `chs_schema_describe`.
|
|
22
|
+
- The v2 spec publishes the documented platform differences (`spec/abi-v2/documented-platform/`), and its decline catalog adds `setting-format-pair`, `string-reading` and `equals-fold` (52 reasons).
|
|
19
23
|
- A `410 Gone` is a retired repository, and permanent (#571). Any request the fetch layer makes (a tag, an alias, a digest, a blob, `tags/list`, the referrers API or its fallback tag, after a redirect too) that answers `410` is now the new code `CHTYPES_SOURCE_RETIRED` (exit status 10), never retried and never sent to the next base, nor from the dev channel's alias to its tag. It was `CHTYPES_SOURCE_UNREACHABLE` (exit 3) with the registry's message dropped, and Go, Python and Rust moved on to the next base, so a mirror's 404 could hide the retirement. The error names the URL that answered and carries the registry's own message, with control and bidirectional-formatting characters removed and cut to 256 code points: Go `CodeSourceRetired` (`errors.Is(err, ErrSourceRetired)`), Python and TypeScript `SourceRetiredError`, Rust `Error::SourceRetired`. The rule is `docs/guides/fetch-v1.md` section 2, "A retired repository", and the `retired-410-*` and `list-tags-retired-410` conformance cases and one shared message table (`tests/fixtures/retired-message/cases.json`) hold all four bindings to it.
|
|
20
24
|
- 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.
|
|
21
25
|
|
|
22
26
|
### Changed
|
|
23
27
|
|
|
28
|
+
- `chtypes list` and the tag listing name the published lines only (two-part, numeric order), as Go's always did; `chtypes resolve <line>` prints the exact version a line resolves to (#613).
|
|
29
|
+
- **Breaking: the public surface the parity gate carried as exceptions is resolved, before the v2 lock (#499, #500).** A convenience exists in every binding or in none (`docs/reference/bindings-v1.md` §1, principle 2). Migration, old → new:
|
|
30
|
+
- `chtypes.errors.internal` and `chtypes.errors.misuse` → private (#499): they were the binding's own construction helpers, never documented. Catch `InternalError` and `UsageError`, as before.
|
|
31
|
+
- `SourceUnreachableError.retryable` and `.retry_after`, and the constructor's `retryable=` and `retry_after=` → removed: `retry_after` was never set, and the fetch layer has spent its retry table (`docs/guides/fetch-v1.md` §7) before it raises; a `Retry-After` it refused is named in the message.
|
|
32
|
+
- `FetchOptions(trusted_keys=(TrustedKey(keyid, public_key), …))` → `FetchOptions(trusted_keys=("<64 hex digits>", …))`: raw ed25519 public keys as hex, the `CHTYPES_TRUSTED_KEYS` spelling and Go's and Rust's, with each key id derived (`sha256-first16hex`). `chtypes.TrustedKey` is no longer public. A key that is not 64 hex digits is a `UsageError`; a channel that honors no trust override (this 2.0.0-dev SDK) ignores the option, with its one warning, as before.
|
|
33
|
+
- The `CODE_*` constants are now generated from `spec/fetch-v1/constants.json` (the private `chtypes._codes`); their names and values are unchanged.
|
|
24
34
|
- `compile_filter` is the library's `CHS_DECLINED`, `UnsupportedError`, on a schema whose `filter_declined_settings` is not empty from any layer, and its message names each setting and its layer (ABI v2, #588). A declined setting from the defaults or the schema's own settings now fails the compile, where it was named at evaluation before; a filter's own settings and an evaluation's are still named at evaluation.
|
|
25
35
|
- **Enforcement gate lifted** for `26.3` on `linux-amd64`.
|
|
26
36
|
- **Enforcement gate lifted** for `26.7` on `linux-amd64`.
|
|
@@ -44,6 +54,17 @@ The four bindings in this repository are released together and give one answer,
|
|
|
44
54
|
- **Enforcement gate restored** for `26.7` on `darwin-arm64`.
|
|
45
55
|
- **Enforcement gate restored** for `26.8` on `darwin-arm64`.
|
|
46
56
|
- **Enforcement gate restored** for `26.9` on `darwin-arm64`.
|
|
57
|
+
- **Enforcement gate lifted** for `26.3` on `linux-amd64`.
|
|
58
|
+
- **Enforcement gate lifted** for `26.7` on `linux-amd64`.
|
|
59
|
+
- **Enforcement gate lifted** for `26.8` on `linux-amd64`.
|
|
60
|
+
- **Enforcement gate lifted** for `26.9` on `linux-amd64`.
|
|
61
|
+
- **Enforcement gate lifted** for `26.3` on `linux-arm64`.
|
|
62
|
+
- **Enforcement gate lifted** for `26.7` on `linux-arm64`.
|
|
63
|
+
- **Enforcement gate lifted** for `26.8` on `linux-arm64`.
|
|
64
|
+
- **Enforcement gate lifted** for `26.9` on `linux-arm64`.
|
|
65
|
+
- **Enforcement gate lifted** for `26.7` on `darwin-arm64`.
|
|
66
|
+
- **Enforcement gate lifted** for `26.8` on `darwin-arm64`.
|
|
67
|
+
- **Enforcement gate lifted** for `26.9` on `darwin-arm64`.
|
|
47
68
|
|
|
48
69
|
### Removed
|
|
49
70
|
|
|
@@ -51,6 +72,9 @@ The four bindings in this repository are released together and give one answer,
|
|
|
51
72
|
|
|
52
73
|
### Fixed
|
|
53
74
|
|
|
75
|
+
- A declined export's payload is absent, as documented: it was an empty payload when the library returned a zero-length buffer for a declined export. `BatchResult.payload` and `spans` are `None` for a declined export. Whether an export was declined is decided from the document's `export_declined`, never from the buffer's length.
|
|
76
|
+
- The layout's `index.json` never gains a second entry for one manifest (#487): concurrent installs of one build each appended an entry (7 or 8 entries for the same digest after 8 concurrent installs, measured on 1.0.2). The merge now deduplicates by digest, as Go and TypeScript do.
|
|
77
|
+
- `--frozen` with a warm, verified cache makes zero requests, and `--frozen --offline` verifies the installed build against the lock with zero network (#414). An installed build whose `verified.json` is for the platform and records exactly the lock's pinned manifest, layer and bundle digests is used as it is. `--frozen --offline` is `CHTYPES_ARTIFACT_MISSING` when the pinned build is not installed, and `CHTYPES_ARTIFACT_PINNED` when there is no lock entry or the installed record does not match the pin; `--offline` without `--frozen` is unchanged. The `frozen-warm-cache-zero-requests`, `frozen-offline-lock-match`, `frozen-offline-lock-mismatch` and `frozen-offline-lock-not-installed` conformance cases hold all four bindings to it, under both corpora. The Python frozen path also compares the record's layer and bundle digests to the pin, and looks in the system directories too.
|
|
54
78
|
- A 2.0.0-dev SDK whose own-fingerprint alias has expired, with a cached build of that fingerprint for the request, now says so in the `CHTYPES_ARTIFACT_UNPUBLISHED` message of `fetch` (#581): `; a cached build for this fingerprint (build <id>) is still usable by open; upgrade the SDK to get newer builds` is appended. The code, the exit status (4) and the cache are unchanged, and all four bindings give the same message (`fp-ahead-cached-own-fp`).
|
|
55
79
|
- A 2.0.0-dev SDK whose fingerprint no published build carries yet now fails closed with a precise message (#578). When every base answers its own alias 404 and the tag names a build signed for another fingerprint, `fetch` is `CHTYPES_ARTIFACT_UNPUBLISHED` (exit status 4): `no published build for this SDK's fingerprint <own fp>; newest published on this channel is <fp> (build <id>)`. It used to install that build, or report it already installed, and the lookup at open then answered `CHTYPES_ARTIFACT_MISSING` with advice to run `fetch`: a loop with no pointer to the cause. Nothing is downloaded or installed and the cache is unchanged. An alias that answers, a tag at the SDK's own fingerprint and the v1 contract are unchanged. All four bindings run the same fetch conformance cases, `fp-ahead-*`, and the rule is `docs/guides/fetch-v1.md` section 3.
|
|
56
80
|
- Opening a line no longer waits for another line's fetch: the registry never holds its lock across network I/O, and concurrent opens of one request share one fetch (#491).
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: chtypes
|
|
3
|
-
Version: 2.0.0.
|
|
3
|
+
Version: 2.0.0.dev8
|
|
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
|
|
@@ -88,7 +88,7 @@ A bad **row** is a verdict, not an exception: `outcome` becomes `Outcome.REJECTE
|
|
|
88
88
|
|
|
89
89
|
## Fetching from the command line
|
|
90
90
|
|
|
91
|
-
`chtypes fetch 26.8` (or `python -m chtypes fetch 26.8`) resolves, verifies and installs a build into the per-user cache; `chtypes verify`, `chtypes list` and `chtypes
|
|
91
|
+
`chtypes fetch 26.8` (or `python -m chtypes fetch 26.8`) resolves, verifies and installs a build into the per-user cache; `chtypes verify`, `chtypes list`, `chtypes where`, `chtypes resolve` and `chtypes prune` complete the six commands. `--lock FILE` records what was installed and `--frozen` fetches only what the lock pins. See the [Python API reference](https://github.com/wave-rf/chtypes/blob/main/docs/reference/python.md#the-command).
|
|
92
92
|
|
|
93
93
|
## Things specific to this binding
|
|
94
94
|
|
|
@@ -69,7 +69,7 @@ A bad **row** is a verdict, not an exception: `outcome` becomes `Outcome.REJECTE
|
|
|
69
69
|
|
|
70
70
|
## Fetching from the command line
|
|
71
71
|
|
|
72
|
-
`chtypes fetch 26.8` (or `python -m chtypes fetch 26.8`) resolves, verifies and installs a build into the per-user cache; `chtypes verify`, `chtypes list` and `chtypes
|
|
72
|
+
`chtypes fetch 26.8` (or `python -m chtypes fetch 26.8`) resolves, verifies and installs a build into the per-user cache; `chtypes verify`, `chtypes list`, `chtypes where`, `chtypes resolve` and `chtypes prune` complete the six commands. `--lock FILE` records what was installed and `--frozen` fetches only what the lock pins. See the [Python API reference](https://github.com/wave-rf/chtypes/blob/main/docs/reference/python.md#the-command).
|
|
73
73
|
|
|
74
74
|
## Things specific to this binding
|
|
75
75
|
|
|
@@ -98,7 +98,7 @@ from .errors import (
|
|
|
98
98
|
UsageError,
|
|
99
99
|
)
|
|
100
100
|
from .library import Block, Filter, Library, Schema, Server, open_unverified
|
|
101
|
-
from .registry import FetchOptions, Registry, Resolved,
|
|
101
|
+
from .registry import FetchOptions, Registry, Resolved, cache_root, search_dirs
|
|
102
102
|
from .results import (
|
|
103
103
|
AtMergeEntry,
|
|
104
104
|
BatchResult,
|
|
@@ -201,7 +201,6 @@ __all__ = [
|
|
|
201
201
|
"Span",
|
|
202
202
|
"Status",
|
|
203
203
|
"Transform",
|
|
204
|
-
"TrustedKey",
|
|
205
204
|
"UnsupportedError",
|
|
206
205
|
"UsageError",
|
|
207
206
|
"Value",
|
|
@@ -6,6 +6,10 @@
|
|
|
6
6
|
chtypes verify [--cache <dir>] [--strict] re-verify every installed build
|
|
7
7
|
chtypes list [--cache <dir>] [--offline] [--strict]
|
|
8
8
|
chtypes where [--cache <dir>] [--strict] [--all] the cache root; --all: every search dir
|
|
9
|
+
chtypes resolve <spelling> [--json] [--cache <dir>] [--offline] [--strict]
|
|
10
|
+
the build and manifest each platform resolves to, verified; installs nothing
|
|
11
|
+
chtypes prune [--line <line>] [--keep <n>] [--dry-run] [--cache <dir>] [--strict]
|
|
12
|
+
remove the builds of each line newer ones supersede, keeping the newest n
|
|
9
13
|
|
|
10
14
|
A spelling is `26.8`, `26.8.15` or `26.8.15.10`. Exit statuses come from the
|
|
11
15
|
`errors` table of spec/fetch-v1/constants.json (generated into the fetch layer
|
|
@@ -33,6 +37,7 @@ from collections.abc import Sequence
|
|
|
33
37
|
|
|
34
38
|
from . import __version__ as chtypes_version
|
|
35
39
|
from . import _ocifetch as fetch_layer
|
|
40
|
+
from ._codes import CODE_ARTIFACT_CORRUPT
|
|
36
41
|
from ._ocifetch import _channel
|
|
37
42
|
from ._ocifetch import _constants as C
|
|
38
43
|
from ._ocifetch._ensure import _translate_transport_error, detect_host_platform, missing_notes
|
|
@@ -41,6 +46,7 @@ from ._ocifetch._faults import probe_roots
|
|
|
41
46
|
from ._ocifetch._http import FetchPolicy, RetryPolicy, TransportError, fetch_from_bases
|
|
42
47
|
from ._ocifetch._layout import resolve_cache_root, search_roots
|
|
43
48
|
from ._ocifetch._lock import load_lock
|
|
49
|
+
from ._ocifetch._prune import is_line
|
|
44
50
|
from .errors import ChtypesError
|
|
45
51
|
from .registry import FetchOptions
|
|
46
52
|
|
|
@@ -50,7 +56,7 @@ EXIT_OK = 0
|
|
|
50
56
|
EXIT_USAGE = 2
|
|
51
57
|
# A failure that carries no code from the table (a lock file that cannot be
|
|
52
58
|
# read, an interrupted run): the status the table gives its verification codes.
|
|
53
|
-
_EXIT_FALLBACK = C.ERROR_EXIT_CODES[
|
|
59
|
+
_EXIT_FALLBACK = C.ERROR_EXIT_CODES[CODE_ARTIFACT_CORRUPT]
|
|
54
60
|
_EXIT_INTERRUPTED = 130
|
|
55
61
|
|
|
56
62
|
|
|
@@ -153,7 +159,7 @@ def build_parser() -> argparse.ArgumentParser:
|
|
|
153
159
|
listing = sub.add_parser(
|
|
154
160
|
"list",
|
|
155
161
|
help="what is installed, and what the registry publishes",
|
|
156
|
-
description="What is installed, and the
|
|
162
|
+
description="What is installed, and the lines the registry publishes.",
|
|
157
163
|
)
|
|
158
164
|
cache_option(listing)
|
|
159
165
|
listing.add_argument(
|
|
@@ -173,6 +179,55 @@ def build_parser() -> argparse.ArgumentParser:
|
|
|
173
179
|
action="store_true",
|
|
174
180
|
help="every directory searched, in order, the cache root first (one per line)",
|
|
175
181
|
)
|
|
182
|
+
|
|
183
|
+
resolving = sub.add_parser(
|
|
184
|
+
"resolve",
|
|
185
|
+
help="the build and manifest each platform resolves to, verified; installs nothing",
|
|
186
|
+
description=(
|
|
187
|
+
"What a line or version resolves to on every platform the registry offers, "
|
|
188
|
+
"each signature verified, with no layer downloaded (docs/guides/fetch-v1.md section 3)."
|
|
189
|
+
),
|
|
190
|
+
)
|
|
191
|
+
resolving.add_argument(
|
|
192
|
+
"spelling",
|
|
193
|
+
metavar="<spelling>",
|
|
194
|
+
help="26.8, 26.8.15 or 26.8.15.10 (no v prefix, no channel suffix)",
|
|
195
|
+
)
|
|
196
|
+
resolving.add_argument(
|
|
197
|
+
"--json",
|
|
198
|
+
action="store_true",
|
|
199
|
+
help="print one JSON array, one object per platform, instead of one line per platform",
|
|
200
|
+
)
|
|
201
|
+
cache_option(resolving)
|
|
202
|
+
resolving.add_argument(
|
|
203
|
+
"--offline",
|
|
204
|
+
action="store_true",
|
|
205
|
+
help="answer from the cache; no network (or set CHTYPES_OFFLINE=1)",
|
|
206
|
+
)
|
|
207
|
+
|
|
208
|
+
pruning = sub.add_parser(
|
|
209
|
+
"prune",
|
|
210
|
+
help="remove the builds of each line newer ones supersede, keeping the newest n",
|
|
211
|
+
description=(
|
|
212
|
+
"Remove the installed builds of each line that newer installed builds of the same line "
|
|
213
|
+
"and platform supersede, keeping the newest n, and never one a running process holds "
|
|
214
|
+
"(docs/guides/fetch-v1.md section 1)."
|
|
215
|
+
),
|
|
216
|
+
)
|
|
217
|
+
pruning.add_argument(
|
|
218
|
+
"--line", metavar="<line>", help="prune only this line, such as 26.8 (default: every line)"
|
|
219
|
+
)
|
|
220
|
+
pruning.add_argument(
|
|
221
|
+
"--keep",
|
|
222
|
+
metavar="<n>",
|
|
223
|
+
type=int,
|
|
224
|
+
default=1,
|
|
225
|
+
help="how many of each line's newest builds to keep, per platform (at least 1; default 1)",
|
|
226
|
+
)
|
|
227
|
+
pruning.add_argument(
|
|
228
|
+
"--dry-run", action="store_true", help="print what would be removed, and remove nothing"
|
|
229
|
+
)
|
|
230
|
+
cache_option(pruning)
|
|
176
231
|
return parser
|
|
177
232
|
|
|
178
233
|
|
|
@@ -198,13 +253,15 @@ def _options(args: argparse.Namespace, *, platform: str | None = None):
|
|
|
198
253
|
return fetch._to_options(platform)
|
|
199
254
|
|
|
200
255
|
|
|
201
|
-
|
|
256
|
+
# The published lines: exactly two parts, no leading zeros (Go's `lineTag`).
|
|
257
|
+
_LINE_SPELLING = re.compile(r"^(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)$")
|
|
202
258
|
|
|
203
259
|
|
|
204
260
|
def _published_tags(options) -> list[str]:
|
|
205
|
-
"""The registry's `tags/list`, kept to the
|
|
206
|
-
|
|
207
|
-
else a repository carries are
|
|
261
|
+
"""The registry's `tags/list`, kept to the published lines (two-part tags),
|
|
262
|
+
in numeric order: three- and four-part versions, aliases, the referrers
|
|
263
|
+
fallback tags (`sha256-<hex>`) and anything else a repository carries are
|
|
264
|
+
dropped (docs/guides/fetch-v1.md section 3)."""
|
|
208
265
|
try:
|
|
209
266
|
resp = fetch_from_bases(
|
|
210
267
|
options.resolved_bases(),
|
|
@@ -221,7 +278,7 @@ def _published_tags(options) -> list[str]:
|
|
|
221
278
|
tags = json.loads(resp.body.decode("utf-8")).get("tags") or []
|
|
222
279
|
except (ValueError, AttributeError) as exc:
|
|
223
280
|
raise ArtifactCorruptError(f"chtypes: tags/list was not a tag list ({exc})") from exc
|
|
224
|
-
keep = [t for t in tags if isinstance(t, str) and
|
|
281
|
+
keep = [t for t in tags if isinstance(t, str) and _LINE_SPELLING.fullmatch(t)]
|
|
225
282
|
return sorted(set(keep), key=lambda t: tuple(int(p) for p in t.split(".")))
|
|
226
283
|
|
|
227
284
|
|
|
@@ -250,7 +307,7 @@ def _cmd_fetch(args: argparse.Namespace) -> int:
|
|
|
250
307
|
lock = load_lock(options.lock_path)
|
|
251
308
|
spellings = list(lock.requests)
|
|
252
309
|
else:
|
|
253
|
-
spellings =
|
|
310
|
+
spellings = _published_tags(options)
|
|
254
311
|
if not spellings:
|
|
255
312
|
_say("chtypes: the registry publishes no lines")
|
|
256
313
|
else:
|
|
@@ -297,7 +354,7 @@ def _cmd_verify(args: argparse.Namespace) -> int:
|
|
|
297
354
|
_say(f"chtypes: FAILED {r.version} {r.platform} {r.dir}: {r.detail}")
|
|
298
355
|
if failed:
|
|
299
356
|
_say(f"chtypes: {failed} of {len(results)} installed build(s) FAILED verification")
|
|
300
|
-
return C.ERROR_EXIT_CODES[
|
|
357
|
+
return C.ERROR_EXIT_CODES[CODE_ARTIFACT_CORRUPT]
|
|
301
358
|
return EXIT_OK
|
|
302
359
|
|
|
303
360
|
|
|
@@ -328,7 +385,55 @@ def _cmd_where(args: argparse.Namespace) -> int:
|
|
|
328
385
|
return EXIT_OK
|
|
329
386
|
|
|
330
387
|
|
|
331
|
-
|
|
388
|
+
def _cmd_resolve(args: argparse.Namespace) -> int:
|
|
389
|
+
options = _options(args)
|
|
390
|
+
request = fetch_layer.Request(args.spelling) # a spelling error: usage, before any I/O
|
|
391
|
+
rows, warnings = fetch_layer.resolve(request, options)
|
|
392
|
+
for warning in warnings:
|
|
393
|
+
_say(f"chtypes: warning: {warning}")
|
|
394
|
+
if args.json:
|
|
395
|
+
doc = [
|
|
396
|
+
{"platform": r.platform, "version": r.version, "build": r.build, "manifest": r.manifest}
|
|
397
|
+
for r in rows
|
|
398
|
+
]
|
|
399
|
+
sys.stdout.write(json.dumps(doc, separators=(",", ":")) + "\n")
|
|
400
|
+
else:
|
|
401
|
+
for r in rows:
|
|
402
|
+
sys.stdout.write(f"resolved {r.version} {r.platform} {r.build} {r.manifest}\n")
|
|
403
|
+
sys.stdout.flush()
|
|
404
|
+
return EXIT_OK
|
|
405
|
+
|
|
406
|
+
|
|
407
|
+
def _cmd_prune(args: argparse.Namespace) -> int:
|
|
408
|
+
if args.keep < 1:
|
|
409
|
+
raise ValueError(f"chtypes: --keep is at least 1, not {args.keep}")
|
|
410
|
+
if args.line is not None and not is_line(args.line):
|
|
411
|
+
raise ValueError(f"chtypes: --line takes a two-part line such as 26.8, not {args.line!r}")
|
|
412
|
+
options = _options(args)
|
|
413
|
+
results = fetch_layer.prune(options, line=args.line, keep=args.keep, dry_run=args.dry_run)
|
|
414
|
+
removed = 0
|
|
415
|
+
for r in results:
|
|
416
|
+
if r.in_use:
|
|
417
|
+
word = "in-use"
|
|
418
|
+
else:
|
|
419
|
+
removed += 1
|
|
420
|
+
word = "would-prune" if args.dry_run else "pruned"
|
|
421
|
+
sys.stdout.write(f"{word} {r.version} {r.platform} {r.dir}\n")
|
|
422
|
+
sys.stdout.flush()
|
|
423
|
+
_say_notes(options)
|
|
424
|
+
root = resolve_cache_root(args.cache)
|
|
425
|
+
_say(f"chtypes: {'would prune' if args.dry_run else 'pruned'} {removed} build(s) under {root}")
|
|
426
|
+
return EXIT_OK
|
|
427
|
+
|
|
428
|
+
|
|
429
|
+
_COMMANDS = {
|
|
430
|
+
"fetch": _cmd_fetch,
|
|
431
|
+
"verify": _cmd_verify,
|
|
432
|
+
"list": _cmd_list,
|
|
433
|
+
"where": _cmd_where,
|
|
434
|
+
"resolve": _cmd_resolve,
|
|
435
|
+
"prune": _cmd_prune,
|
|
436
|
+
}
|
|
332
437
|
|
|
333
438
|
|
|
334
439
|
def main(argv: Sequence[str] | None = None) -> int:
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# Code generated by scripts/fetch-v1/gen-constants.py from
|
|
2
|
+
# spec/fetch-v1/constants.json. DO NOT EDIT.
|
|
3
|
+
# Source sha256: 82afa8d6cba96c10b5945c6f7c1c8360a504cc8ac6231e74017cc6c20b908df6
|
|
4
|
+
"""The shared error codes (docs/guides/fetch-v1.md §8), one per entry of the
|
|
5
|
+
fetch constants' `errors`, in their order. `chtypes.errors` re-exports them
|
|
6
|
+
(docs/reference/bindings-v1.md §4, the codes)."""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
from typing import Final
|
|
11
|
+
|
|
12
|
+
CODE_ARTIFACT_MISSING: Final[str] = "CHTYPES_ARTIFACT_MISSING"
|
|
13
|
+
CODE_ARTIFACT_UNTRUSTED: Final[str] = "CHTYPES_ARTIFACT_UNTRUSTED"
|
|
14
|
+
CODE_ARTIFACT_CORRUPT: Final[str] = "CHTYPES_ARTIFACT_CORRUPT"
|
|
15
|
+
CODE_ARTIFACT_PINNED: Final[str] = "CHTYPES_ARTIFACT_PINNED"
|
|
16
|
+
CODE_ARTIFACT_UNPUBLISHED: Final[str] = "CHTYPES_ARTIFACT_UNPUBLISHED"
|
|
17
|
+
CODE_SOURCE_UNREACHABLE: Final[str] = "CHTYPES_SOURCE_UNREACHABLE"
|
|
18
|
+
CODE_SOURCE_UNAUTHORIZED: Final[str] = "CHTYPES_SOURCE_UNAUTHORIZED"
|
|
19
|
+
CODE_SOURCE_FORBIDDEN: Final[str] = "CHTYPES_SOURCE_FORBIDDEN"
|
|
20
|
+
CODE_SOURCE_INCOMPATIBLE: Final[str] = "CHTYPES_SOURCE_INCOMPATIBLE"
|
|
21
|
+
CODE_ARTIFACT_INCOMPATIBLE: Final[str] = "CHTYPES_ARTIFACT_INCOMPATIBLE"
|
|
22
|
+
CODE_CACHE_UNUSABLE: Final[str] = "CHTYPES_CACHE_UNUSABLE"
|
|
23
|
+
CODE_SOURCE_RETIRED: Final[str] = "CHTYPES_SOURCE_RETIRED"
|
|
@@ -39,7 +39,8 @@ from ._abi2._vocab import (
|
|
|
39
39
|
Source,
|
|
40
40
|
Verdict,
|
|
41
41
|
)
|
|
42
|
-
from .errors import ArtifactCorruptError
|
|
42
|
+
from .errors import ArtifactCorruptError
|
|
43
|
+
from .errors import _internal as internal
|
|
43
44
|
from .results import (
|
|
44
45
|
AtMergeEntry,
|
|
45
46
|
BatchResult,
|
|
@@ -427,6 +428,12 @@ def decode_batch(raw: bytes, export: bytes | None) -> BatchResult:
|
|
|
427
428
|
framing = obj.get("framing")
|
|
428
429
|
engine_rows = None if obj.get("engine_rows") is None else _engine_rows(obj, where)
|
|
429
430
|
spans = None if obj.get("row_spans") is None else _spans(obj, "row_spans", where)
|
|
431
|
+
export_declined = _bytes(obj, "export_declined", where)
|
|
432
|
+
if export_declined:
|
|
433
|
+
# A declined export has no payload and no spans, whatever buffer the call
|
|
434
|
+
# returned: the library may hand back a zero-length one, so the document's
|
|
435
|
+
# export_declined decides, never the buffer's length.
|
|
436
|
+
export, spans = None, None
|
|
430
437
|
return BatchResult(
|
|
431
438
|
outcome=Outcome.of(_text(obj, "outcome", where)),
|
|
432
439
|
err_code=_int(obj, "code", where),
|
|
@@ -438,7 +445,7 @@ def decode_batch(raw: bytes, export: bytes | None) -> BatchResult:
|
|
|
438
445
|
engine_rows=engine_rows,
|
|
439
446
|
payload=export,
|
|
440
447
|
spans=spans,
|
|
441
|
-
export_declined=
|
|
448
|
+
export_declined=export_declined,
|
|
442
449
|
rows_passed=_u64(obj, "rows_passed", where),
|
|
443
450
|
rows_cut=_u64(obj, "rows_cut", where),
|
|
444
451
|
partition_count=_opt_u64(obj, "partition_count", where),
|
|
@@ -6,7 +6,8 @@ logic lives in sibling modules. The seam (docs/guides/fetch-v1.md "The seam")
|
|
|
6
6
|
is the only surface anything outside this package may use: `ensure`,
|
|
7
7
|
`resolve_installed`, `list_installed`, `verify_installed` and
|
|
8
8
|
`fetch_signed`, plus the `Request`/`Options`/`Resolved`/
|
|
9
|
-
`VerifyResult` shapes they take and return
|
|
9
|
+
`VerifyResult` shapes they take and return; and beside it the CLI's `resolve`
|
|
10
|
+
and `prune` and the registry's in-use `hold` (§1, "In use"). Every other module here
|
|
10
11
|
(`_http`, `_oci`, `_referrers`, `_dsse`, `_unpack`, `_layout`,
|
|
11
12
|
`_lock`, `_errors`) is a private implementation detail of `_ensure`.
|
|
12
13
|
|
|
@@ -30,15 +31,25 @@ from chtypes._ocifetch._ensure import (
|
|
|
30
31
|
resolve_installed,
|
|
31
32
|
verify_installed,
|
|
32
33
|
)
|
|
34
|
+
from chtypes._ocifetch._hold import hold
|
|
35
|
+
from chtypes._ocifetch._prune import Superseded, prune
|
|
36
|
+
from chtypes._ocifetch._resolve import PlatformOutcome, Resolution, resolve, resolve_each
|
|
33
37
|
|
|
34
38
|
__all__ = [
|
|
35
39
|
"Options",
|
|
40
|
+
"PlatformOutcome",
|
|
36
41
|
"Request",
|
|
42
|
+
"Resolution",
|
|
37
43
|
"Resolved",
|
|
44
|
+
"Superseded",
|
|
38
45
|
"VerifyResult",
|
|
39
46
|
"ensure",
|
|
40
47
|
"fetch_signed",
|
|
48
|
+
"hold",
|
|
41
49
|
"list_installed",
|
|
50
|
+
"prune",
|
|
51
|
+
"resolve",
|
|
52
|
+
"resolve_each",
|
|
42
53
|
"resolve_installed",
|
|
43
54
|
"verify_installed",
|
|
44
55
|
]
|
|
@@ -21,6 +21,7 @@ import base64
|
|
|
21
21
|
import hashlib
|
|
22
22
|
import json
|
|
23
23
|
import os
|
|
24
|
+
from collections.abc import Sequence
|
|
24
25
|
from dataclasses import dataclass
|
|
25
26
|
|
|
26
27
|
from chtypes import _ed25519
|
|
@@ -86,12 +87,21 @@ def env_trusted_keys() -> tuple[TrustedKey, ...] | None:
|
|
|
86
87
|
items = [part.strip() for part in raw.split(",") if part.strip()]
|
|
87
88
|
if not items:
|
|
88
89
|
return None
|
|
90
|
+
return trusted_keys_from_hex(items, C.ENV_TRUSTED_KEYS_NAME)
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def trusted_keys_from_hex(items: Sequence[str], where: str) -> tuple[TrustedKey, ...]:
|
|
94
|
+
"""Raw 32-byte ed25519 public keys, each as 64 hex digits (the
|
|
95
|
+
`CHTYPES_TRUSTED_KEYS` spelling, and the `trusted_keys` option's, as in
|
|
96
|
+
every binding), as trust-list records whose key id is the generated
|
|
97
|
+
constants' algorithm, sha256-first16hex of the raw key. A bad key is a
|
|
98
|
+
ValueError naming `where`."""
|
|
89
99
|
keys = []
|
|
90
100
|
for item in items:
|
|
91
101
|
try:
|
|
92
|
-
public = _hex_to_bytes(item)
|
|
102
|
+
public = _hex_to_bytes(item.strip())
|
|
93
103
|
except ValueError as exc:
|
|
94
|
-
raise ValueError(f"chtypes: {
|
|
104
|
+
raise ValueError(f"chtypes: {where} holds a bad key: {exc}") from None
|
|
95
105
|
keys.append(TrustedKey(keyid=hashlib.sha256(public).hexdigest()[:16], public_key=public))
|
|
96
106
|
return tuple(keys)
|
|
97
107
|
|
|
@@ -867,7 +867,13 @@ def _ensure_floating(
|
|
|
867
867
|
assert record is not None
|
|
868
868
|
|
|
869
869
|
def _add_entry(doc: dict) -> dict:
|
|
870
|
-
entries =
|
|
870
|
+
entries = list(doc.get("manifests") or [])
|
|
871
|
+
# One entry per manifest digest: a re-install, or a concurrent one that
|
|
872
|
+
# landed first, never writes a second (public issue #487).
|
|
873
|
+
if not any(
|
|
874
|
+
isinstance(e, dict) and e.get("digest") == platform_desc.digest for e in entries
|
|
875
|
+
):
|
|
876
|
+
entries.append({"digest": platform_desc.digest})
|
|
871
877
|
return {**doc, "manifests": entries}
|
|
872
878
|
|
|
873
879
|
try:
|
|
@@ -899,6 +905,82 @@ def _ensure_floating(
|
|
|
899
905
|
return resolved
|
|
900
906
|
|
|
901
907
|
|
|
908
|
+
def _installed_by_pin(
|
|
909
|
+
request: Request, platform_key: str, options: Options, pin: LockPin
|
|
910
|
+
) -> Resolved | None:
|
|
911
|
+
return _lookup_by_pin(request, platform_key, options, pin)[0]
|
|
912
|
+
|
|
913
|
+
|
|
914
|
+
def _lookup_by_pin(
|
|
915
|
+
request: Request, platform_key: str, options: Options, pin: LockPin
|
|
916
|
+
) -> tuple[Resolved | None, bool]:
|
|
917
|
+
"""The installed build a lock pin names, with no network: an unpacked
|
|
918
|
+
directory named by the pinned manifest digest, in the cache or a system
|
|
919
|
+
directory, whose `verified.json` is for this platform and records exactly
|
|
920
|
+
the pinned manifest, layer and bundle digests (public issues #414, #487).
|
|
921
|
+
`None` when no installed build matches; the flag is true when a record
|
|
922
|
+
exists under the pinned manifest but disagrees with the pin."""
|
|
923
|
+
roots = search_roots(options.cache_dir, options.system_dirs)
|
|
924
|
+
manifest_hex = parse_digest(pin.manifest)
|
|
925
|
+
mismatch = False
|
|
926
|
+
for root in roots:
|
|
927
|
+
udir = unpacked_dir_for(root, manifest_hex)
|
|
928
|
+
record = read_verified_record(udir)
|
|
929
|
+
if record is None:
|
|
930
|
+
continue
|
|
931
|
+
if (
|
|
932
|
+
record.platform == platform_key
|
|
933
|
+
and record.manifest == pin.manifest
|
|
934
|
+
and record.layer == pin.layer
|
|
935
|
+
and record.bundle == pin.bundle
|
|
936
|
+
):
|
|
937
|
+
return (
|
|
938
|
+
_record_to_resolved(
|
|
939
|
+
udir,
|
|
940
|
+
record,
|
|
941
|
+
request_spelling=request.spelling,
|
|
942
|
+
already_installed=True,
|
|
943
|
+
source=_source_for_dir(udir, roots),
|
|
944
|
+
),
|
|
945
|
+
False,
|
|
946
|
+
)
|
|
947
|
+
mismatch = True
|
|
948
|
+
return None, mismatch
|
|
949
|
+
|
|
950
|
+
|
|
951
|
+
def _resolve_frozen_offline(
|
|
952
|
+
request: Request, platform_key: str, options: Options, lock: Lock | None
|
|
953
|
+
) -> Resolved:
|
|
954
|
+
"""`--frozen --offline`: verify the installed build against the lock with
|
|
955
|
+
zero network. No lock entry for the request, or an installed record that
|
|
956
|
+
does not match the pin, is `CHTYPES_ARTIFACT_PINNED`; a pinned build that
|
|
957
|
+
is not installed is `CHTYPES_ARTIFACT_MISSING`."""
|
|
958
|
+
if lock is None:
|
|
959
|
+
raise ArtifactPinnedError("chtypes: --frozen requires a lock file; none was given")
|
|
960
|
+
pin = lock.pin_for(request.spelling, platform_key)
|
|
961
|
+
if pin is None:
|
|
962
|
+
raise ArtifactPinnedError(
|
|
963
|
+
f"chtypes: --frozen: no pin for {request.spelling!r} ({platform_key}) in the lock"
|
|
964
|
+
)
|
|
965
|
+
roots = search_roots(options.cache_dir, options.system_dirs)
|
|
966
|
+
warnings = probe_roots(roots, options.resolved_strict())
|
|
967
|
+
installed, mismatch = _lookup_by_pin(request, platform_key, options, pin)
|
|
968
|
+
if installed is None:
|
|
969
|
+
if mismatch:
|
|
970
|
+
raise ArtifactPinnedError(
|
|
971
|
+
f"chtypes: --frozen --offline: the installed build for {request.spelling} "
|
|
972
|
+
f"({platform_key}) does not match the lock's pin {pin.manifest}"
|
|
973
|
+
)
|
|
974
|
+
raise ArtifactMissingError(
|
|
975
|
+
with_notes(
|
|
976
|
+
f"chtypes: --frozen --offline: the build the lock pins ({pin.manifest}) "
|
|
977
|
+
f"is not installed for {request.spelling} ({platform_key})",
|
|
978
|
+
missing_notes(options),
|
|
979
|
+
)
|
|
980
|
+
)
|
|
981
|
+
return replace(installed, warnings=(*warnings, *installed.warnings))
|
|
982
|
+
|
|
983
|
+
|
|
902
984
|
def _ensure_frozen(
|
|
903
985
|
request: Request, platform_key: str, options: Options, lock: Lock | None
|
|
904
986
|
) -> Resolved:
|
|
@@ -916,17 +998,9 @@ def _ensure_frozen(
|
|
|
916
998
|
cache_root_path = roots[0]
|
|
917
999
|
scratch_root = _scratch_root(cache_root_path)
|
|
918
1000
|
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
if existing_record is not None and existing_record.platform == platform_key:
|
|
923
|
-
return _record_to_resolved(
|
|
924
|
-
existing_dir,
|
|
925
|
-
existing_record,
|
|
926
|
-
request_spelling=request.spelling,
|
|
927
|
-
already_installed=True,
|
|
928
|
-
source="cache",
|
|
929
|
-
)
|
|
1001
|
+
installed = _installed_by_pin(request, platform_key, options, pin)
|
|
1002
|
+
if installed is not None:
|
|
1003
|
+
return installed
|
|
930
1004
|
|
|
931
1005
|
manifest_doc, _raw = fetch_manifest_by_digest(bases, pin.manifest, policy=policy, retry=retry)
|
|
932
1006
|
layer_desc = manifest_layer_descriptor(manifest_doc)
|
|
@@ -1049,6 +1123,9 @@ def _ensure(
|
|
|
1049
1123
|
request: Request, platform_key: str, options: Options, lock: Lock | None, roots: Sequence[Path]
|
|
1050
1124
|
) -> Resolved:
|
|
1051
1125
|
try:
|
|
1126
|
+
if options.resolved_offline() and options.frozen:
|
|
1127
|
+
return _resolve_frozen_offline(request, platform_key, options, lock)
|
|
1128
|
+
|
|
1052
1129
|
if options.resolved_offline():
|
|
1053
1130
|
resolved = resolve_installed(request, platform_key, options)
|
|
1054
1131
|
if resolved is None:
|
|
@@ -47,17 +47,19 @@ __all__ = [
|
|
|
47
47
|
"SourceUnreachableError",
|
|
48
48
|
]
|
|
49
49
|
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
CODE_ARTIFACT_PINNED
|
|
54
|
-
CODE_ARTIFACT_UNPUBLISHED
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
CODE_SOURCE_FORBIDDEN
|
|
58
|
-
CODE_SOURCE_INCOMPATIBLE
|
|
59
|
-
|
|
60
|
-
|
|
50
|
+
from chtypes._codes import (
|
|
51
|
+
CODE_ARTIFACT_CORRUPT,
|
|
52
|
+
CODE_ARTIFACT_MISSING,
|
|
53
|
+
CODE_ARTIFACT_PINNED,
|
|
54
|
+
CODE_ARTIFACT_UNPUBLISHED,
|
|
55
|
+
CODE_ARTIFACT_UNTRUSTED,
|
|
56
|
+
CODE_CACHE_UNUSABLE,
|
|
57
|
+
CODE_SOURCE_FORBIDDEN,
|
|
58
|
+
CODE_SOURCE_INCOMPATIBLE,
|
|
59
|
+
CODE_SOURCE_RETIRED,
|
|
60
|
+
CODE_SOURCE_UNAUTHORIZED,
|
|
61
|
+
CODE_SOURCE_UNREACHABLE,
|
|
62
|
+
)
|
|
61
63
|
|
|
62
64
|
_CODES_USED_HERE = (
|
|
63
65
|
CODE_ARTIFACT_MISSING,
|