cachekit 0.19.0__tar.gz → 0.20.0__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 (90) hide show
  1. {cachekit-0.19.0 → cachekit-0.20.0}/Cargo.lock +3 -3
  2. {cachekit-0.19.0 → cachekit-0.20.0}/PKG-INFO +46 -21
  3. {cachekit-0.19.0 → cachekit-0.20.0}/README.md +44 -20
  4. {cachekit-0.19.0 → cachekit-0.20.0}/pyproject.toml +2 -1
  5. {cachekit-0.19.0 → cachekit-0.20.0}/rust/Cargo.toml +2 -2
  6. {cachekit-0.19.0 → cachekit-0.20.0}/rust/README.md +44 -20
  7. {cachekit-0.19.0 → cachekit-0.20.0}/rust/src/python_bindings.rs +18 -9
  8. {cachekit-0.19.0 → cachekit-0.20.0}/src/cachekit/__init__.py +5 -4
  9. {cachekit-0.19.0 → cachekit-0.20.0}/src/cachekit/backends/__init__.py +10 -4
  10. {cachekit-0.19.0 → cachekit-0.20.0}/src/cachekit/backends/base.py +73 -5
  11. {cachekit-0.19.0 → cachekit-0.20.0}/src/cachekit/backends/base_config.py +7 -1
  12. {cachekit-0.19.0 → cachekit-0.20.0}/src/cachekit/backends/cachekitio/backend.py +115 -34
  13. cachekit-0.20.0/src/cachekit/backends/cachekitio/client.py +187 -0
  14. {cachekit-0.19.0 → cachekit-0.20.0}/src/cachekit/backends/cachekitio/config.py +35 -4
  15. {cachekit-0.19.0 → cachekit-0.20.0}/src/cachekit/backends/errors.py +10 -0
  16. {cachekit-0.19.0 → cachekit-0.20.0}/src/cachekit/backends/memcached/config.py +5 -4
  17. {cachekit-0.19.0 → cachekit-0.20.0}/src/cachekit/backends/provider.py +13 -17
  18. {cachekit-0.19.0 → cachekit-0.20.0}/src/cachekit/backends/redis/error_handler.py +20 -16
  19. cachekit-0.20.0/src/cachekit/backends/redis/provider.py +804 -0
  20. {cachekit-0.19.0 → cachekit-0.20.0}/src/cachekit/cache_handler.py +354 -316
  21. {cachekit-0.19.0 → cachekit-0.20.0}/src/cachekit/config/decorator.py +85 -93
  22. {cachekit-0.19.0 → cachekit-0.20.0}/src/cachekit/config/nested.py +55 -16
  23. {cachekit-0.19.0 → cachekit-0.20.0}/src/cachekit/config/settings.py +13 -139
  24. {cachekit-0.19.0 → cachekit-0.20.0}/src/cachekit/config/singleton.py +2 -2
  25. cachekit-0.20.0/src/cachekit/config/validation.py +243 -0
  26. {cachekit-0.19.0 → cachekit-0.20.0}/src/cachekit/decorators/intent.py +60 -25
  27. {cachekit-0.19.0 → cachekit-0.20.0}/src/cachekit/decorators/orchestrator.py +30 -92
  28. {cachekit-0.19.0 → cachekit-0.20.0}/src/cachekit/decorators/wrapper.py +523 -316
  29. {cachekit-0.19.0 → cachekit-0.20.0}/src/cachekit/hash_utils.py +6 -7
  30. {cachekit-0.19.0 → cachekit-0.20.0}/src/cachekit/interop.py +31 -7
  31. {cachekit-0.19.0 → cachekit-0.20.0}/src/cachekit/key_generator.py +79 -10
  32. {cachekit-0.19.0 → cachekit-0.20.0}/src/cachekit/l1_cache.py +150 -4
  33. {cachekit-0.19.0 → cachekit-0.20.0}/src/cachekit/logging.py +12 -98
  34. {cachekit-0.19.0 → cachekit-0.20.0}/src/cachekit/reliability/__init__.py +0 -2
  35. {cachekit-0.19.0 → cachekit-0.20.0}/src/cachekit/reliability/async_metrics.py +186 -31
  36. {cachekit-0.19.0 → cachekit-0.20.0}/src/cachekit/reliability/circuit_breaker.py +53 -36
  37. {cachekit-0.19.0 → cachekit-0.20.0}/src/cachekit/reliability/profiles.py +0 -88
  38. {cachekit-0.19.0 → cachekit-0.20.0}/src/cachekit/serializers/__init__.py +3 -1
  39. {cachekit-0.19.0 → cachekit-0.20.0}/src/cachekit/serializers/auto_serializer.py +340 -112
  40. {cachekit-0.19.0 → cachekit-0.20.0}/src/cachekit/serializers/base.py +51 -12
  41. {cachekit-0.19.0 → cachekit-0.20.0}/src/cachekit/serializers/encryption_wrapper.py +68 -35
  42. {cachekit-0.19.0 → cachekit-0.20.0}/src/cachekit/serializers/standard_serializer.py +58 -19
  43. {cachekit-0.19.0 → cachekit-0.20.0}/src/cachekit/serializers/wrapper.py +16 -2
  44. cachekit-0.19.0/src/cachekit/backends/cachekitio/client.py +0 -122
  45. cachekit-0.19.0/src/cachekit/backends/redis/provider.py +0 -515
  46. cachekit-0.19.0/src/cachekit/config/validation.py +0 -102
  47. cachekit-0.19.0/src/cachekit/monitoring/correlation_tracking.py +0 -308
  48. {cachekit-0.19.0 → cachekit-0.20.0}/Cargo.toml +0 -0
  49. {cachekit-0.19.0 → cachekit-0.20.0}/LICENSE +0 -0
  50. {cachekit-0.19.0 → cachekit-0.20.0}/rust/Makefile +0 -0
  51. {cachekit-0.19.0 → cachekit-0.20.0}/rust/TEST_EXPANSION_SUMMARY.md +0 -0
  52. {cachekit-0.19.0 → cachekit-0.20.0}/rust/src/lib.rs +0 -0
  53. {cachekit-0.19.0 → cachekit-0.20.0}/rust/src/msgpack_bounds.rs +0 -0
  54. {cachekit-0.19.0 → cachekit-0.20.0}/rust/tsan_suppressions.txt +0 -0
  55. {cachekit-0.19.0 → cachekit-0.20.0}/src/cachekit/backends/cachekitio/__init__.py +0 -0
  56. {cachekit-0.19.0 → cachekit-0.20.0}/src/cachekit/backends/cachekitio/error_handler.py +0 -0
  57. {cachekit-0.19.0 → cachekit-0.20.0}/src/cachekit/backends/cachekitio/session.py +0 -0
  58. {cachekit-0.19.0 → cachekit-0.20.0}/src/cachekit/backends/file/__init__.py +0 -0
  59. {cachekit-0.19.0 → cachekit-0.20.0}/src/cachekit/backends/file/backend.py +0 -0
  60. {cachekit-0.19.0 → cachekit-0.20.0}/src/cachekit/backends/file/config.py +0 -0
  61. {cachekit-0.19.0 → cachekit-0.20.0}/src/cachekit/backends/memcached/__init__.py +0 -0
  62. {cachekit-0.19.0 → cachekit-0.20.0}/src/cachekit/backends/memcached/backend.py +0 -0
  63. {cachekit-0.19.0 → cachekit-0.20.0}/src/cachekit/backends/memcached/error_handler.py +0 -0
  64. {cachekit-0.19.0 → cachekit-0.20.0}/src/cachekit/backends/redis/__init__.py +0 -0
  65. {cachekit-0.19.0 → cachekit-0.20.0}/src/cachekit/backends/redis/backend.py +0 -0
  66. {cachekit-0.19.0 → cachekit-0.20.0}/src/cachekit/backends/redis/client.py +0 -0
  67. {cachekit-0.19.0 → cachekit-0.20.0}/src/cachekit/backends/redis/config.py +0 -0
  68. {cachekit-0.19.0 → cachekit-0.20.0}/src/cachekit/config/__init__.py +0 -0
  69. {cachekit-0.19.0 → cachekit-0.20.0}/src/cachekit/decorators/__init__.py +0 -0
  70. {cachekit-0.19.0 → cachekit-0.20.0}/src/cachekit/decorators/local_wrapper.py +0 -0
  71. {cachekit-0.19.0 → cachekit-0.20.0}/src/cachekit/decorators/main.py +0 -0
  72. {cachekit-0.19.0 → cachekit-0.20.0}/src/cachekit/decorators/session.py +0 -0
  73. {cachekit-0.19.0 → cachekit-0.20.0}/src/cachekit/decorators/stats_context.py +0 -0
  74. {cachekit-0.19.0 → cachekit-0.20.0}/src/cachekit/decorators/tenant_context.py +0 -0
  75. {cachekit-0.19.0 → cachekit-0.20.0}/src/cachekit/decorators/utils/__init__.py +0 -0
  76. {cachekit-0.19.0 → cachekit-0.20.0}/src/cachekit/di.py +0 -0
  77. {cachekit-0.19.0 → cachekit-0.20.0}/src/cachekit/health.py +0 -0
  78. {cachekit-0.19.0 → cachekit-0.20.0}/src/cachekit/hiredis_compat.py +0 -0
  79. {cachekit-0.19.0 → cachekit-0.20.0}/src/cachekit/imports.py +0 -0
  80. {cachekit-0.19.0 → cachekit-0.20.0}/src/cachekit/monitoring/__init__.py +0 -0
  81. {cachekit-0.19.0 → cachekit-0.20.0}/src/cachekit/monitoring/pool_monitor.py +0 -0
  82. {cachekit-0.19.0 → cachekit-0.20.0}/src/cachekit/monitoring/protocols.py +0 -0
  83. {cachekit-0.19.0 → cachekit-0.20.0}/src/cachekit/object_cache.py +0 -0
  84. {cachekit-0.19.0 → cachekit-0.20.0}/src/cachekit/py.typed +0 -0
  85. {cachekit-0.19.0 → cachekit-0.20.0}/src/cachekit/reliability/error_classification.py +0 -0
  86. {cachekit-0.19.0 → cachekit-0.20.0}/src/cachekit/reliability/load_control.py +0 -0
  87. {cachekit-0.19.0 → cachekit-0.20.0}/src/cachekit/reliability/metrics_collection.py +0 -0
  88. {cachekit-0.19.0 → cachekit-0.20.0}/src/cachekit/serializers/arrow_serializer.py +0 -0
  89. {cachekit-0.19.0 → cachekit-0.20.0}/src/cachekit/serializers/interop_serializer.py +0 -0
  90. {cachekit-0.19.0 → cachekit-0.20.0}/src/cachekit/serializers/orjson_serializer.py +0 -0
@@ -245,9 +245,9 @@ checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33"
245
245
 
246
246
  [[package]]
247
247
  name = "cachekit-core"
248
- version = "0.5.0"
248
+ version = "0.6.0"
249
249
  source = "registry+https://github.com/rust-lang/crates.io-index"
250
- checksum = "12089baacc5ff661a62d2071588c895973bc48e42ed359178afaee22decb5559"
250
+ checksum = "93adc5646956ba8da140f4179a02e60a2cc7a401b83ebb1270cc4d03748e1fac"
251
251
  dependencies = [
252
252
  "aes",
253
253
  "aes-gcm",
@@ -271,7 +271,7 @@ dependencies = [
271
271
 
272
272
  [[package]]
273
273
  name = "cachekit-rs"
274
- version = "0.19.0"
274
+ version = "0.20.0"
275
275
  dependencies = [
276
276
  "cachekit-core",
277
277
  "criterion",
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: cachekit
3
- Version: 0.19.0
3
+ Version: 0.20.0
4
4
  Classifier: Development Status :: 4 - Beta
5
5
  Classifier: Intended Audience :: Developers
6
6
  Classifier: License :: OSI Approved :: MIT License
@@ -10,6 +10,7 @@ Classifier: Programming Language :: Python :: 3.10
10
10
  Classifier: Programming Language :: Python :: 3.11
11
11
  Classifier: Programming Language :: Python :: 3.12
12
12
  Classifier: Programming Language :: Python :: 3.13
13
+ Classifier: Programming Language :: Python :: 3.14
13
14
  Classifier: Programming Language :: Python :: Implementation :: CPython
14
15
  Classifier: Programming Language :: Rust
15
16
  Classifier: Topic :: Software Development :: Libraries :: Python Modules
@@ -207,26 +208,29 @@ def get_price(symbol: str):
207
208
  def process_payment(amount):
208
209
  return payment_gateway.charge(amount)
209
210
 
210
- # Security-critical: PII, medical, financial
211
- @cache.secure
211
+ # Security-critical: PII, medical, financial (needs a key: master_key= or CACHEKIT_MASTER_KEY)
212
+ @cache.secure(master_key=secret_key)
212
213
  def get_user_profile(user_id: int):
213
214
  return db.fetch_user(user_id)
214
215
  ```
215
216
 
216
217
  | Feature | `@cache.minimal` | `@cache.dev` | `@cache.test` | `@cache.production` | `@cache.secure` |
217
218
  |:--------|:----------------:|:------------:|:-------------:|:-------------------:|:---------------:|
219
+ | Default TTL | 300 s | 300 s | 300 s | 600 s | 600 s |
218
220
  | Circuit Breaker | - | ✅ | - | ✅ | ✅ |
219
221
  | Backpressure | ✅ | ✅ | - | ✅ | ✅ |
220
222
  | Integrity Checking | - | ✅ | - | ✅ | ✅ 🔒 |
221
223
  | Encryption | - | - | - | - | ✅ Required |
222
- | L1 SWR (L1-only mode) | - | ✅ | - | ✅ | ✅ |
224
+ | L1 SWR (L1-only mode) | - | ✅ | - | ✅ | - |
223
225
  | L1 Invalidation | - | - | - | ✅ | ✅ |
224
226
  | Prometheus Metrics | - | - | - | ✅ | ✅ |
225
227
  | Tracing | - | ✅ | - | ✅ | ✅ |
226
228
  | Structured Logging | - | ✅ | - | ✅ | ✅ |
227
229
  | **Use Case** | High throughput | Local debugging | Deterministic tests | Production reliability | Compliance/security |
228
230
 
229
- > 🔒 `@cache.secure` forces `integrity_checking=True` — it cannot be overridden.
231
+ > 🔒 `@cache.secure` forces `integrity_checking=True` — passing `integrity_checking=False` raises `ConfigurationError` at decoration, including as an override next to `config=DecoratorConfig.secure(...)`. `@cache.secure` also rejects `config=`; the RORO form is `@cache(config=DecoratorConfig.secure(...))`.
232
+ >
233
+ > **Default TTL** follows the cross-SDK [intent-preset spec](https://github.com/cachekit-io/protocol/blob/main/spec/intent-presets.md#default-ttl) (`@cache.io` 3600 s) — the same numbers as cachekit-rs and cachekit-ts. `ttl=` overrides it; `ttl=None` is the explicit never-expire opt-in ([details](docs/configuration.md#intent-presets)).
230
234
  >
231
235
  > **L1 SWR** (within-TTL background refresh) runs only in L1-only mode (`backend=None`) — with a backend configured it has no effect. `@cache.io` additionally ships past-TTL SWR via `stale_ttl` ([docs](docs/configuration.md#stale-while-revalidate-stale_ttl)).
232
236
  >
@@ -290,7 +294,7 @@ def test_cached_function():
290
294
  - Connection pooling with thread affinity (+28% throughput)
291
295
  - Distributed locking prevents cache stampedes
292
296
  - Pluggable backend abstraction (Redis, CachekitIO, File, Memcached, custom)
293
- - Untrusted-decode bounds: nesting depth and header-declared allocation are capped on every cache read (a forged entry is a bounded cache miss), verified against the protocol's shared [`decode-bounds.json`](https://github.com/cachekit-io/protocol/blob/2d56cce231e193141f09df9316f9afac17a1538e/test-vectors/decode-bounds.json) vectors
297
+ - Untrusted-decode bounds: nesting depth and header-declared allocation are capped on every cache read (a forged entry is a bounded cache miss), verified against the protocol's shared [`decode-bounds.json`](https://github.com/cachekit-io/protocol/blob/2736a81f2f853cf08c5563c0fe7c8361331fa3ad/test-vectors/decode-bounds.json) vectors
294
298
 
295
299
  > [!NOTE]
296
300
  > All reliability features are **enabled by default** with `@cache.production`. Use `@cache.minimal` to disable them for maximum throughput.
@@ -308,7 +312,7 @@ def test_cached_function():
308
312
  <summary><strong>Serializer Examples</strong></summary>
309
313
 
310
314
  ```python
311
- from cachekit.serializers import OrjsonSerializer, ArrowSerializer, EncryptionWrapper
315
+ from cachekit.serializers import OrjsonSerializer, ArrowSerializer
312
316
 
313
317
  # Fast JSON for API responses
314
318
  @cache.production(serializer=OrjsonSerializer())
@@ -319,11 +323,30 @@ def get_api_response(endpoint: str):
319
323
  @cache(serializer=ArrowSerializer())
320
324
  def get_large_dataset(date: str):
321
325
  return pd.read_csv(f"data/{date}.csv")
326
+ ```
322
327
 
323
- # Encrypted DataFrames for sensitive data
324
- @cache(serializer=EncryptionWrapper(serializer=ArrowSerializer()))
328
+ Encrypted DataFrames go through `@cache.secure`, which takes any serializer. A file backend keeps this example self-contained; production uses Redis or cachekit.io:
329
+
330
+ ```python
331
+ import tempfile
332
+ from cachekit.backends.file import FileBackend, FileBackendConfig
333
+ from cachekit.serializers import ArrowSerializer
334
+
335
+ calls = 0
336
+
337
+ @cache.secure(
338
+ master_key=secret_key,
339
+ serializer=ArrowSerializer(),
340
+ backend=FileBackend(FileBackendConfig(cache_dir=tempfile.mkdtemp())),
341
+ )
325
342
  def get_patient_data(hospital_id: int):
326
- return pd.read_sql("SELECT * FROM patients WHERE hospital_id = ?", conn, params=[hospital_id])
343
+ global calls
344
+ calls += 1
345
+ return pd.DataFrame({"hospital_id": [hospital_id], "patients": [42]})
346
+
347
+ get_patient_data(7)
348
+ get_patient_data(7) # second call is served from the encrypted cache
349
+ assert calls == 1
327
350
  ```
328
351
 
329
352
  </details>
@@ -346,11 +369,14 @@ def get_patient_data(hospital_id: int):
346
369
  > [!CAUTION]
347
370
  > When handling PII, medical, or financial data, always use `@cache.secure` to enforce encryption.
348
371
 
349
- **Zero-downtime key rotation**: promote a new `CACHEKIT_MASTER_KEY` and keep the
350
- retiring key readable via `CACHEKIT_PREVIOUS_MASTER_KEYS` (comma-separated hex,
351
- max 3 decrypt-only keys). Entries are selected by exact key fingerprint — never
352
- trial decryption — and old entries age out via TTL, no cache flush required. See
353
- [Zero-Knowledge Encryption](docs/features/zero-knowledge-encryption.md#key-rotation-pattern).
372
+ **Key rotation**: keep a retiring key readable with
373
+ `CACHEKIT_PREVIOUS_MASTER_KEYS` (comma-separated hex, max 3 decrypt-only keys)
374
+ while new writes use `CACHEKIT_MASTER_KEY`. A one-deploy key swap is not
375
+ zero-miss; follow the [key rotation
376
+ runbook](https://docs.cachekit.io/concepts/key-rotation/), including its Before
377
+ You Rotate checks. CK-framed entries are selected by exact key fingerprint —
378
+ never trial decryption; Interop-mode entries carry no CK frame and attempt
379
+ keyring keys sequentially instead.
354
380
 
355
381
  cachekit employs comprehensive security tooling:
356
382
 
@@ -384,7 +410,7 @@ See [SECURITY.md][security-url] for vulnerability reporting and detailed documen
384
410
 
385
411
  - **Per-function statistics** - `cache_info()` on every decorated function, modelled on `functools.lru_cache`
386
412
  - **Prometheus metrics** - Recorded by default (your app owns exposition)
387
- - **Structured logging** - Context-aware with correlation IDs
413
+ - **Structured logging** - Context-aware, per-operation fields
388
414
  - **Health checks** - Comprehensive status endpoints
389
415
 
390
416
  Every decorated function exposes `cache_info()`, returning a `CacheInfo` named tuple with
@@ -418,8 +444,8 @@ exposition setup.
418
444
  free-threaded 3.14 with the GIL verified disabled (CI job
419
445
  `test-freethreaded`), and the Rust extension declares free-threaded safety
420
446
  (`gil_used = false`). Free-threaded wheels are **not yet published** and
421
- free-threaded builds are not officially supported — blocked on upstream
422
- wheels (orjson, hiredis; numpy/pandas/pyarrow for `[data]`). See
447
+ free-threaded builds are not officially supported — blocked upstream on
448
+ orjson (no free-threaded wheels) and hiredis (re-enables the GIL on import). See
423
449
  [measured performance results](docs/free-threading.md#measured-performance) and the
424
450
  full concurrency audit: [docs/free-threading.md](docs/free-threading.md).
425
451
 
@@ -470,7 +496,7 @@ info = expensive_func.cache_info()
470
496
  | [Prometheus Metrics][prometheus-url] | Built-in observability |
471
497
  | [Zero-Knowledge Encryption][encryption-url] | Client-side security |
472
498
  | [Interop Mode][interop-url] | Cross-SDK cache sharing with cachekit-ts/rs |
473
- | [L1 Invalidation & SWR][l1-invalidation-url] | Process-local invalidation, stale-while-revalidate |
499
+ | [L1 Invalidation & SWR][l1-invalidation-url] | Invalidation scope (incl. cross-process whole-function on tenant-scoped Redis from the environment or `RedisBackendProvider`), stale-while-revalidate |
474
500
  | [Reference Caching][reference-caching-url] | `@cache.local()` for non-serializable objects |
475
501
  | [Rust Serialization][rust-serialization-url] | ByteStorage layer: LZ4, xxHash3, AES-256-GCM |
476
502
  | [SSRF Protection][ssrf-url] | URL allowlisting for the CachekitIO backend |
@@ -487,7 +513,7 @@ CACHEKIT_REDIS_URL="redis://localhost:6379" # Primary (preferred)
487
513
  REDIS_URL="redis://localhost:6379" # Fallback
488
514
 
489
515
  # CachekitIO SaaS Backend (closed beta — request access at cachekit.io)
490
- CACHEKIT_API_KEY="your-api-key" # Required for @cache.io() # pragma: allowlist secret
516
+ CACHEKIT_API_KEY="your-api-key" # For @cache.io() — or pass api_key= directly # pragma: allowlist secret
491
517
  CACHEKIT_API_URL="https://api.cachekit.io" # Default SaaS endpoint
492
518
 
493
519
  # Memcached Backend (optional: pip install cachekit[memcached])
@@ -497,7 +523,6 @@ CACHEKIT_MEMCACHED_TIMEOUT=1.0 # Default: 1.0 seconds
497
523
  CACHEKIT_MEMCACHED_KEY_PREFIX="myapp:" # Default: "" (none)
498
524
 
499
525
  # Optional Configuration
500
- CACHEKIT_DEFAULT_TTL=3600
501
526
  CACHEKIT_MAX_VALUE_SIZE=104857600
502
527
  CACHEKIT_ARROW_COMPRESSION=zstd
503
528
  ```
@@ -152,26 +152,29 @@ def get_price(symbol: str):
152
152
  def process_payment(amount):
153
153
  return payment_gateway.charge(amount)
154
154
 
155
- # Security-critical: PII, medical, financial
156
- @cache.secure
155
+ # Security-critical: PII, medical, financial (needs a key: master_key= or CACHEKIT_MASTER_KEY)
156
+ @cache.secure(master_key=secret_key)
157
157
  def get_user_profile(user_id: int):
158
158
  return db.fetch_user(user_id)
159
159
  ```
160
160
 
161
161
  | Feature | `@cache.minimal` | `@cache.dev` | `@cache.test` | `@cache.production` | `@cache.secure` |
162
162
  |:--------|:----------------:|:------------:|:-------------:|:-------------------:|:---------------:|
163
+ | Default TTL | 300 s | 300 s | 300 s | 600 s | 600 s |
163
164
  | Circuit Breaker | - | ✅ | - | ✅ | ✅ |
164
165
  | Backpressure | ✅ | ✅ | - | ✅ | ✅ |
165
166
  | Integrity Checking | - | ✅ | - | ✅ | ✅ 🔒 |
166
167
  | Encryption | - | - | - | - | ✅ Required |
167
- | L1 SWR (L1-only mode) | - | ✅ | - | ✅ | ✅ |
168
+ | L1 SWR (L1-only mode) | - | ✅ | - | ✅ | - |
168
169
  | L1 Invalidation | - | - | - | ✅ | ✅ |
169
170
  | Prometheus Metrics | - | - | - | ✅ | ✅ |
170
171
  | Tracing | - | ✅ | - | ✅ | ✅ |
171
172
  | Structured Logging | - | ✅ | - | ✅ | ✅ |
172
173
  | **Use Case** | High throughput | Local debugging | Deterministic tests | Production reliability | Compliance/security |
173
174
 
174
- > 🔒 `@cache.secure` forces `integrity_checking=True` — it cannot be overridden.
175
+ > 🔒 `@cache.secure` forces `integrity_checking=True` — passing `integrity_checking=False` raises `ConfigurationError` at decoration, including as an override next to `config=DecoratorConfig.secure(...)`. `@cache.secure` also rejects `config=`; the RORO form is `@cache(config=DecoratorConfig.secure(...))`.
176
+ >
177
+ > **Default TTL** follows the cross-SDK [intent-preset spec](https://github.com/cachekit-io/protocol/blob/main/spec/intent-presets.md#default-ttl) (`@cache.io` 3600 s) — the same numbers as cachekit-rs and cachekit-ts. `ttl=` overrides it; `ttl=None` is the explicit never-expire opt-in ([details](docs/configuration.md#intent-presets)).
175
178
  >
176
179
  > **L1 SWR** (within-TTL background refresh) runs only in L1-only mode (`backend=None`) — with a backend configured it has no effect. `@cache.io` additionally ships past-TTL SWR via `stale_ttl` ([docs](docs/configuration.md#stale-while-revalidate-stale_ttl)).
177
180
  >
@@ -235,7 +238,7 @@ def test_cached_function():
235
238
  - Connection pooling with thread affinity (+28% throughput)
236
239
  - Distributed locking prevents cache stampedes
237
240
  - Pluggable backend abstraction (Redis, CachekitIO, File, Memcached, custom)
238
- - Untrusted-decode bounds: nesting depth and header-declared allocation are capped on every cache read (a forged entry is a bounded cache miss), verified against the protocol's shared [`decode-bounds.json`](https://github.com/cachekit-io/protocol/blob/2d56cce231e193141f09df9316f9afac17a1538e/test-vectors/decode-bounds.json) vectors
241
+ - Untrusted-decode bounds: nesting depth and header-declared allocation are capped on every cache read (a forged entry is a bounded cache miss), verified against the protocol's shared [`decode-bounds.json`](https://github.com/cachekit-io/protocol/blob/2736a81f2f853cf08c5563c0fe7c8361331fa3ad/test-vectors/decode-bounds.json) vectors
239
242
 
240
243
  > [!NOTE]
241
244
  > All reliability features are **enabled by default** with `@cache.production`. Use `@cache.minimal` to disable them for maximum throughput.
@@ -253,7 +256,7 @@ def test_cached_function():
253
256
  <summary><strong>Serializer Examples</strong></summary>
254
257
 
255
258
  ```python
256
- from cachekit.serializers import OrjsonSerializer, ArrowSerializer, EncryptionWrapper
259
+ from cachekit.serializers import OrjsonSerializer, ArrowSerializer
257
260
 
258
261
  # Fast JSON for API responses
259
262
  @cache.production(serializer=OrjsonSerializer())
@@ -264,11 +267,30 @@ def get_api_response(endpoint: str):
264
267
  @cache(serializer=ArrowSerializer())
265
268
  def get_large_dataset(date: str):
266
269
  return pd.read_csv(f"data/{date}.csv")
270
+ ```
267
271
 
268
- # Encrypted DataFrames for sensitive data
269
- @cache(serializer=EncryptionWrapper(serializer=ArrowSerializer()))
272
+ Encrypted DataFrames go through `@cache.secure`, which takes any serializer. A file backend keeps this example self-contained; production uses Redis or cachekit.io:
273
+
274
+ ```python
275
+ import tempfile
276
+ from cachekit.backends.file import FileBackend, FileBackendConfig
277
+ from cachekit.serializers import ArrowSerializer
278
+
279
+ calls = 0
280
+
281
+ @cache.secure(
282
+ master_key=secret_key,
283
+ serializer=ArrowSerializer(),
284
+ backend=FileBackend(FileBackendConfig(cache_dir=tempfile.mkdtemp())),
285
+ )
270
286
  def get_patient_data(hospital_id: int):
271
- return pd.read_sql("SELECT * FROM patients WHERE hospital_id = ?", conn, params=[hospital_id])
287
+ global calls
288
+ calls += 1
289
+ return pd.DataFrame({"hospital_id": [hospital_id], "patients": [42]})
290
+
291
+ get_patient_data(7)
292
+ get_patient_data(7) # second call is served from the encrypted cache
293
+ assert calls == 1
272
294
  ```
273
295
 
274
296
  </details>
@@ -291,11 +313,14 @@ def get_patient_data(hospital_id: int):
291
313
  > [!CAUTION]
292
314
  > When handling PII, medical, or financial data, always use `@cache.secure` to enforce encryption.
293
315
 
294
- **Zero-downtime key rotation**: promote a new `CACHEKIT_MASTER_KEY` and keep the
295
- retiring key readable via `CACHEKIT_PREVIOUS_MASTER_KEYS` (comma-separated hex,
296
- max 3 decrypt-only keys). Entries are selected by exact key fingerprint — never
297
- trial decryption — and old entries age out via TTL, no cache flush required. See
298
- [Zero-Knowledge Encryption](docs/features/zero-knowledge-encryption.md#key-rotation-pattern).
316
+ **Key rotation**: keep a retiring key readable with
317
+ `CACHEKIT_PREVIOUS_MASTER_KEYS` (comma-separated hex, max 3 decrypt-only keys)
318
+ while new writes use `CACHEKIT_MASTER_KEY`. A one-deploy key swap is not
319
+ zero-miss; follow the [key rotation
320
+ runbook](https://docs.cachekit.io/concepts/key-rotation/), including its Before
321
+ You Rotate checks. CK-framed entries are selected by exact key fingerprint —
322
+ never trial decryption; Interop-mode entries carry no CK frame and attempt
323
+ keyring keys sequentially instead.
299
324
 
300
325
  cachekit employs comprehensive security tooling:
301
326
 
@@ -329,7 +354,7 @@ See [SECURITY.md][security-url] for vulnerability reporting and detailed documen
329
354
 
330
355
  - **Per-function statistics** - `cache_info()` on every decorated function, modelled on `functools.lru_cache`
331
356
  - **Prometheus metrics** - Recorded by default (your app owns exposition)
332
- - **Structured logging** - Context-aware with correlation IDs
357
+ - **Structured logging** - Context-aware, per-operation fields
333
358
  - **Health checks** - Comprehensive status endpoints
334
359
 
335
360
  Every decorated function exposes `cache_info()`, returning a `CacheInfo` named tuple with
@@ -363,8 +388,8 @@ exposition setup.
363
388
  free-threaded 3.14 with the GIL verified disabled (CI job
364
389
  `test-freethreaded`), and the Rust extension declares free-threaded safety
365
390
  (`gil_used = false`). Free-threaded wheels are **not yet published** and
366
- free-threaded builds are not officially supported — blocked on upstream
367
- wheels (orjson, hiredis; numpy/pandas/pyarrow for `[data]`). See
391
+ free-threaded builds are not officially supported — blocked upstream on
392
+ orjson (no free-threaded wheels) and hiredis (re-enables the GIL on import). See
368
393
  [measured performance results](docs/free-threading.md#measured-performance) and the
369
394
  full concurrency audit: [docs/free-threading.md](docs/free-threading.md).
370
395
 
@@ -415,7 +440,7 @@ info = expensive_func.cache_info()
415
440
  | [Prometheus Metrics][prometheus-url] | Built-in observability |
416
441
  | [Zero-Knowledge Encryption][encryption-url] | Client-side security |
417
442
  | [Interop Mode][interop-url] | Cross-SDK cache sharing with cachekit-ts/rs |
418
- | [L1 Invalidation & SWR][l1-invalidation-url] | Process-local invalidation, stale-while-revalidate |
443
+ | [L1 Invalidation & SWR][l1-invalidation-url] | Invalidation scope (incl. cross-process whole-function on tenant-scoped Redis from the environment or `RedisBackendProvider`), stale-while-revalidate |
419
444
  | [Reference Caching][reference-caching-url] | `@cache.local()` for non-serializable objects |
420
445
  | [Rust Serialization][rust-serialization-url] | ByteStorage layer: LZ4, xxHash3, AES-256-GCM |
421
446
  | [SSRF Protection][ssrf-url] | URL allowlisting for the CachekitIO backend |
@@ -432,7 +457,7 @@ CACHEKIT_REDIS_URL="redis://localhost:6379" # Primary (preferred)
432
457
  REDIS_URL="redis://localhost:6379" # Fallback
433
458
 
434
459
  # CachekitIO SaaS Backend (closed beta — request access at cachekit.io)
435
- CACHEKIT_API_KEY="your-api-key" # Required for @cache.io() # pragma: allowlist secret
460
+ CACHEKIT_API_KEY="your-api-key" # For @cache.io() — or pass api_key= directly # pragma: allowlist secret
436
461
  CACHEKIT_API_URL="https://api.cachekit.io" # Default SaaS endpoint
437
462
 
438
463
  # Memcached Backend (optional: pip install cachekit[memcached])
@@ -442,7 +467,6 @@ CACHEKIT_MEMCACHED_TIMEOUT=1.0 # Default: 1.0 seconds
442
467
  CACHEKIT_MEMCACHED_KEY_PREFIX="myapp:" # Default: "" (none)
443
468
 
444
469
  # Optional Configuration
445
- CACHEKIT_DEFAULT_TTL=3600
446
470
  CACHEKIT_MAX_VALUE_SIZE=104857600
447
471
  CACHEKIT_ARROW_COMPRESSION=zstd
448
472
  ```
@@ -4,7 +4,7 @@ build-backend = "maturin"
4
4
 
5
5
  [project]
6
6
  name = "cachekit"
7
- version = "0.19.0"
7
+ version = "0.20.0"
8
8
  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."
9
9
  readme = "README.md"
10
10
  license = {text = "MIT"}
@@ -40,6 +40,7 @@ classifiers = [
40
40
  "Programming Language :: Python :: 3.11",
41
41
  "Programming Language :: Python :: 3.12",
42
42
  "Programming Language :: Python :: 3.13",
43
+ "Programming Language :: Python :: 3.14",
43
44
  "Programming Language :: Python :: Implementation :: CPython",
44
45
  "Programming Language :: Rust",
45
46
  "Topic :: Software Development :: Libraries :: Python Modules",
@@ -1,6 +1,6 @@
1
1
  [package]
2
2
  name = "cachekit-rs"
3
- version = "0.19.0"
3
+ version = "0.20.0"
4
4
  edition = "2021"
5
5
  authors = ["cachekit Contributors"]
6
6
  description = "High-performance storage engine for caching with compression and encryption"
@@ -20,7 +20,7 @@ crate-type = ["cdylib", "rlib"]
20
20
 
21
21
  [dependencies]
22
22
  # Compression, checksums, encryption (https://crates.io/crates/cachekit-core)
23
- cachekit-core = { version = "0.5.0", features = ["compression", "checksum", "messagepack", "encryption"] }
23
+ cachekit-core = { version = "0.6.0", features = ["compression", "checksum", "messagepack", "encryption"] }
24
24
 
25
25
  # Python integration - optional for Rust-only builds
26
26
  pyo3 = { workspace = true, optional = true }
@@ -152,26 +152,29 @@ def get_price(symbol: str):
152
152
  def process_payment(amount):
153
153
  return payment_gateway.charge(amount)
154
154
 
155
- # Security-critical: PII, medical, financial
156
- @cache.secure
155
+ # Security-critical: PII, medical, financial (needs a key: master_key= or CACHEKIT_MASTER_KEY)
156
+ @cache.secure(master_key=secret_key)
157
157
  def get_user_profile(user_id: int):
158
158
  return db.fetch_user(user_id)
159
159
  ```
160
160
 
161
161
  | Feature | `@cache.minimal` | `@cache.dev` | `@cache.test` | `@cache.production` | `@cache.secure` |
162
162
  |:--------|:----------------:|:------------:|:-------------:|:-------------------:|:---------------:|
163
+ | Default TTL | 300 s | 300 s | 300 s | 600 s | 600 s |
163
164
  | Circuit Breaker | - | ✅ | - | ✅ | ✅ |
164
165
  | Backpressure | ✅ | ✅ | - | ✅ | ✅ |
165
166
  | Integrity Checking | - | ✅ | - | ✅ | ✅ 🔒 |
166
167
  | Encryption | - | - | - | - | ✅ Required |
167
- | L1 SWR (L1-only mode) | - | ✅ | - | ✅ | ✅ |
168
+ | L1 SWR (L1-only mode) | - | ✅ | - | ✅ | - |
168
169
  | L1 Invalidation | - | - | - | ✅ | ✅ |
169
170
  | Prometheus Metrics | - | - | - | ✅ | ✅ |
170
171
  | Tracing | - | ✅ | - | ✅ | ✅ |
171
172
  | Structured Logging | - | ✅ | - | ✅ | ✅ |
172
173
  | **Use Case** | High throughput | Local debugging | Deterministic tests | Production reliability | Compliance/security |
173
174
 
174
- > 🔒 `@cache.secure` forces `integrity_checking=True` — it cannot be overridden.
175
+ > 🔒 `@cache.secure` forces `integrity_checking=True` — passing `integrity_checking=False` raises `ConfigurationError` at decoration, including as an override next to `config=DecoratorConfig.secure(...)`. `@cache.secure` also rejects `config=`; the RORO form is `@cache(config=DecoratorConfig.secure(...))`.
176
+ >
177
+ > **Default TTL** follows the cross-SDK [intent-preset spec](https://github.com/cachekit-io/protocol/blob/main/spec/intent-presets.md#default-ttl) (`@cache.io` 3600 s) — the same numbers as cachekit-rs and cachekit-ts. `ttl=` overrides it; `ttl=None` is the explicit never-expire opt-in ([details](docs/configuration.md#intent-presets)).
175
178
  >
176
179
  > **L1 SWR** (within-TTL background refresh) runs only in L1-only mode (`backend=None`) — with a backend configured it has no effect. `@cache.io` additionally ships past-TTL SWR via `stale_ttl` ([docs](docs/configuration.md#stale-while-revalidate-stale_ttl)).
177
180
  >
@@ -235,7 +238,7 @@ def test_cached_function():
235
238
  - Connection pooling with thread affinity (+28% throughput)
236
239
  - Distributed locking prevents cache stampedes
237
240
  - Pluggable backend abstraction (Redis, CachekitIO, File, Memcached, custom)
238
- - Untrusted-decode bounds: nesting depth and header-declared allocation are capped on every cache read (a forged entry is a bounded cache miss), verified against the protocol's shared [`decode-bounds.json`](https://github.com/cachekit-io/protocol/blob/2d56cce231e193141f09df9316f9afac17a1538e/test-vectors/decode-bounds.json) vectors
241
+ - Untrusted-decode bounds: nesting depth and header-declared allocation are capped on every cache read (a forged entry is a bounded cache miss), verified against the protocol's shared [`decode-bounds.json`](https://github.com/cachekit-io/protocol/blob/2736a81f2f853cf08c5563c0fe7c8361331fa3ad/test-vectors/decode-bounds.json) vectors
239
242
 
240
243
  > [!NOTE]
241
244
  > All reliability features are **enabled by default** with `@cache.production`. Use `@cache.minimal` to disable them for maximum throughput.
@@ -253,7 +256,7 @@ def test_cached_function():
253
256
  <summary><strong>Serializer Examples</strong></summary>
254
257
 
255
258
  ```python
256
- from cachekit.serializers import OrjsonSerializer, ArrowSerializer, EncryptionWrapper
259
+ from cachekit.serializers import OrjsonSerializer, ArrowSerializer
257
260
 
258
261
  # Fast JSON for API responses
259
262
  @cache.production(serializer=OrjsonSerializer())
@@ -264,11 +267,30 @@ def get_api_response(endpoint: str):
264
267
  @cache(serializer=ArrowSerializer())
265
268
  def get_large_dataset(date: str):
266
269
  return pd.read_csv(f"data/{date}.csv")
270
+ ```
267
271
 
268
- # Encrypted DataFrames for sensitive data
269
- @cache(serializer=EncryptionWrapper(serializer=ArrowSerializer()))
272
+ Encrypted DataFrames go through `@cache.secure`, which takes any serializer. A file backend keeps this example self-contained; production uses Redis or cachekit.io:
273
+
274
+ ```python
275
+ import tempfile
276
+ from cachekit.backends.file import FileBackend, FileBackendConfig
277
+ from cachekit.serializers import ArrowSerializer
278
+
279
+ calls = 0
280
+
281
+ @cache.secure(
282
+ master_key=secret_key,
283
+ serializer=ArrowSerializer(),
284
+ backend=FileBackend(FileBackendConfig(cache_dir=tempfile.mkdtemp())),
285
+ )
270
286
  def get_patient_data(hospital_id: int):
271
- return pd.read_sql("SELECT * FROM patients WHERE hospital_id = ?", conn, params=[hospital_id])
287
+ global calls
288
+ calls += 1
289
+ return pd.DataFrame({"hospital_id": [hospital_id], "patients": [42]})
290
+
291
+ get_patient_data(7)
292
+ get_patient_data(7) # second call is served from the encrypted cache
293
+ assert calls == 1
272
294
  ```
273
295
 
274
296
  </details>
@@ -291,11 +313,14 @@ def get_patient_data(hospital_id: int):
291
313
  > [!CAUTION]
292
314
  > When handling PII, medical, or financial data, always use `@cache.secure` to enforce encryption.
293
315
 
294
- **Zero-downtime key rotation**: promote a new `CACHEKIT_MASTER_KEY` and keep the
295
- retiring key readable via `CACHEKIT_PREVIOUS_MASTER_KEYS` (comma-separated hex,
296
- max 3 decrypt-only keys). Entries are selected by exact key fingerprint — never
297
- trial decryption — and old entries age out via TTL, no cache flush required. See
298
- [Zero-Knowledge Encryption](docs/features/zero-knowledge-encryption.md#key-rotation-pattern).
316
+ **Key rotation**: keep a retiring key readable with
317
+ `CACHEKIT_PREVIOUS_MASTER_KEYS` (comma-separated hex, max 3 decrypt-only keys)
318
+ while new writes use `CACHEKIT_MASTER_KEY`. A one-deploy key swap is not
319
+ zero-miss; follow the [key rotation
320
+ runbook](https://docs.cachekit.io/concepts/key-rotation/), including its Before
321
+ You Rotate checks. CK-framed entries are selected by exact key fingerprint —
322
+ never trial decryption; Interop-mode entries carry no CK frame and attempt
323
+ keyring keys sequentially instead.
299
324
 
300
325
  cachekit employs comprehensive security tooling:
301
326
 
@@ -329,7 +354,7 @@ See [SECURITY.md][security-url] for vulnerability reporting and detailed documen
329
354
 
330
355
  - **Per-function statistics** - `cache_info()` on every decorated function, modelled on `functools.lru_cache`
331
356
  - **Prometheus metrics** - Recorded by default (your app owns exposition)
332
- - **Structured logging** - Context-aware with correlation IDs
357
+ - **Structured logging** - Context-aware, per-operation fields
333
358
  - **Health checks** - Comprehensive status endpoints
334
359
 
335
360
  Every decorated function exposes `cache_info()`, returning a `CacheInfo` named tuple with
@@ -363,8 +388,8 @@ exposition setup.
363
388
  free-threaded 3.14 with the GIL verified disabled (CI job
364
389
  `test-freethreaded`), and the Rust extension declares free-threaded safety
365
390
  (`gil_used = false`). Free-threaded wheels are **not yet published** and
366
- free-threaded builds are not officially supported — blocked on upstream
367
- wheels (orjson, hiredis; numpy/pandas/pyarrow for `[data]`). See
391
+ free-threaded builds are not officially supported — blocked upstream on
392
+ orjson (no free-threaded wheels) and hiredis (re-enables the GIL on import). See
368
393
  [measured performance results](docs/free-threading.md#measured-performance) and the
369
394
  full concurrency audit: [docs/free-threading.md](docs/free-threading.md).
370
395
 
@@ -415,7 +440,7 @@ info = expensive_func.cache_info()
415
440
  | [Prometheus Metrics][prometheus-url] | Built-in observability |
416
441
  | [Zero-Knowledge Encryption][encryption-url] | Client-side security |
417
442
  | [Interop Mode][interop-url] | Cross-SDK cache sharing with cachekit-ts/rs |
418
- | [L1 Invalidation & SWR][l1-invalidation-url] | Process-local invalidation, stale-while-revalidate |
443
+ | [L1 Invalidation & SWR][l1-invalidation-url] | Invalidation scope (incl. cross-process whole-function on tenant-scoped Redis from the environment or `RedisBackendProvider`), stale-while-revalidate |
419
444
  | [Reference Caching][reference-caching-url] | `@cache.local()` for non-serializable objects |
420
445
  | [Rust Serialization][rust-serialization-url] | ByteStorage layer: LZ4, xxHash3, AES-256-GCM |
421
446
  | [SSRF Protection][ssrf-url] | URL allowlisting for the CachekitIO backend |
@@ -432,7 +457,7 @@ CACHEKIT_REDIS_URL="redis://localhost:6379" # Primary (preferred)
432
457
  REDIS_URL="redis://localhost:6379" # Fallback
433
458
 
434
459
  # CachekitIO SaaS Backend (closed beta — request access at cachekit.io)
435
- CACHEKIT_API_KEY="your-api-key" # Required for @cache.io() # pragma: allowlist secret
460
+ CACHEKIT_API_KEY="your-api-key" # For @cache.io() — or pass api_key= directly # pragma: allowlist secret
436
461
  CACHEKIT_API_URL="https://api.cachekit.io" # Default SaaS endpoint
437
462
 
438
463
  # Memcached Backend (optional: pip install cachekit[memcached])
@@ -442,7 +467,6 @@ CACHEKIT_MEMCACHED_TIMEOUT=1.0 # Default: 1.0 seconds
442
467
  CACHEKIT_MEMCACHED_KEY_PREFIX="myapp:" # Default: "" (none)
443
468
 
444
469
  # Optional Configuration
445
- CACHEKIT_DEFAULT_TTL=3600
446
470
  CACHEKIT_MAX_VALUE_SIZE=104857600
447
471
  CACHEKIT_ARROW_COMPRESSION=zstd
448
472
  ```
@@ -243,13 +243,18 @@ pyo3::create_exception!(
243
243
  _rust_serializer,
244
244
  KeyringConfigurationError,
245
245
  PyValueError,
246
- "A LOCAL keyring configuration fault on the decrypt path.\n\
246
+ "A LOCAL keyring configuration fault.\n\
247
247
  \n\
248
- Strictly limited to faults whose input is our own configuration: an invalid\n\
249
- tenant_id reaching HKDF (`KeyDerivation`) and a keyring entry index that does\n\
250
- not exist (`KeyringIndexOutOfRange`). These are deploy or caller bugs, and\n\
251
- recording them as `auth_tamper` pages an operator for an attack that never\n\
252
- happened.\n\
248
+ Strictly limited to faults whose input is our own configuration or build.\n\
249
+ On the decrypt path: an invalid tenant_id reaching HKDF (`KeyDerivation`) and\n\
250
+ a keyring entry index that does not exist (`KeyringIndexOutOfRange`). At\n\
251
+ EncryptionWrapper construction: a rejected keyring (`Keyring(...)`: cap\n\
252
+ exceeded, current key in the decrypt-only list), a previous master key below\n\
253
+ the 32-byte floor, a keyring fingerprint derivation failure, and the\n\
254
+ keyring-entry-0 vs derive_tenant_keys fingerprint drift guard. These are\n\
255
+ deploy, caller or build bugs: recording them as `auth_tamper` pages an\n\
256
+ operator for an attack that never happened, and letting them become a miss\n\
257
+ hides a broken keyring behind silent recomputes.\n\
253
258
  \n\
254
259
  Everything whose input is the STORED CIPHERTEXT stays on the tamper path,\n\
255
260
  including short/garbled ciphertext. An attacker with backend write access can\n\
@@ -467,8 +472,9 @@ impl PyKeyring {
467
472
  let decrypt_only: Vec<Zeroizing<Vec<u8>>> =
468
473
  decrypt_only.into_iter().map(Zeroizing::new).collect();
469
474
  let refs: Vec<&[u8]> = decrypt_only.iter().map(|key| key.as_slice()).collect();
470
- let inner = Keyring::new(current, &refs)
471
- .map_err(|e| PyValueError::new_err(format!("Keyring configuration invalid: {}", e)))?;
475
+ let inner = Keyring::new(current, &refs).map_err(|e| {
476
+ KeyringConfigurationError::new_err(format!("Keyring configuration invalid: {}", e))
477
+ })?;
472
478
  Ok(Self { inner })
473
479
  }
474
480
 
@@ -479,7 +485,10 @@ impl PyKeyring {
479
485
  #[pyo3(name = "encryption_fingerprints")]
480
486
  pub fn encryption_fingerprints(&self, tenant_id: &str) -> PyResult<Vec<Vec<u8>>> {
481
487
  let fingerprints = self.inner.encryption_fingerprints(tenant_id).map_err(|e| {
482
- PyValueError::new_err(format!("Keyring fingerprint derivation failed: {}", e))
488
+ KeyringConfigurationError::new_err(format!(
489
+ "Keyring fingerprint derivation failed: {}",
490
+ e
491
+ ))
483
492
  })?;
484
493
  Ok(fingerprints.into_iter().map(|fp| fp.to_vec()).collect())
485
494
  }
@@ -13,7 +13,7 @@ Key Features:
13
13
  - **Backpressure control** to prevent backend overload
14
14
  - **Connection pooling** for optimized performance
15
15
  - **Health check methods** for comprehensive monitoring
16
- - **Structured logging** with correlation IDs and distributed tracing
16
+ - **Structured logging** with per-operation fields
17
17
  - **Statistics collection** for Prometheus metrics integration
18
18
 
19
19
  Architecture Overview:
@@ -34,6 +34,7 @@ Born from production debugging of caching failures:
34
34
  Example Usage:
35
35
  ```python
36
36
  from cachekit import cache
37
+ from cachekit.config.nested import CircuitBreakerConfig
37
38
 
38
39
  # Intelligent cache with zero configuration (90% of use cases)
39
40
  @cache
@@ -58,7 +59,7 @@ Example Usage:
58
59
  return fetch_remote(key)
59
60
 
60
61
  # Manual configuration when needed (1% of use cases)
61
- @cache(ttl=3600, namespace="custom", circuit_breaker=True)
62
+ @cache(ttl=3600, namespace="custom", circuit_breaker=CircuitBreakerConfig(failure_threshold=10))
62
63
  def custom_function():
63
64
  return special_computation()
64
65
 
@@ -68,7 +69,7 @@ Example Usage:
68
69
  ```
69
70
  """
70
71
 
71
- __version__ = "0.19.0"
72
+ __version__ = "0.20.0"
72
73
 
73
74
  from collections.abc import Callable
74
75
  from typing import Any, TypeVar
@@ -99,7 +100,7 @@ from .health import (
99
100
  )
100
101
 
101
102
  # Interop mode (interop/v1): cross-SDK key generation and value codec.
102
- # The decorator API is @cache(interop="op", namespace="ns"); these helpers are
103
+ # The decorator API is @cache(interop="op", namespace="users"); these helpers are
103
104
  # for manual key/value handling (debugging, out-of-band writers, migrations).
104
105
  from .interop import (
105
106
  InteropError,