cachekit 0.21.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.
Files changed (96) hide show
  1. {cachekit-0.21.0 → cachekit-0.23.0}/Cargo.lock +1 -1
  2. {cachekit-0.21.0 → cachekit-0.23.0}/PKG-INFO +50 -24
  3. {cachekit-0.21.0 → cachekit-0.23.0}/README.md +47 -19
  4. {cachekit-0.21.0 → cachekit-0.23.0}/pyproject.toml +11 -30
  5. {cachekit-0.21.0 → cachekit-0.23.0}/rust/Cargo.toml +1 -1
  6. {cachekit-0.21.0 → cachekit-0.23.0}/rust/README.md +47 -19
  7. {cachekit-0.21.0 → cachekit-0.23.0}/src/cachekit/__init__.py +9 -8
  8. cachekit-0.23.0/src/cachekit/_fork.py +65 -0
  9. {cachekit-0.21.0 → cachekit-0.23.0}/src/cachekit/backends/__init__.py +15 -4
  10. cachekit-0.23.0/src/cachekit/backends/_uninterrupted.py +114 -0
  11. {cachekit-0.21.0 → cachekit-0.23.0}/src/cachekit/backends/base.py +14 -3
  12. {cachekit-0.21.0 → cachekit-0.23.0}/src/cachekit/backends/cachekitio/backend.py +491 -181
  13. cachekit-0.23.0/src/cachekit/backends/cachekitio/client.py +283 -0
  14. {cachekit-0.21.0 → cachekit-0.23.0}/src/cachekit/backends/cachekitio/config.py +87 -78
  15. cachekit-0.23.0/src/cachekit/backends/cachekitio/error_handler.py +181 -0
  16. {cachekit-0.21.0 → cachekit-0.23.0}/src/cachekit/backends/file/backend.py +271 -158
  17. {cachekit-0.21.0 → cachekit-0.23.0}/src/cachekit/backends/memcached/backend.py +3 -2
  18. {cachekit-0.21.0 → cachekit-0.23.0}/src/cachekit/backends/provider.py +64 -16
  19. cachekit-0.23.0/src/cachekit/backends/redis/__init__.py +17 -0
  20. {cachekit-0.21.0 → cachekit-0.23.0}/src/cachekit/backends/redis/backend.py +131 -100
  21. {cachekit-0.21.0 → cachekit-0.23.0}/src/cachekit/backends/redis/config.py +13 -3
  22. {cachekit-0.21.0 → cachekit-0.23.0}/src/cachekit/backends/redis/error_handler.py +70 -16
  23. cachekit-0.23.0/src/cachekit/backends/redis/provider.py +1011 -0
  24. {cachekit-0.21.0 → cachekit-0.23.0}/src/cachekit/cache_handler.py +153 -43
  25. {cachekit-0.21.0 → cachekit-0.23.0}/src/cachekit/config/__init__.py +2 -1
  26. {cachekit-0.21.0 → cachekit-0.23.0}/src/cachekit/config/decorator.py +144 -49
  27. {cachekit-0.21.0 → cachekit-0.23.0}/src/cachekit/config/nested.py +36 -15
  28. {cachekit-0.21.0 → cachekit-0.23.0}/src/cachekit/config/settings.py +99 -43
  29. {cachekit-0.21.0 → cachekit-0.23.0}/src/cachekit/config/validation.py +70 -4
  30. {cachekit-0.21.0 → cachekit-0.23.0}/src/cachekit/decorators/intent.py +115 -73
  31. {cachekit-0.21.0 → cachekit-0.23.0}/src/cachekit/decorators/local_wrapper.py +69 -16
  32. {cachekit-0.21.0 → cachekit-0.23.0}/src/cachekit/decorators/orchestrator.py +52 -70
  33. cachekit-0.23.0/src/cachekit/decorators/single_flight.py +283 -0
  34. {cachekit-0.21.0 → cachekit-0.23.0}/src/cachekit/decorators/wrapper.py +1382 -875
  35. {cachekit-0.21.0 → cachekit-0.23.0}/src/cachekit/hash_utils.py +63 -4
  36. {cachekit-0.21.0 → cachekit-0.23.0}/src/cachekit/health.py +6 -1
  37. cachekit-0.23.0/src/cachekit/hiredis_compat.py +98 -0
  38. {cachekit-0.21.0 → cachekit-0.23.0}/src/cachekit/interop.py +50 -16
  39. cachekit-0.23.0/src/cachekit/invalidation.py +493 -0
  40. {cachekit-0.21.0 → cachekit-0.23.0}/src/cachekit/l1_cache.py +129 -62
  41. cachekit-0.23.0/src/cachekit/logging.py +234 -0
  42. {cachekit-0.21.0 → cachekit-0.23.0}/src/cachekit/object_cache.py +180 -86
  43. {cachekit-0.21.0 → cachekit-0.23.0}/src/cachekit/reliability/async_metrics.py +152 -69
  44. {cachekit-0.21.0 → cachekit-0.23.0}/src/cachekit/reliability/circuit_breaker.py +97 -27
  45. {cachekit-0.21.0 → cachekit-0.23.0}/src/cachekit/reliability/load_control.py +60 -49
  46. {cachekit-0.21.0 → cachekit-0.23.0}/src/cachekit/reliability/metrics_collection.py +15 -6
  47. {cachekit-0.21.0 → cachekit-0.23.0}/src/cachekit/serializers/__init__.py +2 -2
  48. {cachekit-0.21.0 → cachekit-0.23.0}/src/cachekit/serializers/arrow_serializer.py +21 -5
  49. {cachekit-0.21.0 → cachekit-0.23.0}/src/cachekit/serializers/auto_serializer.py +179 -125
  50. {cachekit-0.21.0 → cachekit-0.23.0}/src/cachekit/serializers/base.py +25 -12
  51. {cachekit-0.21.0 → cachekit-0.23.0}/src/cachekit/serializers/encryption_wrapper.py +101 -23
  52. {cachekit-0.21.0 → cachekit-0.23.0}/src/cachekit/serializers/interop_serializer.py +1 -1
  53. {cachekit-0.21.0 → cachekit-0.23.0}/src/cachekit/serializers/orjson_serializer.py +2 -1
  54. {cachekit-0.21.0 → cachekit-0.23.0}/src/cachekit/serializers/standard_serializer.py +26 -9
  55. cachekit-0.23.0/src/cachekit/serializers/wrapper.py +270 -0
  56. cachekit-0.21.0/src/cachekit/backends/cachekitio/client.py +0 -245
  57. cachekit-0.21.0/src/cachekit/backends/cachekitio/error_handler.py +0 -134
  58. cachekit-0.21.0/src/cachekit/backends/redis/__init__.py +0 -12
  59. cachekit-0.21.0/src/cachekit/backends/redis/provider.py +0 -811
  60. cachekit-0.21.0/src/cachekit/hiredis_compat.py +0 -98
  61. cachekit-0.21.0/src/cachekit/logging.py +0 -386
  62. cachekit-0.21.0/src/cachekit/monitoring/pool_monitor.py +0 -346
  63. cachekit-0.21.0/src/cachekit/serializers/wrapper.py +0 -169
  64. {cachekit-0.21.0 → cachekit-0.23.0}/Cargo.toml +0 -0
  65. {cachekit-0.21.0 → cachekit-0.23.0}/LICENSE +0 -0
  66. {cachekit-0.21.0 → cachekit-0.23.0}/rust/Makefile +0 -0
  67. {cachekit-0.21.0 → cachekit-0.23.0}/rust/TEST_EXPANSION_SUMMARY.md +0 -0
  68. {cachekit-0.21.0 → cachekit-0.23.0}/rust/src/lib.rs +0 -0
  69. {cachekit-0.21.0 → cachekit-0.23.0}/rust/src/msgpack_bounds.rs +0 -0
  70. {cachekit-0.21.0 → cachekit-0.23.0}/rust/src/python_bindings.rs +0 -0
  71. {cachekit-0.21.0 → cachekit-0.23.0}/rust/tsan_suppressions.txt +0 -0
  72. {cachekit-0.21.0 → cachekit-0.23.0}/src/cachekit/backends/base_config.py +0 -0
  73. {cachekit-0.21.0 → cachekit-0.23.0}/src/cachekit/backends/cachekitio/__init__.py +0 -0
  74. {cachekit-0.21.0 → cachekit-0.23.0}/src/cachekit/backends/cachekitio/session.py +0 -0
  75. {cachekit-0.21.0 → cachekit-0.23.0}/src/cachekit/backends/errors.py +0 -0
  76. {cachekit-0.21.0 → cachekit-0.23.0}/src/cachekit/backends/file/__init__.py +0 -0
  77. {cachekit-0.21.0 → cachekit-0.23.0}/src/cachekit/backends/file/config.py +0 -0
  78. {cachekit-0.21.0 → cachekit-0.23.0}/src/cachekit/backends/memcached/__init__.py +0 -0
  79. {cachekit-0.21.0 → cachekit-0.23.0}/src/cachekit/backends/memcached/config.py +0 -0
  80. {cachekit-0.21.0 → cachekit-0.23.0}/src/cachekit/backends/memcached/error_handler.py +0 -0
  81. {cachekit-0.21.0 → cachekit-0.23.0}/src/cachekit/backends/redis/client.py +0 -0
  82. {cachekit-0.21.0 → cachekit-0.23.0}/src/cachekit/config/singleton.py +0 -0
  83. {cachekit-0.21.0 → cachekit-0.23.0}/src/cachekit/decorators/__init__.py +0 -0
  84. {cachekit-0.21.0 → cachekit-0.23.0}/src/cachekit/decorators/main.py +0 -0
  85. {cachekit-0.21.0 → cachekit-0.23.0}/src/cachekit/decorators/session.py +0 -0
  86. {cachekit-0.21.0 → cachekit-0.23.0}/src/cachekit/decorators/stats_context.py +0 -0
  87. {cachekit-0.21.0 → cachekit-0.23.0}/src/cachekit/decorators/tenant_context.py +0 -0
  88. {cachekit-0.21.0 → cachekit-0.23.0}/src/cachekit/decorators/utils/__init__.py +0 -0
  89. {cachekit-0.21.0 → cachekit-0.23.0}/src/cachekit/di.py +0 -0
  90. {cachekit-0.21.0 → cachekit-0.23.0}/src/cachekit/imports.py +0 -0
  91. {cachekit-0.21.0 → cachekit-0.23.0}/src/cachekit/key_generator.py +0 -0
  92. {cachekit-0.21.0 → cachekit-0.23.0}/src/cachekit/monitoring/__init__.py +0 -0
  93. {cachekit-0.21.0 → cachekit-0.23.0}/src/cachekit/monitoring/protocols.py +0 -0
  94. {cachekit-0.21.0 → cachekit-0.23.0}/src/cachekit/py.typed +0 -0
  95. {cachekit-0.21.0 → cachekit-0.23.0}/src/cachekit/reliability/__init__.py +0 -0
  96. {cachekit-0.21.0 → cachekit-0.23.0}/src/cachekit/reliability/error_classification.py +0 -0
@@ -280,7 +280,7 @@ dependencies = [
280
280
 
281
281
  [[package]]
282
282
  name = "cachekit-rs"
283
- version = "0.21.0"
283
+ version = "0.23.0"
284
284
  dependencies = [
285
285
  "cachekit-core",
286
286
  "criterion 0.8.2",
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: cachekit
3
- Version: 0.21.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
@@ -28,11 +28,9 @@ Requires-Dist: psutil>=7.0.0
28
28
  Requires-Dist: blake3>=1.0.5
29
29
  Requires-Dist: msgpack>=1.2.1
30
30
  Requires-Dist: xxhash>=3.5.0
31
- Requires-Dist: httpx[http2]>=0.28.1
32
- Requires-Dist: anyio>=4.14.2
33
- Requires-Dist: h2>=4.4.1
31
+ Requires-Dist: urllib3>=2.8.0
34
32
  Requires-Dist: numpy>=2.0.2 ; extra == 'data'
35
- Requires-Dist: pandas>=1.3.0 ; extra == 'data'
33
+ Requires-Dist: pandas>=2.2.2 ; extra == 'data'
36
34
  Requires-Dist: pyarrow>=21.0.0 ; extra == 'data'
37
35
  Requires-Dist: orjson>=3.9.0 ; extra == 'json'
38
36
  Requires-Dist: pymemcache>=4.0.0 ; extra == 'memcached'
@@ -116,7 +114,23 @@ Or with [uv][uv-url] (recommended):
116
114
  uv add cachekit
117
115
  ```
118
116
 
119
- ### Setup (choose a backend)
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
120
134
 
121
135
  cachekit exposes one decorator API over a pluggable backend abstraction. Pick the
122
136
  backend that fits your infrastructure — they're peers behind the same `@cache` API:
@@ -126,7 +140,7 @@ backend that fits your infrastructure — they're peers behind the same `@cache`
126
140
  | Redis | Self-hosted, full control | `REDIS_URL` / `CACHEKIT_REDIS_URL` |
127
141
  | CachekitIO | Managed, zero-ops (beta) | `CACHEKIT_API_KEY` |
128
142
  | Memcached | High-throughput, existing infra | `CACHEKIT_MEMCACHED_SERVERS` |
129
- | 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`) |
130
144
 
131
145
  ```bash
132
146
  # Run Redis locally or use your existing infrastructure
@@ -136,13 +150,16 @@ export REDIS_URL="redis://localhost:6379"
136
150
  ```python
137
151
  from cachekit import cache
138
152
 
139
- @cache # Auto-detects backend (defaults to Redis at localhost)
153
+ @cache # REDIS_URL set: L1 + Redis, shared across processes
140
154
  def expensive_api_call(user_id: int):
141
155
  return fetch_user_data(user_id)
142
156
  ```
143
157
 
144
- > [!TIP]
145
- > No Redis? No worries! Use `@cache(backend=None)` for L1-only in-memory caching, like `lru_cache`, but with all the bells and whistles.
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).
146
163
 
147
164
  ### More Backends
148
165
 
@@ -163,6 +180,8 @@ def expensive_api_call(user_id: int):
163
180
 
164
181
  *cachekit.io is in closed beta — [request access](https://cachekit.io) to get started.*
165
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
+
166
185
  </details>
167
186
 
168
187
  <details>
@@ -226,26 +245,29 @@ Setting `CACHEKIT_MASTER_KEY` instead of passing `master_key=` means every prese
226
245
  | Integrity Checking | - | ✅ | - | ✅ | ✅ 🔒 |
227
246
  | Encryption | - | - | - | - | ✅ Required |
228
247
  | L1 SWR (L1-only mode) | - | ✅ | - | ✅ | - |
229
- | L1 Invalidation | - | - | - | ✅ | ✅ |
230
248
  | Prometheus Metrics | - | -¹ | - | ✅ | ✅ |
231
- | Tracing | - | ✅ | - | ✅ | ✅ |
232
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 |
233
251
  | **Use Case** | High throughput | Local debugging | Deterministic tests | Production reliability | Compliance/security |
234
252
 
235
253
  > ¹ `@cache.dev` exports no Prometheus metrics except `circuit_breaker_state`, which is recorded for every function whose circuit breaker is enabled.
236
254
  >
237
- > 🔒 `@cache.secure` forces `integrity_checking=True` — passing `integrity_checking=False` raises `ConfigurationError` at decoration, including as an override next to `config=DecoratorConfig.secure(...)`. `@cache.secure` also rejects `config=`; the RORO form is `@cache(config=DecoratorConfig.secure(...))`.
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(...))`.
238
258
  >
239
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)).
240
260
  >
261
+ > **Cross-process L1 eviction** is not a preset feature: every preset's `invalidate_cache()` deletes from L1 and L2 alike, and evicting other processes' L1 copies is one process-wide opt-in, `CACHEKIT_INVALIDATION_LISTENER_ENABLED` ([L1 invalidation](docs/features/l1-invalidation.md#cross-process-l1-eviction)).
262
+ >
241
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)).
242
264
  >
243
- > **`@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.
244
266
 
245
267
  <details>
246
268
  <summary><strong>Additional Presets: <code>@cache.dev</code> and <code>@cache.test</code></strong></summary>
247
269
 
248
- 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.
249
271
 
250
272
  ```python
251
273
  # Development: verbose logging, integrity checks on, Prometheus off except circuit_breaker_state
@@ -298,9 +320,10 @@ def test_cached_function():
298
320
 
299
321
  - Circuit breaker with graceful degradation
300
322
  - Connection pooling with thread affinity (+28% throughput)
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/2736a81f2f853cf08c5563c0fe7c8361331fa3ad/test-vectors/decode-bounds.json) vectors
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,10 +332,10 @@ def test_cached_function():
309
332
 
310
333
  | Serializer | Speed | Use Case |
311
334
  |:-----------|:-----:|:---------|
312
- | **StandardSerializer** | ★★★★☆ | General Python types, NumPy, Pandas |
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** | ★★★★★ | Large DataFrames (6-23x faster for 10K+ rows) |
315
- | **EncryptionWrapper** | ★★★★☆ | Wraps any serializer with AES-256-GCM |
337
+ | **ArrowSerializer** | — | pandas DataFrames (columnar, exact round trip) |
338
+ | **+ Encryption** | ★★★★☆ | Any serializer above, AES-256-GCM encrypted: `@cache.secure(master_key=..., serializer=...)` |
316
339
 
317
340
  <details>
318
341
  <summary><strong>Serializer Examples</strong></summary>
@@ -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
- # Zero-copy DataFrames for large datasets
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
@@ -451,7 +474,9 @@ free-threaded 3.14 with the GIL verified disabled (CI job
451
474
  `test-freethreaded`), and the Rust extension declares free-threaded safety
452
475
  (`gil_used = false`). Free-threaded wheels are **not yet published** and
453
476
  free-threaded builds are not officially supported — blocked upstream on
454
- orjson (no free-threaded wheels) and hiredis (re-enables the GIL on import). See
477
+ orjson (no free-threaded wheels) and hiredis (re-enables the GIL on import;
478
+ cachekit loads redis-py only for its Redis backend, and keeps hiredis out there,
479
+ so redis-py uses its pure-Python parser). See
455
480
  [measured performance results](docs/free-threading.md#measured-performance) and the
456
481
  full concurrency audit: [docs/free-threading.md](docs/free-threading.md).
457
482
 
@@ -502,7 +527,7 @@ info = expensive_func.cache_info()
502
527
  | [Prometheus Metrics][prometheus-url] | Built-in observability |
503
528
  | [Zero-Knowledge Encryption][encryption-url] | Client-side security |
504
529
  | [Interop Mode][interop-url] | Cross-SDK cache sharing with cachekit-ts/rs |
505
- | [L1 Invalidation & SWR][l1-invalidation-url] | Invalidation scope (incl. cross-process whole-function on tenant-scoped Redis from the environment or `RedisBackendProvider`), stale-while-revalidate |
530
+ | [L1 Invalidation & SWR][l1-invalidation-url] | Invalidation scope (incl. cross-process whole-function on tenant-scoped Redis from the environment or `RedisBackendProvider`), opt-in cross-process L1 eviction, stale-while-revalidate |
506
531
  | [Reference Caching][reference-caching-url] | `@cache.local()` for non-serializable objects |
507
532
  | [Rust Serialization][rust-serialization-url] | ByteStorage layer: LZ4, xxHash3, AES-256-GCM |
508
533
  | [SSRF Protection][ssrf-url] | URL allowlisting for the CachekitIO backend |
@@ -517,7 +542,8 @@ info = expensive_func.cache_info()
517
542
  # Backend selection: set exactly ONE of CACHEKIT_REDIS_URL, CACHEKIT_API_KEY,
518
543
  # CACHEKIT_MEMCACHED_SERVERS, CACHEKIT_FILE_CACHE_DIR. Two or more is a ConfigurationError at
519
544
  # first call, and decorators relying on env auto-detection run uncached (docs/backends/README.md).
520
- # 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).
521
547
 
522
548
  # Redis Connection (priority: CACHEKIT_REDIS_URL > REDIS_URL)
523
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 (choose a backend)
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 # Auto-detects backend (defaults to Redis at localhost)
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
- > [!TIP]
89
- > No Redis? No worries! Use `@cache(backend=None)` for L1-only in-memory caching, like `lru_cache`, but with all the bells and whistles.
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>
@@ -170,26 +191,29 @@ Setting `CACHEKIT_MASTER_KEY` instead of passing `master_key=` means every prese
170
191
  | Integrity Checking | - | ✅ | - | ✅ | ✅ 🔒 |
171
192
  | Encryption | - | - | - | - | ✅ Required |
172
193
  | L1 SWR (L1-only mode) | - | ✅ | - | ✅ | - |
173
- | L1 Invalidation | - | - | - | ✅ | ✅ |
174
194
  | Prometheus Metrics | - | -¹ | - | ✅ | ✅ |
175
- | Tracing | - | ✅ | - | ✅ | ✅ |
176
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 |
177
197
  | **Use Case** | High throughput | Local debugging | Deterministic tests | Production reliability | Compliance/security |
178
198
 
179
199
  > ¹ `@cache.dev` exports no Prometheus metrics except `circuit_breaker_state`, which is recorded for every function whose circuit breaker is enabled.
180
200
  >
181
- > 🔒 `@cache.secure` forces `integrity_checking=True` — passing `integrity_checking=False` raises `ConfigurationError` at decoration, including as an override next to `config=DecoratorConfig.secure(...)`. `@cache.secure` also rejects `config=`; the RORO form is `@cache(config=DecoratorConfig.secure(...))`.
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(...))`.
182
204
  >
183
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)).
184
206
  >
207
+ > **Cross-process L1 eviction** is not a preset feature: every preset's `invalidate_cache()` deletes from L1 and L2 alike, and evicting other processes' L1 copies is one process-wide opt-in, `CACHEKIT_INVALIDATION_LISTENER_ENABLED` ([L1 invalidation](docs/features/l1-invalidation.md#cross-process-l1-eviction)).
208
+ >
185
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)).
186
210
  >
187
- > **`@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.
188
212
 
