cachekit 0.18.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 (95) hide show
  1. {cachekit-0.18.0 → cachekit-0.20.0}/Cargo.lock +3 -3
  2. {cachekit-0.18.0 → cachekit-0.20.0}/PKG-INFO +56 -19
  3. {cachekit-0.18.0 → cachekit-0.20.0}/README.md +52 -18
  4. {cachekit-0.18.0 → cachekit-0.20.0}/pyproject.toml +45 -20
  5. {cachekit-0.18.0 → cachekit-0.20.0}/rust/Cargo.toml +2 -2
  6. {cachekit-0.18.0 → cachekit-0.20.0}/rust/README.md +52 -18
  7. {cachekit-0.18.0 → cachekit-0.20.0}/rust/TEST_EXPANSION_SUMMARY.md +2 -2
  8. {cachekit-0.18.0 → cachekit-0.20.0}/rust/src/lib.rs +28 -2
  9. cachekit-0.20.0/rust/src/msgpack_bounds.rs +87 -0
  10. {cachekit-0.18.0 → cachekit-0.20.0}/rust/src/python_bindings.rs +126 -47
  11. {cachekit-0.18.0 → cachekit-0.20.0}/src/cachekit/__init__.py +5 -4
  12. {cachekit-0.18.0 → cachekit-0.20.0}/src/cachekit/backends/__init__.py +10 -4
  13. {cachekit-0.18.0 → cachekit-0.20.0}/src/cachekit/backends/base.py +83 -9
  14. {cachekit-0.18.0 → cachekit-0.20.0}/src/cachekit/backends/base_config.py +7 -1
  15. {cachekit-0.18.0 → cachekit-0.20.0}/src/cachekit/backends/cachekitio/backend.py +148 -40
  16. cachekit-0.20.0/src/cachekit/backends/cachekitio/client.py +187 -0
  17. {cachekit-0.18.0 → cachekit-0.20.0}/src/cachekit/backends/cachekitio/config.py +35 -4
  18. {cachekit-0.18.0 → cachekit-0.20.0}/src/cachekit/backends/cachekitio/error_handler.py +11 -6
  19. {cachekit-0.18.0 → cachekit-0.20.0}/src/cachekit/backends/errors.py +21 -4
  20. {cachekit-0.18.0 → cachekit-0.20.0}/src/cachekit/backends/file/backend.py +81 -22
  21. {cachekit-0.18.0 → cachekit-0.20.0}/src/cachekit/backends/memcached/backend.py +4 -1
  22. {cachekit-0.18.0 → cachekit-0.20.0}/src/cachekit/backends/memcached/config.py +5 -4
  23. {cachekit-0.18.0 → cachekit-0.20.0}/src/cachekit/backends/memcached/error_handler.py +18 -8
  24. {cachekit-0.18.0 → cachekit-0.20.0}/src/cachekit/backends/provider.py +23 -25
  25. {cachekit-0.18.0 → cachekit-0.20.0}/src/cachekit/backends/redis/backend.py +5 -5
  26. {cachekit-0.18.0 → cachekit-0.20.0}/src/cachekit/backends/redis/error_handler.py +30 -21
  27. cachekit-0.20.0/src/cachekit/backends/redis/provider.py +804 -0
  28. {cachekit-0.18.0 → cachekit-0.20.0}/src/cachekit/cache_handler.py +500 -368
  29. {cachekit-0.18.0 → cachekit-0.20.0}/src/cachekit/config/decorator.py +85 -93
  30. {cachekit-0.18.0 → cachekit-0.20.0}/src/cachekit/config/nested.py +55 -16
  31. {cachekit-0.18.0 → cachekit-0.20.0}/src/cachekit/config/settings.py +13 -139
  32. {cachekit-0.18.0 → cachekit-0.20.0}/src/cachekit/config/singleton.py +2 -2
  33. cachekit-0.20.0/src/cachekit/config/validation.py +243 -0
  34. {cachekit-0.18.0 → cachekit-0.20.0}/src/cachekit/decorators/intent.py +65 -26
  35. {cachekit-0.18.0 → cachekit-0.20.0}/src/cachekit/decorators/orchestrator.py +56 -101
  36. {cachekit-0.18.0 → cachekit-0.20.0}/src/cachekit/decorators/session.py +16 -8
  37. {cachekit-0.18.0 → cachekit-0.20.0}/src/cachekit/decorators/wrapper.py +751 -373
  38. cachekit-0.20.0/src/cachekit/hash_utils.py +145 -0
  39. {cachekit-0.18.0 → cachekit-0.20.0}/src/cachekit/hiredis_compat.py +5 -3
  40. {cachekit-0.18.0 → cachekit-0.20.0}/src/cachekit/interop.py +33 -11
  41. {cachekit-0.18.0 → cachekit-0.20.0}/src/cachekit/key_generator.py +79 -10
  42. {cachekit-0.18.0 → cachekit-0.20.0}/src/cachekit/l1_cache.py +165 -9
  43. {cachekit-0.18.0 → cachekit-0.20.0}/src/cachekit/logging.py +41 -226
  44. {cachekit-0.18.0 → cachekit-0.20.0}/src/cachekit/monitoring/pool_monitor.py +5 -5
  45. {cachekit-0.18.0 → cachekit-0.20.0}/src/cachekit/object_cache.py +1 -1
  46. {cachekit-0.18.0 → cachekit-0.20.0}/src/cachekit/reliability/__init__.py +0 -2
  47. {cachekit-0.18.0 → cachekit-0.20.0}/src/cachekit/reliability/async_metrics.py +190 -33
  48. {cachekit-0.18.0 → cachekit-0.20.0}/src/cachekit/reliability/circuit_breaker.py +53 -36
  49. {cachekit-0.18.0 → cachekit-0.20.0}/src/cachekit/reliability/metrics_collection.py +47 -32
  50. {cachekit-0.18.0 → cachekit-0.20.0}/src/cachekit/reliability/profiles.py +0 -102
  51. {cachekit-0.18.0 → cachekit-0.20.0}/src/cachekit/serializers/__init__.py +6 -3
  52. cachekit-0.20.0/src/cachekit/serializers/auto_serializer.py +1262 -0
  53. {cachekit-0.18.0 → cachekit-0.20.0}/src/cachekit/serializers/base.py +158 -0
  54. {cachekit-0.18.0 → cachekit-0.20.0}/src/cachekit/serializers/encryption_wrapper.py +68 -35
  55. {cachekit-0.18.0 → cachekit-0.20.0}/src/cachekit/serializers/orjson_serializer.py +1 -1
  56. {cachekit-0.18.0 → cachekit-0.20.0}/src/cachekit/serializers/standard_serializer.py +60 -15
  57. {cachekit-0.18.0 → cachekit-0.20.0}/src/cachekit/serializers/wrapper.py +16 -2
  58. cachekit-0.18.0/rust/supply-chain/audits.toml +0 -4
  59. cachekit-0.18.0/rust/supply-chain/config.toml +0 -1341
  60. cachekit-0.18.0/rust/supply-chain/imports.lock +0 -2
  61. cachekit-0.18.0/src/cachekit/backends/cachekitio/client.py +0 -122
  62. cachekit-0.18.0/src/cachekit/backends/redis/provider.py +0 -501
  63. cachekit-0.18.0/src/cachekit/config/validation.py +0 -102
  64. cachekit-0.18.0/src/cachekit/hash_utils.py +0 -50
  65. cachekit-0.18.0/src/cachekit/monitoring/correlation_tracking.py +0 -308
  66. cachekit-0.18.0/src/cachekit/serializers/auto_serializer.py +0 -919
  67. {cachekit-0.18.0 → cachekit-0.20.0}/Cargo.toml +0 -0
  68. {cachekit-0.18.0 → cachekit-0.20.0}/LICENSE +0 -0
  69. {cachekit-0.18.0 → cachekit-0.20.0}/rust/Makefile +0 -0
  70. {cachekit-0.18.0 → cachekit-0.20.0}/rust/tsan_suppressions.txt +0 -0
  71. {cachekit-0.18.0 → cachekit-0.20.0}/src/cachekit/backends/cachekitio/__init__.py +0 -0
  72. {cachekit-0.18.0 → cachekit-0.20.0}/src/cachekit/backends/cachekitio/session.py +0 -0
  73. {cachekit-0.18.0 → cachekit-0.20.0}/src/cachekit/backends/file/__init__.py +0 -0
  74. {cachekit-0.18.0 → cachekit-0.20.0}/src/cachekit/backends/file/config.py +0 -0
  75. {cachekit-0.18.0 → cachekit-0.20.0}/src/cachekit/backends/memcached/__init__.py +0 -0
  76. {cachekit-0.18.0 → cachekit-0.20.0}/src/cachekit/backends/redis/__init__.py +0 -0
  77. {cachekit-0.18.0 → cachekit-0.20.0}/src/cachekit/backends/redis/client.py +0 -0
  78. {cachekit-0.18.0 → cachekit-0.20.0}/src/cachekit/backends/redis/config.py +0 -0
  79. {cachekit-0.18.0 → cachekit-0.20.0}/src/cachekit/config/__init__.py +0 -0
  80. {cachekit-0.18.0 → cachekit-0.20.0}/src/cachekit/decorators/__init__.py +0 -0
  81. {cachekit-0.18.0 → cachekit-0.20.0}/src/cachekit/decorators/local_wrapper.py +0 -0
  82. {cachekit-0.18.0 → cachekit-0.20.0}/src/cachekit/decorators/main.py +0 -0
  83. {cachekit-0.18.0 → cachekit-0.20.0}/src/cachekit/decorators/stats_context.py +0 -0
  84. {cachekit-0.18.0 → cachekit-0.20.0}/src/cachekit/decorators/tenant_context.py +0 -0
  85. {cachekit-0.18.0 → cachekit-0.20.0}/src/cachekit/decorators/utils/__init__.py +0 -0
  86. {cachekit-0.18.0 → cachekit-0.20.0}/src/cachekit/di.py +0 -0
  87. {cachekit-0.18.0 → cachekit-0.20.0}/src/cachekit/health.py +0 -0
  88. {cachekit-0.18.0 → cachekit-0.20.0}/src/cachekit/imports.py +0 -0
  89. {cachekit-0.18.0 → cachekit-0.20.0}/src/cachekit/monitoring/__init__.py +0 -0
  90. {cachekit-0.18.0 → cachekit-0.20.0}/src/cachekit/monitoring/protocols.py +0 -0
  91. {cachekit-0.18.0 → cachekit-0.20.0}/src/cachekit/py.typed +0 -0
  92. {cachekit-0.18.0 → cachekit-0.20.0}/src/cachekit/reliability/error_classification.py +0 -0
  93. {cachekit-0.18.0 → cachekit-0.20.0}/src/cachekit/reliability/load_control.py +0 -0
  94. {cachekit-0.18.0 → cachekit-0.20.0}/src/cachekit/serializers/arrow_serializer.py +0 -0
  95. {cachekit-0.18.0 → cachekit-0.20.0}/src/cachekit/serializers/interop_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.18.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.18.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
@@ -28,6 +29,8 @@ Requires-Dist: blake3>=1.0.5
28
29
  Requires-Dist: msgpack>=1.2.1
29
30
  Requires-Dist: xxhash>=3.5.0
30
31
  Requires-Dist: httpx[http2]>=0.28.1
32
+ Requires-Dist: anyio>=4.14.2
33
+ Requires-Dist: h2>=4.4.1
31
34
  Requires-Dist: numpy>=2.0.2 ; extra == 'data'
32
35
  Requires-Dist: pandas>=1.3.0 ; extra == 'data'
33
36
  Requires-Dist: pyarrow>=21.0.0 ; extra == 'data'
@@ -205,26 +208,29 @@ def get_price(symbol: str):
205
208
  def process_payment(amount):
206
209
  return payment_gateway.charge(amount)
207
210
 
208
- # Security-critical: PII, medical, financial
209
- @cache.secure
211
+ # Security-critical: PII, medical, financial (needs a key: master_key= or CACHEKIT_MASTER_KEY)
212
+ @cache.secure(master_key=secret_key)
210
213
  def get_user_profile(user_id: int):
211
214
  return db.fetch_user(user_id)
212
215
  ```
213
216
 
214
217
  | Feature | `@cache.minimal` | `@cache.dev` | `@cache.test` | `@cache.production` | `@cache.secure` |
215
218
  |:--------|:----------------:|:------------:|:-------------:|:-------------------:|:---------------:|
219
+ | Default TTL | 300 s | 300 s | 300 s | 600 s | 600 s |
216
220
  | Circuit Breaker | - | ✅ | - | ✅ | ✅ |
217
221
  | Backpressure | ✅ | ✅ | - | ✅ | ✅ |
218
222
  | Integrity Checking | - | ✅ | - | ✅ | ✅ 🔒 |
219
223
  | Encryption | - | - | - | - | ✅ Required |
220
- | L1 SWR (L1-only mode) | - | ✅ | - | ✅ | ✅ |
224
+ | L1 SWR (L1-only mode) | - | ✅ | - | ✅ | - |
221
225
  | L1 Invalidation | - | - | - | ✅ | ✅ |
222
226
  | Prometheus Metrics | - | - | - | ✅ | ✅ |
223
227
  | Tracing | - | ✅ | - | ✅ | ✅ |
224
228
  | Structured Logging | - | ✅ | - | ✅ | ✅ |
225
229
  | **Use Case** | High throughput | Local debugging | Deterministic tests | Production reliability | Compliance/security |
226
230
 
227
- > 🔒 `@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)).
228
234
  >
229
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)).
230
236
  >
@@ -288,6 +294,7 @@ def test_cached_function():
288
294
  - Connection pooling with thread affinity (+28% throughput)
289
295
  - Distributed locking prevents cache stampedes
290
296
  - Pluggable backend abstraction (Redis, CachekitIO, File, Memcached, custom)
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
291
298
 
292
299
  > [!NOTE]
293
300
  > All reliability features are **enabled by default** with `@cache.production`. Use `@cache.minimal` to disable them for maximum throughput.
@@ -305,7 +312,7 @@ def test_cached_function():
305
312
  <summary><strong>Serializer Examples</strong></summary>
306
313
 
307
314
  ```python
308
- from cachekit.serializers import OrjsonSerializer, ArrowSerializer, EncryptionWrapper
315
+ from cachekit.serializers import OrjsonSerializer, ArrowSerializer
309
316
 
310
317
  # Fast JSON for API responses
311
318
  @cache.production(serializer=OrjsonSerializer())
@@ -316,11 +323,30 @@ def get_api_response(endpoint: str):
316
323
  @cache(serializer=ArrowSerializer())
317
324
  def get_large_dataset(date: str):
318
325
  return pd.read_csv(f"data/{date}.csv")
326
+ ```
327
+
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
319
336
 
320
- # Encrypted DataFrames for sensitive data
321
- @cache(serializer=EncryptionWrapper(serializer=ArrowSerializer()))
337
+ @cache.secure(
338
+ master_key=secret_key,
339
+ serializer=ArrowSerializer(),
340
+ backend=FileBackend(FileBackendConfig(cache_dir=tempfile.mkdtemp())),
341
+ )
322
342
  def get_patient_data(hospital_id: int):
