Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .release-please-manifest.json
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
{
".": "0.22.0"
".": "0.23.0"
}
64 changes: 64 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,69 @@
# Changelog

## [0.23.0](https://github.com/cachekit-io/cachekit-py/compare/v0.22.0...v0.23.0) (2026-10-10)


### ⚠ BREAKING CHANGES

* **config:** refuse a malformed deployment UUID at load without quoting it (LAB-8772) ([#570](https://github.com/cachekit-io/cachekit-py/issues/570))
* **config:** refuse an invalid assignment to the loaded settings and write nothing (LAB-8739) ([#567](https://github.com/cachekit-io/cachekit-py/issues/567))
* **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))
* **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.
* **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.
* **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.
* **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()`.
* **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`.
* **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.
* **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.
* **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.

### Bug Fixes

* **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))
* **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))
* **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))
* **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))
* **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))
* **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))
* **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))
* **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))
* **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))
* **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))
* **config:** refuse a malformed deployment UUID at load without quoting it (LAB-8772) ([#570](https://github.com/cachekit-io/cachekit-py/issues/570)) ([277aac3](https://github.com/cachekit-io/cachekit-py/commit/277aac3206d5095ec6b48e266747056be68b907e))
* **config:** refuse an invalid assignment to the loaded settings and write nothing (LAB-8739) ([#567](https://github.com/cachekit-io/cachekit-py/issues/567)) ([29c6a59](https://github.com/cachekit-io/cachekit-py/commit/29c6a5946c38a15c6457910ca502b1ee9e37a410))
* **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))
* **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))
* **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))
* **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))
* **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))
* **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))
* **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))
* **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))
* **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))
* **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))
* **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))
* **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))
* **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))
* **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))
* **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))
* **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))
* **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))
* **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))
* **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))


### Performance Improvements

* **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))
* **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))
* **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))
* **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))
* **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))
* **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))
* **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))
* **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))
* **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))

## [0.22.0](https://github.com/cachekit-io/cachekit-py/compare/v0.21.0...v0.22.0) (2026-10-04)


Expand Down
2 changes: 1 addition & 1 deletion Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ build-backend = "maturin"

[project]
name = "cachekit"
version = "0.22.0"
version = "0.23.0"
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."
readme = "README.md"
license = {text = "MIT"}
Expand Down
2 changes: 1 addition & 1 deletion rust/Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "cachekit-rs"
version = "0.22.0"
version = "0.23.0"
edition = "2021"
authors = ["cachekit Contributors"]
description = "High-performance storage engine for caching with compression and encryption"
Expand Down
2 changes: 1 addition & 1 deletion src/cachekit/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@ def custom_function():
```
"""

__version__ = "0.22.0"
__version__ = "0.23.0"

from collections.abc import Callable
from typing import Any, TypeVar
Expand Down
2 changes: 1 addition & 1 deletion uv.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading