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.
Files changed (101) hide show
  1. {chtypes-2.0.0.dev6 → chtypes-2.0.0.dev8}/CHANGELOG.md +24 -0
  2. {chtypes-2.0.0.dev6 → chtypes-2.0.0.dev8}/PKG-INFO +2 -2
  3. {chtypes-2.0.0.dev6 → chtypes-2.0.0.dev8}/README.md +1 -1
  4. {chtypes-2.0.0.dev6 → chtypes-2.0.0.dev8}/pyproject.toml +1 -1
  5. {chtypes-2.0.0.dev6 → chtypes-2.0.0.dev8}/src/chtypes/__init__.py +1 -2
  6. {chtypes-2.0.0.dev6 → chtypes-2.0.0.dev8}/src/chtypes/__main__.py +115 -10
  7. {chtypes-2.0.0.dev6 → chtypes-2.0.0.dev8}/src/chtypes/_abi2/_errors.py +1 -1
  8. chtypes-2.0.0.dev8/src/chtypes/_codes.py +23 -0
  9. {chtypes-2.0.0.dev6 → chtypes-2.0.0.dev8}/src/chtypes/_decode.py +9 -2
  10. {chtypes-2.0.0.dev6 → chtypes-2.0.0.dev8}/src/chtypes/_guard.py +1 -1
  11. {chtypes-2.0.0.dev6 → chtypes-2.0.0.dev8}/src/chtypes/_input.py +1 -1
  12. {chtypes-2.0.0.dev6 → chtypes-2.0.0.dev8}/src/chtypes/_ocifetch/__init__.py +12 -1
  13. {chtypes-2.0.0.dev6 → chtypes-2.0.0.dev8}/src/chtypes/_ocifetch/_dsse.py +12 -2
  14. {chtypes-2.0.0.dev6 → chtypes-2.0.0.dev8}/src/chtypes/_ocifetch/_ensure.py +89 -12
  15. {chtypes-2.0.0.dev6 → chtypes-2.0.0.dev8}/src/chtypes/_ocifetch/_errors.py +13 -11
  16. chtypes-2.0.0.dev8/src/chtypes/_ocifetch/_hold.py +133 -0
  17. chtypes-2.0.0.dev8/src/chtypes/_ocifetch/_prune.py +282 -0
  18. chtypes-2.0.0.dev8/src/chtypes/_ocifetch/_resolve.py +309 -0
  19. {chtypes-2.0.0.dev6 → chtypes-2.0.0.dev8}/src/chtypes/_setup.py +1 -1
  20. {chtypes-2.0.0.dev6 → chtypes-2.0.0.dev8}/src/chtypes/errors.py +20 -34
  21. {chtypes-2.0.0.dev6 → chtypes-2.0.0.dev8}/src/chtypes/registry.py +132 -15
  22. {chtypes-2.0.0.dev6 → chtypes-2.0.0.dev8}/tests/abi2/test_public_registry.py +1 -1
  23. {chtypes-2.0.0.dev6 → chtypes-2.0.0.dev8}/tests/abi2/test_reader_rules.py +4 -0
  24. chtypes-2.0.0.dev8/tests/abi2/test_registry_fetch.py +157 -0
  25. {chtypes-2.0.0.dev6 → chtypes-2.0.0.dev8}/tests/abi2/test_registry_flight.py +3 -3
  26. {chtypes-2.0.0.dev6 → chtypes-2.0.0.dev8}/tests/api/test_decode.py +21 -0
  27. chtypes-2.0.0.dev8/tests/api/test_trusted_keys.py +49 -0
  28. {chtypes-2.0.0.dev6 → chtypes-2.0.0.dev8}/tests/api/test_vocab.py +4 -2
  29. {chtypes-2.0.0.dev6 → chtypes-2.0.0.dev8}/tests/cli/test_main.py +2 -2
  30. chtypes-2.0.0.dev8/tests/cli/test_resolve_prune.py +125 -0
  31. {chtypes-2.0.0.dev6 → chtypes-2.0.0.dev8}/tests/ocifetch/test_conformance.py +76 -1
  32. {chtypes-2.0.0.dev6 → chtypes-2.0.0.dev8}/tests/ocifetch/test_ensure.py +23 -0
  33. {chtypes-2.0.0.dev6 → chtypes-2.0.0.dev8}/tests/test_devchannel_registry.py +1 -1
  34. chtypes-2.0.0.dev8/tests/test_support_v2_gen.py +81 -0
  35. {chtypes-2.0.0.dev6 → chtypes-2.0.0.dev8}/uv.lock +1 -1
  36. {chtypes-2.0.0.dev6 → chtypes-2.0.0.dev8}/.gitignore +0 -0
  37. {chtypes-2.0.0.dev6 → chtypes-2.0.0.dev8}/LICENSE +0 -0
  38. {chtypes-2.0.0.dev6 → chtypes-2.0.0.dev8}/src/chtypes/_abi2/__init__.py +0 -0
  39. {chtypes-2.0.0.dev6 → chtypes-2.0.0.dev8}/src/chtypes/_abi2/_decls.py +0 -0
  40. {chtypes-2.0.0.dev6 → chtypes-2.0.0.dev8}/src/chtypes/_abi2/_errmap.py +0 -0
  41. {chtypes-2.0.0.dev6 → chtypes-2.0.0.dev8}/src/chtypes/_abi2/_loader.py +0 -0
  42. {chtypes-2.0.0.dev6 → chtypes-2.0.0.dev8}/src/chtypes/_abi2/_vocab.py +0 -0
  43. {chtypes-2.0.0.dev6 → chtypes-2.0.0.dev8}/src/chtypes/_ed25519.py +0 -0
  44. {chtypes-2.0.0.dev6 → chtypes-2.0.0.dev8}/src/chtypes/_ocifetch/_channel.py +0 -0
  45. {chtypes-2.0.0.dev6 → chtypes-2.0.0.dev8}/src/chtypes/_ocifetch/_constants.py +0 -0
  46. {chtypes-2.0.0.dev6 → chtypes-2.0.0.dev8}/src/chtypes/_ocifetch/_faults.py +0 -0
  47. {chtypes-2.0.0.dev6 → chtypes-2.0.0.dev8}/src/chtypes/_ocifetch/_goldens.py +0 -0
  48. {chtypes-2.0.0.dev6 → chtypes-2.0.0.dev8}/src/chtypes/_ocifetch/_http.py +0 -0
  49. {chtypes-2.0.0.dev6 → chtypes-2.0.0.dev8}/src/chtypes/_ocifetch/_layout.py +0 -0
  50. {chtypes-2.0.0.dev6 → chtypes-2.0.0.dev8}/src/chtypes/_ocifetch/_lock.py +0 -0
  51. {chtypes-2.0.0.dev6 → chtypes-2.0.0.dev8}/src/chtypes/_ocifetch/_oci.py +0 -0
  52. {chtypes-2.0.0.dev6 → chtypes-2.0.0.dev8}/src/chtypes/_ocifetch/_referrers.py +0 -0
  53. {chtypes-2.0.0.dev6 → chtypes-2.0.0.dev8}/src/chtypes/_ocifetch/_retired.py +0 -0
  54. {chtypes-2.0.0.dev6 → chtypes-2.0.0.dev8}/src/chtypes/_ocifetch/_unpack.py +0 -0
  55. {chtypes-2.0.0.dev6 → chtypes-2.0.0.dev8}/src/chtypes/library.py +0 -0
  56. {chtypes-2.0.0.dev6 → chtypes-2.0.0.dev8}/src/chtypes/py.typed +0 -0
  57. {chtypes-2.0.0.dev6 → chtypes-2.0.0.dev8}/src/chtypes/results.py +0 -0
  58. {chtypes-2.0.0.dev6 → chtypes-2.0.0.dev8}/tests/abi2/__init__.py +0 -0
  59. {chtypes-2.0.0.dev6 → chtypes-2.0.0.dev8}/tests/abi2/_stub_tree.py +0 -0
  60. {chtypes-2.0.0.dev6 → chtypes-2.0.0.dev8}/tests/abi2/conftest.py +0 -0
  61. {chtypes-2.0.0.dev6 → chtypes-2.0.0.dev8}/tests/abi2/test_conformance.py +0 -0
  62. {chtypes-2.0.0.dev6 → chtypes-2.0.0.dev8}/tests/abi2/test_dev_fingerprint.py +0 -0
  63. {chtypes-2.0.0.dev6 → chtypes-2.0.0.dev8}/tests/abi2/test_goldens_runner_stub.py +0 -0
  64. {chtypes-2.0.0.dev6 → chtypes-2.0.0.dev8}/tests/abi2/test_loader.py +0 -0
  65. {chtypes-2.0.0.dev6 → chtypes-2.0.0.dev8}/tests/abi2/test_public_library.py +0 -0
  66. {chtypes-2.0.0.dev6 → chtypes-2.0.0.dev8}/tests/abi2/test_public_open.py +0 -0
  67. {chtypes-2.0.0.dev6 → chtypes-2.0.0.dev8}/tests/abi2/test_public_server.py +0 -0
  68. {chtypes-2.0.0.dev6 → chtypes-2.0.0.dev8}/tests/abi2/test_public_setup_cases.py +0 -0
  69. {chtypes-2.0.0.dev6 → chtypes-2.0.0.dev8}/tests/api/__init__.py +0 -0
  70. {chtypes-2.0.0.dev6 → chtypes-2.0.0.dev8}/tests/api/test_input.py +0 -0
  71. {chtypes-2.0.0.dev6 → chtypes-2.0.0.dev8}/tests/cli/__init__.py +0 -0
  72. {chtypes-2.0.0.dev6 → chtypes-2.0.0.dev8}/tests/cli/conftest.py +0 -0
  73. {chtypes-2.0.0.dev6 → chtypes-2.0.0.dev8}/tests/cli/test_devchannel.py +0 -0
  74. {chtypes-2.0.0.dev6 → chtypes-2.0.0.dev8}/tests/conftest.py +0 -0
  75. {chtypes-2.0.0.dev6 → chtypes-2.0.0.dev8}/tests/goldens_v1/__init__.py +0 -0
  76. {chtypes-2.0.0.dev6 → chtypes-2.0.0.dev8}/tests/goldens_v1/data/stub-goldens.json +0 -0
  77. {chtypes-2.0.0.dev6 → chtypes-2.0.0.dev8}/tests/goldens_v1/test_runner.py +0 -0
  78. {chtypes-2.0.0.dev6 → chtypes-2.0.0.dev8}/tests/ocifetch/__init__.py +0 -0
  79. {chtypes-2.0.0.dev6 → chtypes-2.0.0.dev8}/tests/ocifetch/_bundle_support.py +0 -0
  80. {chtypes-2.0.0.dev6 → chtypes-2.0.0.dev8}/tests/ocifetch/_registry_support.py +0 -0
  81. {chtypes-2.0.0.dev6 → chtypes-2.0.0.dev8}/tests/ocifetch/_sign_support.py +0 -0
  82. {chtypes-2.0.0.dev6 → chtypes-2.0.0.dev8}/tests/ocifetch/conftest.py +0 -0
  83. {chtypes-2.0.0.dev6 → chtypes-2.0.0.dev8}/tests/ocifetch/test_cache_roots.py +0 -0
  84. {chtypes-2.0.0.dev6 → chtypes-2.0.0.dev8}/tests/ocifetch/test_cache_strict.py +0 -0
  85. {chtypes-2.0.0.dev6 → chtypes-2.0.0.dev8}/tests/ocifetch/test_devchannel.py +0 -0
  86. {chtypes-2.0.0.dev6 → chtypes-2.0.0.dev8}/tests/ocifetch/test_dsse.py +0 -0
  87. {chtypes-2.0.0.dev6 → chtypes-2.0.0.dev8}/tests/ocifetch/test_goldens.py +0 -0
  88. {chtypes-2.0.0.dev6 → chtypes-2.0.0.dev8}/tests/ocifetch/test_http.py +0 -0
  89. {chtypes-2.0.0.dev6 → chtypes-2.0.0.dev8}/tests/ocifetch/test_layout.py +0 -0
  90. {chtypes-2.0.0.dev6 → chtypes-2.0.0.dev8}/tests/ocifetch/test_lock.py +0 -0
  91. {chtypes-2.0.0.dev6 → chtypes-2.0.0.dev8}/tests/ocifetch/test_oci.py +0 -0
  92. {chtypes-2.0.0.dev6 → chtypes-2.0.0.dev8}/tests/ocifetch/test_prodv2_smoke.py +0 -0
  93. {chtypes-2.0.0.dev6 → chtypes-2.0.0.dev8}/tests/ocifetch/test_prodv2channel.py +0 -0
  94. {chtypes-2.0.0.dev6 → chtypes-2.0.0.dev8}/tests/ocifetch/test_retired.py +0 -0
  95. {chtypes-2.0.0.dev6 → chtypes-2.0.0.dev8}/tests/ocifetch/test_seam_gate.py +0 -0
  96. {chtypes-2.0.0.dev6 → chtypes-2.0.0.dev8}/tests/ocifetch/test_unpack.py +0 -0
  97. {chtypes-2.0.0.dev6 → chtypes-2.0.0.dev8}/tests/test_abi_identity.py +0 -0
  98. {chtypes-2.0.0.dev6 → chtypes-2.0.0.dev8}/tests/test_cache_roots_api.py +0 -0
  99. {chtypes-2.0.0.dev6 → chtypes-2.0.0.dev8}/tests/test_ed25519.py +0 -0
  100. {chtypes-2.0.0.dev6 → chtypes-2.0.0.dev8}/tests/test_examples_tour.py +0 -0
  101. {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.dev6
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 where` complete the four 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).
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 where` complete the four 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).
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
 
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "chtypes"
3
- version = "2.0.0.dev6"
3
+ version = "2.0.0.dev8"
4
4
  description = "Python binding for the chtypes artifacts: ClickHouse's own type system, per version, behind the frozen chs_* C ABI"