323
- 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
324
350
  ```
325
351
 
326
352
  </details>
@@ -343,15 +369,18 @@ def get_patient_data(hospital_id: int):
343
369
  > [!CAUTION]
344
370
  > When handling PII, medical, or financial data, always use `@cache.secure` to enforce encryption.
345
371
 
346
- **Zero-downtime key rotation**: promote a new `CACHEKIT_MASTER_KEY` and keep the
347
- retiring key readable via `CACHEKIT_PREVIOUS_MASTER_KEYS` (comma-separated hex,
348
- max 3 decrypt-only keys). Entries are selected by exact key fingerprint — never
349
- trial decryption — and old entries age out via TTL, no cache flush required. See
350
- [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.
351
380
 
352
381
  cachekit employs comprehensive security tooling:
353
382
 
354
- - **Supply Chain Security**: cargo-deny for license compliance + RustSec scanning
383
+ - **Dependency Security**: cargo-deny for license compliance + cargo-audit for RustSec scanning
355
384
  - **Formal Verification**: Kani proves correctness of compression, checksums, encryption
356
385
  - **Runtime Analysis**: Miri + sanitizers for memory safety
357
386
  - **Fuzzing**: Coverage-guided testing with >80% code coverage
@@ -381,7 +410,7 @@ See [SECURITY.md][security-url] for vulnerability reporting and detailed documen
381
410
 
382
411
  - **Per-function statistics** - `cache_info()` on every decorated function, modelled on `functools.lru_cache`
383
412
  - **Prometheus metrics** - Recorded by default (your app owns exposition)
384
- - **Structured logging** - Context-aware with correlation IDs
413
+ - **Structured logging** - Context-aware, per-operation fields
385
414
  - **Health checks** - Comprehensive status endpoints
386
415
 
387
416
  Every decorated function exposes `cache_info()`, returning a `CacheInfo` named tuple with
@@ -411,6 +440,15 @@ exposition setup.
411
440
  <details>
412
441
  <summary><strong>Thread Safety Details</strong></summary>
413
442
 
443
+ **Free-threaded CPython (3.14t):** the core suites run green on
444
+ free-threaded 3.14 with the GIL verified disabled (CI job
445
+ `test-freethreaded`), and the Rust extension declares free-threaded safety
446
+ (`gil_used = false`). Free-threaded wheels are **not yet published** and
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
449
+ [measured performance results](docs/free-threading.md#measured-performance) and the
450
+ full concurrency audit: [docs/free-threading.md](docs/free-threading.md).
451
+
414
452
  **Per-Function Statistics:**
415
453
  - Statistics tracked per function identity (`module.qualname`), shared across all calls and across re-decorations of the same function
416
454
  - Thread-safe via RLock (all methods safe for concurrent access)
@@ -458,7 +496,7 @@ info = expensive_func.cache_info()
458
496
  | [Prometheus Metrics][prometheus-url] | Built-in observability |
459
497
  | [Zero-Knowledge Encryption][encryption-url] | Client-side security |
460
498
  | [Interop Mode][interop-url] | Cross-SDK cache sharing with cachekit-ts/rs |
461
- | [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 |
462
500
  | [Reference Caching][reference-caching-url] | `@cache.local()` for non-serializable objects |
463
501
  | [Rust Serialization][rust-serialization-url] | ByteStorage layer: LZ4, xxHash3, AES-256-GCM |
464
502
  | [SSRF Protection][ssrf-url] | URL allowlisting for the CachekitIO backend |
@@ -475,7 +513,7 @@ CACHEKIT_REDIS_URL="redis://localhost:6379" # Primary (preferred)
475
513
  REDIS_URL="redis://localhost:6379" # Fallback
476
514
 
477
515
  # CachekitIO SaaS Backend (closed beta — request access at cachekit.io)
478
- 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
479
517
  CACHEKIT_API_URL="https://api.cachekit.io" # Default SaaS endpoint
480
518
 
481
519
  # Memcached Backend (optional: pip install cachekit[memcached])
@@ -485,7 +523,6 @@ CACHEKIT_MEMCACHED_TIMEOUT=1.0 # Default: 1.0 seconds
485
523
  CACHEKIT_MEMCACHED_KEY_PREFIX="myapp:" # Default: "" (none)
486
524
 
487
525
  # Optional Configuration
488
- CACHEKIT_DEFAULT_TTL=3600
489
526
  CACHEKIT_MAX_VALUE_SIZE=104857600
490
527
  CACHEKIT_ARROW_COMPRESSION=zstd
491
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,6 +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)
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
238
242
 
239
243
  > [!NOTE]
240
244
  > All reliability features are **enabled by default** with `@cache.production`. Use `@cache.minimal` to disable them for maximum throughput.
@@ -252,7 +256,7 @@ def test_cached_function():
252
256
  <summary><strong>Serializer Examples</strong></summary>
253
257
 
254
258
  ```python