189
213
  <details>
190
214
  <summary><strong>Additional Presets: <code>@cache.dev</code> and <code>@cache.test</code></strong></summary>
191
215
 
192
- 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.
193
217
 
194
218
  ```python
195
219
  # Development: verbose logging, integrity checks on, Prometheus off except circuit_breaker_state
@@ -242,9 +266,10 @@ def test_cached_function():
242
266
 
243
267
  - Circuit breaker with graceful degradation
244
268
  - Connection pooling with thread affinity (+28% throughput)
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
245
270
  - Distributed locking prevents cache stampedes
246
271
  - Pluggable backend abstraction (Redis, CachekitIO, File, Memcached, custom)
247
- - Untrusted-decode bounds: nesting depth and header-declared allocation are capped on every cache read (a forged entry is a bounded cache miss), verified against the protocol's shared [`decode-bounds.json`](https://github.com/cachekit-io/protocol/blob/2736a81f2f853cf08c5563c0fe7c8361331fa3ad/test-vectors/decode-bounds.json) vectors
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
248
273
 
249
274
  > [!NOTE]
250
275
  > All reliability features are **enabled by default** with `@cache.production`. Use `@cache.minimal` to disable them for maximum throughput.
@@ -253,10 +278,10 @@ def test_cached_function():
253
278
 
254
279
  | Serializer | Speed | Use Case |
255
280
  |:-----------|:-----:|:---------|
256
- | **StandardSerializer** | ★★★★☆ | General Python types, NumPy, Pandas |
281
+ | **StandardSerializer** | ★★★★☆ | General Python types; not NumPy arrays or pandas objects |
257
282
  | **OrjsonSerializer** | ★★★★★ | JSON APIs (2-5x faster than stdlib) — requires `cachekit[json]` |
258
- | **ArrowSerializer** | ★★★★★ | Large DataFrames (6-23x faster for 10K+ rows) |
259
- | **EncryptionWrapper** | ★★★★☆ | Wraps any serializer with AES-256-GCM |
283
+ | **ArrowSerializer** | — | pandas DataFrames (columnar, exact round trip) |
284
+ | **+ Encryption** | ★★★★☆ | Any serializer above, AES-256-GCM encrypted: `@cache.secure(master_key=..., serializer=...)` |
260
285
 
261
286
  <details>
262
287
  <summary><strong>Serializer Examples</strong></summary>
@@ -269,13 +294,13 @@ from cachekit.serializers import OrjsonSerializer, ArrowSerializer
269
294
  def get_api_response(endpoint: str):
270
295
  return {"status": "success", "data": fetch_api(endpoint)}
271
296
 
272
- # Zero-copy DataFrames for large datasets
297
+ # Columnar DataFrames (Arrow IPC, zstd-compressed by default)
273
298
  @cache(serializer=ArrowSerializer())
274
299
  def get_large_dataset(date: str):
275
300
  return pd.read_csv(f"data/{date}.csv")
276
301
  ```
