cachekit 0.22.0__tar.gz → 0.23.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.22.0 → cachekit-0.23.0}/Cargo.lock +1 -1
- {cachekit-0.22.0 → cachekit-0.23.0}/PKG-INFO +41 -17
- {cachekit-0.22.0 → cachekit-0.23.0}/README.md +39 -15
- {cachekit-0.22.0 → cachekit-0.23.0}/pyproject.toml +5 -4
- {cachekit-0.22.0 → cachekit-0.23.0}/rust/Cargo.toml +1 -1
- {cachekit-0.22.0 → cachekit-0.23.0}/rust/README.md +39 -15
- {cachekit-0.22.0 → cachekit-0.23.0}/src/cachekit/__init__.py +9 -5
- cachekit-0.23.0/src/cachekit/_fork.py +65 -0
- {cachekit-0.22.0 → cachekit-0.23.0}/src/cachekit/backends/__init__.py +1 -1
- cachekit-0.23.0/src/cachekit/backends/_uninterrupted.py +114 -0
- {cachekit-0.22.0 → cachekit-0.23.0}/src/cachekit/backends/base.py +14 -3
- {cachekit-0.22.0 → cachekit-0.23.0}/src/cachekit/backends/cachekitio/backend.py +108 -53
- {cachekit-0.22.0 → cachekit-0.23.0}/src/cachekit/backends/cachekitio/client.py +8 -4
- {cachekit-0.22.0 → cachekit-0.23.0}/src/cachekit/backends/cachekitio/config.py +80 -73
- {cachekit-0.22.0 → cachekit-0.23.0}/src/cachekit/backends/cachekitio/error_handler.py +38 -9
- {cachekit-0.22.0 → cachekit-0.23.0}/src/cachekit/backends/file/backend.py +271 -158
- {cachekit-0.22.0 → cachekit-0.23.0}/src/cachekit/backends/memcached/backend.py +3 -2
- {cachekit-0.22.0 → cachekit-0.23.0}/src/cachekit/backends/provider.py +56 -12
- {cachekit-0.22.0 → cachekit-0.23.0}/src/cachekit/backends/redis/backend.py +131 -100
- {cachekit-0.22.0 → cachekit-0.23.0}/src/cachekit/backends/redis/config.py +7 -1
- {cachekit-0.22.0 → cachekit-0.23.0}/src/cachekit/backends/redis/error_handler.py +70 -16
- {cachekit-0.22.0 → cachekit-0.23.0}/src/cachekit/backends/redis/provider.py +413 -250
- {cachekit-0.22.0 → cachekit-0.23.0}/src/cachekit/cache_handler.py +79 -36
- {cachekit-0.22.0 → cachekit-0.23.0}/src/cachekit/config/__init__.py +2 -1
- {cachekit-0.22.0 → cachekit-0.23.0}/src/cachekit/config/decorator.py +142 -49
- {cachekit-0.22.0 → cachekit-0.23.0}/src/cachekit/config/nested.py +24 -7
- {cachekit-0.22.0 → cachekit-0.23.0}/src/cachekit/config/settings.py +86 -41
- {cachekit-0.22.0 → cachekit-0.23.0}/src/cachekit/config/validation.py +70 -4
- {cachekit-0.22.0 → cachekit-0.23.0}/src/cachekit/decorators/intent.py +114 -79
- {cachekit-0.22.0 → cachekit-0.23.0}/src/cachekit/decorators/local_wrapper.py +69 -16
- {cachekit-0.22.0 → cachekit-0.23.0}/src/cachekit/decorators/orchestrator.py +32 -71
- cachekit-0.23.0/src/cachekit/decorators/single_flight.py +283 -0
- {cachekit-0.22.0 → cachekit-0.23.0}/src/cachekit/decorators/wrapper.py +1011 -760
- {cachekit-0.22.0 → cachekit-0.23.0}/src/cachekit/interop.py +50 -16
- {cachekit-0.22.0 → cachekit-0.23.0}/src/cachekit/invalidation.py +60 -50
- {cachekit-0.22.0 → cachekit-0.23.0}/src/cachekit/l1_cache.py +8 -7
- cachekit-0.23.0/src/cachekit/logging.py +234 -0
- {cachekit-0.22.0 → cachekit-0.23.0}/src/cachekit/object_cache.py +21 -0
- {cachekit-0.22.0 → cachekit-0.23.0}/src/cachekit/reliability/async_metrics.py +114 -63
- {cachekit-0.22.0 → cachekit-0.23.0}/src/cachekit/reliability/circuit_breaker.py +52 -23
- {cachekit-0.22.0 → cachekit-0.23.0}/src/cachekit/reliability/load_control.py +60 -49
- {cachekit-0.22.0 → cachekit-0.23.0}/src/cachekit/reliability/metrics_collection.py +15 -6
- {cachekit-0.22.0 → cachekit-0.23.0}/src/cachekit/serializers/arrow_serializer.py +12 -5
- {cachekit-0.22.0 → cachekit-0.23.0}/src/cachekit/serializers/auto_serializer.py +133 -98
- {cachekit-0.22.0 → cachekit-0.23.0}/src/cachekit/serializers/base.py +25 -12
- {cachekit-0.22.0 → cachekit-0.23.0}/src/cachekit/serializers/encryption_wrapper.py +101 -23
- {cachekit-0.22.0 → cachekit-0.23.0}/src/cachekit/serializers/interop_serializer.py +1 -1
- {cachekit-0.22.0 → cachekit-0.23.0}/src/cachekit/serializers/orjson_serializer.py +2 -1
- {cachekit-0.22.0 → cachekit-0.23.0}/src/cachekit/serializers/standard_serializer.py +26 -9
- cachekit-0.22.0/src/cachekit/backends/_uninterrupted.py +0 -38
- cachekit-0.22.0/src/cachekit/logging.py +0 -386
- cachekit-0.22.0/src/cachekit/monitoring/pool_monitor.py +0 -346
- {cachekit-0.22.0 → cachekit-0.23.0}/Cargo.toml +0 -0
- {cachekit-0.22.0 → cachekit-0.23.0}/LICENSE +0 -0
- {cachekit-0.22.0 → cachekit-0.23.0}/rust/Makefile +0 -0
- {cachekit-0.22.0 → cachekit-0.23.0}/rust/TEST_EXPANSION_SUMMARY.md +0 -0
- {cachekit-0.22.0 → cachekit-0.23.0}/rust/src/lib.rs +0 -0
- {cachekit-0.22.0 → cachekit-0.23.0}/rust/src/msgpack_bounds.rs +0 -0
- {cachekit-0.22.0 → cachekit-0.23.0}/rust/src/python_bindings.rs +0 -0
- {cachekit-0.22.0 → cachekit-0.23.0}/rust/tsan_suppressions.txt +0 -0
- {cachekit-0.22.0 → cachekit-0.23.0}/src/cachekit/backends/base_config.py +0 -0
- {cachekit-0.22.0 → cachekit-0.23.0}/src/cachekit/backends/cachekitio/__init__.py +0 -0
- {cachekit-0.22.0 → cachekit-0.23.0}/src/cachekit/backends/cachekitio/session.py +0 -0
- {cachekit-0.22.0 → cachekit-0.23.0}/src/cachekit/backends/errors.py +0 -0
- {cachekit-0.22.0 → cachekit-0.23.0}/src/cachekit/backends/file/__init__.py +0 -0
- {cachekit-0.22.0 → cachekit-0.23.0}/src/cachekit/backends/file/config.py +0 -0
- {cachekit-0.22.0 → cachekit-0.23.0}/src/cachekit/backends/memcached/__init__.py +0 -0
- {cachekit-0.22.0 → cachekit-0.23.0}/src/cachekit/backends/memcached/config.py +0 -0
- {cachekit-0.22.0 → cachekit-0.23.0}/src/cachekit/backends/memcached/error_handler.py +0 -0
- {cachekit-0.22.0 → cachekit-0.23.0}/src/cachekit/backends/redis/__init__.py +0 -0
- {cachekit-0.22.0 → cachekit-0.23.0}/src/cachekit/backends/redis/client.py +0 -0
- {cachekit-0.22.0 → cachekit-0.23.0}/src/cachekit/config/singleton.py +0 -0
- {cachekit-0.22.0 → cachekit-0.23.0}/src/cachekit/decorators/__init__.py +0 -0
- {cachekit-0.22.0 → cachekit-0.23.0}/src/cachekit/decorators/main.py +0 -0
- {cachekit-0.22.0 → cachekit-0.23.0}/src/cachekit/decorators/session.py +0 -0
- {cachekit-0.22.0 → cachekit-0.23.0}/src/cachekit/decorators/stats_context.py +0 -0
- {cachekit-0.22.0 → cachekit-0.23.0}/src/cachekit/decorators/tenant_context.py +0 -0
- {cachekit-0.22.0 → cachekit-0.23.0}/src/cachekit/decorators/utils/__init__.py +0 -0
- {cachekit-0.22.0 → cachekit-0.23.0}/src/cachekit/di.py +0 -0
- {cachekit-0.22.0 → cachekit-0.23.0}/src/cachekit/hash_utils.py +0 -0
- {cachekit-0.22.0 → cachekit-0.23.0}/src/cachekit/health.py +0 -0
- {cachekit-0.22.0 → cachekit-0.23.0}/src/cachekit/hiredis_compat.py +0 -0
- {cachekit-0.22.0 → cachekit-0.23.0}/src/cachekit/imports.py +0 -0
- {cachekit-0.22.0 → cachekit-0.23.0}/src/cachekit/key_generator.py +0 -0
- {cachekit-0.22.0 → cachekit-0.23.0}/src/cachekit/monitoring/__init__.py +0 -0
- {cachekit-0.22.0 → cachekit-0.23.0}/src/cachekit/monitoring/protocols.py +0 -0
- {cachekit-0.22.0 → cachekit-0.23.0}/src/cachekit/py.typed +0 -0
- {cachekit-0.22.0 → cachekit-0.23.0}/src/cachekit/reliability/__init__.py +0 -0
- {cachekit-0.22.0 → cachekit-0.23.0}/src/cachekit/reliability/error_classification.py +0 -0
- {cachekit-0.22.0 → cachekit-0.23.0}/src/cachekit/serializers/__init__.py +0 -0
- {cachekit-0.22.0 → cachekit-0.23.0}/src/cachekit/serializers/wrapper.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: cachekit
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.23.0
|
|
4
4
|
Classifier: Development Status :: 4 - Beta
|
|
5
5
|
Classifier: Intended Audience :: Developers
|
|
6
6
|
Classifier: License :: OSI Approved :: MIT License
|
|
@@ -30,7 +30,7 @@ Requires-Dist: msgpack>=1.2.1
|
|
|
30
30
|
Requires-Dist: xxhash>=3.5.0
|
|
31
31
|
Requires-Dist: urllib3>=2.8.0
|
|
32
32
|
Requires-Dist: numpy>=2.0.2 ; extra == 'data'
|
|
33
|
-
Requires-Dist: pandas>=
|
|
33
|
+
Requires-Dist: pandas>=2.2.2 ; extra == 'data'
|
|
34
34
|
Requires-Dist: pyarrow>=21.0.0 ; extra == 'data'
|
|
35
35
|
Requires-Dist: orjson>=3.9.0 ; extra == 'json'
|
|
36
36
|
Requires-Dist: pymemcache>=4.0.0 ; extra == 'memcached'
|
|
@@ -114,7 +114,23 @@ Or with [uv][uv-url] (recommended):
|
|
|
114
114
|
uv add cachekit
|
|
115
115
|
```
|
|
116
116
|
|
|
117
|
-
### Setup
|
|
117
|
+
### Setup
|
|
118
|
+
|
|
119
|
+
With nothing configured, `@cache` caches in L1, in process, like `lru_cache`: no backend,
|
|
120
|
+
no connection attempt, no warning.
|
|
121
|
+
|
|
122
|
+
```python
|
|
123
|
+
from cachekit import cache
|
|
124
|
+
|
|
125
|
+
@cache # nothing configured: L1-only, per process
|
|
126
|
+
def expensive_api_call(user_id: int):
|
|
127
|
+
return fetch_user_data(user_id)
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
An L1-only cache is not shared between processes, holds values by reference, and is chosen
|
|
131
|
+
at the function's first call, so configure a backend before then.
|
|
132
|
+
|
|
133
|
+
### Choose a backend
|
|
118
134
|
|
|
119
135
|
cachekit exposes one decorator API over a pluggable backend abstraction. Pick the
|
|
120
136
|
backend that fits your infrastructure — they're peers behind the same `@cache` API:
|
|
@@ -124,7 +140,7 @@ backend that fits your infrastructure — they're peers behind the same `@cache`
|
|
|
124
140
|
| Redis | Self-hosted, full control | `REDIS_URL` / `CACHEKIT_REDIS_URL` |
|
|
125
141
|
| CachekitIO | Managed, zero-ops (beta) | `CACHEKIT_API_KEY` |
|
|
126
142
|
| Memcached | High-throughput, existing infra | `CACHEKIT_MEMCACHED_SERVERS` |
|
|
127
|
-
| File / L1-only | Local dev, tests, no external deps | `CACHEKIT_FILE_CACHE_DIR` / `backend=None` |
|
|
143
|
+
| File / L1-only | Local dev, tests, no external deps | `CACHEKIT_FILE_CACHE_DIR` / nothing (or `backend=None`) |
|
|
128
144
|
|
|
129
145
|
```bash
|
|
130
146
|
# Run Redis locally or use your existing infrastructure
|
|
@@ -134,13 +150,16 @@ export REDIS_URL="redis://localhost:6379"
|
|
|
134
150
|
```python
|
|
135
151
|
from cachekit import cache
|
|
136
152
|
|
|
137
|
-
@cache #
|
|
153
|
+
@cache # REDIS_URL set: L1 + Redis, shared across processes
|
|
138
154
|
def expensive_api_call(user_id: int):
|
|
139
155
|
return fetch_user_data(user_id)
|
|
140
156
|
```
|
|
141
157
|
|
|
142
|
-
> [!
|
|
143
|
-
>
|
|
158
|
+
> [!WARNING]
|
|
159
|
+
> Earlier releases connected to `redis://localhost:6379` when nothing was configured. To keep
|
|
160
|
+
> that Redis, set `REDIS_URL=redis://localhost:6379` (or `CACHEKIT_REDIS_URL`). `@cache.secure`,
|
|
161
|
+
> `encryption=True` and `interop=` still need a backend and still fall back to localhost Redis;
|
|
162
|
+
> see [Backend Resolution Priority](docs/backends/README.md#backend-resolution-priority).
|
|
144
163
|
|
|
145
164
|
### More Backends
|
|
146
165
|
|
|
@@ -161,6 +180,8 @@ def expensive_api_call(user_id: int):
|
|
|
161
180
|
|
|
162
181
|
*cachekit.io is in closed beta — [request access](https://cachekit.io) to get started.*
|
|
163
182
|
|
|
183
|
+
How fresh a cachekit.io read is, and how long a deleted project's data stays readable: [Consistency and Deletion](https://docs.cachekit.io/concepts/consistency/).
|
|
184
|
+
|
|
164
185
|
</details>
|
|
165
186
|
|
|
166
187
|
<details>
|
|
@@ -225,13 +246,15 @@ Setting `CACHEKIT_MASTER_KEY` instead of passing `master_key=` means every prese
|
|
|
225
246
|
| Encryption | - | - | - | - | ✅ Required |
|
|
226
247
|
| L1 SWR (L1-only mode) | - | ✅ | - | ✅ | - |
|
|
227
248
|
| Prometheus Metrics | - | -¹ | - | ✅ | ✅ |
|
|
228
|
-
| Tracing | - | ✅ | - | ✅ | ✅ |
|
|
229
249
|
| Structured Logging | - | ✅ | - | ✅ | ✅ |
|
|
250
|
+
| Backend unreachable² | Function runs, no error | Function runs, no error | Function runs, no error | Function runs, no error | Function runs, no error |
|
|
230
251
|
| **Use Case** | High throughput | Local debugging | Deterministic tests | Production reliability | Compliance/security |
|
|
231
252
|
|
|
232
253
|
> ¹ `@cache.dev` exports no Prometheus metrics except `circuit_breaker_state`, which is recorded for every function whose circuit breaker is enabled.
|
|
233
254
|
>
|
|
234
|
-
>
|
|
255
|
+
> ² 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).
|
|
256
|
+
>
|
|
257
|
+
> 🔒 `@cache.secure` forces `integrity_checking=True` — passing `integrity_checking=False` raises `ConfigurationError` at decoration, including as an override next to `config=DecoratorConfig.secure(...)`. Its encryption is fixed the same way: `encryption=` raises `ConfigurationError` on `@cache.secure` and next to `config=DecoratorConfig.secure(...)`, so pass `fail_closed=` and the other encryption options to the preset itself. `@cache.secure` also rejects `config=`; the RORO form is `@cache(config=DecoratorConfig.secure(...))`.
|
|
235
258
|
>
|
|
236
259
|
> **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)).
|
|
237
260
|
>
|
|
@@ -239,12 +262,12 @@ Setting `CACHEKIT_MASTER_KEY` instead of passing `master_key=` means every prese
|
|
|
239
262
|
>
|
|
240
263
|
> **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)).
|
|
241
264
|
>
|
|
242
|
-
> **`@cache.io()`** mirrors `@cache.production` (full reliability + observability) but routes to the managed CachekitIO SaaS backend instead of Redis. **`@cache.local()`** is a separate in-process path backed by `ObjectCache` (raw object references, entry-count LRU, no serialization) — the reliability and encryption features listed above do not apply to it.
|
|
265
|
+
> **`@cache.io()`** mirrors `@cache.production` (full reliability + observability) but routes to the managed CachekitIO SaaS backend 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.
|
|
243
266
|
|
|
244
267
|
<details>
|
|
245
268
|
<summary><strong>Additional Presets: <code>@cache.dev</code> and <code>@cache.test</code></strong></summary>
|
|
246
269
|
|
|
247
|
-
See the comparison table above for the exact feature set of each preset.
|
|
270
|
+
`@cache.dev` and `@cache.test` are Python-only, SDK-local presets. See the comparison table above for the exact feature set of each preset.
|
|
248
271
|
|
|
249
272
|
```python
|
|
250
273
|
# Development: verbose logging, integrity checks on, Prometheus off except circuit_breaker_state
|
|
@@ -300,7 +323,7 @@ def test_cached_function():
|
|
|
300
323
|
- CachekitIO keeps idle connections pooled until the server closes them, with TCP keepalive probes, so a request after a pause skips a new TLS handshake
|
|
301
324
|
- Distributed locking prevents cache stampedes
|
|
302
325
|
- Pluggable backend abstraction (Redis, CachekitIO, File, Memcached, custom)
|
|
303
|
-
- Untrusted-decode bounds: nesting depth and header-declared allocation are capped on every cache read (a forged entry is a bounded cache miss), verified against the protocol's shared [`decode-bounds.json`](https://github.com/cachekit-io/protocol/blob/
|
|
326
|
+
- 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
|
|
304
327
|
|
|
305
328
|
> [!NOTE]
|
|
306
329
|
> All reliability features are **enabled by default** with `@cache.production`. Use `@cache.minimal` to disable them for maximum throughput.
|
|
@@ -309,9 +332,9 @@ def test_cached_function():
|
|
|
309
332
|
|
|
310
333
|
| Serializer | Speed | Use Case |
|
|
311
334
|
|:-----------|:-----:|:---------|
|
|
312
|
-
| **StandardSerializer** | ★★★★☆ | General Python types
|
|
335
|
+
| **StandardSerializer** | ★★★★☆ | General Python types; not NumPy arrays or pandas objects |
|
|
313
336
|
| **OrjsonSerializer** | ★★★★★ | JSON APIs (2-5x faster than stdlib) — requires `cachekit[json]` |
|
|
314
|
-
| **ArrowSerializer** |
|
|
337
|
+
| **ArrowSerializer** | — | pandas DataFrames (columnar, exact round trip) |
|
|
315
338
|
| **+ Encryption** | ★★★★☆ | Any serializer above, AES-256-GCM encrypted: `@cache.secure(master_key=..., serializer=...)` |
|
|
316
339
|
|
|
317
340
|
<details>
|
|
@@ -325,13 +348,13 @@ from cachekit.serializers import OrjsonSerializer, ArrowSerializer
|
|
|
325
348
|
def get_api_response(endpoint: str):
|
|
326
349
|
return {"status": "success", "data": fetch_api(endpoint)}
|
|
327
350
|
|
|
328
|
-
#
|
|
351
|
+
# Columnar DataFrames (Arrow IPC, zstd-compressed by default)
|
|
329
352
|
@cache(serializer=ArrowSerializer())
|
|
330
353
|
def get_large_dataset(date: str):
|
|
331
354
|
return pd.read_csv(f"data/{date}.csv")
|
|
332
355
|
```
|
|
333
356
|
|
|
334
|
-
Encrypted DataFrames go through `@cache.secure`, which takes any serializer. A file backend keeps this example self-contained; production uses Redis or cachekit.io:
|
|
357
|
+
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.io:
|
|
335
358
|
|
|
336
359
|
```python
|
|
337
360
|
import tempfile
|
|
@@ -519,7 +542,8 @@ info = expensive_func.cache_info()
|
|
|
519
542
|
# Backend selection: set exactly ONE of CACHEKIT_REDIS_URL, CACHEKIT_API_KEY,
|
|
520
543
|
# CACHEKIT_MEMCACHED_SERVERS, CACHEKIT_FILE_CACHE_DIR. Two or more is a ConfigurationError at
|
|
521
544
|
# first call, and decorators relying on env auto-detection run uncached (docs/backends/README.md).
|
|
522
|
-
# REDIS_URL is only a fallback and never conflicts.
|
|
545
|
+
# REDIS_URL is only a fallback and never conflicts. With none of them set, decorators cache
|
|
546
|
+
# L1-only, in process (encrypted and interop decorators still use localhost Redis).
|
|
523
547
|
|
|
524
548
|
# Redis Connection (priority: CACHEKIT_REDIS_URL > REDIS_URL)
|
|
525
549
|
CACHEKIT_REDIS_URL="redis://localhost:6379" # Primary (preferred)
|
|
@@ -60,7 +60,23 @@ Or with [uv][uv-url] (recommended):
|
|
|
60
60
|
uv add cachekit
|
|
61
61
|
```
|
|
62
62
|
|
|
63
|
-
### Setup
|
|
63
|
+
### Setup
|
|
64
|
+
|
|
65
|
+
With nothing configured, `@cache` caches in L1, in process, like `lru_cache`: no backend,
|
|
66
|
+
no connection attempt, no warning.
|
|
67
|
+
|
|
68
|
+
```python
|
|
69
|
+
from cachekit import cache
|
|
70
|
+
|
|
71
|
+
@cache # nothing configured: L1-only, per process
|
|
72
|
+
def expensive_api_call(user_id: int):
|
|
73
|
+
return fetch_user_data(user_id)
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
An L1-only cache is not shared between processes, holds values by reference, and is chosen
|
|
77
|
+
at the function's first call, so configure a backend before then.
|
|
78
|
+
|
|
79
|
+
### Choose a backend
|
|
64
80
|
|
|
65
81
|
cachekit exposes one decorator API over a pluggable backend abstraction. Pick the
|
|
66
82
|
backend that fits your infrastructure — they're peers behind the same `@cache` API:
|
|
@@ -70,7 +86,7 @@ backend that fits your infrastructure — they're peers behind the same `@cache`
|
|
|
70
86
|
| Redis | Self-hosted, full control | `REDIS_URL` / `CACHEKIT_REDIS_URL` |
|
|
71
87
|
| CachekitIO | Managed, zero-ops (beta) | `CACHEKIT_API_KEY` |
|
|
72
88
|
| Memcached | High-throughput, existing infra | `CACHEKIT_MEMCACHED_SERVERS` |
|
|
73
|
-
| File / L1-only | Local dev, tests, no external deps | `CACHEKIT_FILE_CACHE_DIR` / `backend=None` |
|
|
89
|
+
| File / L1-only | Local dev, tests, no external deps | `CACHEKIT_FILE_CACHE_DIR` / nothing (or `backend=None`) |
|
|
74
90
|
|
|
75
91
|
```bash
|
|
76
92
|
# Run Redis locally or use your existing infrastructure
|
|
@@ -80,13 +96,16 @@ export REDIS_URL="redis://localhost:6379"
|
|
|
80
96
|
```python
|
|
81
97
|
from cachekit import cache
|
|
82
98
|
|
|
83
|
-
@cache #
|
|
99
|
+
@cache # REDIS_URL set: L1 + Redis, shared across processes
|
|
84
100
|
def expensive_api_call(user_id: int):
|
|
85
101
|
return fetch_user_data(user_id)
|
|
86
102
|
```
|
|
87
103
|
|
|
88
|
-
> [!
|
|
89
|
-
>
|
|
104
|
+
> [!WARNING]
|
|
105
|
+
> Earlier releases connected to `redis://localhost:6379` when nothing was configured. To keep
|
|
106
|
+
> that Redis, set `REDIS_URL=redis://localhost:6379` (or `CACHEKIT_REDIS_URL`). `@cache.secure`,
|
|
107
|
+
> `encryption=True` and `interop=` still need a backend and still fall back to localhost Redis;
|
|
108
|
+
> see [Backend Resolution Priority](docs/backends/README.md#backend-resolution-priority).
|
|
90
109
|
|
|
91
110
|
### More Backends
|
|
92
111
|
|
|
@@ -107,6 +126,8 @@ def expensive_api_call(user_id: int):
|
|
|
107
126
|
|
|
108
127
|
*cachekit.io is in closed beta — [request access](https://cachekit.io) to get started.*
|
|
109
128
|
|
|
129
|
+
How fresh a cachekit.io read is, and how long a deleted project's data stays readable: [Consistency and Deletion](https://docs.cachekit.io/concepts/consistency/).
|
|
130
|
+
|
|
110
131
|
</details>
|
|
111
132
|
|
|
112
133
|
<details>
|
|
@@ -171,13 +192,15 @@ Setting `CACHEKIT_MASTER_KEY` instead of passing `master_key=` means every prese
|
|
|
171
192
|
| Encryption | - | - | - | - | ✅ Required |
|
|
172
193
|
| L1 SWR (L1-only mode) | - | ✅ | - | ✅ | - |
|
|
173
194
|
| Prometheus Metrics | - | -¹ | - | ✅ | ✅ |
|
|
174
|
-
| Tracing | - | ✅ | - | ✅ | ✅ |
|
|
175
195
|
| Structured Logging | - | ✅ | - | ✅ | ✅ |
|
|
196
|
+
| Backend unreachable² | Function runs, no error | Function runs, no error | Function runs, no error | Function runs, no error | Function runs, no error |
|
|
176
197
|
| **Use Case** | High throughput | Local debugging | Deterministic tests | Production reliability | Compliance/security |
|
|
177
198
|
|
|
178
199
|
> ¹ `@cache.dev` exports no Prometheus metrics except `circuit_breaker_state`, which is recorded for every function whose circuit breaker is enabled.
|
|
179
200
|
>
|
|
180
|
-
>
|
|
201
|
+
> ² 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
|
+
>
|
|
203
|
+
> 🔒 `@cache.secure` forces `integrity_checking=True` — passing `integrity_checking=False` raises `ConfigurationError` at decoration, including as an override next to `config=DecoratorConfig.secure(...)`. Its encryption is fixed the same way: `encryption=` raises `ConfigurationError` on `@cache.secure` and next to `config=DecoratorConfig.secure(...)`, so pass `fail_closed=` and the other encryption options to the preset itself. `@cache.secure` also rejects `config=`; the RORO form is `@cache(config=DecoratorConfig.secure(...))`.
|
|
181
204
|
>
|
|
182
205
|
> **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)).
|
|
183
206
|
>
|
|
@@ -185,12 +208,12 @@ Setting `CACHEKIT_MASTER_KEY` instead of passing `master_key=` means every prese
|
|
|
185
208
|
>
|
|
186
209
|
> **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)).
|
|
187
210
|
>
|
|
188
|
-
> **`@cache.io()`** mirrors `@cache.production` (full reliability + observability) but routes to the managed CachekitIO SaaS backend instead of Redis. **`@cache.local()`** is a separate in-process path backed by `ObjectCache` (raw object references, entry-count LRU, no serialization) — the reliability and encryption features listed above do not apply to it.
|
|
211
|
+
> **`@cache.io()`** mirrors `@cache.production` (full reliability + observability) but routes to the managed CachekitIO SaaS backend 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.
|
|
189
212
|
|
|
190
213
|
<details>
|
|
191
214
|
<summary><strong>Additional Presets: <code>@cache.dev</code> and <code>@cache.test</code></strong></summary>
|
|
192
215
|
|
|
193
|
-
See the comparison table above for the exact feature set of each preset.
|
|
216
|
+
`@cache.dev` and `@cache.test` are Python-only, SDK-local presets. See the comparison table above for the exact feature set of each preset.
|
|
194
217
|
|
|
195
218
|
```python
|
|
196
219
|
# Development: verbose logging, integrity checks on, Prometheus off except circuit_breaker_state
|
|
@@ -246,7 +269,7 @@ def test_cached_function():
|
|
|
246
269
|
- CachekitIO keeps idle connections pooled until the server closes them, with TCP keepalive probes, so a request after a pause skips a new TLS handshake
|
|
247
270
|
- Distributed locking prevents cache stampedes
|
|
248
271
|
- Pluggable backend abstraction (Redis, CachekitIO, File, Memcached, custom)
|
|
249
|
-
- Untrusted-decode bounds: nesting depth and header-declared allocation are capped on every cache read (a forged entry is a bounded cache miss), verified against the protocol's shared [`decode-bounds.json`](https://github.com/cachekit-io/protocol/blob/
|
|
272
|
+
- 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
|
|
250
273
|
|
|
251
274
|
> [!NOTE]
|
|
252
275
|
> All reliability features are **enabled by default** with `@cache.production`. Use `@cache.minimal` to disable them for maximum throughput.
|
|
@@ -255,9 +278,9 @@ def test_cached_function():
|
|
|
255
278
|
|
|
256
279
|
| Serializer | Speed | Use Case |
|
|
257
280
|
|:-----------|:-----:|:---------|
|
|
258
|
-
| **StandardSerializer** | ★★★★☆ | General Python types
|
|
281
|
+
| **StandardSerializer** | ★★★★☆ | General Python types; not NumPy arrays or pandas objects |
|
|
259
282
|
| **OrjsonSerializer** | ★★★★★ | JSON APIs (2-5x faster than stdlib) — requires `cachekit[json]` |
|
|
260
|
-
| **ArrowSerializer** |
|
|
283
|
+
| **ArrowSerializer** | — | pandas DataFrames (columnar, exact round trip) |
|
|
261
284
|
| **+ Encryption** | ★★★★☆ | Any serializer above, AES-256-GCM encrypted: `@cache.secure(master_key=..., serializer=...)` |
|
|
262
285
|
|
|
263
286
|
<details>
|
|
@@ -271,13 +294,13 @@ from cachekit.serializers import OrjsonSerializer, ArrowSerializer
|
|
|
271
294
|
def get_api_response(endpoint: str):
|
|
272
295
|
return {"status": "success", "data": fetch_api(endpoint)}
|
|
273
296
|
|
|
274
|
-
#
|
|
297
|
+
# Columnar DataFrames (Arrow IPC, zstd-compressed by default)
|
|
275
298
|
@cache(serializer=ArrowSerializer())
|
|
276
299
|
def get_large_dataset(date: str):
|
|
277
300
|
return pd.read_csv(f"data/{date}.csv")
|
|
278
301
|
```
|
|
279
302
|
|
|
280
|
-
Encrypted DataFrames go through `@cache.secure`, which takes any serializer. A file backend keeps this example self-contained; production uses Redis or cachekit.io:
|
|
303
|
+
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.io:
|
|
281
304
|
|
|
282
305
|
```python
|
|
283
306
|
import tempfile
|
|
@@ -465,7 +488,8 @@ info = expensive_func.cache_info()
|
|
|
465
488
|
# Backend selection: set exactly ONE of CACHEKIT_REDIS_URL, CACHEKIT_API_KEY,
|
|
466
489
|
# CACHEKIT_MEMCACHED_SERVERS, CACHEKIT_FILE_CACHE_DIR. Two or more is a ConfigurationError at
|
|
467
490
|
# first call, and decorators relying on env auto-detection run uncached (docs/backends/README.md).
|
|
468
|
-
# REDIS_URL is only a fallback and never conflicts.
|
|
491
|
+
# REDIS_URL is only a fallback and never conflicts. With none of them set, decorators cache
|
|
492
|
+
# L1-only, in process (encrypted and interop decorators still use localhost Redis).
|
|
469
493
|
|
|
470
494
|
# Redis Connection (priority: CACHEKIT_REDIS_URL > REDIS_URL)
|
|
471
495
|
CACHEKIT_REDIS_URL="redis://localhost:6379" # Primary (preferred)
|
|
@@ -4,7 +4,7 @@ build-backend = "maturin"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "cachekit"
|
|
7
|
-
version = "0.
|
|
7
|
+
version = "0.23.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"}
|
|
@@ -81,7 +81,9 @@ dependencies = [
|
|
|
81
81
|
# User-facing optional features only - dev deps are in [dependency-groups]
|
|
82
82
|
data = [
|
|
83
83
|
"numpy>=2.0.2",
|
|
84
|
-
|
|
84
|
+
# 2.2.2 is the first pandas release built against numpy 2, so an older pandas cannot install
|
|
85
|
+
# beside the numpy floor above.
|
|
86
|
+
"pandas>=2.2.2",
|
|
85
87
|
"pyarrow>=21.0.0",
|
|
86
88
|
]
|
|
87
89
|
json = [
|
|
@@ -135,7 +137,6 @@ ignore = [
|
|
|
135
137
|
"__init__.py" = ["F401"]
|
|
136
138
|
"tests/**/*" = ["S101", "S105", "S110", "S112", "S311", "D", "F841", "N802"]
|
|
137
139
|
"src/cachekit/logging.py" = ["S110"] # Logging errors would create recursion
|
|
138
|
-
"src/cachekit/monitoring/pool_monitor.py" = ["S110"] # Monitoring errors should be silent
|
|
139
140
|
"src/cachekit/reliability/async_metrics.py" = ["S101"] # Internal invariant checks
|
|
140
141
|
"src/cachekit/serializers/__init__.py" = ["S101"] # Module-level validation
|
|
141
142
|
"src/cachekit/serializers/standard_serializer.py" = ["S110"] # Fallback deserialization patterns
|
|
@@ -246,7 +247,7 @@ dev = [
|
|
|
246
247
|
"pip-audit>=2.7.0",
|
|
247
248
|
# Data science support (for testing AutoSerializer with numpy/pandas)
|
|
248
249
|
"numpy>=2.0.2",
|
|
249
|
-
"pandas>=
|
|
250
|
+
"pandas>=2.2.2",
|
|
250
251
|
"pyarrow>=21.0.0",
|
|
251
252
|
# OrjsonSerializer support — now the [json] optional extra; kept here so the
|
|
252
253
|
# orjson tests, doctests, and markdown-docs still resolve it in dev/CI.
|
|
@@ -60,7 +60,23 @@ Or with [uv][uv-url] (recommended):
|
|
|
60
60
|
uv add cachekit
|
|
61
61
|
```
|
|
62
62
|
|
|
63
|
-
### Setup
|
|
63
|
+
### Setup
|
|
64
|
+
|
|
65
|
+
With nothing configured, `@cache` caches in L1, in process, like `lru_cache`: no backend,
|
|
66
|
+
no connection attempt, no warning.
|
|
67
|
+
|
|
68
|
+
```python
|
|
69
|
+
from cachekit import cache
|
|
70
|
+
|
|
71
|
+
@cache # nothing configured: L1-only, per process
|
|
72
|
+
def expensive_api_call(user_id: int):
|
|
73
|
+
return fetch_user_data(user_id)
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
An L1-only cache is not shared between processes, holds values by reference, and is chosen
|
|
77
|
+
at the function's first call, so configure a backend before then.
|
|
78
|
+
|
|
79
|
+
### Choose a backend
|
|
64
80
|
|
|
65
81
|
cachekit exposes one decorator API over a pluggable backend abstraction. Pick the
|
|
66
82
|
backend that fits your infrastructure — they're peers behind the same `@cache` API:
|
|
@@ -70,7 +86,7 @@ backend that fits your infrastructure — they're peers behind the same `@cache`
|
|
|
70
86
|
| Redis | Self-hosted, full control | `REDIS_URL` / `CACHEKIT_REDIS_URL` |
|
|
71
87
|
| CachekitIO | Managed, zero-ops (beta) | `CACHEKIT_API_KEY` |
|
|
72
88
|
| Memcached | High-throughput, existing infra | `CACHEKIT_MEMCACHED_SERVERS` |
|
|
73
|
-
| File / L1-only | Local dev, tests, no external deps | `CACHEKIT_FILE_CACHE_DIR` / `backend=None` |
|
|
89
|
+
| File / L1-only | Local dev, tests, no external deps | `CACHEKIT_FILE_CACHE_DIR` / nothing (or `backend=None`) |
|
|
74
90
|
|
|
75
91
|
```bash
|
|
76
92
|
# Run Redis locally or use your existing infrastructure
|
|
@@ -80,13 +96,16 @@ export REDIS_URL="redis://localhost:6379"
|
|
|
80
96
|
```python
|
|
81
97
|
from cachekit import cache
|
|
82
98
|
|
|
83
|
-
@cache #
|
|
99
|
+
@cache # REDIS_URL set: L1 + Redis, shared across processes
|
|
84
100
|
def expensive_api_call(user_id: int):
|
|
85
101
|
return fetch_user_data(user_id)
|
|
86
102
|
```
|
|
87
103
|
|
|
88
|
-
> [!
|
|
89
|
-
>
|
|
104
|
+
> [!WARNING]
|
|
105
|
+
> Earlier releases connected to `redis://localhost:6379` when nothing was configured. To keep
|
|
106
|
+
> that Redis, set `REDIS_URL=redis://localhost:6379` (or `CACHEKIT_REDIS_URL`). `@cache.secure`,
|
|
107
|
+
> `encryption=True` and `interop=` still need a backend and still fall back to localhost Redis;
|
|
108
|
+
> see [Backend Resolution Priority](docs/backends/README.md#backend-resolution-priority).
|
|
90
109
|
|
|
91
110
|
### More Backends
|
|
92
111
|
|
|
@@ -107,6 +126,8 @@ def expensive_api_call(user_id: int):
|
|
|
107
126
|
|
|
108
127
|
*cachekit.io is in closed beta — [request access](https://cachekit.io) to get started.*
|
|
109
128
|
|
|
129
|
+
How fresh a cachekit.io read is, and how long a deleted project's data stays readable: [Consistency and Deletion](https://docs.cachekit.io/concepts/consistency/).
|
|
130
|
+
|
|
110
131
|
</details>
|
|
111
132
|
|
|
112
133
|
<details>
|
|
@@ -171,13 +192,15 @@ Setting `CACHEKIT_MASTER_KEY` instead of passing `master_key=` means every prese
|
|
|
171
192
|
| Encryption | - | - | - | - | ✅ Required |
|
|
172
193
|
| L1 SWR (L1-only mode) | - | ✅ | - | ✅ | - |
|
|
173
194
|
| Prometheus Metrics | - | -¹ | - | ✅ | ✅ |
|
|
174
|
-
| Tracing | - | ✅ | - | ✅ | ✅ |
|
|
175
195
|
| Structured Logging | - | ✅ | - | ✅ | ✅ |
|
|
196
|
+
| Backend unreachable² | Function runs, no error | Function runs, no error | Function runs, no error | Function runs, no error | Function runs, no error |
|
|
176
197
|
| **Use Case** | High throughput | Local debugging | Deterministic tests | Production reliability | Compliance/security |
|
|
177
198
|
|
|
178
199
|
> ¹ `@cache.dev` exports no Prometheus metrics except `circuit_breaker_state`, which is recorded for every function whose circuit breaker is enabled.
|
|
179
200
|
>
|
|
180
|
-
>
|
|
201
|
+
> ² 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
|
+
>
|
|
203
|
+
> 🔒 `@cache.secure` forces `integrity_checking=True` — passing `integrity_checking=False` raises `ConfigurationError` at decoration, including as an override next to `config=DecoratorConfig.secure(...)`. Its encryption is fixed the same way: `encryption=` raises `ConfigurationError` on `@cache.secure` and next to `config=DecoratorConfig.secure(...)`, so pass `fail_closed=` and the other encryption options to the preset itself. `@cache.secure` also rejects `config=`; the RORO form is `@cache(config=DecoratorConfig.secure(...))`.
|
|
181
204
|
>
|
|
182
205
|
> **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)).
|
|
183
206
|
>
|
|
@@ -185,12 +208,12 @@ Setting `CACHEKIT_MASTER_KEY` instead of passing `master_key=` means every prese
|
|
|
185
208
|
>
|
|
186
209
|
> **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)).
|
|
187
210
|
>
|
|
188
|
-
> **`@cache.io()`** mirrors `@cache.production` (full reliability + observability) but routes to the managed CachekitIO SaaS backend instead of Redis. **`@cache.local()`** is a separate in-process path backed by `ObjectCache` (raw object references, entry-count LRU, no serialization) — the reliability and encryption features listed above do not apply to it.
|
|
211
|
+
> **`@cache.io()`** mirrors `@cache.production` (full reliability + observability) but routes to the managed CachekitIO SaaS backend 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.
|
|
189
212
|
|
|
190
213
|
<details>
|
|
191
214
|
<summary><strong>Additional Presets: <code>@cache.dev</code> and <code>@cache.test</code></strong></summary>
|
|
192
215
|
|
|
193
|
-
See the comparison table above for the exact feature set of each preset.
|
|
216
|
+
`@cache.dev` and `@cache.test` are Python-only, SDK-local presets. See the comparison table above for the exact feature set of each preset.
|
|
194
217
|
|
|
195
218
|
```python
|
|
196
219
|
# Development: verbose logging, integrity checks on, Prometheus off except circuit_breaker_state
|
|
@@ -246,7 +269,7 @@ def test_cached_function():
|
|
|
246
269
|
- CachekitIO keeps idle connections pooled until the server closes them, with TCP keepalive probes, so a request after a pause skips a new TLS handshake
|
|
247
270
|
- Distributed locking prevents cache stampedes
|
|
248
271
|
- Pluggable backend abstraction (Redis, CachekitIO, File, Memcached, custom)
|
|
249
|
-
- Untrusted-decode bounds: nesting depth and header-declared allocation are capped on every cache read (a forged entry is a bounded cache miss), verified against the protocol's shared [`decode-bounds.json`](https://github.com/cachekit-io/protocol/blob/
|
|
272
|
+
- 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
|
|
250
273
|
|
|
251
274
|
> [!NOTE]
|
|
252
275
|
> All reliability features are **enabled by default** with `@cache.production`. Use `@cache.minimal` to disable them for maximum throughput.
|
|
@@ -255,9 +278,9 @@ def test_cached_function():
|
|
|
255
278
|
|
|
256
279
|
| Serializer | Speed | Use Case |
|
|
257
280
|
|:-----------|:-----:|:---------|
|
|
258
|
-
| **StandardSerializer** | ★★★★☆ | General Python types
|
|
281
|
+
| **StandardSerializer** | ★★★★☆ | General Python types; not NumPy arrays or pandas objects |
|
|
259
282
|
| **OrjsonSerializer** | ★★★★★ | JSON APIs (2-5x faster than stdlib) — requires `cachekit[json]` |
|
|
260
|
-
| **ArrowSerializer** |
|
|
283
|
+
| **ArrowSerializer** | — | pandas DataFrames (columnar, exact round trip) |
|
|
261
284
|
| **+ Encryption** | ★★★★☆ | Any serializer above, AES-256-GCM encrypted: `@cache.secure(master_key=..., serializer=...)` |
|
|
262
285
|
|
|
263
286
|
<details>
|
|
@@ -271,13 +294,13 @@ from cachekit.serializers import OrjsonSerializer, ArrowSerializer
|
|
|
271
294
|
def get_api_response(endpoint: str):
|
|
272
295
|
return {"status": "success", "data": fetch_api(endpoint)}
|
|
273
296
|
|
|
274
|
-
#
|
|
297
|
+
# Columnar DataFrames (Arrow IPC, zstd-compressed by default)
|
|
275
298
|
@cache(serializer=ArrowSerializer())
|
|
276
299
|
def get_large_dataset(date: str):
|
|
277
300
|
return pd.read_csv(f"data/{date}.csv")
|
|
278
301
|
```
|
|
279
302
|
|
|
280
|
-
Encrypted DataFrames go through `@cache.secure`, which takes any serializer. A file backend keeps this example self-contained; production uses Redis or cachekit.io:
|
|
303
|
+
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.io:
|
|
281
304
|
|
|
282
305
|
```python
|
|
283
306
|
import tempfile
|
|
@@ -465,7 +488,8 @@ info = expensive_func.cache_info()
|
|
|
465
488
|
# Backend selection: set exactly ONE of CACHEKIT_REDIS_URL, CACHEKIT_API_KEY,
|
|
466
489
|
# CACHEKIT_MEMCACHED_SERVERS, CACHEKIT_FILE_CACHE_DIR. Two or more is a ConfigurationError at
|
|
467
490
|
# first call, and decorators relying on env auto-detection run uncached (docs/backends/README.md).
|
|
468
|
-
# REDIS_URL is only a fallback and never conflicts.
|
|
491
|
+
# REDIS_URL is only a fallback and never conflicts. With none of them set, decorators cache
|
|
492
|
+
# L1-only, in process (encrypted and interop decorators still use localhost Redis).
|
|
469
493
|
|
|
470
494
|
# Redis Connection (priority: CACHEKIT_REDIS_URL > REDIS_URL)
|
|
471
495
|
CACHEKIT_REDIS_URL="redis://localhost:6379" # Primary (preferred)
|
|
@@ -68,14 +68,17 @@ Example Usage:
|
|
|
68
68
|
```
|
|
69
69
|
"""
|
|
70
70
|
|
|
71
|
-
__version__ = "0.
|
|
71
|
+
__version__ = "0.23.0"
|
|
72
72
|
|
|
73
73
|
from collections.abc import Callable
|
|
74
74
|
from typing import Any, TypeVar
|
|
75
75
|
|
|
76
|
-
|
|
77
|
-
#
|
|
78
|
-
|
|
76
|
+
from . import (
|
|
77
|
+
_fork, # noqa: F401 - first: whether this process inherited a threaded parent's dead threads (cachekit._fork)
|
|
78
|
+
# Must run BEFORE any redis import: CACHEKIT_DISABLE_HIREDIS=true keeps hiredis out from here on.
|
|
79
|
+
# The free-threaded default runs later, when cachekit.backends.redis loads (see cachekit.hiredis_compat).
|
|
80
|
+
hiredis_compat, # noqa: F401
|
|
81
|
+
)
|
|
79
82
|
|
|
80
83
|
# Import the configuration classes
|
|
81
84
|
# Redis client is automatically fast - no special import needed
|
|
@@ -83,7 +86,7 @@ from .config import DecoratorConfig
|
|
|
83
86
|
|
|
84
87
|
# Import the intelligent cache decorator and CacheInfo
|
|
85
88
|
from .decorators import cache
|
|
86
|
-
from .decorators.wrapper import CacheInfo
|
|
89
|
+
from .decorators.wrapper import CacheInfo, InvalidationOutcome
|
|
87
90
|
|
|
88
91
|
# Import health check functionality
|
|
89
92
|
from .health import (
|
|
@@ -129,4 +132,5 @@ __all__ = [
|
|
|
129
132
|
"HealthLevel",
|
|
130
133
|
"HealthStatus",
|
|
131
134
|
"InteropError",
|
|
135
|
+
"InvalidationOutcome",
|
|
132
136
|
]
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
"""Whether this process first imported cachekit in the child of a fork made from C whose parent ran threads.
|
|
2
|
+
|
|
3
|
+
Such a fork, as uWSGI's is without ``--py-call-uwsgi-fork-hooks``, runs no at-fork hook, CPython's own after-fork
|
|
4
|
+
repair included, so a thread started in the child can hang in ``Thread.start()`` or crash the interpreter. A
|
|
5
|
+
process that imported cachekit before the fork sees its PID change; one that first imports it after the fork
|
|
6
|
+
(uWSGI ``--lazy-apps``) has no PID to compare with. ``l1_cache`` and ``async_metrics`` take ``IMPORT_PID`` as the
|
|
7
|
+
PID that imported them, so such a process counts as hookless there, and every gate that reads them applies.
|
|
8
|
+
|
|
9
|
+
Stdlib only, and imported first by ``cachekit``: the check runs once, before cachekit starts anything.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
import os
|
|
13
|
+
import threading
|
|
14
|
+
from typing import Optional
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def inherits_dead_threads() -> bool:
|
|
18
|
+
"""Whether ``threading`` lists a thread this process does not have, which only a fork made from C leaves.
|
|
19
|
+
|
|
20
|
+
An ``os.fork()`` child runs ``threading``'s after-fork hook, which drops the parent's threads. A fork made
|
|
21
|
+
from C runs none, so the parent's threads stay listed under native ids that are no task of this process.
|
|
22
|
+
So this is False in a fresh process, live threads or not, and in the child of a parent that ran no Python
|
|
23
|
+
threads, where a thread start is safe.
|
|
24
|
+
|
|
25
|
+
Neither the current thread nor the main thread counts, nor any thread listed under either's native id.
|
|
26
|
+
After any fork, the forking thread's object may keep the parent's native id: always after a fork made
|
|
27
|
+
from C, and after ``os.fork()`` on Python before 3.13. It is the main thread after ``os.fork()``, and after
|
|
28
|
+
a fork made from C from the parent's main thread, as uWSGI's is; it is the current thread when cachekit is
|
|
29
|
+
imported on it. A thread under the same id, such as a gevent green thread, ran on that thread and not on
|
|
30
|
+
one of its own. So a fork made from C from a thread other than the main one loses the parent's main thread
|
|
31
|
+
as evidence: only its other threads count. Nor does a ``_DummyThread``, which ``threading`` may keep listed after its
|
|
32
|
+
thread ends, nor a thread that has no native id yet. Threads ``threading`` never registered
|
|
33
|
+
(``_thread.start_new_thread``, foreign C threads) are not seen. Linux only: with no ``/proc/self/task``,
|
|
34
|
+
or with this thread's own id absent from it (another PID namespace, or a patched ``get_native_id``), it
|
|
35
|
+
cannot tell and returns False, as if fresh.
|
|
36
|
+
|
|
37
|
+
Other threads may start and end while this runs. Each thread's task is looked up by its id, never read off a
|
|
38
|
+
listing of ``/proc/self/task``, which can skip a live task while threads come and go. And a thread counts only if
|
|
39
|
+
``threading`` still lists it after that lookup: a thread leaves the list before its task ends.
|
|
40
|
+
"""
|
|
41
|
+
if not _has_task(threading.get_native_id()):
|
|
42
|
+
return False
|
|
43
|
+
forkers = {threading.current_thread().native_id, threading.main_thread().native_id}
|
|
44
|
+
gone = [
|
|
45
|
+
thread
|
|
46
|
+
for thread in threading.enumerate()
|
|
47
|
+
if thread.native_id is not None
|
|
48
|
+
and thread.native_id not in forkers
|
|
49
|
+
and not isinstance(thread, threading._DummyThread)
|
|
50
|
+
and not _has_task(thread.native_id)
|
|
51
|
+
]
|
|
52
|
+
if not gone:
|
|
53
|
+
return False
|
|
54
|
+
listed = {id(thread) for thread in threading.enumerate()} # identity: gone keeps each object alive
|
|
55
|
+
return any(id(thread) in listed for thread in gone)
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def _has_task(native_id: int) -> bool:
|
|
59
|
+
"""Whether this process has a thread with this native id; False also when /proc cannot say."""
|
|
60
|
+
return os.path.exists(f"/proc/self/task/{native_id}")
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
# The PID that imported cachekit, or None in a process that inherited a threaded parent's dead threads: no PID
|
|
64
|
+
# of such a process is safe for a thread. An os.fork() child of it runs the at-fork hooks, so it is safe again.
|
|
65
|
+
IMPORT_PID: Optional[int] = None if inherits_dead_threads() else os.getpid()
|
|
@@ -14,7 +14,7 @@ Public API:
|
|
|
14
14
|
- BackendError: Exception raised by backend operations
|
|
15
15
|
- BackendErrorType: Error classification enum
|
|
16
16
|
- CapabilityNotAvailableError: Exception for missing optional capabilities
|
|
17
|
-
- RedisBackend: Redis implementation
|
|
17
|
+
- RedisBackend: Redis implementation
|
|
18
18
|
- MemcachedBackend: Memcached implementation (requires pymemcache)
|
|
19
19
|
|
|
20
20
|
Usage:
|