Skip to content

Commit a3c41c8

Browse files
chore(main): release 0.23.0
1 parent cf73c28 commit a3c41c8

5 files changed

Lines changed: 64 additions & 4 deletions

File tree

‎.release-please-manifest.json‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
11
{
2-
".": "0.22.0"
2+
".": "0.23.0"
33
}

‎CHANGELOG.md‎

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,65 @@
11
# Changelog
22

3+
## [0.23.0](https://github.com/cachekit-io/cachekit-py/compare/v0.22.0...v0.23.0) (2026-10-10)
4+
5+
6+
### ⚠ BREAKING CHANGES
7+
8+
* **decorators:** share one call between concurrent misses on a key, run in its own task (LAB-7441) ([#547](https://github.com/cachekit-io/cachekit-py/issues/547))
9+
* **redis:** code that reads redis-py's exception off the error stops seeing it. `BackendError.original_exception` and `__cause__` for a redis-py failure are now a `RedisClientError`, not redis-py's exception. Code that checked `isinstance(err.original_exception, redis.exceptions.AuthenticationError)` should check `issubclass(err.original_exception.exc_type, ...)`. A `RedisBackend` error carried no `original_exception` before; it now carries the same `RedisClientError`. `repr(RedisBackendConfig(...))` and `str(...)` no longer show `redis_url`; the attribute is unchanged.
10+
* **observability:** the tracing surface, which never traced, is removed: `MonitoringConfig.enable_tracing` (passing it raises `TypeError`), the `enable_tracing` key of `DecoratorConfig.to_dict()`, `create_cache_wrapper(enable_tracing=)`, `FeatureOrchestrator.create_span`, `FeatureOrchestrator.set_span_attributes`, `FeatureOrchestrator.record_exception`, `handle_cache_error(span=)`, and `StructuredLogger.set_trace_id` / `clear_trace_id`. A failed sync client creation or L2 GET no longer records a second `cache_operations_total` sample with `operation="exception"`; it records one failure sample, as the async paths do.
11+
* **config:** `master_key=` on `@cache`, `@cache.secure`, `DecoratorConfig.secure()`, `EncryptionConfig`, `CacheSerializationHandler` and `validate_encryption_config` raises `TypeError` for a `bytes`, `bytearray` or `memoryview` key: pass `key.hex()`. Only `EncryptionWrapper` takes raw key bytes. With encryption on, such a key already failed, with `AttributeError`. With encryption unset, the decorator raised `ConfigurationError` for it, and now raises `TypeError`. With encryption off (`encryption=False`, `EncryptionConfig(enabled=False)`) it was accepted, and failed only when an encrypted entry was read; it now raises where it is passed. On Python 3.14, `CacheSerializationHandler` read a bytes key holding ASCII hex digits as that hex; it now raises too.
12+
* **decorators:** `@cache(config=..., encryption=...)` raises `ConfigurationError` when the config is encrypted (`DecoratorConfig.secure(...)`, or a config built with `encryption=EncryptionConfig(enabled=True, ...)`), and `@cache(config=DecoratorConfig.io(...), backend=...)` raises `ConfigurationError`. A keyword that names no `DecoratorConfig` field raises `ConfigurationError`, not `TypeError`, from `@cache`, from every preset but `@cache.local`, from the `DecoratorConfig` preset classmethods and beside `config=`; so does `encryption=` on `@cache.secure` and `DecoratorConfig.secure()`. `EncryptionWrapper(master_key=...)` raises `EncryptionError`, and `previous_master_keys=` raises `KeyringConfigurationError`, for a raw key that is not exactly 32 bytes: pass `bytes.fromhex(hex_key)`, never `hex_key.encode()`.
13+
* **serializers:** `EncryptionWrapper` now raises `ConfigurationError` at construction when its serializer's class does not declare `cross_sdk_compatible = True`. `AutoSerializer` and unmarked custom serializers are refused. This is a breaking change for code that builds the wrapper directly around `AutoSerializer`.
14+
* **cachekitio:** a subdomain of the two API hosts now needs `CACHEKIT_ALLOW_CUSTOM_HOST=true`. A custom host with non-ASCII characters must be given in its ASCII (`xn--`) form.
15+
* **decorators:** `DecoratorConfig().backend` is `UNSET`, not `None`. `UNSET` is falsy, so `if config.backend:` reads as before, and `to_dict()` still reports an unset backend as `None`. Code that tests `config.backend is None` on an unset backend must test `is UNSET` instead. Type checkers now see `UNSET` in the field's type, so narrowing with `is not None` and then using the value as a backend is flagged; narrow with `if config.backend:` or `is not UNSET`. `DecoratorConfig(..., backend=None)` and every preset with `backend=None` now mean L1-only instead of "resolve the default backend". To keep the old behaviour, omit `backend` from the config. `DecoratorConfig.secure(..., backend=None)` and an interop config with `backend=None`, which decorated before, now raise `ConfigurationError` at decoration.
16+
* **encryption:** encrypted entries written by earlier releases through a prefixing backend (`MemcachedBackend` with a `key_prefix`, the tenant-scoped Redis backend from env auto-detection or `RedisBackendProvider`, or a custom backend with a non-empty `key_prefix`) no longer authenticate. By default each one is read once as a miss, counted as `auth_tamper`, recomputed and overwritten. With `fail_closed=True`, every read of one raises `DecryptionAuthenticationError` until it expires or is deleted. Delete those entries, or move the cache to a fresh key space (a new `namespace` or Memcached `key_prefix`), rather than wait out the TTL. During a rolling upgrade, old and new processes read each other's writes as failed authentication, so give the new release a fresh key space or stop the old processes first. There is no fallback to the earlier AAD. Unaffected: a `RedisBackend` you construct yourself, `FileBackend`, `CachekitIOBackend`, and interop mode.
17+
18+
### Bug Fixes
19+
20+
* **backends:** declare with_timeout's decorated shape and restore the pool timeout after overlapping windows (LAB-4489) ([#558](https://github.com/cachekit-io/cachekit-py/issues/558)) ([93d4d93](https://github.com/cachekit-io/cachekit-py/commit/93d4d93b2b0339a1cb94ea4a167b0d4a46780012))
21+
* **backends:** drop with_timeout windows a forked child inherits (LAB-4489) ([#559](https://github.com/cachekit-io/cachekit-py/issues/559)) ([70ffd59](https://github.com/cachekit-io/cachekit-py/commit/70ffd594e65fdcb26a6d86cb3f244602fa45b51c))
22+
* **cachekitio:** hold a bytes api_key only wrapped when a later check raises (LAB-8288) ([#529](https://github.com/cachekit-io/cachekit-py/issues/529)) ([707aad2](https://github.com/cachekit-io/cachekit-py/commit/707aad2696b206226df7cfca729b9863b576499c))
23+
* **cachekitio:** keep the API key off a failed request's exception chain (LAB-8304) ([#539](https://github.com/cachekit-io/cachekit-py/issues/539)) ([6919e13](https://github.com/cachekit-io/cachekit-py/commit/6919e13d3509de98ea2bd1e9ac97a103153e01f3))
24+
* **cachekitio:** keep the API key off an interrupt raised mid-request (LAB-8339) ([#541](https://github.com/cachekit-io/cachekit-py/issues/541)) ([966f25f](https://github.com/cachekit-io/cachekit-py/commit/966f25f4275a2ad7bc0752d043962de6b22e1d58))
25+
* **cachekitio:** re-lease a client that close_http_clients() closed (LAB-8032) ([#533](https://github.com/cachekit-io/cachekit-py/issues/533)) ([f2bf102](https://github.com/cachekit-io/cachekit-py/commit/f2bf102d75b0da14e1bf94536de5c646abb19d9a))
26+
* **cachekitio:** send X-CacheKit-TTL alone and ceil sub-second TTLs (LAB-8206) ([#520](https://github.com/cachekit-io/cachekit-py/issues/520)) ([ead1a09](https://github.com/cachekit-io/cachekit-py/commit/ead1a097662fa92f1562688e8de9d567db80cde6))
27+
* **cachekitio:** validate the host the HTTP client connects to (LAB-8207) ([#519](https://github.com/cachekit-io/cachekit-py/issues/519)) ([f38fd55](https://github.com/cachekit-io/cachekit-py/commit/f38fd55129119a2982ac83b3511a5711fd6ffa2f))
28+
* **circuit-breaker:** count a probe's outcome only toward the cycle that admitted it (LAB-5374) ([#565](https://github.com/cachekit-io/cachekit-py/issues/565)) ([a2c5f2c](https://github.com/cachekit-io/cachekit-py/commit/a2c5f2ce31405772c67be280e20391ab007589db))
29+
* **config:** refuse a bytes master_key where a hex string is taken (LAB-8281) ([#526](https://github.com/cachekit-io/cachekit-py/issues/526)) ([22d330b](https://github.com/cachekit-io/cachekit-py/commit/22d330b45c43387eb25df20ef9f7ea4d40e840f5))
30+
* **decorators:** count the async backed miss in cache_info() (LAB-8298) ([#538](https://github.com/cachekit-io/cachekit-py/issues/538)) ([904e4bb](https://github.com/cachekit-io/cachekit-py/commit/904e4bbe86af580fdde46aed3fcbb531b839c585))
31+
* **decorators:** keep backend=None L1-only on reused decorators and in DecoratorConfig (LAB-8059) ([#515](https://github.com/cachekit-io/cachekit-py/issues/515)) ([da70f3a](https://github.com/cachekit-io/cachekit-py/commit/da70f3a0abad86ba5a2c4b4943453806fb68bf7f))
32+
* **decorators:** refuse an out-of-model interop return on uncached calls (LAB-5375) ([#530](https://github.com/cachekit-io/cachekit-py/issues/530)) ([2e1fba5](https://github.com/cachekit-io/cachekit-py/commit/2e1fba57701de4847f29d6a3dcb1e1cc0a7b6e25))
33+
* **decorators:** reject keywords a preset does not take, and raw keys that are not 32 bytes (LAB-8223) ([#524](https://github.com/cachekit-io/cachekit-py/issues/524)) ([9b3eb30](https://github.com/cachekit-io/cachekit-py/commit/9b3eb304a138d6a2b48feec426c243fc0463e520))
34+
* **decorators:** restore the stats context on every sync exit, interrupts included (LAB-8741) ([#568](https://github.com/cachekit-io/cachekit-py/issues/568)) ([d2dc83a](https://github.com/cachekit-io/cachekit-py/commit/d2dc83a58afb7ce352afaf573d5b36aab08b4599))
35+
* **decorators:** time each latency sample over the operation it names (LAB-7148) ([#554](https://github.com/cachekit-io/cachekit-py/issues/554)) ([e1a4f8f](https://github.com/cachekit-io/cachekit-py/commit/e1a4f8fe70adc5ecaaa4fac55f2a7142bd03a9fc))
36+
* **encryption:** bind the backend key prefix into the AES-GCM AAD (LAB-8115) ([#518](https://github.com/cachekit-io/cachekit-py/issues/518)) ([30201ee](https://github.com/cachekit-io/cachekit-py/commit/30201ee9999b16df213f55b69d14615c3174c808))
37+
* **encryption:** refuse bare Arrow after decrypt and a non-bool compressed AAD token (LAB-8182) ([#517](https://github.com/cachekit-io/cachekit-py/issues/517)) ([256f01d](https://github.com/cachekit-io/cachekit-py/commit/256f01df5e41f73a7a4584ac4904b8d95c5daee0))
38+
* **encryption:** refuse master_key= repeated in previous keys at decoration (LAB-8701) ([#563](https://github.com/cachekit-io/cachekit-py/issues/563)) ([97bc0b6](https://github.com/cachekit-io/cachekit-py/commit/97bc0b6ca0af651a872bba2a09acd963e18c2441))
39+
* **file:** expire a File-backend entry at its expiry second (LAB-8073) ([#514](https://github.com/cachekit-io/cachekit-py/issues/514)) ([9312ba7](https://github.com/cachekit-io/cachekit-py/commit/9312ba7a02ca6663cef00baec1e18116d3712c1c))
40+
* **interop:** decode non-string map keys; run interop-mode 1.3.0 and decode-bounds 1.2.0 vectors (LAB-8307) ([#537](https://github.com/cachekit-io/cachekit-py/issues/537)) ([819460b](https://github.com/cachekit-io/cachekit-py/commit/819460b4ac6003a92964ca84be9ad8b912d82053))
41+
* **observability:** remove the no-op tracing surface (LAB-6369) ([#546](https://github.com/cachekit-io/cachekit-py/issues/546)) ([ba6368b](https://github.com/cachekit-io/cachekit-py/commit/ba6368b210fa600fc2f414c6474c92eda74740a2))
42+
* **redis:** clear an interrupt's frames on its way out of a Redis operation (LAB-8396) ([#564](https://github.com/cachekit-io/cachekit-py/issues/564)) ([09c2b8a](https://github.com/cachekit-io/cachekit-py/commit/09c2b8aaab33b485fadaa94c3739aedfc76c512b))
43+
* **redis:** free a failed op's frames on LOADING/WRONGPASS, and log a release failure after the lock block raised (LAB-8383) ([#551](https://github.com/cachekit-io/cachekit-py/issues/551)) ([67006b5](https://github.com/cachekit-io/cachekit-py/commit/67006b5ee31c4f7f828cae5353374e3678df001f))
44+
* **redis:** keep the Redis password off a failed operation's traceback (LAB-8338) ([#542](https://github.com/cachekit-io/cachekit-py/issues/542)) ([84ec76c](https://github.com/cachekit-io/cachekit-py/commit/84ec76c14ec624fa7f3b3a027a39326345ca2ea7))
45+
* **serializers:** EncryptionWrapper refuses a serializer that is not cross-SDK (LAB-8195) ([#521](https://github.com/cachekit-io/cachekit-py/issues/521)) ([fc45785](https://github.com/cachekit-io/cachekit-py/commit/fc45785cbf01570c5b2c2a967516120c4bf77d10))
46+
* **serializers:** raise SerializationError when numpy or pandas cannot load on decode (LAB-8131) ([#534](https://github.com/cachekit-io/cachekit-py/issues/534)) ([7aeb26c](https://github.com/cachekit-io/cachekit-py/commit/7aeb26c112608f02bdad14777fed9f3cf69c77d1))
47+
* **serializers:** skip the envelope probe on decrypted StandardSerializer reads (LAB-8371) ([#544](https://github.com/cachekit-io/cachekit-py/issues/544)) ([1e779a0](https://github.com/cachekit-io/cachekit-py/commit/1e779a0ca45ff231788dab13197e8de20d076f89))
48+
* **tests:** draw redis_proc ports below the Kubernetes NodePort range (LAB-8072) ([#516](https://github.com/cachekit-io/cachekit-py/issues/516)) ([78d1fa6](https://github.com/cachekit-io/cachekit-py/commit/78d1fa6b33db38b29b5261249f7fe142a2df4128))
49+
50+
51+
### Performance Improvements
52+
53+
* **cachekitio:** free a failed request's frames without the cyclic GC (LAB-8378) ([#552](https://github.com/cachekit-io/cachekit-py/issues/552)) ([c0b5cab](https://github.com/cachekit-io/cachekit-py/commit/c0b5cabfcf2fd10cff5b3c40893a90018d39d395))
54+
* **decorators:** build the L2 cache handler once per backend; class-based backpressure permit (LAB-7112) ([#553](https://github.com/cachekit-io/cachekit-py/issues/553)) ([25bbabe](https://github.com/cachekit-io/cachekit-py/commit/25bbabeb63531e37fa6cc4c8ea7b86b98866b976))
55+
* **decorators:** share one call between concurrent misses on a key, run in its own task (LAB-7441) ([#547](https://github.com/cachekit-io/cachekit-py/issues/547)) ([b8967d3](https://github.com/cachekit-io/cachekit-py/commit/b8967d33fd178af553c56c885e9979f85bdd56f1))
56+
* **file:** keep entry count and bytes in counters instead of two directory scans per set (LAB-7107) ([#512](https://github.com/cachekit-io/cachekit-py/issues/512)) ([b1cfc89](https://github.com/cachekit-io/cachekit-py/commit/b1cfc896f602de44027b0da39ba5528466a51384))
57+
* **file:** narrow the FileBackend lock to the rename commit and resolve cache_dir once (LAB-7076) ([#535](https://github.com/cachekit-io/cachekit-py/issues/535)) ([6ad08ae](https://github.com/cachekit-io/cachekit-py/commit/6ad08ae8f3c42a56f298e44e242e15bdb4a1619a))
58+
* **ir:** ratchet four stale perf-ir budgets to main's cost (LAB-8629) ([#556](https://github.com/cachekit-io/cachekit-py/issues/556)) ([1c049a1](https://github.com/cachekit-io/cachekit-py/commit/1c049a19a12861fe0e5ab34cc9f711dc864019fa))
59+
* **serializers:** load numpy, pandas and pyarrow on first use, not at import cachekit (LAB-7093) ([#509](https://github.com/cachekit-io/cachekit-py/issues/509)) ([9533b55](https://github.com/cachekit-io/cachekit-py/commit/9533b553e78f60f360c7a8603a21c59e3bfc8913))
60+
* **serializers:** skip the standard object_hook's .get() calls on non-marker maps (LAB-7113) ([#508](https://github.com/cachekit-io/cachekit-py/issues/508)) ([3db2941](https://github.com/cachekit-io/cachekit-py/commit/3db2941b214667ec929a4058a3d418e30611e34a))
61+
* **telemetry:** skip disabled cache-op logging and bind each Prometheus series once (LAB-7067) ([#555](https://github.com/cachekit-io/cachekit-py/issues/555)) ([fef0bb1](https://github.com/cachekit-io/cachekit-py/commit/fef0bb1cf818158af924a66bd48bbeafa49ecb3d))
62+
363
## [0.22.0](https://github.com/cachekit-io/cachekit-py/compare/v0.21.0...v0.22.0) (2026-10-04)
464

565

‎pyproject.toml‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ build-backend = "maturin"
44

55
[project]
66
name = "cachekit"
7-
version = "0.22.0"
7+
version = "0.23.0"
88
description = "Backend-agnostic caching for Python — intent-based decorators with circuit breaker, distributed locking, Prometheus metrics, and optional zero-knowledge AES-256-GCM encryption, on a Rust-powered core. Zero-config L1 in-memory; scales to Redis, Memcached, File, or CachekitIO."
99
readme = "README.md"
1010
license = {text = "MIT"}

‎rust/Cargo.toml‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[package]
22
name = "cachekit-rs"
3-
version = "0.22.0"
3+
version = "0.23.0"
44
edition = "2021"
55
authors = ["cachekit Contributors"]
66
description = "High-performance storage engine for caching with compression and encryption"

‎src/cachekit/__init__.py‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,7 @@ def custom_function():
6868
```
6969
"""
7070

71-
__version__ = "0.22.0"
71+
__version__ = "0.23.0"
7272

7373
from collections.abc import Callable
7474
from typing import Any, TypeVar

0 commit comments

Comments
 (0)