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