255
- from cachekit.serializers import OrjsonSerializer, ArrowSerializer, EncryptionWrapper
259
+ from cachekit.serializers import OrjsonSerializer, ArrowSerializer
256
260
 
257
261
  # Fast JSON for API responses
258
262
  @cache.production(serializer=OrjsonSerializer())
@@ -263,11 +267,30 @@ def get_api_response(endpoint: str):
263
267
  @cache(serializer=ArrowSerializer())
264
268
  def get_large_dataset(date: str):
265
269
  return pd.read_csv(f"data/{date}.csv")
270
+ ```
271
+
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
266
280
 
267
- # Encrypted DataFrames for sensitive data
268
- @cache(serializer=EncryptionWrapper(serializer=ArrowSerializer()))
281
+ @cache.secure(
282
+ master_key=secret_key,
283
+ serializer=ArrowSerializer(),
284
+ backend=FileBackend(FileBackendConfig(cache_dir=tempfile.mkdtemp())),
285
+ )
269
286
  def get_patient_data(hospital_id: int):
270
- 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
271
294
  ```
272
295
 
273
296
  </details>
@@ -290,15 +313,18 @@ def get_patient_data(hospital_id: int):
290
313
  > [!CAUTION]
291
314
  > When handling PII, medical, or financial data, always use `@cache.secure` to enforce encryption.
