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.
Files changed (91) hide show
  1. {cachekit-0.22.0 → cachekit-0.23.0}/Cargo.lock +1 -1
  2. {cachekit-0.22.0 → cachekit-0.23.0}/PKG-INFO +41 -17
  3. {cachekit-0.22.0 → cachekit-0.23.0}/README.md +39 -15
  4. {cachekit-0.22.0 → cachekit-0.23.0}/pyproject.toml +5 -4
  5. {cachekit-0.22.0 → cachekit-0.23.0}/rust/Cargo.toml +1 -1
  6. {cachekit-0.22.0 → cachekit-0.23.0}/rust/README.md +39 -15
  7. {cachekit-0.22.0 → cachekit-0.23.0}/src/cachekit/__init__.py +9 -5
  8. cachekit-0.23.0/src/cachekit/_fork.py +65 -0
  9. {cachekit-0.22.0 → cachekit-0.23.0}/src/cachekit/backends/__init__.py +1 -1
  10. cachekit-0.23.0/src/cachekit/backends/_uninterrupted.py +114 -0
  11. {cachekit-0.22.0 → cachekit-0.23.0}/src/cachekit/backends/base.py +14 -3
  12. {cachekit-0.22.0 → cachekit-0.23.0}/src/cachekit/backends/cachekitio/backend.py +108 -53
  13. {cachekit-0.22.0 → cachekit-0.23.0}/src/cachekit/backends/cachekitio/client.py +8 -4
  14. {cachekit-0.22.0 → cachekit-0.23.0}/src/cachekit/backends/cachekitio/config.py +80 -73
  15. {cachekit-0.22.0 → cachekit-0.23.0}/src/cachekit/backends/cachekitio/error_handler.py +38 -9
  16. {cachekit-0.22.0 → cachekit-0.23.0}/src/cachekit/backends/file/backend.py +271 -158
  17. {cachekit-0.22.0 → cachekit-0.23.0}/src/cachekit/backends/memcached/backend.py +3 -2
  18. {cachekit-0.22.0 → cachekit-0.23.0}/src/cachekit/backends/provider.py +56 -12
  19. {cachekit-0.22.0 → cachekit-0.23.0}/src/cachekit/backends/redis/backend.py +131 -100
  20. {cachekit-0.22.0 → cachekit-0.23.0}/src/cachekit/backends/redis/config.py +7 -1
  21. {cachekit-0.22.0 → cachekit-0.23.0}/src/cachekit/backends/redis/error_handler.py +70 -16
  22. {cachekit-0.22.0 → cachekit-0.23.0}/src/cachekit/backends/redis/provider.py +413 -250
  23. {cachekit-0.22.0 → cachekit-0.23.0}/src/cachekit/cache_handler.py +79 -36
  24. {cachekit-0.22.0 → cachekit-0.23.0}/src/cachekit/config/__init__.py +2 -1
  25. {cachekit-0.22.0 → cachekit-0.23.0}/src/cachekit/config/decorator.py +142 -49
  26. {cachekit-0.22.0 → cachekit-0.23.0}/src/cachekit/config/nested.py +24 -7
  27. {cachekit-0.22.0 → cachekit-0.23.0}/src/cachekit/config/settings.py +86 -41
  28. {cachekit-0.22.0 → cachekit-0.23.0}/src/cachekit/config/validation.py +70 -4
  29. {cachekit-0.22.0 → cachekit-0.23.0}/src/cachekit/decorators/intent.py +114 -79
  30. {cachekit-0.22.0 → cachekit-0.23.0}/src/cachekit/decorators/local_wrapper.py +69 -16
  31. {cachekit-0.22.0 → cachekit-0.23.0}/src/cachekit/decorators/orchestrator.py +32 -71
  32. cachekit-0.23.0/src/cachekit/decorators/single_flight.py +283 -0
  33. {cachekit-0.22.0 → cachekit-0.23.0}/src/cachekit/decorators/wrapper.py +1011 -760
  34. {cachekit-0.22.0 → cachekit-0.23.0}/src/cachekit/interop.py +50 -16
  35. {cachekit-0.22.0 → cachekit-0.23.0}/src/cachekit/invalidation.py +60 -50
  36. {cachekit-0.22.0 → cachekit-0.23.0}/src/cachekit/l1_cache.py +8 -7
  37. cachekit-0.23.0/src/cachekit/logging.py +234 -0
  38. {cachekit-0.22.0 → cachekit-0.23.0}/src/cachekit/object_cache.py +21 -0
  39. {cachekit-0.22.0 → cachekit-0.23.0}/src/cachekit/reliability/async_metrics.py +114 -63
  40. {cachekit-0.22.0 → cachekit-0.23.0}/src/cachekit/reliability/circuit_breaker.py +52 -23
  41. {cachekit-0.22.0 → cachekit-0.23.0}/src/cachekit/reliability/load_control.py +60 -49
  42. {cachekit-0.22.0 → cachekit-0.23.0}/src/cachekit/reliability/metrics_collection.py +15 -6
  43. {cachekit-0.22.0 → cachekit-0.23.0}/src/cachekit/serializers/arrow_serializer.py +12 -5
  44. {cachekit-0.22.0 → cachekit-0.23.0}/src/cachekit/serializers/auto_serializer.py +133 -98
  45. {cachekit-0.22.0 → cachekit-0.23.0}/src/cachekit/serializers/base.py +25 -12
  46. {cachekit-0.22.0 → cachekit-0.23.0}/src/cachekit/serializers/encryption_wrapper.py +101 -23
  47. {cachekit-0.22.0 → cachekit-0.23.0}/src/cachekit/serializers/interop_serializer.py +1 -1
  48. {cachekit-0.22.0 → cachekit-0.23.0}/src/cachekit/serializers/orjson_serializer.py +2 -1
  49. {cachekit-0.22.0 → cachekit-0.23.0}/src/cachekit/serializers/standard_serializer.py +26 -9
  50. cachekit-0.22.0/src/cachekit/backends/_uninterrupted.py +0 -38
  51. cachekit-0.22.0/src/cachekit/logging.py +0 -386
  52. cachekit-0.22.0/src/cachekit/monitoring/pool_monitor.py +0 -346
  53. {cachekit-0.22.0 → cachekit-0.23.0}/Cargo.toml +0 -0
  54. {cachekit-0.22.0 → cachekit-0.23.0}/LICENSE +0 -0
  55. {cachekit-0.22.0 → cachekit-0.23.0}/rust/Makefile +0 -0
  56. {cachekit-0.22.0 → cachekit-0.23.0}/rust/TEST_EXPANSION_SUMMARY.md +0 -0
  57. {cachekit-0.22.0 → cachekit-0.23.0}/rust/src/lib.rs +0 -0
  58. {cachekit-0.22.0 → cachekit-0.23.0}/rust/src/msgpack_bounds.rs +0 -0
  59. {cachekit-0.22.0 → cachekit-0.23.0}/rust/src/python_bindings.rs +0 -0
  60. {cachekit-0.22.0 → cachekit-0.23.0}/rust/tsan_suppressions.txt +0 -0
  61. {cachekit-0.22.0 → cachekit-0.23.0}/src/cachekit/backends/base_config.py +0 -0
  62. {cachekit-0.22.0 → cachekit-0.23.0}/src/cachekit/backends/cachekitio/__init__.py +0 -0
  63. {cachekit-0.22.0 → cachekit-0.23.0}/src/cachekit/backends/cachekitio/session.py +0 -0
  64. {cachekit-0.22.0 → cachekit-0.23.0}/src/cachekit/backends/errors.py +0 -0
  65. {cachekit-0.22.0 → cachekit-0.23.0}/src/cachekit/backends/file/__init__.py +0 -0
  66. {cachekit-0.22.0 → cachekit-0.23.0}/src/cachekit/backends/file/config.py +0 -0
  67. {cachekit-0.22.0 → cachekit-0.23.0}/src/cachekit/backends/memcached/__init__.py +0 -0
  68. {cachekit-0.22.0 → cachekit-0.23.0}/src/cachekit/backends/memcached/config.py +0 -0
  69. {cachekit-0.22.0 → cachekit-0.23.0}/src/cachekit/backends/memcached/error_handler.py +0 -0
  70. {cachekit-0.22.0 → cachekit-0.23.0}/src/cachekit/backends/redis/__init__.py +0 -0
  71. {cachekit-0.22.0 → cachekit-0.23.0}/src/cachekit/backends/redis/client.py +0 -0
  72. {cachekit-0.22.0 → cachekit-0.23.0}/src/cachekit/config/singleton.py +0 -0
  73. {cachekit-0.22.0 → cachekit-0.23.0}/src/cachekit/decorators/__init__.py +0 -0
  74. {cachekit-0.22.0 → cachekit-0.23.0}/src/cachekit/decorators/main.py +0 -0
  75. {cachekit-0.22.0 → cachekit-0.23.0}/src/cachekit/decorators/session.py +0 -0
  76. {cachekit-0.22.0 → cachekit-0.23.0}/src/cachekit/decorators/stats_context.py +0 -0
  77. {cachekit-0.22.0 → cachekit-0.23.0}/src/cachekit/decorators/tenant_context.py +0 -0
  78. {cachekit-0.22.0 → cachekit-0.23.0}/src/cachekit/decorators/utils/__init__.py +0 -0
  79. {cachekit-0.22.0 → cachekit-0.23.0}/src/cachekit/di.py +0 -0
  80. {cachekit-0.22.0 → cachekit-0.23.0}/src/cachekit/hash_utils.py +0 -0
  81. {cachekit-0.22.0 → cachekit-0.23.0}/src/cachekit/health.py +0 -0
  82. {cachekit-0.22.0 → cachekit-0.23.0}/src/cachekit/hiredis_compat.py +0 -0
  83. {cachekit-0.22.0 → cachekit-0.23.0}/src/cachekit/imports.py +0 -0
  84. {cachekit-0.22.0 → cachekit-0.23.0}/src/cachekit/key_generator.py +0 -0
  85. {cachekit-0.22.0 → cachekit-0.23.0}/src/cachekit/monitoring/__init__.py +0 -0
  86. {cachekit-0.22.0 → cachekit-0.23.0}/src/cachekit/monitoring/protocols.py +0 -0
  87. {cachekit-0.22.0 → cachekit-0.23.0}/src/cachekit/py.typed +0 -0
  88. {cachekit-0.22.0 → cachekit-0.23.0}/src/cachekit/reliability/__init__.py +0 -0
  89. {cachekit-0.22.0 → cachekit-0.23.0}/src/cachekit/reliability/error_classification.py +0 -0
  90. {cachekit-0.22.0 → cachekit-0.23.0}/src/cachekit/serializers/__init__.py +0 -0
  91. {cachekit-0.22.0 → cachekit-0.23.0}/src/cachekit/serializers/wrapper.py +0 -0
