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.
- {cachekit-0.18.0 → cachekit-0.20.0}/Cargo.lock +3 -3
- {cachekit-0.18.0 → cachekit-0.20.0}/PKG-INFO +56 -19
- {cachekit-0.18.0 → cachekit-0.20.0}/README.md +52 -18
- {cachekit-0.18.0 → cachekit-0.20.0}/pyproject.toml +45 -20
- {cachekit-0.18.0 → cachekit-0.20.0}/rust/Cargo.toml +2 -2
- {cachekit-0.18.0 → cachekit-0.20.0}/rust/README.md +52 -18
- {cachekit-0.18.0 → cachekit-0.20.0}/rust/TEST_EXPANSION_SUMMARY.md +2 -2
- {cachekit-0.18.0 → cachekit-0.20.0}/rust/src/lib.rs +28 -2
- cachekit-0.20.0/rust/src/msgpack_bounds.rs +87 -0
- {cachekit-0.18.0 → cachekit-0.20.0}/rust/src/python_bindings.rs +126 -47
- {cachekit-0.18.0 → cachekit-0.20.0}/src/cachekit/__init__.py +5 -4
- {cachekit-0.18.0 → cachekit-0.20.0}/src/cachekit/backends/__init__.py +10 -4
- {cachekit-0.18.0 → cachekit-0.20.0}/src/cachekit/backends/base.py +83 -9
- {cachekit-0.18.0 → cachekit-0.20.0}/src/cachekit/backends/base_config.py +7 -1
- {cachekit-0.18.0 → cachekit-0.20.0}/src/cachekit/backends/cachekitio/backend.py +148 -40
- cachekit-0.20.0/src/cachekit/backends/cachekitio/client.py +187 -0
- {cachekit-0.18.0 → cachekit-0.20.0}/src/cachekit/backends/cachekitio/config.py +35 -4
- {cachekit-0.18.0 → cachekit-0.20.0}/src/cachekit/backends/cachekitio/error_handler.py +11 -6
- {cachekit-0.18.0 → cachekit-0.20.0}/src/cachekit/backends/errors.py +21 -4
- {cachekit-0.18.0 → cachekit-0.20.0}/src/cachekit/backends/file/backend.py +81 -22
- {cachekit-0.18.0 → cachekit-0.20.0}/src/cachekit/backends/memcached/backend.py +4 -1
- {cachekit-0.18.0 → cachekit-0.20.0}/src/cachekit/backends/memcached/config.py +5 -4
- {cachekit-0.18.0 → cachekit-0.20.0}/src/cachekit/backends/memcached/error_handler.py +18 -8
- {cachekit-0.18.0 → cachekit-0.20.0}/src/cachekit/backends/provider.py +23 -25
- {cachekit-0.18.0 → cachekit-0.20.0}/src/cachekit/backends/redis/backend.py +5 -5
- {cachekit-0.18.0 → cachekit-0.20.0}/src/cachekit/backends/redis/error_handler.py +30 -21
- cachekit-0.20.0/src/cachekit/backends/redis/provider.py +804 -0
- {cachekit-0.18.0 → cachekit-0.20.0}/src/cachekit/cache_handler.py +500 -368
- {cachekit-0.18.0 → cachekit-0.20.0}/src/cachekit/config/decorator.py +85 -93
- {cachekit-0.18.0 → cachekit-0.20.0}/src/cachekit/config/nested.py +55 -16
- {cachekit-0.18.0 → cachekit-0.20.0}/src/cachekit/config/settings.py +13 -139
- {cachekit-0.18.0 → cachekit-0.20.0}/src/cachekit/config/singleton.py +2 -2
- cachekit-0.20.0/src/cachekit/config/validation.py +243 -0
- {cachekit-0.18.0 → cachekit-0.20.0}/src/cachekit/decorators/intent.py +65 -26
- {cachekit-0.18.0 → cachekit-0.20.0}/src/cachekit/decorators/orchestrator.py +56 -101
- {cachekit-0.18.0 → cachekit-0.20.0}/src/cachekit/decorators/session.py +16 -8
- {cachekit-0.18.0 → cachekit-0.20.0}/src/cachekit/decorators/wrapper.py +751 -373
- cachekit-0.20.0/src/cachekit/hash_utils.py +145 -0
- {cachekit-0.18.0 → cachekit-0.20.0}/src/cachekit/hiredis_compat.py +5 -3
- {cachekit-0.18.0 → cachekit-0.20.0}/src/cachekit/interop.py +33 -11
- {cachekit-0.18.0 → cachekit-0.20.0}/src/cachekit/key_generator.py +79 -10
- {cachekit-0.18.0 → cachekit-0.20.0}/src/cachekit/l1_cache.py +165 -9
- {cachekit-0.18.0 → cachekit-0.20.0}/src/cachekit/logging.py +41 -226
- {cachekit-0.18.0 → cachekit-0.20.0}/src/cachekit/monitoring/pool_monitor.py +5 -5
- {cachekit-0.18.0 → cachekit-0.20.0}/src/cachekit/object_cache.py +1 -1
- {cachekit-0.18.0 → cachekit-0.20.0}/src/cachekit/reliability/__init__.py +0 -2
- {cachekit-0.18.0 → cachekit-0.20.0}/src/cachekit/reliability/async_metrics.py +190 -33
- {cachekit-0.18.0 → cachekit-0.20.0}/src/cachekit/reliability/circuit_breaker.py +53 -36
- {cachekit-0.18.0 → cachekit-0.20.0}/src/cachekit/reliability/metrics_collection.py +47 -32
- {cachekit-0.18.0 → cachekit-0.20.0}/src/cachekit/reliability/profiles.py +0 -102
- {cachekit-0.18.0 → cachekit-0.20.0}/src/cachekit/serializers/__init__.py +6 -3
- cachekit-0.20.0/src/cachekit/serializers/auto_serializer.py +1262 -0
- {cachekit-0.18.0 → cachekit-0.20.0}/src/cachekit/serializers/base.py +158 -0
- {cachekit-0.18.0 → cachekit-0.20.0}/src/cachekit/serializers/encryption_wrapper.py +68 -35
- {cachekit-0.18.0 → cachekit-0.20.0}/src/cachekit/serializers/orjson_serializer.py +1 -1
- {cachekit-0.18.0 → cachekit-0.20.0}/src/cachekit/serializers/standard_serializer.py +60 -15
- {cachekit-0.18.0 → cachekit-0.20.0}/src/cachekit/serializers/wrapper.py +16 -2
- cachekit-0.18.0/rust/supply-chain/audits.toml +0 -4
- cachekit-0.18.0/rust/supply-chain/config.toml +0 -1341
- cachekit-0.18.0/rust/supply-chain/imports.lock +0 -2
- cachekit-0.18.0/src/cachekit/backends/cachekitio/client.py +0 -122
- cachekit-0.18.0/src/cachekit/backends/redis/provider.py +0 -501
- cachekit-0.18.0/src/cachekit/config/validation.py +0 -102
- cachekit-0.18.0/src/cachekit/hash_utils.py +0 -50
- cachekit-0.18.0/src/cachekit/monitoring/correlation_tracking.py +0 -308
- cachekit-0.18.0/src/cachekit/serializers/auto_serializer.py +0 -919
- {cachekit-0.18.0 → cachekit-0.20.0}/Cargo.toml +0 -0
- {cachekit-0.18.0 → cachekit-0.20.0}/LICENSE +0 -0
- {cachekit-0.18.0 → cachekit-0.20.0}/rust/Makefile +0 -0
- {cachekit-0.18.0 → cachekit-0.20.0}/rust/tsan_suppressions.txt +0 -0
- {cachekit-0.18.0 → cachekit-0.20.0}/src/cachekit/backends/cachekitio/__init__.py +0 -0
- {cachekit-0.18.0 → cachekit-0.20.0}/src/cachekit/backends/cachekitio/session.py +0 -0
- {cachekit-0.18.0 → cachekit-0.20.0}/src/cachekit/backends/file/__init__.py +0 -0
- {cachekit-0.18.0 → cachekit-0.20.0}/src/cachekit/backends/file/config.py +0 -0
- {cachekit-0.18.0 → cachekit-0.20.0}/src/cachekit/backends/memcached/__init__.py +0 -0
- {cachekit-0.18.0 → cachekit-0.20.0}/src/cachekit/backends/redis/__init__.py +0 -0
- {cachekit-0.18.0 → cachekit-0.20.0}/src/cachekit/backends/redis/client.py +0 -0
- {cachekit-0.18.0 → cachekit-0.20.0}/src/cachekit/backends/redis/config.py +0 -0
- {cachekit-0.18.0 → cachekit-0.20.0}/src/cachekit/config/__init__.py +0 -0
- {cachekit-0.18.0 → cachekit-0.20.0}/src/cachekit/decorators/__init__.py +0 -0
- {cachekit-0.18.0 → cachekit-0.20.0}/src/cachekit/decorators/local_wrapper.py +0 -0
- {cachekit-0.18.0 → cachekit-0.20.0}/src/cachekit/decorators/main.py +0 -0
- {cachekit-0.18.0 → cachekit-0.20.0}/src/cachekit/decorators/stats_context.py +0 -0
- {cachekit-0.18.0 → cachekit-0.20.0}/src/cachekit/decorators/tenant_context.py +0 -0
- {cachekit-0.18.0 → cachekit-0.20.0}/src/cachekit/decorators/utils/__init__.py +0 -0
- {cachekit-0.18.0 → cachekit-0.20.0}/src/cachekit/di.py +0 -0
- {cachekit-0.18.0 → cachekit-0.20.0}/src/cachekit/health.py +0 -0
- {cachekit-0.18.0 → cachekit-0.20.0}/src/cachekit/imports.py +0 -0
- {cachekit-0.18.0 → cachekit-0.20.0}/src/cachekit/monitoring/__init__.py +0 -0
- {cachekit-0.18.0 → cachekit-0.20.0}/src/cachekit/monitoring/protocols.py +0 -0
- {cachekit-0.18.0 → cachekit-0.20.0}/src/cachekit/py.typed +0 -0
- {cachekit-0.18.0 → cachekit-0.20.0}/src/cachekit/reliability/error_classification.py +0 -0
- {cachekit-0.18.0 → cachekit-0.20.0}/src/cachekit/reliability/load_control.py +0 -0
- {cachekit-0.18.0 → cachekit-0.20.0}/src/cachekit/serializers/arrow_serializer.py +0 -0
- {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.
|
|
248
|
+
version = "0.6.0"
|
|
249
249
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
250
|
-
checksum = "
|
|
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.
|
|
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.
|
|
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` —
|
|
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
|
|
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
|
-
|
|
321
|
-
|
|
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
|
-
|
|
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
|
-
**
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
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
|
-
- **
|
|
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
|
|
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] |
|
|
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" #
|
|
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` —
|
|
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
|
|
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
|
-
|
|
268
|
-
|
|
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
|
-
|
|
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
|
-
**
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
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
|
-
- **
|
|
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
|
|
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] |
|
|
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" #
|
|
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.
|
|
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
|
-
|
|
204
|
-
|
|
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
|
-
|
|
254
|
-
|
|
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.
|
|
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.
|
|
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 }
|