292
315
 
293
- **Zero-downtime key rotation**: promote a new `CACHEKIT_MASTER_KEY` and keep the
294
- retiring key readable via `CACHEKIT_PREVIOUS_MASTER_KEYS` (comma-separated hex,
295
- max 3 decrypt-only keys). Entries are selected by exact key fingerprint — never
296
- trial decryption — and old entries age out via TTL, no cache flush required. See
297
- [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.
298
324
 
299
325
  cachekit employs comprehensive security tooling:
300
326
 
301
- - **Supply Chain Security**: cargo-deny for license compliance + RustSec scanning
327
+ - **Dependency Security**: cargo-deny for license compliance + cargo-audit for RustSec scanning
302
328
  - **Formal Verification**: Kani proves correctness of compression, checksums, encryption
303
329
  - **Runtime Analysis**: Miri + sanitizers for memory safety
304
330
  - **Fuzzing**: Coverage-guided testing with >80% code coverage
@@ -328,7 +354,7 @@ See [SECURITY.md][security-url] for vulnerability reporting and detailed documen
328
354
 
329
355
  - **Per-function statistics** - `cache_info()` on every decorated function, modelled on `functools.lru_cache`
330
356
  - **Prometheus metrics** - Recorded by default (your app owns exposition)
331
- - **Structured logging** - Context-aware with correlation IDs
357
+ - **Structured logging** - Context-aware, per-operation fields
332
358
  - **Health checks** - Comprehensive status endpoints
333
359
 
334
360
  Every decorated function exposes `cache_info()`, returning a `CacheInfo` named tuple with
@@ -358,6 +384,15 @@ exposition setup.
358
384
  <details>
359
385
  <summary><strong>Thread Safety Details</strong></summary>
360
386
 
387
+ **Free-threaded CPython (3.14t):** the core suites run green on
388
+ free-threaded 3.14 with the GIL verified disabled (CI job
389
+ `test-freethreaded`), and the Rust extension declares free-threaded safety
390
+ (`gil_used = false`). Free-threaded wheels are **not yet published** and
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
393
+ [measured performance results](docs/free-threading.md#measured-performance) and the
394
+ full concurrency audit: [docs/free-threading.md](docs/free-threading.md).
395
+
361
396
  **Per-Function Statistics:**
362
397
  - Statistics tracked per function identity (`module.qualname`), shared across all calls and across re-decorations of the same function
363
398
  - Thread-safe via RLock (all methods safe for concurrent access)
@@ -405,7 +440,7 @@ info = expensive_func.cache_info()
405
440
  | [Prometheus Metrics][prometheus-url] | Built-in observability |
406
441
  | [Zero-Knowledge Encryption][encryption-url] | Client-side security |
407
442
  | [Interop Mode][interop-url] | Cross-SDK cache sharing with cachekit-ts/rs |
408
- | [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 |
409
444
  | [Reference Caching][reference-caching-url] | `@cache.local()` for non-serializable objects |
410
445
  | [Rust Serialization][rust-serialization-url] | ByteStorage layer: LZ4, xxHash3, AES-256-GCM |
411
446
  | [SSRF Protection][ssrf-url] | URL allowlisting for the CachekitIO backend |
@@ -422,7 +457,7 @@ CACHEKIT_REDIS_URL="redis://localhost:6379" # Primary (preferred)
422
457
  REDIS_URL="redis://localhost:6379" # Fallback
423
458
 
424
459
  # CachekitIO SaaS Backend (closed beta — request access at cachekit.io)
425
- 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
426
461
  CACHEKIT_API_URL="https://api.cachekit.io" # Default SaaS endpoint
427
462
 
428
463
  # Memcached Backend (optional: pip install cachekit[memcached])
@@ -432,7 +467,6 @@ CACHEKIT_MEMCACHED_TIMEOUT=1.0 # Default: 1.0 seconds
432
467
  CACHEKIT_MEMCACHED_KEY_PREFIX="myapp:" # Default: "" (none)
433
468
 
434
469
  # Optional Configuration
435
- CACHEKIT_DEFAULT_TTL=3600
436
470
  CACHEKIT_MAX_VALUE_SIZE=104857600
437
471
  CACHEKIT_ARROW_COMPRESSION=zstd
438
472
  ```
@@ -4,7 +4,7 @@ build-backend = "maturin"
4
4
 
5
5
  [project]
6
6
  name = "cachekit"
7
- version = "0.18.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",
@@ -66,6 +67,22 @@ dependencies = [
66
67
  "xxhash>=3.5.0",
67
68
  # HTTP client for SaaS backend (cachekit.io)
68
69
  "httpx[http2]>=0.28.1",
70
+ # anyio is transitive via the mandatory httpx dependency above, so it ships
71
+ # to EVERY install, not just dev. Declared here rather than as a
72
+ # [tool.uv] constraint because that table is uv-local: it never reaches
73
+ # requires-dist, so `pip install cachekit` would ignore it. 4.14.2 fixes
74
+ # GHSA-82r6-8w77-94w6 / CVE-2026-63374 (IDNA-2003 hostname encoding lets a
75
+ # hijacked connection to an internationalised domain pass TLS certificate
76
+ # validation; CVSS 9.3), GHSA-5p39-cfhj-2xmp / CVE-2026-64847 (undrained
77
+ # process-pool stderr pipe deadlocks the worker) and GHSA-3w57-8xmc-8v26 /
78
+ # CVE-2026-63349 (extra_groups ignored, parent supplementary groups kept).
79
+ "anyio>=4.14.2",
80
+ # h2 arrives via the http2 extra on that same mandatory httpx dependency, so
81
+ # it ships to every install too, and was declared as a [tool.uv] constraint
82
+ # with the same no-op effect. 4.4.1 fixes GHSA-6hr6-w5qg-qmwg (duplicate Host
83
+ # headers forwarded across an HTTP/2 -> HTTP/1.1 downgrade — a request
84
+ # smuggling primitive).
85
+ "h2>=4.4.1",
69
86
  ]
70
87
 
71
88
  [project.optional-dependencies]
@@ -150,7 +167,6 @@ asyncio_default_fixture_loop_scope = "function"
150
167
  addopts = [
151
168
  "--strict-markers",
152
169
  "--verbose",
153
- "--basetemp=/tmp/pytest",
154
170
  "--doctest-modules", # Validate docstring examples
155
171
  "--doctest-continue-on-failure", # Report all doctest failures, not just first
156
172
  "--markdown-docs", # Validate markdown documentation examples
@@ -200,8 +216,12 @@ exclude_lines = [
200
216
  ]
201
217
 
202
218
  [dependency-groups]
203
- dev = [
204
- # Testing
219
+ # Core test toolchain — everything tests/unit + tests/critical need on ANY
220
+ # interpreter, including free-threaded CPython: the free-threaded CI lane
221
+ # installs ONLY this group (LAB-511). A dep may live here only if it ships
222
+ # free-threaded wheels or builds cleanly from source on 3.14t; deps that
223
+ # don't (orjson, numpy, pandas, pyarrow) stay in dev below.
224
+ test = [
205
225
  "fakeredis>=2.21.0",
206
226
  "pytest>=7.0.0",
207
227
  "pytest-asyncio>=0.21.0",
@@ -210,6 +230,19 @@ dev = [
210
230
  "pytest-markdown-docs>=0.6.0",
211
231
  "pytest-redis>=3.0.0",
212
232
  "pymemcache>=4.0.0",
233
+ # Utilities
234
+ "faker>=20.0.0",
235
+ "httpx>=0.28.1",
236
+ "hypothesis>=6.0.0",
237
+ "requests>=2.33.0; python_version >= '3.10'",
238
+ "psutil>=5.9.0",
239
+ "python-dotenv>=1.0.0",
240
+ "pyyaml>=6.0.3",
241
+ "pytest-xdist>=3.8.0",
242
+ "time-machine>=2.19.0",
243
+ ]
244
+ dev = [
245
+ { include-group = "test" },
213
246
  # Competitive comparison suite (tests/competitive/ benchmarks cachekit vs these)
214
247
  "cachetools>=5.3.0",
215
248
  "aiocache>=0.12.0",
@@ -218,14 +251,7 @@ dev = [
218
251
  "ruff>=0.6.0",
219
252
  # Utilities
220
253
  "bashlex>=0.18",
221
- "faker>=20.0.0",
222
- "httpx>=0.28.1",
223
- "hypothesis>=6.0.0",
224
254
  "pip-audit>=2.7.0",
225
- "requests>=2.33.0; python_version >= '3.10'",
226
- "psutil>=5.9.0",
227
- "python-dotenv>=1.0.0",
228
- "pyyaml>=6.0.3",
229
255
  # Data science support (for testing AutoSerializer with numpy/pandas)
230
256
  "numpy>=2.0.2",
231
257
  "pandas>=1.3.0",
@@ -233,15 +259,17 @@ dev = [
233
259
  # OrjsonSerializer support — now the [json] optional extra; kept here so the
234
260
  # orjson tests, doctests, and markdown-docs still resolve it in dev/CI.
235
261
  "orjson>=3.9.0",
236
- "pytest-xdist>=3.8.0",
237
- "time-machine>=2.19.0",
238
262
  ]
239
263
  # Linux CI only - Atheris requires libFuzzer (not available on macOS without building LLVM)
240
264
  fuzz = [
241
265
  "atheris>=2.3.0",
242
266
  ]
243
267
 
244
- # Override vulnerable transitive dependencies
268
+ # Override vulnerable DEV-ONLY transitive dependencies.
269
+ # This table is uv-local: it constrains resolution of this repo's lockfile and
270
+ # never reaches requires-dist, so a floor placed here does NOT protect anyone who
271
+ # runs `pip install cachekit`. A transitive that reaches users belongs in
272
+ # [project] dependencies instead — see the anyio/h2 entries above.
245
273
  [tool.uv]
246
274
  constraint-dependencies = [
247
275
  "urllib3>=2.7.0",
@@ -249,10 +277,7 @@ constraint-dependencies = [
249
277
  "werkzeug>=3.1.4",
250
278
  # pip is a dev-only transitive dep (pip-audit -> pip-api -> pip). 26.1.2 fixes
251
279
  # PYSEC-2026-196 (entry-point path traversal), GHSA-58qw-9mgm-455v (tar/zip
252
- # confusion) and GHSA-jp4c-xjxw-mgf9 (self-update import ordering).
253
- "pip>=26.1.2",
254
- # h2 is a transitive dep (httpx[http2] -> h2). 4.4.1 fixes
255
- # GHSA-6hr6-w5qg-qmwg (duplicate Host headers forwarded on HTTP/2 ->
256
- # HTTP/1.1 downgrade — request smuggling primitive).
257
- "h2>=4.4.1",
280
+ # confusion) and GHSA-jp4c-xjxw-mgf9 (self-update import ordering); 26.2 fixes
281
+ # PYSEC-2026-3721 (doubly-encoded index URLs install to arbitrary paths).
282
+ "pip>=26.2",
258
283
  ]
@@ -1,6 +1,6 @@
1
1
  [package]
2
2
  name = "cachekit-rs"
3
- version = "0.18.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 }