277
302
 
278
- 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:
279
304
 
280
305
  ```python
281
306
  import tempfile
@@ -395,7 +420,9 @@ free-threaded 3.14 with the GIL verified disabled (CI job
395
420
  `test-freethreaded`), and the Rust extension declares free-threaded safety
396
421
  (`gil_used = false`). Free-threaded wheels are **not yet published** and
397
422
  free-threaded builds are not officially supported — blocked upstream on
398
- orjson (no free-threaded wheels) and hiredis (re-enables the GIL on import). See
423
+ orjson (no free-threaded wheels) and hiredis (re-enables the GIL on import;
424
+ cachekit loads redis-py only for its Redis backend, and keeps hiredis out there,
425
+ so redis-py uses its pure-Python parser). See
399
426
  [measured performance results](docs/free-threading.md#measured-performance) and the
400
427
  full concurrency audit: [docs/free-threading.md](docs/free-threading.md).
401
428
 
@@ -446,7 +473,7 @@ info = expensive_func.cache_info()
446
473
  | [Prometheus Metrics][prometheus-url] | Built-in observability |
447
474
  | [Zero-Knowledge Encryption][encryption-url] | Client-side security |
448
475
  | [Interop Mode][interop-url] | Cross-SDK cache sharing with cachekit-ts/rs |
449
- | [L1 Invalidation & SWR][l1-invalidation-url] | Invalidation scope (incl. cross-process whole-function on tenant-scoped Redis from the environment or `RedisBackendProvider`), stale-while-revalidate |
476
+ | [L1 Invalidation & SWR][l1-invalidation-url] | Invalidation scope (incl. cross-process whole-function on tenant-scoped Redis from the environment or `RedisBackendProvider`), opt-in cross-process L1 eviction, stale-while-revalidate |
450
477
  | [Reference Caching][reference-caching-url] | `@cache.local()` for non-serializable objects |
451
478
  | [Rust Serialization][rust-serialization-url] | ByteStorage layer: LZ4, xxHash3, AES-256-GCM |
452
479
  | [SSRF Protection][ssrf-url] | URL allowlisting for the CachekitIO backend |
@@ -461,7 +488,8 @@ info = expensive_func.cache_info()
461
488
  # Backend selection: set exactly ONE of CACHEKIT_REDIS_URL, CACHEKIT_API_KEY,
462
489
  # CACHEKIT_MEMCACHED_SERVERS, CACHEKIT_FILE_CACHE_DIR. Two or more is a ConfigurationError at
463
490
  # first call, and decorators relying on env auto-detection run uncached (docs/backends/README.md).
464
- # 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).
465
493
 
466
494
  # Redis Connection (priority: CACHEKIT_REDIS_URL > REDIS_URL)
467
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.21.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"}
@@ -71,31 +71,19 @@ dependencies = [
71
71
  "blake3>=1.0.5",
72
72
  "msgpack>=1.2.1",
73
73
  "xxhash>=3.5.0",
74
- # HTTP client for SaaS backend (cachekit.io)
75
- "httpx[http2]>=0.28.1",
76
- # anyio is transitive via the mandatory httpx dependency above, so it ships
77
- # to EVERY install, not just dev. Declared here rather than as a
78
- # [tool.uv] constraint because that table is uv-local: it never reaches
79
- # requires-dist, so `pip install cachekit` would ignore it. 4.14.2 fixes
80
- # GHSA-82r6-8w77-94w6 / CVE-2026-63374 (IDNA-2003 hostname encoding lets a
81
- # hijacked connection to an internationalised domain pass TLS certificate
82
- # validation; CVSS 9.3), GHSA-5p39-cfhj-2xmp / CVE-2026-64847 (undrained
83
- # process-pool stderr pipe deadlocks the worker) and GHSA-3w57-8xmc-8v26 /
84
- # CVE-2026-63349 (extra_groups ignored, parent supplementary groups kept).
85
- "anyio>=4.14.2",
86
- # h2 arrives via the http2 extra on that same mandatory httpx dependency, so
87
- # it ships to every install too, and was declared as a [tool.uv] constraint
88
- # with the same no-op effect. 4.4.1 fixes GHSA-6hr6-w5qg-qmwg (duplicate Host
89
- # headers forwarded across an HTTP/2 -> HTTP/1.1 downgrade — a request
90
- # smuggling primitive).
91
- "h2>=4.4.1",
74
+ # HTTP client for the SaaS backend (cachekit.io). It has no dependencies of its own. 2.8.0 fixes
75
+ # GHSA-8988-9cw3-xx77 (HTTPS proxy TLS config ignored), GHSA-gh4c-6fx4-qh6g (chunked deflate
76
+ # infinite loop) and GHSA-vxq7-64xx-v4gw (unbounded chunk-size line buffering).
77
+ "urllib3>=2.8.0",
92
78
  ]
93
79
 
94
80
  [project.optional-dependencies]
95
81
  # User-facing optional features only - dev deps are in [dependency-groups]
96
82
  data = [
97
83
  "numpy>=2.0.2",
98
- "pandas>=1.3.0",
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",
99
87
  "pyarrow>=21.0.0",
100
88
  ]
101
89
  json = [
@@ -149,7 +137,6 @@ ignore = [
149
137
  "__init__.py" = ["F401"]
150
138
  "tests/**/*" = ["S101", "S105", "S110", "S112", "S311", "D", "F841", "N802"]
151
139
  "src/cachekit/logging.py" = ["S110"] # Logging errors would create recursion
152
- "src/cachekit/monitoring/pool_monitor.py" = ["S110"] # Monitoring errors should be silent
153
140
  "src/cachekit/reliability/async_metrics.py" = ["S101"] # Internal invariant checks
154
141
  "src/cachekit/serializers/__init__.py" = ["S101"] # Module-level validation
155
142
  "src/cachekit/serializers/standard_serializer.py" = ["S110"] # Fallback deserialization patterns
@@ -238,7 +225,6 @@ test = [
238
225
  "pymemcache>=4.0.0",
239
226
  # Utilities
240
227
  "faker>=20.0.0",
241
- "httpx>=0.28.1",
242
228
  "hypothesis>=6.0.0",
243
229
  "requests>=2.33.0; python_version >= '3.10'",
244
230
  "psutil>=5.9.0",
@@ -246,6 +232,7 @@ test = [
246
232
  "pyyaml>=6.0.3",
247
233
  "pytest-xdist>=3.8.0",
248
234
  "time-machine>=2.19.0",
235
+ "h11>=0.16.0",
249
236
  ]
250
237
  dev = [
251
238
  { include-group = "test" },
@@ -260,7 +247,7 @@ dev = [
260
247
  "pip-audit>=2.7.0",
261
248
  # Data science support (for testing AutoSerializer with numpy/pandas)
262
249
  "numpy>=2.0.2",
263
- "pandas>=1.3.0",
250
+ "pandas>=2.2.2",
264
251
  "pyarrow>=21.0.0",
265
252
  # OrjsonSerializer support — now the [json] optional extra; kept here so the
266
253
  # orjson tests, doctests, and markdown-docs still resolve it in dev/CI.
@@ -275,15 +262,9 @@ fuzz = [
275
262
  # This table is uv-local: it constrains resolution of this repo's lockfile and
276
263
  # never reaches requires-dist, so a floor placed here does NOT protect anyone who
277
264
  # runs `pip install cachekit`. A transitive that reaches users belongs in
278
- # [project] dependencies instead — see the anyio/h2 entries above.
265
+ # [project] dependencies instead — see the urllib3 entry above.
279
266
  [tool.uv]
280
267
  constraint-dependencies = [
281
- # urllib3 is a dev-only transitive dep (requests -> urllib3; requests is a
282
- # dev dependency and also arrives via pip-audit).
283
- # 2.8.0 fixes GHSA-8988-9cw3-xx77 (HTTPS proxy TLS config ignored),
284
- # GHSA-gh4c-6fx4-qh6g (chunked deflate infinite loop) and
285
- # GHSA-vxq7-64xx-v4gw (unbounded chunk-size line buffering).
286
- "urllib3>=2.8.0",
287
268
  "fonttools>=4.60.2",
288
269
  "werkzeug>=3.1.4",
289
270
  # pip is a dev-only transitive dep (pip-audit -> pip-api -> pip). 26.1.2 fixes
@@ -1,6 +1,6 @@
1
1
  [package]
2
2
  name = "cachekit-rs"
3
- version = "0.21.0"
3
+ version = "0.23.0"
4
4
  edition = "2021"
5
5
  authors = ["cachekit Contributors"]
6
6
  description = "High-performance storage engine for caching with compression and encryption"