@@ -280,7 +280,7 @@ dependencies = [
280
280
 
281
281
  [[package]]
282
282
  name = "cachekit-rs"
283
- version = "0.22.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.22.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>=1.3.0 ; extra == 'data'
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 (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
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 # Auto-detects backend (defaults to Redis at localhost)
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
- > [!TIP]
143
- > 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).
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
- > 🔒 `@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(...))`.
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/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,9 +332,9 @@ 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) |
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
- # 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
@@ -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 (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>
@@ -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
- > 🔒 `@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(...))`.
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/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
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, NumPy, Pandas |
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** | ★★★★★ | Large DataFrames (6-23x faster for 10K+ rows) |
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
- # Zero-copy DataFrames for large datasets
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.22.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
- "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",
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>=1.3.0",
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.
@@ -1,6 +1,6 @@
1
1
  [package]
2
2
  name = "cachekit-rs"
3
- version = "0.22.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"
@@ -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>
@@ -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
- > 🔒 `@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(...))`.
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/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
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, NumPy, Pandas |
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** | ★★★★★ | Large DataFrames (6-23x faster for 10K+ rows) |
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
- # Zero-copy DataFrames for large datasets
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.22.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
- # Must run BEFORE any redis import: CACHEKIT_DISABLE_HIREDIS=true keeps hiredis out from here on.
77
- # The free-threaded default runs later, when cachekit.backends.redis loads (see cachekit.hiredis_compat).
78
- from . import hiredis_compat # noqa: F401
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 (default)
17
+ - RedisBackend: Redis implementation
18
18
  - MemcachedBackend: Memcached implementation (requires pymemcache)
19
19
 
20
20
  Usage: