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.
- {cachekit-0.21.0 → cachekit-0.22.0}/Cargo.lock +1 -1
- {cachekit-0.21.0 → cachekit-0.22.0}/PKG-INFO +10 -8
- {cachekit-0.21.0 → cachekit-0.22.0}/README.md +8 -4
- {cachekit-0.21.0 → cachekit-0.22.0}/pyproject.toml +7 -27
- {cachekit-0.21.0 → cachekit-0.22.0}/rust/Cargo.toml +1 -1
- {cachekit-0.21.0 → cachekit-0.22.0}/rust/README.md +8 -4
- {cachekit-0.21.0 → cachekit-0.22.0}/src/cachekit/__init__.py +4 -7
- {cachekit-0.21.0 → cachekit-0.22.0}/src/cachekit/backends/__init__.py +14 -3
- cachekit-0.22.0/src/cachekit/backends/_uninterrupted.py +38 -0
- {cachekit-0.21.0 → cachekit-0.22.0}/src/cachekit/backends/cachekitio/backend.py +420 -165
- cachekit-0.22.0/src/cachekit/backends/cachekitio/client.py +279 -0
- {cachekit-0.21.0 → cachekit-0.22.0}/src/cachekit/backends/cachekitio/config.py +6 -4
- {cachekit-0.21.0 → cachekit-0.22.0}/src/cachekit/backends/cachekitio/error_handler.py +37 -19
- {cachekit-0.21.0 → cachekit-0.22.0}/src/cachekit/backends/provider.py +8 -4
- cachekit-0.22.0/src/cachekit/backends/redis/__init__.py +17 -0
- {cachekit-0.21.0 → cachekit-0.22.0}/src/cachekit/backends/redis/config.py +6 -2
- {cachekit-0.21.0 → cachekit-0.22.0}/src/cachekit/backends/redis/provider.py +61 -24
- {cachekit-0.21.0 → cachekit-0.22.0}/src/cachekit/cache_handler.py +74 -7
- {cachekit-0.21.0 → cachekit-0.22.0}/src/cachekit/config/decorator.py +2 -0
- {cachekit-0.21.0 → cachekit-0.22.0}/src/cachekit/config/nested.py +12 -8
- {cachekit-0.21.0 → cachekit-0.22.0}/src/cachekit/config/settings.py +13 -2
- {cachekit-0.21.0 → cachekit-0.22.0}/src/cachekit/decorators/intent.py +8 -1
- {cachekit-0.21.0 → cachekit-0.22.0}/src/cachekit/decorators/orchestrator.py +25 -4
- {cachekit-0.21.0 → cachekit-0.22.0}/src/cachekit/decorators/wrapper.py +544 -288
- {cachekit-0.21.0 → cachekit-0.22.0}/src/cachekit/hash_utils.py +63 -4
- {cachekit-0.21.0 → cachekit-0.22.0}/src/cachekit/health.py +6 -1
- cachekit-0.22.0/src/cachekit/hiredis_compat.py +98 -0
- cachekit-0.22.0/src/cachekit/invalidation.py +483 -0
- {cachekit-0.21.0 → cachekit-0.22.0}/src/cachekit/l1_cache.py +123 -57
- {cachekit-0.21.0 → cachekit-0.22.0}/src/cachekit/object_cache.py +159 -86
- {cachekit-0.21.0 → cachekit-0.22.0}/src/cachekit/reliability/async_metrics.py +72 -40
- {cachekit-0.21.0 → cachekit-0.22.0}/src/cachekit/reliability/circuit_breaker.py +49 -8
- {cachekit-0.21.0 → cachekit-0.22.0}/src/cachekit/serializers/__init__.py +2 -2
- {cachekit-0.21.0 → cachekit-0.22.0}/src/cachekit/serializers/arrow_serializer.py +9 -0
- {cachekit-0.21.0 → cachekit-0.22.0}/src/cachekit/serializers/auto_serializer.py +46 -27
- cachekit-0.22.0/src/cachekit/serializers/wrapper.py +270 -0
- cachekit-0.21.0/src/cachekit/backends/cachekitio/client.py +0 -245
- cachekit-0.21.0/src/cachekit/backends/redis/__init__.py +0 -12
- cachekit-0.21.0/src/cachekit/hiredis_compat.py +0 -98
- cachekit-0.21.0/src/cachekit/serializers/wrapper.py +0 -169
- {cachekit-0.21.0 → cachekit-0.22.0}/Cargo.toml +0 -0
- {cachekit-0.21.0 → cachekit-0.22.0}/LICENSE +0 -0
- {cachekit-0.21.0 → cachekit-0.22.0}/rust/Makefile +0 -0
- {cachekit-0.21.0 → cachekit-0.22.0}/rust/TEST_EXPANSION_SUMMARY.md +0 -0
- {cachekit-0.21.0 → cachekit-0.22.0}/rust/src/lib.rs +0 -0
- {cachekit-0.21.0 → cachekit-0.22.0}/rust/src/msgpack_bounds.rs +0 -0
- {cachekit-0.21.0 → cachekit-0.22.0}/rust/src/python_bindings.rs +0 -0
- {cachekit-0.21.0 → cachekit-0.22.0}/rust/tsan_suppressions.txt +0 -0
- {cachekit-0.21.0 → cachekit-0.22.0}/src/cachekit/backends/base.py +0 -0
- {cachekit-0.21.0 → cachekit-0.22.0}/src/cachekit/backends/base_config.py +0 -0
- {cachekit-0.21.0 → cachekit-0.22.0}/src/cachekit/backends/cachekitio/__init__.py +0 -0
- {cachekit-0.21.0 → cachekit-0.22.0}/src/cachekit/backends/cachekitio/session.py +0 -0
- {cachekit-0.21.0 → cachekit-0.22.0}/src/cachekit/backends/errors.py +0 -0
- {cachekit-0.21.0 → cachekit-0.22.0}/src/cachekit/backends/file/__init__.py +0 -0
- {cachekit-0.21.0 → cachekit-0.22.0}/src/cachekit/backends/file/backend.py +0 -0
- {cachekit-0.21.0 → cachekit-0.22.0}/src/cachekit/backends/file/config.py +0 -0
- {cachekit-0.21.0 → cachekit-0.22.0}/src/cachekit/backends/memcached/__init__.py +0 -0
- {cachekit-0.21.0 → cachekit-0.22.0}/src/cachekit/backends/memcached/backend.py +0 -0
- {cachekit-0.21.0 → cachekit-0.22.0}/src/cachekit/backends/memcached/config.py +0 -0
- {cachekit-0.21.0 → cachekit-0.22.0}/src/cachekit/backends/memcached/error_handler.py +0 -0
- {cachekit-0.21.0 → cachekit-0.22.0}/src/cachekit/backends/redis/backend.py +0 -0
- {cachekit-0.21.0 → cachekit-0.22.0}/src/cachekit/backends/redis/client.py +0 -0
- {cachekit-0.21.0 → cachekit-0.22.0}/src/cachekit/backends/redis/error_handler.py +0 -0
- {cachekit-0.21.0 → cachekit-0.22.0}/src/cachekit/config/__init__.py +0 -0
- {cachekit-0.21.0 → cachekit-0.22.0}/src/cachekit/config/singleton.py +0 -0
- {cachekit-0.21.0 → cachekit-0.22.0}/src/cachekit/config/validation.py +0 -0
- {cachekit-0.21.0 → cachekit-0.22.0}/src/cachekit/decorators/__init__.py +0 -0
- {cachekit-0.21.0 → cachekit-0.22.0}/src/cachekit/decorators/local_wrapper.py +0 -0
- {cachekit-0.21.0 → cachekit-0.22.0}/src/cachekit/decorators/main.py +0 -0
- {cachekit-0.21.0 → cachekit-0.22.0}/src/cachekit/decorators/session.py +0 -0
- {cachekit-0.21.0 → cachekit-0.22.0}/src/cachekit/decorators/stats_context.py +0 -0
- {cachekit-0.21.0 → cachekit-0.22.0}/src/cachekit/decorators/tenant_context.py +0 -0
- {cachekit-0.21.0 → cachekit-0.22.0}/src/cachekit/decorators/utils/__init__.py +0 -0
- {cachekit-0.21.0 → cachekit-0.22.0}/src/cachekit/di.py +0 -0
- {cachekit-0.21.0 → cachekit-0.22.0}/src/cachekit/imports.py +0 -0
- {cachekit-0.21.0 → cachekit-0.22.0}/src/cachekit/interop.py +0 -0
- {cachekit-0.21.0 → cachekit-0.22.0}/src/cachekit/key_generator.py +0 -0
- {cachekit-0.21.0 → cachekit-0.22.0}/src/cachekit/logging.py +0 -0
- {cachekit-0.21.0 → cachekit-0.22.0}/src/cachekit/monitoring/__init__.py +0 -0
- {cachekit-0.21.0 → cachekit-0.22.0}/src/cachekit/monitoring/pool_monitor.py +0 -0
- {cachekit-0.21.0 → cachekit-0.22.0}/src/cachekit/monitoring/protocols.py +0 -0
- {cachekit-0.21.0 → cachekit-0.22.0}/src/cachekit/py.typed +0 -0
- {cachekit-0.21.0 → cachekit-0.22.0}/src/cachekit/reliability/__init__.py +0 -0
- {cachekit-0.21.0 → cachekit-0.22.0}/src/cachekit/reliability/error_classification.py +0 -0
- {cachekit-0.21.0 → cachekit-0.22.0}/src/cachekit/reliability/load_control.py +0 -0
- {cachekit-0.21.0 → cachekit-0.22.0}/src/cachekit/reliability/metrics_collection.py +0 -0
- {cachekit-0.21.0 → cachekit-0.22.0}/src/cachekit/serializers/base.py +0 -0
- {cachekit-0.21.0 → cachekit-0.22.0}/src/cachekit/serializers/encryption_wrapper.py +0 -0
- {cachekit-0.21.0 → cachekit-0.22.0}/src/cachekit/serializers/interop_serializer.py +0 -0
- {cachekit-0.21.0 → cachekit-0.22.0}/src/cachekit/serializers/orjson_serializer.py +0 -0
- {cachekit-0.21.0 → cachekit-0.22.0}/src/cachekit/serializers/standard_serializer.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: cachekit
|
|
3
|
-
Version: 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:
|
|
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
|
-
| **
|
|
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
|
|
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
|
-
| **
|
|
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
|
|
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.
|
|
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
|
-
|
|
76
|
-
#
|
|
77
|
-
|
|
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
|
|
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
|
|
@@ -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
|
-
| **
|
|
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
|
|
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.
|
|
71
|
+
__version__ = "0.22.0"
|
|
72
72
|
|
|
73
73
|
from collections.abc import Callable
|
|
74
74
|
from typing import Any, TypeVar
|
|
75
75
|
|
|
76
|
-
#
|
|
77
|
-
#
|
|
78
|
-
|
|
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
|
-
|
|
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
|
|
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()
|