5
5
  readme = "README.md"
6
6
  license = "Apache-2.0"
@@ -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, TrustedKey, cache_root, search_dirs
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["CHTYPES_ARTIFACT_CORRUPT"]
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 version tags the registry publishes.",
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
- _LINE_SPELLING = re.compile(r"^[0-9]+\.[0-9]+$")
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 tags that are v1 version spellings
206
- (`spelling.regex`): the referrers fallback tags (`sha256-<hex>`) and anything
207
- else a repository carries are not versions."""
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 re.fullmatch(C.SPELLING_REGEX, t)]
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 = [t for t in _published_tags(options) if _LINE_SPELLING.match(t)]
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["CHTYPES_ARTIFACT_CORRUPT"]
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
- _COMMANDS = {"fetch": _cmd_fetch, "verify": _cmd_verify, "list": _cmd_list, "where": _cmd_where}
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:
@@ -15,8 +15,8 @@ from chtypes.errors import (
15
15
  SchemaError,
16
16
  UnsupportedError,
17
17
  UsageError,
18
- misuse,
19
18
  )
19
+ from chtypes.errors import _misuse as misuse
20
20
 
21
21
  __all__ = [
22
22
  "ArtifactCorruptError",
@@ -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, internal
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=_bytes(obj, "export_declined", where),
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),
@@ -14,7 +14,7 @@ import threading
14
14
  from collections.abc import Callable
15
15
  from types import TracebackType
16
16
 
17
- from .errors import misuse
17
+ from .errors import _misuse as misuse
18
18
 
19
19
  __all__ = ["CloseGuard"]
20
20
 
@@ -14,7 +14,7 @@ import json
14
14
  from collections.abc import Mapping, Sequence
15
15
  from dataclasses import dataclass
16
16
 
17
- from .errors import misuse
17
+ from .errors import _misuse as misuse
18
18
 
19
19
  __all__ = ["BytesIn", "ServerProfile", "Settings"]
20
20
 
@@ -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. Every other module here
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: {C.ENV_TRUSTED_KEYS_NAME} holds a bad key: {exc}") from None
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 = [*(doc.get("manifests") or []), {"digest": platform_desc.digest}]
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
- manifest_hex = parse_digest(pin.manifest)
920
- existing_dir = unpacked_dir_for(cache_root_path, manifest_hex)
921
- existing_record = read_verified_record(existing_dir)
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
- CODE_ARTIFACT_MISSING = "CHTYPES_ARTIFACT_MISSING"
51
- CODE_ARTIFACT_UNTRUSTED = "CHTYPES_ARTIFACT_UNTRUSTED"
52
- CODE_ARTIFACT_CORRUPT = "CHTYPES_ARTIFACT_CORRUPT"
53
- CODE_ARTIFACT_PINNED = "CHTYPES_ARTIFACT_PINNED"
54
- CODE_ARTIFACT_UNPUBLISHED = "CHTYPES_ARTIFACT_UNPUBLISHED"
55
- CODE_SOURCE_UNREACHABLE = "CHTYPES_SOURCE_UNREACHABLE"
56
- CODE_SOURCE_UNAUTHORIZED = "CHTYPES_SOURCE_UNAUTHORIZED"
57
- CODE_SOURCE_FORBIDDEN = "CHTYPES_SOURCE_FORBIDDEN"
58
- CODE_SOURCE_INCOMPATIBLE = "CHTYPES_SOURCE_INCOMPATIBLE"
59
- CODE_CACHE_UNUSABLE = "CHTYPES_CACHE_UNUSABLE"
60
- CODE_SOURCE_RETIRED = "CHTYPES_SOURCE_RETIRED"
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,