cachekit 0.21.0__tar.gz → 0.22.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 (91) hide show
  1. {cachekit-0.21.0 → cachekit-0.22.0}/Cargo.lock +1 -1
  2. {cachekit-0.21.0 → cachekit-0.22.0}/PKG-INFO +10 -8
  3. {cachekit-0.21.0 → cachekit-0.22.0}/README.md +8 -4
  4. {cachekit-0.21.0 → cachekit-0.22.0}/pyproject.toml +7 -27
  5. {cachekit-0.21.0 → cachekit-0.22.0}/rust/Cargo.toml +1 -1
  6. {cachekit-0.21.0 → cachekit-0.22.0}/rust/README.md +8 -4
  7. {cachekit-0.21.0 → cachekit-0.22.0}/src/cachekit/__init__.py +4 -7
  8. {cachekit-0.21.0 → cachekit-0.22.0}/src/cachekit/backends/__init__.py +14 -3
  9. cachekit-0.22.0/src/cachekit/backends/_uninterrupted.py +38 -0
  10. {cachekit-0.21.0 → cachekit-0.22.0}/src/cachekit/backends/cachekitio/backend.py +420 -165
  11. cachekit-0.22.0/src/cachekit/backends/cachekitio/client.py +279 -0
  12. {cachekit-0.21.0 → cachekit-0.22.0}/src/cachekit/backends/cachekitio/config.py +6 -4
  13. {cachekit-0.21.0 → cachekit-0.22.0}/src/cachekit/backends/cachekitio/error_handler.py +37 -19
  14. {cachekit-0.21.0 → cachekit-0.22.0}/src/cachekit/backends/provider.py +8 -4
  15. cachekit-0.22.0/src/cachekit/backends/redis/__init__.py +17 -0
  16. {cachekit-0.21.0 → cachekit-0.22.0}/src/cachekit/backends/redis/config.py +6 -2
  17. {cachekit-0.21.0 → cachekit-0.22.0}/src/cachekit/backends/redis/provider.py +61 -24
  18. {cachekit-0.21.0 → cachekit-0.22.0}/src/cachekit/cache_handler.py +74 -7
  19. {cachekit-0.21.0 → cachekit-0.22.0}/src/cachekit/config/decorator.py +2 -0
  20. {cachekit-0.21.0 → cachekit-0.22.0}/src/cachekit/config/nested.py +12 -8
  21. {cachekit-0.21.0 → cachekit-0.22.0}/src/cachekit/config/settings.py +13 -2
  22. {cachekit-0.21.0 → cachekit-0.22.0}/src/cachekit/decorators/intent.py +8 -1
  23. {cachekit-0.21.0 → cachekit-0.22.0}/src/cachekit/decorators/orchestrator.py +25 -4
  24. {cachekit-0.21.0 → cachekit-0.22.0}/src/cachekit/decorators/wrapper.py +544 -288
  25. {cachekit-0.21.0 → cachekit-0.22.0}/src/cachekit/hash_utils.py +63 -4
  26. {cachekit-0.21.0 → cachekit-0.22.0}/src/cachekit/health.py +6 -1
  27. cachekit-0.22.0/src/cachekit/hiredis_compat.py +98 -0
  28. cachekit-0.22.0/src/cachekit/invalidation.py +483 -0
  29. {cachekit-0.21.0 → cachekit-0.22.0}/src/cachekit/l1_cache.py +123 -57
  30. {cachekit-0.21.0 → cachekit-0.22.0}/src/cachekit/object_cache.py +159 -86
  31. {cachekit-0.21.0 → cachekit-0.22.0}/src/cachekit/reliability/async_metrics.py +72 -40
  32. {cachekit-0.21.0 → cachekit-0.22.0}/src/cachekit/reliability/circuit_breaker.py +49 -8
  33. {cachekit-0.21.0 → cachekit-0.22.0}/src/cachekit/serializers/__init__.py +2 -2
  34. {cachekit-0.21.0 → cachekit-0.22.0}/src/cachekit/serializers/arrow_serializer.py +9 -0
  35. {cachekit-0.21.0 → cachekit-0.22.0}/src/cachekit/serializers/auto_serializer.py +46 -27
  36. cachekit-0.22.0/src/cachekit/serializers/wrapper.py +270 -0
  37. cachekit-0.21.0/src/cachekit/backends/cachekitio/client.py +0 -245
  38. cachekit-0.21.0/src/cachekit/backends/redis/__init__.py +0 -12
  39. cachekit-0.21.0/src/cachekit/hiredis_compat.py +0 -98
  40. cachekit-0.21.0/src/cachekit/serializers/wrapper.py +0 -169
  41. {cachekit-0.21.0 → cachekit-0.22.0}/Cargo.toml +0 -0
  42. {cachekit-0.21.0 → cachekit-0.22.0}/LICENSE +0 -0
  43. {cachekit-0.21.0 → cachekit-0.22.0}/rust/Makefile +0 -0
  44. {cachekit-0.21.0 → cachekit-0.22.0}/rust/TEST_EXPANSION_SUMMARY.md +0 -0
  45. {cachekit-0.21.0 → cachekit-0.22.0}/rust/src/lib.rs +0 -0
  46. {cachekit-0.21.0 → cachekit-0.22.0}/rust/src/msgpack_bounds.rs +0 -0
  47. {cachekit-0.21.0 → cachekit-0.22.0}/rust/src/python_bindings.rs +0 -0
  48. {cachekit-0.21.0 → cachekit-0.22.0}/rust/tsan_suppressions.txt +0 -0
  49. {cachekit-0.21.0 → cachekit-0.22.0}/src/cachekit/backends/base.py +0 -0
  50. {cachekit-0.21.0 → cachekit-0.22.0}/src/cachekit/backends/base_config.py +0 -0
  51. {cachekit-0.21.0 → cachekit-0.22.0}/src/cachekit/backends/cachekitio/__init__.py +0 -0
  52. {cachekit-0.21.0 → cachekit-0.22.0}/src/cachekit/backends/cachekitio/session.py +0 -0
  53. {cachekit-0.21.0 → cachekit-0.22.0}/src/cachekit/backends/errors.py +0 -0
  54. {cachekit-0.21.0 → cachekit-0.22.0}/src/cachekit/backends/file/__init__.py +0 -0
  55. {cachekit-0.21.0 → cachekit-0.22.0}/src/cachekit/backends/file/backend.py +0 -0
  56. {cachekit-0.21.0 → cachekit-0.22.0}/src/cachekit/backends/file/config.py +0 -0
  57. {cachekit-0.21.0 → cachekit-0.22.0}/src/cachekit/backends/memcached/__init__.py +0 -0
  58. {cachekit-0.21.0 → cachekit-0.22.0}/src/cachekit/backends/memcached/backend.py +0 -0
  59. {cachekit-0.21.0 → cachekit-0.22.0}/src/cachekit/backends/memcached/config.py +0 -0
  60. {cachekit-0.21.0 → cachekit-0.22.0}/src/cachekit/backends/memcached/error_handler.py +0 -0
  61. {cachekit-0.21.0 → cachekit-0.22.0}/src/cachekit/backends/redis/backend.py +0 -0
  62. {cachekit-0.21.0 → cachekit-0.22.0}/src/cachekit/backends/redis/client.py +0 -0
  63. {cachekit-0.21.0 → cachekit-0.22.0}/src/cachekit/backends/redis/error_handler.py +0 -0
  64. {cachekit-0.21.0 → cachekit-0.22.0}/src/cachekit/config/__init__.py +0 -0
  65. {cachekit-0.21.0 → cachekit-0.22.0}/src/cachekit/config/singleton.py +0 -0
  66. {cachekit-0.21.0 → cachekit-0.22.0}/src/cachekit/config/validation.py +0 -0
  67. {cachekit-0.21.0 → cachekit-0.22.0}/src/cachekit/decorators/__init__.py +0 -0
  68. {cachekit-0.21.0 → cachekit-0.22.0}/src/cachekit/decorators/local_wrapper.py +0 -0
  69. {cachekit-0.21.0 → cachekit-0.22.0}/src/cachekit/decorators/main.py +0 -0
  70. {cachekit-0.21.0 → cachekit-0.22.0}/src/cachekit/decorators/session.py +0 -0
  71. {cachekit-0.21.0 → cachekit-0.22.0}/src/cachekit/decorators/stats_context.py +0 -0
  72. {cachekit-0.21.0 → cachekit-0.22.0}/src/cachekit/decorators/tenant_context.py +0 -0
  73. {cachekit-0.21.0 → cachekit-0.22.0}/src/cachekit/decorators/utils/__init__.py +0 -0
  74. {cachekit-0.21.0 → cachekit-0.22.0}/src/cachekit/di.py +0 -0
  75. {cachekit-0.21.0 → cachekit-0.22.0}/src/cachekit/imports.py +0 -0
  76. {cachekit-0.21.0 → cachekit-0.22.0}/src/cachekit/interop.py +0 -0
  77. {cachekit-0.21.0 → cachekit-0.22.0}/src/cachekit/key_generator.py +0 -0
  78. {cachekit-0.21.0 → cachekit-0.22.0}/src/cachekit/logging.py +0 -0
  79. {cachekit-0.21.0 → cachekit-0.22.0}/src/cachekit/monitoring/__init__.py +0 -0
  80. {cachekit-0.21.0 → cachekit-0.22.0}/src/cachekit/monitoring/pool_monitor.py +0 -0
  81. {cachekit-0.21.0 → cachekit-0.22.0}/src/cachekit/monitoring/protocols.py +0 -0
  82. {cachekit-0.21.0 → cachekit-0.22.0}/src/cachekit/py.typed +0 -0
  83. {cachekit-0.21.0 → cachekit-0.22.0}/src/cachekit/reliability/__init__.py +0 -0
  84. {cachekit-0.21.0 → cachekit-0.22.0}/src/cachekit/reliability/error_classification.py +0 -0
  85. {cachekit-0.21.0 → cachekit-0.22.0}/src/cachekit/reliability/load_control.py +0 -0
  86. {cachekit-0.21.0 → cachekit-0.22.0}/src/cachekit/reliability/metrics_collection.py +0 -0
  87. {cachekit-0.21.0 → cachekit-0.22.0}/src/cachekit/serializers/base.py +0 -0
  88. {cachekit-0.21.0 → cachekit-0.22.0}/src/cachekit/serializers/encryption_wrapper.py +0 -0
  89. {cachekit-0.21.0 → cachekit-0.22.0}/src/cachekit/serializers/interop_serializer.py +0 -0
  90. {cachekit-0.21.0 → cachekit-0.22.0}/src/cachekit/serializers/orjson_serializer.py +0 -0
  91. {cachekit-0.21.0 → cachekit-0.22.0}/src/cachekit/serializers/standard_serializer.py +0 -0
@@ -280,7 +280,7 @@ dependencies = [
280
280
 
281
281
  [[package]]
282
282
  name = "cachekit-rs"
283
- version = "0.21.0"
283
+ version = "0.22.0"
284
284
  dependencies = [
285
285
  "cachekit-core",
286
286
  "criterion 0.8.2",
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: cachekit
3
- Version: 0.21.0
3
+ Version: 0.22.0
4
4
  Classifier: Development Status :: 4 - Beta
5
5
  Classifier: Intended Audience :: Developers
6
6
  Classifier: License :: OSI Approved :: MIT License
@@ -28,9 +28,7 @@ Requires-Dist: psutil>=7.0.0
28
28
  Requires-Dist: blake3>=1.0.5
29
29
  Requires-Dist: msgpack>=1.2.1
30
30
  Requires-Dist: xxhash>=3.5.0
31
- Requires-Dist: httpx[http2]>=0.28.1
32
- Requires-Dist: anyio>=4.14.2
33
- Requires-Dist: h2>=4.4.1
31
+ Requires-Dist: urllib3>=2.8.0
34
32
  Requires-Dist: numpy>=2.0.2 ; extra == 'data'
35
33
  Requires-Dist: pandas>=1.3.0 ; extra == 'data'
36
34
  Requires-Dist: pyarrow>=21.0.0 ; extra == 'data'
@@ -226,7 +224,6 @@ Setting `CACHEKIT_MASTER_KEY` instead of passing `master_key=` means every prese
226
224
  | Integrity Checking | - | ✅ | - | ✅ | ✅ 🔒 |
227
225
  | Encryption | - | - | - | - | ✅ Required |
228
226
  | L1 SWR (L1-only mode) | - | ✅ | - | ✅ | - |
229
- | L1 Invalidation | - | - | - | ✅ | ✅ |
230
227
  | Prometheus Metrics | - | -¹ | - | ✅ | ✅ |
231
228
  | Tracing | - | ✅ | - | ✅ | ✅ |
232
229
  | Structured Logging | - | ✅ | - | ✅ | ✅ |
@@ -238,6 +235,8 @@ Setting `CACHEKIT_MASTER_KEY` instead of passing `master_key=` means every prese
238
235
  >
239
236
  > **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)).
240
237
  >
238
+ > **Cross-process L1 eviction** is not a preset feature: every preset's `invalidate_cache()` deletes from L1 and L2 alike, and evicting other processes' L1 copies is one process-wide opt-in, `CACHEKIT_INVALIDATION_LISTENER_ENABLED` ([L1 invalidation](docs/features/l1-invalidation.md#cross-process-l1-eviction)).
239
+ >
241
240
  > **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)).
242
241
  >
243
242
  > **`@cache.io()`** mirrors `@cache.production` (full reliability + observability) but routes to the managed CachekitIO SaaS backend instead of Redis. **`@cache.local()`** is a separate in-process path backed by `ObjectCache` (raw object references, entry-count LRU, no serialization) — the reliability and encryption features listed above do not apply to it.
@@ -298,6 +297,7 @@ def test_cached_function():
298
297
 
299
298
  - Circuit breaker with graceful degradation
300
299
  - Connection pooling with thread affinity (+28% throughput)
300
+ - CachekitIO keeps idle connections pooled until the server closes them, with TCP keepalive probes, so a request after a pause skips a new TLS handshake
301
301
  - Distributed locking prevents cache stampedes
302
302
  - Pluggable backend abstraction (Redis, CachekitIO, File, Memcached, custom)
303
303
  - 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
@@ -312,7 +312,7 @@ def test_cached_function():
312
312
  | **StandardSerializer** | ★★★★☆ | General Python types, NumPy, Pandas |
313
313
  | **OrjsonSerializer** | ★★★★★ | JSON APIs (2-5x faster than stdlib) — requires `cachekit[json]` |
314
314
  | **ArrowSerializer** | ★★★★★ | Large DataFrames (6-23x faster for 10K+ rows) |
315
- | **EncryptionWrapper** | ★★★★☆ | Wraps any serializer with AES-256-GCM |
315
+ | **+ Encryption** | ★★★★☆ | Any serializer above, AES-256-GCM encrypted: `@cache.secure(master_key=..., serializer=...)` |
316
316
 
317
317
  <details>
318
318
  <summary><strong>Serializer Examples</strong></summary>
@@ -451,7 +451,9 @@ free-threaded 3.14 with the GIL verified disabled (CI job
451
451
  `test-freethreaded`), and the Rust extension declares free-threaded safety
452
452
  (`gil_used = false`). Free-threaded wheels are **not yet published** and
453
453
  free-threaded builds are not officially supported — blocked upstream on
454
- orjson (no free-threaded wheels) and hiredis (re-enables the GIL on import). See
454
+ orjson (no free-threaded wheels) and hiredis (re-enables the GIL on import;
455
+ cachekit loads redis-py only for its Redis backend, and keeps hiredis out there,
456
+ so redis-py uses its pure-Python parser). See
455
457
  [measured performance results](docs/free-threading.md#measured-performance) and the
456
458
  full concurrency audit: [docs/free-threading.md](docs/free-threading.md).
457
459
 
@@ -502,7 +504,7 @@ info = expensive_func.cache_info()
502
504
  | [Prometheus Metrics][prometheus-url] | Built-in observability |
503
505
  | [Zero-Knowledge Encryption][encryption-url] | Client-side security |
504
506
  | [Interop Mode][interop-url] | Cross-SDK cache sharing with cachekit-ts/rs |
505
- | [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 |
507
+ | [L1 Invalidation & SWR][l1-invalidation-url] | Invalidation scope (incl. cross-process whole-function on tenant-scoped Redis from the environment or `RedisBackendProvider`), opt-in cross-process L1 eviction, stale-while-revalidate |
506
508
  | [Reference Caching][reference-caching-url] | `@cache.local()` for non-serializable objects |
507
509
  | [Rust Serialization][rust-serialization-url] | ByteStorage layer: LZ4, xxHash3, AES-256-GCM |
508
510
  | [SSRF Protection][ssrf-url] | URL allowlisting for the CachekitIO backend |
@@ -170,7 +170,6 @@ Setting `CACHEKIT_MASTER_KEY` instead of passing `master_key=` means every prese
170
170
  | Integrity Checking | - | ✅ | - | ✅ | ✅ 🔒 |
171
171
  | Encryption | - | - | - | - | ✅ Required |
172
172
  | L1 SWR (L1-only mode) | - | ✅ | - | ✅ | - |
173
- | L1 Invalidation | - | - | - | ✅ | ✅ |
174
173
  | Prometheus Metrics | - | -¹ | - | ✅ | ✅ |
175
174
  | Tracing | - | ✅ | - | ✅ | ✅ |
176
175
  | Structured Logging | - | ✅ | - | ✅ | ✅ |
@@ -182,6 +181,8 @@ Setting `CACHEKIT_MASTER_KEY` instead of passing `master_key=` means every prese
182
181
  >
183
182
  > **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)).
184
183
  >
184
+ > **Cross-process L1 eviction** is not a preset feature: every preset's `invalidate_cache()` deletes from L1 and L2 alike, and evicting other processes' L1 copies is one process-wide opt-in, `CACHEKIT_INVALIDATION_LISTENER_ENABLED` ([L1 invalidation](docs/features/l1-invalidation.md#cross-process-l1-eviction)).
185
+ >
185
186
  > **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)).
186
187
  >
187
188
  > **`@cache.io()`** mirrors `@cache.production` (full reliability + observability) but routes to the managed CachekitIO SaaS backend instead of Redis. **`@cache.local()`** is a separate in-process path backed by `ObjectCache` (raw object references, entry-count LRU, no serialization) — the reliability and encryption features listed above do not apply to it.
@@ -242,6 +243,7 @@ def test_cached_function():
242
243
 
243
244
  - Circuit breaker with graceful degradation
244
245
  - Connection pooling with thread affinity (+28% throughput)
246
+ - CachekitIO keeps idle connections pooled until the server closes them, with TCP keepalive probes, so a request after a pause skips a new TLS handshake
245
247
  - Distributed locking prevents cache stampedes
246
248
  - Pluggable backend abstraction (Redis, CachekitIO, File, Memcached, custom)
247
249
  - 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
@@ -256,7 +258,7 @@ def test_cached_function():
256
258
  | **StandardSerializer** | ★★★★☆ | General Python types, NumPy, Pandas |
257
259
  | **OrjsonSerializer** | ★★★★★ | JSON APIs (2-5x faster than stdlib) — requires `cachekit[json]` |
258
260
  | **ArrowSerializer** | ★★★★★ | Large DataFrames (6-23x faster for 10K+ rows) |
259
- | **EncryptionWrapper** | ★★★★☆ | Wraps any serializer with AES-256-GCM |
261
+ | **+ Encryption** | ★★★★☆ | Any serializer above, AES-256-GCM encrypted: `@cache.secure(master_key=..., serializer=...)` |
260
262
 
261
263
  <details>
262
264
  <summary><strong>Serializer Examples</strong></summary>
@@ -395,7 +397,9 @@ free-threaded 3.14 with the GIL verified disabled (CI job
395
397
  `test-freethreaded`), and the Rust extension declares free-threaded safety
396
398
  (`gil_used = false`). Free-threaded wheels are **not yet published** and
397
399
  free-threaded builds are not officially supported — blocked upstream on
398
- orjson (no free-threaded wheels) and hiredis (re-enables the GIL on import). See
400
+ orjson (no free-threaded wheels) and hiredis (re-enables the GIL on import;
401
+ cachekit loads redis-py only for its Redis backend, and keeps hiredis out there,
402
+ so redis-py uses its pure-Python parser). See
399
403
  [measured performance results](docs/free-threading.md#measured-performance) and the
400
404
  full concurrency audit: [docs/free-threading.md](docs/free-threading.md).
401
405
 
@@ -446,7 +450,7 @@ info = expensive_func.cache_info()
446
450
  | [Prometheus Metrics][prometheus-url] | Built-in observability |
447
451
  | [Zero-Knowledge Encryption][encryption-url] | Client-side security |
448
452
  | [Interop Mode][interop-url] | Cross-SDK cache sharing with cachekit-ts/rs |
449
- | [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 |
453
+ | [L1 Invalidation & SWR][l1-invalidation-url] | Invalidation scope (incl. cross-process whole-function on tenant-scoped Redis from the environment or `RedisBackendProvider`), opt-in cross-process L1 eviction, stale-while-revalidate |
450
454
  | [Reference Caching][reference-caching-url] | `@cache.local()` for non-serializable objects |
451
455
  | [Rust Serialization][rust-serialization-url] | ByteStorage layer: LZ4, xxHash3, AES-256-GCM |
452
456
  | [SSRF Protection][ssrf-url] | URL allowlisting for the CachekitIO backend |
@@ -4,7 +4,7 @@ build-backend = "maturin"
4
4
 
5
5
  [project]
6
6
  name = "cachekit"
7
- version = "0.21.0"
7
+ version = "0.22.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"}
@@ -71,24 +71,10 @@ dependencies = [
71
71
  "blake3>=1.0.5",
72
72
  "msgpack>=1.2.1",
73
73
  "xxhash>=3.5.0",
74
- # HTTP client for SaaS backend (cachekit.io)
75
- "httpx[http2]>=0.28.1",
76
- # anyio is transitive via the mandatory httpx dependency above, so it ships
77
- # to EVERY install, not just dev. Declared here rather than as a
78
- # [tool.uv] constraint because that table is uv-local: it never reaches
79
- # requires-dist, so `pip install cachekit` would ignore it. 4.14.2 fixes
80
- # GHSA-82r6-8w77-94w6 / CVE-2026-63374 (IDNA-2003 hostname encoding lets a
81
- # hijacked connection to an internationalised domain pass TLS certificate
82
- # validation; CVSS 9.3), GHSA-5p39-cfhj-2xmp / CVE-2026-64847 (undrained
83
- # process-pool stderr pipe deadlocks the worker) and GHSA-3w57-8xmc-8v26 /
84
- # CVE-2026-63349 (extra_groups ignored, parent supplementary groups kept).
85
- "anyio>=4.14.2",
86
- # h2 arrives via the http2 extra on that same mandatory httpx dependency, so
87
- # it ships to every install too, and was declared as a [tool.uv] constraint
88
- # with the same no-op effect. 4.4.1 fixes GHSA-6hr6-w5qg-qmwg (duplicate Host
89
- # headers forwarded across an HTTP/2 -> HTTP/1.1 downgrade — a request
90
- # smuggling primitive).
91
- "h2>=4.4.1",
74
+ # HTTP client for the SaaS backend (cachekit.io). It has no dependencies of its own. 2.8.0 fixes
75
+ # GHSA-8988-9cw3-xx77 (HTTPS proxy TLS config ignored), GHSA-gh4c-6fx4-qh6g (chunked deflate
76
+ # infinite loop) and GHSA-vxq7-64xx-v4gw (unbounded chunk-size line buffering).
77
+ "urllib3>=2.8.0",
92
78
  ]
93
79
 
94
80
  [project.optional-dependencies]
@@ -238,7 +224,6 @@ test = [
238
224
  "pymemcache>=4.0.0",
239
225
  # Utilities
240
226
  "faker>=20.0.0",
241
- "httpx>=0.28.1",
242
227
  "hypothesis>=6.0.0",
243
228
  "requests>=2.33.0; python_version >= '3.10'",
244
229
  "psutil>=5.9.0",
@@ -246,6 +231,7 @@ test = [
246
231
  "pyyaml>=6.0.3",
247
232
  "pytest-xdist>=3.8.0",
248
233
  "time-machine>=2.19.0",
234
+ "h11>=0.16.0",
249
235
  ]
250
236
  dev = [
251
237
  { include-group = "test" },
@@ -275,15 +261,9 @@ fuzz = [
275
261
  # This table is uv-local: it constrains resolution of this repo's lockfile and
276
262
  # never reaches requires-dist, so a floor placed here does NOT protect anyone who
277
263
  # runs `pip install cachekit`. A transitive that reaches users belongs in
278
- # [project] dependencies instead — see the anyio/h2 entries above.
264
+ # [project] dependencies instead — see the urllib3 entry above.
279
265
  [tool.uv]
280
266
  constraint-dependencies = [
281
- # urllib3 is a dev-only transitive dep (requests -> urllib3; requests is a
282
- # dev dependency and also arrives via pip-audit).
283
- # 2.8.0 fixes GHSA-8988-9cw3-xx77 (HTTPS proxy TLS config ignored),
284
- # GHSA-gh4c-6fx4-qh6g (chunked deflate infinite loop) and
285
- # GHSA-vxq7-64xx-v4gw (unbounded chunk-size line buffering).
286
- "urllib3>=2.8.0",
287
267
  "fonttools>=4.60.2",
288
268
  "werkzeug>=3.1.4",
289
269
  # pip is a dev-only transitive dep (pip-audit -> pip-api -> pip). 26.1.2 fixes
@@ -1,6 +1,6 @@
1
1
  [package]
2
2
  name = "cachekit-rs"
3
- version = "0.21.0"
3
+ version = "0.22.0"
4
4
  edition = "2021"
5
5
  authors = ["cachekit Contributors"]
6
6
  description = "High-performance storage engine for caching with compression and encryption"
@@ -170,7 +170,6 @@ Setting `CACHEKIT_MASTER_KEY` instead of passing `master_key=` means every prese
170
170
  | Integrity Checking | - | ✅ | - | ✅ | ✅ 🔒 |
171
171
  | Encryption | - | - | - | - | ✅ Required |
172
172
  | L1 SWR (L1-only mode) | - | ✅ | - | ✅ | - |
173
- | L1 Invalidation | - | - | - | ✅ | ✅ |
174
173
  | Prometheus Metrics | - | -¹ | - | ✅ | ✅ |
175
174
  | Tracing | - | ✅ | - | ✅ | ✅ |
176
175
  | Structured Logging | - | ✅ | - | ✅ | ✅ |
@@ -182,6 +181,8 @@ Setting `CACHEKIT_MASTER_KEY` instead of passing `master_key=` means every prese
182
181
  >
183
182
  > **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)).
184
183
  >
184
+ > **Cross-process L1 eviction** is not a preset feature: every preset's `invalidate_cache()` deletes from L1 and L2 alike, and evicting other processes' L1 copies is one process-wide opt-in, `CACHEKIT_INVALIDATION_LISTENER_ENABLED` ([L1 invalidation](docs/features/l1-invalidation.md#cross-process-l1-eviction)).
185
+ >
185
186
  > **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)).
186
187
  >
187
188
  > **`@cache.io()`** mirrors `@cache.production` (full reliability + observability) but routes to the managed CachekitIO SaaS backend instead of Redis. **`@cache.local()`** is a separate in-process path backed by `ObjectCache` (raw object references, entry-count LRU, no serialization) — the reliability and encryption features listed above do not apply to it.
@@ -242,6 +243,7 @@ def test_cached_function():
242
243
 
243
244
  - Circuit breaker with graceful degradation
244
245
  - Connection pooling with thread affinity (+28% throughput)
246
+ - CachekitIO keeps idle connections pooled until the server closes them, with TCP keepalive probes, so a request after a pause skips a new TLS handshake
245
247
  - Distributed locking prevents cache stampedes
246
248
  - Pluggable backend abstraction (Redis, CachekitIO, File, Memcached, custom)
247
249
  - 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
@@ -256,7 +258,7 @@ def test_cached_function():
256
258
  | **StandardSerializer** | ★★★★☆ | General Python types, NumPy, Pandas |
257
259
  | **OrjsonSerializer** | ★★★★★ | JSON APIs (2-5x faster than stdlib) — requires `cachekit[json]` |
258
260
  | **ArrowSerializer** | ★★★★★ | Large DataFrames (6-23x faster for 10K+ rows) |
259
- | **EncryptionWrapper** | ★★★★☆ | Wraps any serializer with AES-256-GCM |
261
+ | **+ Encryption** | ★★★★☆ | Any serializer above, AES-256-GCM encrypted: `@cache.secure(master_key=..., serializer=...)` |
260
262
 
261
263
  <details>
262
264
  <summary><strong>Serializer Examples</strong></summary>
@@ -395,7 +397,9 @@ free-threaded 3.14 with the GIL verified disabled (CI job
395
397
  `test-freethreaded`), and the Rust extension declares free-threaded safety
396
398
  (`gil_used = false`). Free-threaded wheels are **not yet published** and
397
399
  free-threaded builds are not officially supported — blocked upstream on
398
- orjson (no free-threaded wheels) and hiredis (re-enables the GIL on import). See
400
+ orjson (no free-threaded wheels) and hiredis (re-enables the GIL on import;
401
+ cachekit loads redis-py only for its Redis backend, and keeps hiredis out there,
402
+ so redis-py uses its pure-Python parser). See
399
403
  [measured performance results](docs/free-threading.md#measured-performance) and the
400
404
  full concurrency audit: [docs/free-threading.md](docs/free-threading.md).
401
405
 
@@ -446,7 +450,7 @@ info = expensive_func.cache_info()
446
450
  | [Prometheus Metrics][prometheus-url] | Built-in observability |
447
451
  | [Zero-Knowledge Encryption][encryption-url] | Client-side security |
448
452
  | [Interop Mode][interop-url] | Cross-SDK cache sharing with cachekit-ts/rs |
449
- | [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 |
453
+ | [L1 Invalidation & SWR][l1-invalidation-url] | Invalidation scope (incl. cross-process whole-function on tenant-scoped Redis from the environment or `RedisBackendProvider`), opt-in cross-process L1 eviction, stale-while-revalidate |
450
454
  | [Reference Caching][reference-caching-url] | `@cache.local()` for non-serializable objects |
451
455
  | [Rust Serialization][rust-serialization-url] | ByteStorage layer: LZ4, xxHash3, AES-256-GCM |
452
456
  | [SSRF Protection][ssrf-url] | URL allowlisting for the CachekitIO backend |
@@ -68,17 +68,14 @@ Example Usage:
68
68
  ```
69
69
  """
70
70
 
71
- __version__ = "0.21.0"
71
+ __version__ = "0.22.0"
72
72
 
73
73
  from collections.abc import Callable
74
74
  from typing import Any, TypeVar
75
75
 
76
- # Configure hiredis compatibility BEFORE any Redis imports
77
- # This prevents GIL warnings in Python 3.13+ free-threading mode
78
- try:
79
- from . import hiredis_compat
80
- except ImportError:
81
- pass
76
+ # Must run BEFORE any redis import: CACHEKIT_DISABLE_HIREDIS=true keeps hiredis out from here on.
77
+ # The free-threaded default runs later, when cachekit.backends.redis loads (see cachekit.hiredis_compat).
78
+ from . import hiredis_compat # noqa: F401
82
79
 
83
80
  # Import the configuration classes
84
81
  # Redis client is automatically fast - no special import needed
@@ -30,7 +30,7 @@ Dependency injection pattern:
30
30
 
31
31
  from __future__ import annotations
32
32
 
33
- from typing import Protocol
33
+ from typing import TYPE_CHECKING, Protocol
34
34
 
35
35
  from cachekit.backends.base import (
36
36
  BaseBackend,
@@ -45,7 +45,10 @@ from cachekit.backends.errors import (
45
45
  BackendErrorType,
46
46
  CapabilityNotAvailableError,
47
47
  )
48
- from cachekit.backends.redis import RedisBackend
48
+
49
+ if TYPE_CHECKING:
50
+ from cachekit.backends.memcached import MemcachedBackend
51
+ from cachekit.backends.redis import RedisBackend
49
52
 
50
53
  __all__ = [
51
54
  "BaseBackend",
@@ -106,7 +109,15 @@ class BackendProvider(Protocol):
106
109
 
107
110
 
108
111
  def __getattr__(name: str):
109
- """Lazy import for optional backends (pymemcache may not be installed)."""
112
+ """Lazy imports: pymemcache may not be installed, and redis-py loads only for a program that uses Redis.
113
+
114
+ Loading redis-py loads hiredis, which re-enables the GIL on a free-threaded build, so
115
+ ``import cachekit`` must not load it (see cachekit.hiredis_compat).
116
+ """
117
+ if name == "RedisBackend":
118
+ from cachekit.backends.redis import RedisBackend
119
+
120
+ return RedisBackend
110
121
  if name == "MemcachedBackend":
111
122
  from cachekit.backends.memcached import MemcachedBackend
112
123
 
@@ -0,0 +1,38 @@
1
+ """Await an executor or HTTP round-trip to completion through cancellation.
2
+
3
+ Shared by the Redis provider and the CachekitIO backend. It lives here, not in the Redis provider,
4
+ because importing that module loads redis-py, and a CachekitIO-only program must not.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ import asyncio
10
+ from typing import Optional, TypeVar
11
+
12
+ T = TypeVar("T")
13
+
14
+
15
+ async def _await_uninterrupted(fut: asyncio.Future[T]) -> T:
16
+ """Await ``fut`` to completion even if the current task is cancelled meanwhile.
17
+
18
+ ``asyncio.to_thread`` work is uninterruptible once an executor thread picks it up, and a
19
+ still-queued work item is dropped if its future is cancelled first — so a cancelled awaiter
20
+ either loses the outcome of a round-trip that still completes, or loses the round-trip
21
+ itself. ``asyncio.wait`` never cancels its inputs and never unwraps their result, so keep
22
+ waiting on ``fut`` until it is really done, absorbing every cancellation, then re-raise the
23
+ last one: callers read ``fut`` for the real outcome before letting it propagate.
24
+
25
+ Prefer a plain future (``loop.run_in_executor``): ``all_tasks()`` sweeps such as ``asyncio.run``
26
+ teardown cancel Tasks out from under the drain, and the outcome is lost again. A Task
27
+ (``asyncio.ensure_future``) is the route for native coroutines such as an async HTTP request;
28
+ it gives up only that sweep, so the caller must treat a cancelled ``fut`` as having no outcome.
29
+ """
30
+ cancelled: Optional[asyncio.CancelledError] = None
31
+ while not fut.done():
32
+ try:
33
+ await asyncio.wait({fut})
34
+ except asyncio.CancelledError as exc:
35
+ cancelled = exc
36
+ if cancelled is not None:
37
+ raise cancelled
38
+ return fut.result()