cachekit 0.25.1__tar.gz → 0.26.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.25.1 → cachekit-0.26.0}/Cargo.lock +5 -5
- {cachekit-0.25.1 → cachekit-0.26.0}/PKG-INFO +47 -39
- {cachekit-0.25.1 → cachekit-0.26.0}/README.md +46 -38
- {cachekit-0.25.1 → cachekit-0.26.0}/pyproject.toml +1 -1
- {cachekit-0.25.1 → cachekit-0.26.0}/rust/Cargo.toml +1 -1
- {cachekit-0.25.1 → cachekit-0.26.0}/rust/README.md +46 -38
- cachekit-0.26.0/rust/src/l1_hit.rs +588 -0
- {cachekit-0.25.1 → cachekit-0.26.0}/rust/src/lib.rs +48 -4
- cachekit-0.26.0/rust/src/msgpack_decode.rs +687 -0
- {cachekit-0.25.1 → cachekit-0.26.0}/rust/src/python_bindings.rs +61 -6
- cachekit-0.26.0/rust/src/rendezvous.rs +161 -0
- cachekit-0.26.0/rust/src/short_bytes.rs +90 -0
- {cachekit-0.25.1 → cachekit-0.26.0}/src/cachekit/__init__.py +5 -5
- {cachekit-0.25.1 → cachekit-0.26.0}/src/cachekit/backends/base.py +1 -1
- {cachekit-0.25.1 → cachekit-0.26.0}/src/cachekit/backends/cachekitio/backend.py +60 -32
- {cachekit-0.25.1 → cachekit-0.26.0}/src/cachekit/backends/cachekitio/config.py +90 -1
- {cachekit-0.25.1 → cachekit-0.26.0}/src/cachekit/backends/cachekitio/error_handler.py +1 -1
- {cachekit-0.25.1 → cachekit-0.26.0}/src/cachekit/backends/memcached/backend.py +4 -0
- cachekit-0.26.0/src/cachekit/backends/memcached/hasher.py +35 -0
- {cachekit-0.25.1 → cachekit-0.26.0}/src/cachekit/backends/redis/provider.py +77 -10
- {cachekit-0.25.1 → cachekit-0.26.0}/src/cachekit/cache_handler.py +98 -45
- {cachekit-0.25.1 → cachekit-0.26.0}/src/cachekit/config/decorator.py +15 -3
- {cachekit-0.25.1 → cachekit-0.26.0}/src/cachekit/config/nested.py +6 -2
- cachekit-0.26.0/src/cachekit/decorators/l1_latency.py +206 -0
- {cachekit-0.25.1 → cachekit-0.26.0}/src/cachekit/decorators/local_wrapper.py +89 -17
- {cachekit-0.25.1 → cachekit-0.26.0}/src/cachekit/decorators/wrapper.py +388 -337
- {cachekit-0.25.1 → cachekit-0.26.0}/src/cachekit/decorators/write_behind.py +65 -11
- {cachekit-0.25.1 → cachekit-0.26.0}/src/cachekit/interop.py +24 -9
- {cachekit-0.25.1 → cachekit-0.26.0}/src/cachekit/l1_cache.py +155 -34
- cachekit-0.26.0/src/cachekit/object_cache.py +1071 -0
- {cachekit-0.25.1 → cachekit-0.26.0}/src/cachekit/reliability/async_metrics.py +7 -0
- {cachekit-0.25.1 → cachekit-0.26.0}/src/cachekit/serializers/auto_serializer.py +240 -44
- {cachekit-0.25.1 → cachekit-0.26.0}/src/cachekit/serializers/base.py +32 -2
- {cachekit-0.25.1 → cachekit-0.26.0}/src/cachekit/serializers/orjson_serializer.py +54 -26
- {cachekit-0.25.1 → cachekit-0.26.0}/src/cachekit/serializers/standard_serializer.py +29 -48
- cachekit-0.25.1/src/cachekit/object_cache.py +0 -649
- {cachekit-0.25.1 → cachekit-0.26.0}/Cargo.toml +0 -0
- {cachekit-0.25.1 → cachekit-0.26.0}/LICENSE +0 -0
- {cachekit-0.25.1 → cachekit-0.26.0}/rust/Makefile +0 -0
- {cachekit-0.25.1 → cachekit-0.26.0}/rust/TEST_EXPANSION_SUMMARY.md +0 -0
- {cachekit-0.25.1 → cachekit-0.26.0}/rust/src/hit_counter.rs +0 -0
- {cachekit-0.25.1 → cachekit-0.26.0}/rust/tsan_suppressions.txt +0 -0
- {cachekit-0.25.1 → cachekit-0.26.0}/src/cachekit/_fork.py +0 -0
- {cachekit-0.25.1 → cachekit-0.26.0}/src/cachekit/backends/__init__.py +0 -0
- {cachekit-0.25.1 → cachekit-0.26.0}/src/cachekit/backends/_uninterrupted.py +0 -0
- {cachekit-0.25.1 → cachekit-0.26.0}/src/cachekit/backends/base_config.py +0 -0
- {cachekit-0.25.1 → cachekit-0.26.0}/src/cachekit/backends/cachekitio/__init__.py +0 -0
- {cachekit-0.25.1 → cachekit-0.26.0}/src/cachekit/backends/cachekitio/client.py +0 -0
- {cachekit-0.25.1 → cachekit-0.26.0}/src/cachekit/backends/cachekitio/session.py +0 -0
- {cachekit-0.25.1 → cachekit-0.26.0}/src/cachekit/backends/errors.py +0 -0
- {cachekit-0.25.1 → cachekit-0.26.0}/src/cachekit/backends/file/__init__.py +0 -0
- {cachekit-0.25.1 → cachekit-0.26.0}/src/cachekit/backends/file/backend.py +0 -0
- {cachekit-0.25.1 → cachekit-0.26.0}/src/cachekit/backends/file/config.py +0 -0
- {cachekit-0.25.1 → cachekit-0.26.0}/src/cachekit/backends/memcached/__init__.py +0 -0
- {cachekit-0.25.1 → cachekit-0.26.0}/src/cachekit/backends/memcached/config.py +0 -0
- {cachekit-0.25.1 → cachekit-0.26.0}/src/cachekit/backends/memcached/error_handler.py +0 -0
- {cachekit-0.25.1 → cachekit-0.26.0}/src/cachekit/backends/provider.py +0 -0
- {cachekit-0.25.1 → cachekit-0.26.0}/src/cachekit/backends/redis/__init__.py +0 -0
- {cachekit-0.25.1 → cachekit-0.26.0}/src/cachekit/backends/redis/backend.py +0 -0
- {cachekit-0.25.1 → cachekit-0.26.0}/src/cachekit/backends/redis/client.py +0 -0
- {cachekit-0.25.1 → cachekit-0.26.0}/src/cachekit/backends/redis/config.py +0 -0
- {cachekit-0.25.1 → cachekit-0.26.0}/src/cachekit/backends/redis/error_handler.py +0 -0
- {cachekit-0.25.1 → cachekit-0.26.0}/src/cachekit/config/__init__.py +0 -0
- {cachekit-0.25.1 → cachekit-0.26.0}/src/cachekit/config/settings.py +0 -0
- {cachekit-0.25.1 → cachekit-0.26.0}/src/cachekit/config/singleton.py +0 -0
- {cachekit-0.25.1 → cachekit-0.26.0}/src/cachekit/config/validation.py +0 -0
- {cachekit-0.25.1 → cachekit-0.26.0}/src/cachekit/decorators/__init__.py +0 -0
- {cachekit-0.25.1 → cachekit-0.26.0}/src/cachekit/decorators/intent.py +0 -0
- {cachekit-0.25.1 → cachekit-0.26.0}/src/cachekit/decorators/main.py +0 -0
- {cachekit-0.25.1 → cachekit-0.26.0}/src/cachekit/decorators/orchestrator.py +0 -0
- {cachekit-0.25.1 → cachekit-0.26.0}/src/cachekit/decorators/session.py +0 -0
- {cachekit-0.25.1 → cachekit-0.26.0}/src/cachekit/decorators/single_flight.py +0 -0
- {cachekit-0.25.1 → cachekit-0.26.0}/src/cachekit/decorators/stats_context.py +0 -0
- {cachekit-0.25.1 → cachekit-0.26.0}/src/cachekit/decorators/tenant_context.py +0 -0
- {cachekit-0.25.1 → cachekit-0.26.0}/src/cachekit/decorators/utils/__init__.py +0 -0
- {cachekit-0.25.1 → cachekit-0.26.0}/src/cachekit/di.py +0 -0
- {cachekit-0.25.1 → cachekit-0.26.0}/src/cachekit/hash_utils.py +0 -0
- {cachekit-0.25.1 → cachekit-0.26.0}/src/cachekit/health.py +0 -0
- {cachekit-0.25.1 → cachekit-0.26.0}/src/cachekit/hiredis_compat.py +0 -0
- {cachekit-0.25.1 → cachekit-0.26.0}/src/cachekit/imports.py +0 -0
- {cachekit-0.25.1 → cachekit-0.26.0}/src/cachekit/invalidation.py +0 -0
- {cachekit-0.25.1 → cachekit-0.26.0}/src/cachekit/key_generator.py +0 -0
- {cachekit-0.25.1 → cachekit-0.26.0}/src/cachekit/logging.py +0 -0
- {cachekit-0.25.1 → cachekit-0.26.0}/src/cachekit/monitoring/__init__.py +0 -0
- {cachekit-0.25.1 → cachekit-0.26.0}/src/cachekit/monitoring/protocols.py +0 -0
- {cachekit-0.25.1 → cachekit-0.26.0}/src/cachekit/py.typed +0 -0
- {cachekit-0.25.1 → cachekit-0.26.0}/src/cachekit/reliability/__init__.py +0 -0
- {cachekit-0.25.1 → cachekit-0.26.0}/src/cachekit/reliability/circuit_breaker.py +0 -0
- {cachekit-0.25.1 → cachekit-0.26.0}/src/cachekit/reliability/error_classification.py +0 -0
- {cachekit-0.25.1 → cachekit-0.26.0}/src/cachekit/reliability/load_control.py +0 -0
- {cachekit-0.25.1 → cachekit-0.26.0}/src/cachekit/reliability/metrics_collection.py +0 -0
- {cachekit-0.25.1 → cachekit-0.26.0}/src/cachekit/serializers/__init__.py +0 -0
- {cachekit-0.25.1 → cachekit-0.26.0}/src/cachekit/serializers/arrow_serializer.py +0 -0
- {cachekit-0.25.1 → cachekit-0.26.0}/src/cachekit/serializers/encryption_wrapper.py +0 -0
- {cachekit-0.25.1 → cachekit-0.26.0}/src/cachekit/serializers/interop_serializer.py +0 -0
- {cachekit-0.25.1 → cachekit-0.26.0}/src/cachekit/serializers/wrapper.py +0 -0
|
@@ -198,9 +198,9 @@ checksum = "c8efb64bd706a16a1bdde310ae86b351e4d21550d98d056f22f8a7f7a2183fec"
|
|
|
198
198
|
|
|
199
199
|
[[package]]
|
|
200
200
|
name = "cachekit-core"
|
|
201
|
-
version = "0.7.
|
|
201
|
+
version = "0.7.4"
|
|
202
202
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
203
|
-
checksum = "
|
|
203
|
+
checksum = "d9217fe47d4024d7b0d1dda460bbcfc0f613bd80fb94c4fbbc4e7e6d829d7342"
|
|
204
204
|
dependencies = [
|
|
205
205
|
"aes",
|
|
206
206
|
"aes-gcm",
|
|
@@ -221,7 +221,7 @@ dependencies = [
|
|
|
221
221
|
|
|
222
222
|
[[package]]
|
|
223
223
|
name = "cachekit-rs"
|
|
224
|
-
version = "0.
|
|
224
|
+
version = "0.26.0"
|
|
225
225
|
dependencies = [
|
|
226
226
|
"cachekit-core",
|
|
227
227
|
"criterion 0.8.2",
|
|
@@ -892,9 +892,9 @@ checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897"
|
|
|
892
892
|
|
|
893
893
|
[[package]]
|
|
894
894
|
name = "lz4_flex"
|
|
895
|
-
version = "0.
|
|
895
|
+
version = "0.13.1"
|
|
896
896
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
897
|
-
checksum = "
|
|
897
|
+
checksum = "7ef0d4ed8669f8f8826eb00dc878084aa8f253506c4fd5e8f58f5bce72ddb97e"
|
|
898
898
|
|
|
899
899
|
[[package]]
|
|
900
900
|
name = "memchr"
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: cachekit
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.26.0
|
|
4
4
|
Classifier: Development Status :: 4 - Beta
|
|
5
5
|
Classifier: Intended Audience :: Developers
|
|
6
6
|
Classifier: License :: OSI Approved :: MIT License
|
|
@@ -87,15 +87,17 @@ def expensive_function():
|
|
|
87
87
|
return fetch_data()
|
|
88
88
|
```
|
|
89
89
|
|
|
90
|
-
|
|
90
|
+
With nothing configured, that plain `@cache` caches in process memory (L1) only: no backend,
|
|
91
|
+
no distributed lock, no encryption. Configure a backend and pick a [preset](#intent-based-optimization),
|
|
92
|
+
and you get:
|
|
91
93
|
|
|
92
94
|
| Feature | Description |
|
|
93
95
|
|:--------|:------------|
|
|
94
96
|
| **Circuit breaker** | Prevents cascading failures |
|
|
95
|
-
| **Distributed locking** | Multi-pod safety |
|
|
97
|
+
| **Distributed locking** | Multi-pod safety (async functions, on a backend that supports it) |
|
|
96
98
|
| **Prometheus metrics** | Built-in observability |
|
|
97
99
|
| **MessagePack serialization** | Efficient with optional compression |
|
|
98
|
-
| **Zero-knowledge encryption** | Client-side AES-256-GCM |
|
|
100
|
+
| **Zero-knowledge encryption** | Client-side AES-256-GCM, opt-in (`@cache.secure`) |
|
|
99
101
|
|
|
100
102
|
---
|
|
101
103
|
|
|
@@ -137,7 +139,7 @@ backend that fits your infrastructure — they're peers behind the same `@cache`
|
|
|
137
139
|
| Backend | Best for | Select with |
|
|
138
140
|
|---------|----------|-------------|
|
|
139
141
|
| Redis | Self-hosted, full control | `REDIS_URL` / `CACHEKIT_REDIS_URL` |
|
|
140
|
-
|
|
|
142
|
+
| CacheKit Cloud | Managed, zero-ops (closed beta) | `CACHEKIT_API_KEY` |
|
|
141
143
|
| Memcached | High-throughput, existing infra | `CACHEKIT_MEMCACHED_SERVERS` |
|
|
142
144
|
| File / L1-only | Local dev, tests, no external deps | `CACHEKIT_FILE_CACHE_DIR` / nothing (or `backend=None`) |
|
|
143
145
|
|
|
@@ -155,31 +157,36 @@ def expensive_api_call(user_id: int):
|
|
|
155
157
|
```
|
|
156
158
|
|
|
157
159
|
> [!WARNING]
|
|
158
|
-
>
|
|
159
|
-
>
|
|
160
|
-
>
|
|
161
|
-
>
|
|
160
|
+
> With nothing configured, `@cache` never connects to `redis://localhost:6379`. To use that
|
|
161
|
+
> Redis, set `REDIS_URL=redis://localhost:6379` (or `CACHEKIT_REDIS_URL`); upgraders, see the
|
|
162
|
+
> [CHANGELOG](CHANGELOG.md). `@cache.secure`
|
|
163
|
+
> and `encryption=True` still need a backend and still fall back to localhost Redis; see
|
|
164
|
+
> [Backend Resolution Priority](docs/backends/README.md#backend-resolution-priority). `interop=`
|
|
165
|
+
> refuses that Redis, and any Redis resolved without `backend=`, because it prefixes keys with the
|
|
166
|
+
> tenant: pass `backend=RedisBackend()` ([Interop Mode](docs/features/interop-mode.md)).
|
|
162
167
|
|
|
163
168
|
### More Backends
|
|
164
169
|
|
|
165
170
|
<details>
|
|
166
|
-
<summary><strong>
|
|
171
|
+
<summary><strong>CacheKit Cloud — Managed (Closed Beta)</strong></summary>
|
|
167
172
|
|
|
168
173
|
```python notest
|
|
169
174
|
import os
|
|
170
175
|
from cachekit import cache
|
|
171
176
|
|
|
172
|
-
# Set your
|
|
177
|
+
# Set your CacheKit Cloud API key
|
|
173
178
|
# export CACHEKIT_API_KEY="your-api-key" # pragma: allowlist secret
|
|
174
179
|
|
|
175
|
-
@cache.io() # Uses
|
|
180
|
+
@cache.io() # Uses the CacheKit Cloud backend — no Redis to manage
|
|
176
181
|
def expensive_api_call(user_id: int):
|
|
177
182
|
return fetch_user_data(user_id)
|
|
178
183
|
```
|
|
179
184
|
|
|
180
|
-
*
|
|
185
|
+
*CacheKit Cloud is in closed beta: sign-up is by waitlist at [dash.cachekit.io](https://dash.cachekit.io).*
|
|
181
186
|
|
|
182
|
-
|
|
187
|
+
During the beta the backend reports hit counts and L1 hit latency to CacheKit Cloud by default; `CACHEKIT_TELEMETRY=false` turns that off ([Telemetry](docs/backends/cachekitio.md#telemetry)).
|
|
188
|
+
|
|
189
|
+
How fresh a CacheKit Cloud read is, and how long a deleted project's data stays readable: [Consistency and Deletion](https://docs.cachekit.io/concepts/consistency/).
|
|
183
190
|
|
|
184
191
|
</details>
|
|
185
192
|
|
|
@@ -205,9 +212,9 @@ def expensive_api_call(user_id: int):
|
|
|
205
212
|
|
|
206
213
|
---
|
|
207
214
|
|
|
208
|
-
> **
|
|
215
|
+
> **CacheKit Cloud (Closed Beta)**
|
|
209
216
|
> Managed caching with zero infrastructure. L1+L2 caching, circuit breaker, and automatic failover — no Redis to manage.
|
|
210
|
-
> *
|
|
217
|
+
> *CacheKit Cloud is in closed beta: sign-up is by waitlist at [dash.cachekit.io](https://dash.cachekit.io).*
|
|
211
218
|
|
|
212
219
|
---
|
|
213
220
|
|
|
@@ -244,12 +251,12 @@ Setting `CACHEKIT_MASTER_KEY` instead of passing `master_key=` means every prese
|
|
|
244
251
|
| Integrity Checking | - | ✅ | - | ✅ | ✅ 🔒 |
|
|
245
252
|
| Encryption | - | - | - | - | ✅ Required |
|
|
246
253
|
| L1 SWR (L1-only mode) | - | ✅ | - | ✅ | - |
|
|
247
|
-
| Prometheus Metrics | - | -¹ | - | ✅ | ✅ |
|
|
254
|
+
| Prometheus Metrics¹ | - | -¹ | - | ✅ | ✅ |
|
|
248
255
|
| Structured Logging | - | ✅ | - | ✅ | ✅ |
|
|
249
256
|
| Backend unreachable² | Function runs, no error | Function runs, no error | Function runs, no error | Function runs, no error | Function runs, no error |
|
|
250
257
|
| **Use Case** | High throughput | Local debugging | Deterministic tests | Production reliability | Compliance/security |
|
|
251
258
|
|
|
252
|
-
> ¹ `@cache.dev`
|
|
259
|
+
> ¹ A preset with a ✅ records `get` and `set` operations only when it has a backend: in L1-only mode it records none. `@cache.dev` records none of them: it sets `enable_prometheus_metrics=False`. Every preset still records the [series that ignore `MonitoringConfig`](docs/features/prometheus-metrics.md#series-that-ignore-monitoringconfig), such as `circuit_breaker_state`. See [Which Decorators Record](docs/features/prometheus-metrics.md#which-decorators-record).
|
|
253
260
|
>
|
|
254
261
|
> ² A backend that is down or unreachable never raises to the caller, on any preset (`@cache.io` included): the call logs the failure and runs your function. What gets cached meanwhile is in [Connection Errors](docs/error-codes.md#connection-errors).
|
|
255
262
|
>
|
|
@@ -261,7 +268,7 @@ Setting `CACHEKIT_MASTER_KEY` instead of passing `master_key=` means every prese
|
|
|
261
268
|
>
|
|
262
269
|
> **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)).
|
|
263
270
|
>
|
|
264
|
-
> **`@cache.io()`** mirrors `@cache.production` (full reliability + observability) but routes to
|
|
271
|
+
> **`@cache.io()`** mirrors `@cache.production` (full reliability + observability) but routes to CacheKit Cloud instead of Redis. **`@cache.local()`** is a Python-only, SDK-local preset: 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.
|
|
265
272
|
|
|
266
273
|
<details>
|
|
267
274
|
<summary><strong>Additional Presets: <code>@cache.dev</code> and <code>@cache.test</code></strong></summary>
|
|
@@ -269,7 +276,7 @@ Setting `CACHEKIT_MASTER_KEY` instead of passing `master_key=` means every prese
|
|
|
269
276
|
`@cache.dev` and `@cache.test` are Python-only, SDK-local presets. See the comparison table above for the exact feature set of each preset.
|
|
270
277
|
|
|
271
278
|
```python
|
|
272
|
-
# Development: verbose logging, integrity checks on,
|
|
279
|
+
# Development: verbose logging, integrity checks on, no cache-operation Prometheus series
|
|
273
280
|
@cache.dev
|
|
274
281
|
def debug_expensive_call():
|
|
275
282
|
return complex_computation()
|
|
@@ -297,7 +304,7 @@ def test_cached_function():
|
|
|
297
304
|
│ └─────────────┘ └─────────────┘ └─────────────────────┘ │
|
|
298
305
|
├─────────────────────────────────────────────────────────────┤
|
|
299
306
|
│ L1 Cache (In-Memory) │ L2 Cache (Pluggable Backend) │
|
|
300
|
-
│ in-process │ Redis /
|
|
307
|
+
│ in-process │ Redis / CacheKit Cloud / File / │
|
|
301
308
|
│ │ Memcached: one backend round trip │
|
|
302
309
|
├─────────────────────────────────────────────────────────────┤
|
|
303
310
|
│ Rust Core (PyO3) │
|
|
@@ -315,17 +322,17 @@ def test_cached_function():
|
|
|
315
322
|
|
|
316
323
|
## Performance
|
|
317
324
|
|
|
318
|
-
What a call costs inside your process, from the wall-clock guards in `tests/performance/` run on a GitHub-hosted `ubuntu-latest` runner (AMD EPYC 7763, 4 vCPUs) with CPython 3.14.7. Each figure is the mean of five run medians with its 95% band.
|
|
325
|
+
What a call costs inside your process, from the wall-clock guards in `tests/performance/` run on a GitHub-hosted `ubuntu-latest` runner (AMD EPYC 7763, 4 vCPUs) with CPython 3.14.7. Each figure is the mean of five run medians with its 95% band. The two L1-only rows come from [a later run](https://github.com/cachekit-io/cachekit-py/actions/runs/41210621694) on the same CPU model and Python as the other two.
|
|
319
326
|
|
|
320
327
|
| Path | Time per call |
|
|
321
328
|
|:-----|--------------:|
|
|
322
|
-
| `@cache(backend=None)` L1 hit, 100-user nested dict (23.5 KB as MessagePack) | 0.
|
|
323
|
-
| `@cache(backend=None)` L1 hit, 10K-row DataFrame | 0.
|
|
324
|
-
| L1 hit with an L2 backend configured, same dict |
|
|
325
|
-
| Redis L2 hit with L1 disabled, same dict, Redis in a container on the same machine |
|
|
329
|
+
| `@cache(backend=None)` L1 hit, 100-user nested dict (23.5 KB as MessagePack) | 0.341 ± 0.001 µs |
|
|
330
|
+
| `@cache(backend=None)` L1 hit, 10K-row DataFrame | 0.34 ± 0.00 µs |
|
|
331
|
+
| L1 hit with an L2 backend configured, same dict | 284 ± 2 µs |
|
|
332
|
+
| Redis L2 hit with L1 disabled, same dict, Redis in a container on the same machine | 539 ± 3 µs |
|
|
326
333
|
|
|
327
|
-
- With no backend, L1 keeps the returned object itself, whatever the payload's size, and a repeat call with a few short scalar arguments
|
|
328
|
-
- With an L2 backend, L1 holds the serialized bytes and every hit deserializes them: for this dict, about
|
|
334
|
+
- With no backend, L1 keeps the returned object itself, whatever the payload's size, and a repeat call with a few short scalar arguments is answered by one call into the Rust extension that takes no lock ([which calls](https://github.com/cachekit-io/cachekit-py/blob/main/docs/performance.md#decorator--l1-hit-hot-path)). Any other call builds its key every time, as does every call to a function with a backend.
|
|
335
|
+
- With an L2 backend, L1 holds the serialized bytes and every hit deserializes them: for this dict, about 830 times the L1-only hit. This run predates the Rust decode every such hit now runs, which bounds and decodes the MessagePack in one pass at about half the instructions. Encryption adds a decrypt to each such hit; this run measured the difference at 9.3 ± 1.0 µs.
|
|
329
336
|
- An L2 hit adds the backend's round trip. A Redis on another machine adds its network latency to the last row.
|
|
330
337
|
|
|
331
338
|
Hosted runners come with other CPU models too: in a run of an earlier commit, an EPYC 9V45 runner took about half as long on every row, and other machines give other figures. The [Performance Guide](https://github.com/cachekit-io/cachekit-py/blob/main/docs/performance.md#github-hosted-runner) names the guard behind each figure and how to run it, and the repository's `Bench` workflow repeats the run.
|
|
@@ -338,9 +345,9 @@ Hosted runners come with other CPU models too: in a run of an earlier commit, an
|
|
|
338
345
|
|
|
339
346
|
- Circuit breaker with graceful degradation
|
|
340
347
|
- Connection pooling with thread affinity
|
|
341
|
-
-
|
|
342
|
-
- Distributed locking
|
|
343
|
-
- Pluggable backend abstraction (Redis,
|
|
348
|
+
- The CacheKit Cloud backend keeps idle connections pooled until the server closes them, with TCP keepalive probes, so a request after a pause skips a new TLS handshake
|
|
349
|
+
- Distributed locking guards against cache stampedes across pods, best effort (async functions, on a backend that supports it)
|
|
350
|
+
- Pluggable backend abstraction (Redis, CacheKit Cloud, File, Memcached, custom)
|
|
344
351
|
- 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/42ea601f3c1656df0a06904afa2406604f43f57d/test-vectors/decode-bounds.json) vectors
|
|
345
352
|
|
|
346
353
|
> [!NOTE]
|
|
@@ -372,7 +379,7 @@ def get_large_dataset(date: str):
|
|
|
372
379
|
return pd.read_csv(f"data/{date}.csv")
|
|
373
380
|
```
|
|
374
381
|
|
|
375
|
-
Encrypted DataFrames go through `@cache.secure`, which takes any serializer whose class declares `cross_sdk_compatible = True` ([details](docs/serializers/encryption.md#composability)). A file backend keeps this example self-contained; production uses Redis or
|
|
382
|
+
Encrypted DataFrames go through `@cache.secure`, which takes any serializer whose class declares `cross_sdk_compatible = True` ([details](docs/serializers/encryption.md#composability)). A file backend keeps this example self-contained; production uses Redis or CacheKit Cloud:
|
|
376
383
|
|
|
377
384
|
```python
|
|
378
385
|
import tempfile
|
|
@@ -459,7 +466,7 @@ See [SECURITY.md][security-url] for vulnerability reporting and detailed documen
|
|
|
459
466
|
### Monitoring & Observability
|
|
460
467
|
|
|
461
468
|
- **Per-function statistics** - `cache_info()` on every decorated function, modelled on `functools.lru_cache`
|
|
462
|
-
- **Prometheus metrics** - Recorded by default (your app owns exposition)
|
|
469
|
+
- **Prometheus metrics** - Recorded by default by a decorator with a backend (your app owns exposition)
|
|
463
470
|
- **Structured logging** - Context-aware, per-operation fields
|
|
464
471
|
- **Health checks** - Comprehensive status endpoints
|
|
465
472
|
|
|
@@ -494,9 +501,9 @@ exposition setup.
|
|
|
494
501
|
**Free-threaded CPython (3.15t, 3.14t):** the core suites run green on
|
|
495
502
|
free-threaded 3.14 and 3.15 with the GIL verified disabled (CI job
|
|
496
503
|
`test-freethreaded`), and the Rust extension declares free-threaded safety
|
|
497
|
-
(`gil_used = false`). **
|
|
504
|
+
(`gil_used = false`). **3.15t has a wheel**:
|
|
498
505
|
the 3.15+ wheel is `abi3t`, so it installs on 3.15t without a Rust toolchain
|
|
499
|
-
(pip
|
|
506
|
+
([with a recent pip or uv](docs/free-threading.md#cpython-315-one-abi3t-wheel)). **3.14t is tested
|
|
500
507
|
but has no wheel** (it builds from source). Free-threaded builds are not
|
|
501
508
|
officially supported yet — blocked upstream on
|
|
502
509
|
orjson (no free-threaded wheels) and hiredis (re-enables the GIL on import;
|
|
@@ -555,7 +562,7 @@ info = expensive_func.cache_info()
|
|
|
555
562
|
| [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 |
|
|
556
563
|
| [Reference Caching][reference-caching-url] | `@cache.local()` for non-serializable objects |
|
|
557
564
|
| [Rust Serialization][rust-serialization-url] | ByteStorage layer: LZ4, xxHash3, AES-256-GCM |
|
|
558
|
-
| [SSRF Protection][ssrf-url] | URL allowlisting for the
|
|
565
|
+
| [SSRF Protection][ssrf-url] | URL allowlisting for the CacheKit Cloud backend |
|
|
559
566
|
| [Write-Behind][write-behind-url] | Opt-in `write_behind=True`: a miss returns before its L2 write lands |
|
|
560
567
|
|
|
561
568
|
---
|
|
@@ -569,16 +576,17 @@ info = expensive_func.cache_info()
|
|
|
569
576
|
# CACHEKIT_MEMCACHED_SERVERS, CACHEKIT_FILE_CACHE_DIR. Two or more is a ConfigurationError at
|
|
570
577
|
# first call, and decorators relying on env auto-detection run uncached (docs/backends/README.md).
|
|
571
578
|
# REDIS_URL is only a fallback and never conflicts. With none of them set, decorators cache
|
|
572
|
-
# L1-only, in process (encrypted
|
|
579
|
+
# L1-only, in process (encrypted decorators still use localhost Redis; interop decorators refuse
|
|
580
|
+
# any Redis resolved here, which is tenant-prefixed, and need backend=RedisBackend()).
|
|
573
581
|
|
|
574
582
|
# Redis Connection (priority: CACHEKIT_REDIS_URL > REDIS_URL)
|
|
575
583
|
CACHEKIT_REDIS_URL="redis://localhost:6379" # Primary (preferred)
|
|
576
584
|
REDIS_URL="redis://localhost:6379" # Fallback
|
|
577
585
|
|
|
578
|
-
#
|
|
586
|
+
# CacheKit Cloud backend (closed beta: join the waitlist at https://dash.cachekit.io)
|
|
579
587
|
# For @cache.io() next to Redis, pass api_key= from your secret store instead.
|
|
580
588
|
# CACHEKIT_API_KEY="your-api-key" # pragma: allowlist secret
|
|
581
|
-
CACHEKIT_API_URL="https://api.cachekit.io" # Default
|
|
589
|
+
CACHEKIT_API_URL="https://api.cachekit.io" # Default CacheKit Cloud endpoint
|
|
582
590
|
|
|
583
591
|
# Memcached Backend (optional: pip install cachekit[memcached])
|
|
584
592
|
# CACHEKIT_MEMCACHED_SERVERS='["mc1:11211", "mc2:11211"]' # Default: 127.0.0.1:11211
|
|
@@ -34,15 +34,17 @@ def expensive_function():
|
|
|
34
34
|
return fetch_data()
|
|
35
35
|
```
|
|
36
36
|
|
|
37
|
-
|
|
37
|
+
With nothing configured, that plain `@cache` caches in process memory (L1) only: no backend,
|
|
38
|
+
no distributed lock, no encryption. Configure a backend and pick a [preset](#intent-based-optimization),
|
|
39
|
+
and you get:
|
|
38
40
|
|
|
39
41
|
| Feature | Description |
|
|
40
42
|
|:--------|:------------|
|
|
41
43
|
| **Circuit breaker** | Prevents cascading failures |
|
|
42
|
-
| **Distributed locking** | Multi-pod safety |
|
|
44
|
+
| **Distributed locking** | Multi-pod safety (async functions, on a backend that supports it) |
|
|
43
45
|
| **Prometheus metrics** | Built-in observability |
|
|
44
46
|
| **MessagePack serialization** | Efficient with optional compression |
|
|
45
|
-
| **Zero-knowledge encryption** | Client-side AES-256-GCM |
|
|
47
|
+
| **Zero-knowledge encryption** | Client-side AES-256-GCM, opt-in (`@cache.secure`) |
|
|
46
48
|
|
|
47
49
|
---
|
|
48
50
|
|
|
@@ -84,7 +86,7 @@ backend that fits your infrastructure — they're peers behind the same `@cache`
|
|
|
84
86
|
| Backend | Best for | Select with |
|
|
85
87
|
|---------|----------|-------------|
|
|
86
88
|
| Redis | Self-hosted, full control | `REDIS_URL` / `CACHEKIT_REDIS_URL` |
|
|
87
|
-
|
|
|
89
|
+
| CacheKit Cloud | Managed, zero-ops (closed beta) | `CACHEKIT_API_KEY` |
|
|
88
90
|
| Memcached | High-throughput, existing infra | `CACHEKIT_MEMCACHED_SERVERS` |
|
|
89
91
|
| File / L1-only | Local dev, tests, no external deps | `CACHEKIT_FILE_CACHE_DIR` / nothing (or `backend=None`) |
|
|
90
92
|
|
|
@@ -102,31 +104,36 @@ def expensive_api_call(user_id: int):
|
|
|
102
104
|
```
|
|
103
105
|
|
|
104
106
|
> [!WARNING]
|
|
105
|
-
>
|
|
106
|
-
>
|
|
107
|
-
>
|
|
108
|
-
>
|
|
107
|
+
> With nothing configured, `@cache` never connects to `redis://localhost:6379`. To use that
|
|
108
|
+
> Redis, set `REDIS_URL=redis://localhost:6379` (or `CACHEKIT_REDIS_URL`); upgraders, see the
|
|
109
|
+
> [CHANGELOG](CHANGELOG.md). `@cache.secure`
|
|
110
|
+
> and `encryption=True` still need a backend and still fall back to localhost Redis; see
|
|
111
|
+
> [Backend Resolution Priority](docs/backends/README.md#backend-resolution-priority). `interop=`
|
|
112
|
+
> refuses that Redis, and any Redis resolved without `backend=`, because it prefixes keys with the
|
|
113
|
+
> tenant: pass `backend=RedisBackend()` ([Interop Mode](docs/features/interop-mode.md)).
|
|
109
114
|
|
|
110
115
|
### More Backends
|
|
111
116
|
|
|
112
117
|
<details>
|
|
113
|
-
<summary><strong>
|
|
118
|
+
<summary><strong>CacheKit Cloud — Managed (Closed Beta)</strong></summary>
|
|
114
119
|
|
|
115
120
|
```python notest
|
|
116
121
|
import os
|
|
117
122
|
from cachekit import cache
|
|
118
123
|
|
|
119
|
-
# Set your
|
|
124
|
+
# Set your CacheKit Cloud API key
|
|
120
125
|
# export CACHEKIT_API_KEY="your-api-key" # pragma: allowlist secret
|
|
121
126
|
|
|
122
|
-
@cache.io() # Uses
|
|
127
|
+
@cache.io() # Uses the CacheKit Cloud backend — no Redis to manage
|
|
123
128
|
def expensive_api_call(user_id: int):
|
|
124
129
|
return fetch_user_data(user_id)
|
|
125
130
|
```
|
|
126
131
|
|
|
127
|
-
*
|
|
132
|
+
*CacheKit Cloud is in closed beta: sign-up is by waitlist at [dash.cachekit.io](https://dash.cachekit.io).*
|
|
128
133
|
|
|
129
|
-
|
|
134
|
+
During the beta the backend reports hit counts and L1 hit latency to CacheKit Cloud by default; `CACHEKIT_TELEMETRY=false` turns that off ([Telemetry](docs/backends/cachekitio.md#telemetry)).
|
|
135
|
+
|
|
136
|
+
How fresh a CacheKit Cloud read is, and how long a deleted project's data stays readable: [Consistency and Deletion](https://docs.cachekit.io/concepts/consistency/).
|
|
130
137
|
|
|
131
138
|
</details>
|
|
132
139
|
|
|
@@ -152,9 +159,9 @@ def expensive_api_call(user_id: int):
|
|
|
152
159
|
|
|
153
160
|
---
|
|
154
161
|
|
|
155
|
-
> **
|
|
162
|
+
> **CacheKit Cloud (Closed Beta)**
|
|
156
163
|
> Managed caching with zero infrastructure. L1+L2 caching, circuit breaker, and automatic failover — no Redis to manage.
|
|
157
|
-
> *
|
|
164
|
+
> *CacheKit Cloud is in closed beta: sign-up is by waitlist at [dash.cachekit.io](https://dash.cachekit.io).*
|
|
158
165
|
|
|
159
166
|
---
|
|
160
167
|
|
|
@@ -191,12 +198,12 @@ Setting `CACHEKIT_MASTER_KEY` instead of passing `master_key=` means every prese
|
|
|
191
198
|
| Integrity Checking | - | ✅ | - | ✅ | ✅ 🔒 |
|
|
192
199
|
| Encryption | - | - | - | - | ✅ Required |
|
|
193
200
|
| L1 SWR (L1-only mode) | - | ✅ | - | ✅ | - |
|
|
194
|
-
| Prometheus Metrics | - | -¹ | - | ✅ | ✅ |
|
|
201
|
+
| Prometheus Metrics¹ | - | -¹ | - | ✅ | ✅ |
|
|
195
202
|
| Structured Logging | - | ✅ | - | ✅ | ✅ |
|
|
196
203
|
| Backend unreachable² | Function runs, no error | Function runs, no error | Function runs, no error | Function runs, no error | Function runs, no error |
|
|
197
204
|
| **Use Case** | High throughput | Local debugging | Deterministic tests | Production reliability | Compliance/security |
|
|
198
205
|
|
|
199
|
-
> ¹ `@cache.dev`
|
|
206
|
+
> ¹ A preset with a ✅ records `get` and `set` operations only when it has a backend: in L1-only mode it records none. `@cache.dev` records none of them: it sets `enable_prometheus_metrics=False`. Every preset still records the [series that ignore `MonitoringConfig`](docs/features/prometheus-metrics.md#series-that-ignore-monitoringconfig), such as `circuit_breaker_state`. See [Which Decorators Record](docs/features/prometheus-metrics.md#which-decorators-record).
|
|
200
207
|
>
|
|
201
208
|
> ² A backend that is down or unreachable never raises to the caller, on any preset (`@cache.io` included): the call logs the failure and runs your function. What gets cached meanwhile is in [Connection Errors](docs/error-codes.md#connection-errors).
|
|
202
209
|
>
|
|
@@ -208,7 +215,7 @@ Setting `CACHEKIT_MASTER_KEY` instead of passing `master_key=` means every prese
|
|
|
208
215
|
>
|
|
209
216
|
> **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)).
|
|
210
217
|
>
|
|
211
|
-
> **`@cache.io()`** mirrors `@cache.production` (full reliability + observability) but routes to
|
|
218
|
+
> **`@cache.io()`** mirrors `@cache.production` (full reliability + observability) but routes to CacheKit Cloud instead of Redis. **`@cache.local()`** is a Python-only, SDK-local preset: 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.
|
|
212
219
|
|
|
213
220
|
<details>
|
|
214
221
|
<summary><strong>Additional Presets: <code>@cache.dev</code> and <code>@cache.test</code></strong></summary>
|
|
@@ -216,7 +223,7 @@ Setting `CACHEKIT_MASTER_KEY` instead of passing `master_key=` means every prese
|
|
|
216
223
|
`@cache.dev` and `@cache.test` are Python-only, SDK-local presets. See the comparison table above for the exact feature set of each preset.
|
|
217
224
|
|
|
218
225
|
```python
|
|
219
|
-
# Development: verbose logging, integrity checks on,
|
|
226
|
+
# Development: verbose logging, integrity checks on, no cache-operation Prometheus series
|
|
220
227
|
@cache.dev
|
|
221
228
|
def debug_expensive_call():
|
|
222
229
|
return complex_computation()
|
|
@@ -244,7 +251,7 @@ def test_cached_function():
|
|
|
244
251
|
│ └─────────────┘ └─────────────┘ └─────────────────────┘ │
|
|
245
252
|
├─────────────────────────────────────────────────────────────┤
|
|
246
253
|
│ L1 Cache (In-Memory) │ L2 Cache (Pluggable Backend) │
|
|
247
|
-
│ in-process │ Redis /
|
|
254
|
+
│ in-process │ Redis / CacheKit Cloud / File / │
|
|
248
255
|
│ │ Memcached: one backend round trip │
|
|
249
256
|
├─────────────────────────────────────────────────────────────┤
|
|
250
257
|
│ Rust Core (PyO3) │
|
|
@@ -262,17 +269,17 @@ def test_cached_function():
|
|
|
262
269
|
|
|
263
270
|
## Performance
|
|
264
271
|
|
|
265
|
-
What a call costs inside your process, from the wall-clock guards in `tests/performance/` run on a GitHub-hosted `ubuntu-latest` runner (AMD EPYC 7763, 4 vCPUs) with CPython 3.14.7. Each figure is the mean of five run medians with its 95% band.
|
|
272
|
+
What a call costs inside your process, from the wall-clock guards in `tests/performance/` run on a GitHub-hosted `ubuntu-latest` runner (AMD EPYC 7763, 4 vCPUs) with CPython 3.14.7. Each figure is the mean of five run medians with its 95% band. The two L1-only rows come from [a later run](https://github.com/cachekit-io/cachekit-py/actions/runs/41210621694) on the same CPU model and Python as the other two.
|
|
266
273
|
|
|
267
274
|
| Path | Time per call |
|
|
268
275
|
|:-----|--------------:|
|
|
269
|
-
| `@cache(backend=None)` L1 hit, 100-user nested dict (23.5 KB as MessagePack) | 0.
|
|
270
|
-
| `@cache(backend=None)` L1 hit, 10K-row DataFrame | 0.
|
|
271
|
-
| L1 hit with an L2 backend configured, same dict |
|
|
272
|
-
| Redis L2 hit with L1 disabled, same dict, Redis in a container on the same machine |
|
|
276
|
+
| `@cache(backend=None)` L1 hit, 100-user nested dict (23.5 KB as MessagePack) | 0.341 ± 0.001 µs |
|
|
277
|
+
| `@cache(backend=None)` L1 hit, 10K-row DataFrame | 0.34 ± 0.00 µs |
|
|
278
|
+
| L1 hit with an L2 backend configured, same dict | 284 ± 2 µs |
|
|
279
|
+
| Redis L2 hit with L1 disabled, same dict, Redis in a container on the same machine | 539 ± 3 µs |
|
|
273
280
|
|
|
274
|
-
- With no backend, L1 keeps the returned object itself, whatever the payload's size, and a repeat call with a few short scalar arguments
|
|
275
|
-
- With an L2 backend, L1 holds the serialized bytes and every hit deserializes them: for this dict, about
|
|
281
|
+
- With no backend, L1 keeps the returned object itself, whatever the payload's size, and a repeat call with a few short scalar arguments is answered by one call into the Rust extension that takes no lock ([which calls](https://github.com/cachekit-io/cachekit-py/blob/main/docs/performance.md#decorator--l1-hit-hot-path)). Any other call builds its key every time, as does every call to a function with a backend.
|
|
282
|
+
- With an L2 backend, L1 holds the serialized bytes and every hit deserializes them: for this dict, about 830 times the L1-only hit. This run predates the Rust decode every such hit now runs, which bounds and decodes the MessagePack in one pass at about half the instructions. Encryption adds a decrypt to each such hit; this run measured the difference at 9.3 ± 1.0 µs.
|
|
276
283
|
- An L2 hit adds the backend's round trip. A Redis on another machine adds its network latency to the last row.
|
|
277
284
|
|
|
278
285
|
Hosted runners come with other CPU models too: in a run of an earlier commit, an EPYC 9V45 runner took about half as long on every row, and other machines give other figures. The [Performance Guide](https://github.com/cachekit-io/cachekit-py/blob/main/docs/performance.md#github-hosted-runner) names the guard behind each figure and how to run it, and the repository's `Bench` workflow repeats the run.
|
|
@@ -285,9 +292,9 @@ Hosted runners come with other CPU models too: in a run of an earlier commit, an
|
|
|
285
292
|
|
|
286
293
|
- Circuit breaker with graceful degradation
|
|
287
294
|
- Connection pooling with thread affinity
|
|
288
|
-
-
|
|
289
|
-
- Distributed locking
|
|
290
|
-
- Pluggable backend abstraction (Redis,
|
|
295
|
+
- The CacheKit Cloud backend keeps idle connections pooled until the server closes them, with TCP keepalive probes, so a request after a pause skips a new TLS handshake
|
|
296
|
+
- Distributed locking guards against cache stampedes across pods, best effort (async functions, on a backend that supports it)
|
|
297
|
+
- Pluggable backend abstraction (Redis, CacheKit Cloud, File, Memcached, custom)
|
|
291
298
|
- 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/42ea601f3c1656df0a06904afa2406604f43f57d/test-vectors/decode-bounds.json) vectors
|
|
292
299
|
|
|
293
300
|
> [!NOTE]
|
|
@@ -319,7 +326,7 @@ def get_large_dataset(date: str):
|
|
|
319
326
|
return pd.read_csv(f"data/{date}.csv")
|
|
320
327
|
```
|
|
321
328
|
|
|
322
|
-
Encrypted DataFrames go through `@cache.secure`, which takes any serializer whose class declares `cross_sdk_compatible = True` ([details](docs/serializers/encryption.md#composability)). A file backend keeps this example self-contained; production uses Redis or
|
|
329
|
+
Encrypted DataFrames go through `@cache.secure`, which takes any serializer whose class declares `cross_sdk_compatible = True` ([details](docs/serializers/encryption.md#composability)). A file backend keeps this example self-contained; production uses Redis or CacheKit Cloud:
|
|
323
330
|
|
|
324
331
|
```python
|
|
325
332
|
import tempfile
|
|
@@ -406,7 +413,7 @@ See [SECURITY.md][security-url] for vulnerability reporting and detailed documen
|
|
|
406
413
|
### Monitoring & Observability
|
|
407
414
|
|
|
408
415
|
- **Per-function statistics** - `cache_info()` on every decorated function, modelled on `functools.lru_cache`
|
|
409
|
-
- **Prometheus metrics** - Recorded by default (your app owns exposition)
|
|
416
|
+
- **Prometheus metrics** - Recorded by default by a decorator with a backend (your app owns exposition)
|
|
410
417
|
- **Structured logging** - Context-aware, per-operation fields
|
|
411
418
|
- **Health checks** - Comprehensive status endpoints
|
|
412
419
|
|
|
@@ -441,9 +448,9 @@ exposition setup.
|
|
|
441
448
|
**Free-threaded CPython (3.15t, 3.14t):** the core suites run green on
|
|
442
449
|
free-threaded 3.14 and 3.15 with the GIL verified disabled (CI job
|
|
443
450
|
`test-freethreaded`), and the Rust extension declares free-threaded safety
|
|
444
|
-
(`gil_used = false`). **
|
|
451
|
+
(`gil_used = false`). **3.15t has a wheel**:
|
|
445
452
|
the 3.15+ wheel is `abi3t`, so it installs on 3.15t without a Rust toolchain
|
|
446
|
-
(pip
|
|
453
|
+
([with a recent pip or uv](docs/free-threading.md#cpython-315-one-abi3t-wheel)). **3.14t is tested
|
|
447
454
|
but has no wheel** (it builds from source). Free-threaded builds are not
|
|
448
455
|
officially supported yet — blocked upstream on
|
|
449
456
|
orjson (no free-threaded wheels) and hiredis (re-enables the GIL on import;
|
|
@@ -502,7 +509,7 @@ info = expensive_func.cache_info()
|
|
|
502
509
|
| [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 |
|
|
503
510
|
| [Reference Caching][reference-caching-url] | `@cache.local()` for non-serializable objects |
|
|
504
511
|
| [Rust Serialization][rust-serialization-url] | ByteStorage layer: LZ4, xxHash3, AES-256-GCM |
|
|
505
|
-
| [SSRF Protection][ssrf-url] | URL allowlisting for the
|
|
512
|
+
| [SSRF Protection][ssrf-url] | URL allowlisting for the CacheKit Cloud backend |
|
|
506
513
|
| [Write-Behind][write-behind-url] | Opt-in `write_behind=True`: a miss returns before its L2 write lands |
|
|
507
514
|
|
|
508
515
|
---
|
|
@@ -516,16 +523,17 @@ info = expensive_func.cache_info()
|
|
|
516
523
|
# CACHEKIT_MEMCACHED_SERVERS, CACHEKIT_FILE_CACHE_DIR. Two or more is a ConfigurationError at
|
|
517
524
|
# first call, and decorators relying on env auto-detection run uncached (docs/backends/README.md).
|
|
518
525
|
# REDIS_URL is only a fallback and never conflicts. With none of them set, decorators cache
|
|
519
|
-
# L1-only, in process (encrypted
|
|
526
|
+
# L1-only, in process (encrypted decorators still use localhost Redis; interop decorators refuse
|
|
527
|
+
# any Redis resolved here, which is tenant-prefixed, and need backend=RedisBackend()).
|
|
520
528
|
|
|
521
529
|
# Redis Connection (priority: CACHEKIT_REDIS_URL > REDIS_URL)
|
|
522
530
|
CACHEKIT_REDIS_URL="redis://localhost:6379" # Primary (preferred)
|
|
523
531
|
REDIS_URL="redis://localhost:6379" # Fallback
|
|
524
532
|
|
|
525
|
-
#
|
|
533
|
+
# CacheKit Cloud backend (closed beta: join the waitlist at https://dash.cachekit.io)
|
|
526
534
|
# For @cache.io() next to Redis, pass api_key= from your secret store instead.
|
|
527
535
|
# CACHEKIT_API_KEY="your-api-key" # pragma: allowlist secret
|
|
528
|
-
CACHEKIT_API_URL="https://api.cachekit.io" # Default
|
|
536
|
+
CACHEKIT_API_URL="https://api.cachekit.io" # Default CacheKit Cloud endpoint
|
|
529
537
|
|
|
530
538
|
# Memcached Backend (optional: pip install cachekit[memcached])
|
|
531
539
|
# CACHEKIT_MEMCACHED_SERVERS='["mc1:11211", "mc2:11211"]' # Default: 127.0.0.1:11211
|
|
@@ -6,7 +6,7 @@ build-backend = "maturin"
|
|
|
6
6
|
|
|
7
7
|
[project]
|
|
8
8
|
name = "cachekit"
|
|
9
|
-
version = "0.
|
|
9
|
+
version = "0.26.0"
|
|
10
10
|
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."
|
|
11
11
|
readme = "README.md"
|
|
12
12
|
license = {text = "MIT"}
|