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.
- {cachekit-0.21.0 → cachekit-0.23.0}/Cargo.lock +1 -1
- {cachekit-0.21.0 → cachekit-0.23.0}/PKG-INFO +50 -24
- {cachekit-0.21.0 → cachekit-0.23.0}/README.md +47 -19
- {cachekit-0.21.0 → cachekit-0.23.0}/pyproject.toml +11 -30
- {cachekit-0.21.0 → cachekit-0.23.0}/rust/Cargo.toml +1 -1
- {cachekit-0.21.0 → cachekit-0.23.0}/rust/README.md +47 -19
- {cachekit-0.21.0 → cachekit-0.23.0}/src/cachekit/__init__.py +9 -8
- cachekit-0.23.0/src/cachekit/_fork.py +65 -0
- {cachekit-0.21.0 → cachekit-0.23.0}/src/cachekit/backends/__init__.py +15 -4
- cachekit-0.23.0/src/cachekit/backends/_uninterrupted.py +114 -0
- {cachekit-0.21.0 → cachekit-0.23.0}/src/cachekit/backends/base.py +14 -3
- {cachekit-0.21.0 → cachekit-0.23.0}/src/cachekit/backends/cachekitio/backend.py +491 -181
- cachekit-0.23.0/src/cachekit/backends/cachekitio/client.py +283 -0
- {cachekit-0.21.0 → cachekit-0.23.0}/src/cachekit/backends/cachekitio/config.py +87 -78
- cachekit-0.23.0/src/cachekit/backends/cachekitio/error_handler.py +181 -0
- {cachekit-0.21.0 → cachekit-0.23.0}/src/cachekit/backends/file/backend.py +271 -158
- {cachekit-0.21.0 → cachekit-0.23.0}/src/cachekit/backends/memcached/backend.py +3 -2
- {cachekit-0.21.0 → cachekit-0.23.0}/src/cachekit/backends/provider.py +64 -16
- cachekit-0.23.0/src/cachekit/backends/redis/__init__.py +17 -0
- {cachekit-0.21.0 → cachekit-0.23.0}/src/cachekit/backends/redis/backend.py +131 -100
- {cachekit-0.21.0 → cachekit-0.23.0}/src/cachekit/backends/redis/config.py +13 -3
- {cachekit-0.21.0 → cachekit-0.23.0}/src/cachekit/backends/redis/error_handler.py +70 -16
- cachekit-0.23.0/src/cachekit/backends/redis/provider.py +1011 -0
- {cachekit-0.21.0 → cachekit-0.23.0}/src/cachekit/cache_handler.py +153 -43
- {cachekit-0.21.0 → cachekit-0.23.0}/src/cachekit/config/__init__.py +2 -1
- {cachekit-0.21.0 → cachekit-0.23.0}/src/cachekit/config/decorator.py +144 -49
- {cachekit-0.21.0 → cachekit-0.23.0}/src/cachekit/config/nested.py +36 -15
- {cachekit-0.21.0 → cachekit-0.23.0}/src/cachekit/config/settings.py +99 -43
- {cachekit-0.21.0 → cachekit-0.23.0}/src/cachekit/config/validation.py +70 -4
- {cachekit-0.21.0 → cachekit-0.23.0}/src/cachekit/decorators/intent.py +115 -73
- {cachekit-0.21.0 → cachekit-0.23.0}/src/cachekit/decorators/local_wrapper.py +69 -16
- {cachekit-0.21.0 → cachekit-0.23.0}/src/cachekit/decorators/orchestrator.py +52 -70
- cachekit-0.23.0/src/cachekit/decorators/single_flight.py +283 -0
- {cachekit-0.21.0 → cachekit-0.23.0}/src/cachekit/decorators/wrapper.py +1382 -875
- {cachekit-0.21.0 → cachekit-0.23.0}/src/cachekit/hash_utils.py +63 -4
- {cachekit-0.21.0 → cachekit-0.23.0}/src/cachekit/health.py +6 -1
- cachekit-0.23.0/src/cachekit/hiredis_compat.py +98 -0
- {cachekit-0.21.0 → cachekit-0.23.0}/src/cachekit/interop.py +50 -16
- cachekit-0.23.0/src/cachekit/invalidation.py +493 -0
- {cachekit-0.21.0 → cachekit-0.23.0}/src/cachekit/l1_cache.py +129 -62
- cachekit-0.23.0/src/cachekit/logging.py +234 -0
- {cachekit-0.21.0 → cachekit-0.23.0}/src/cachekit/object_cache.py +180 -86
- {cachekit-0.21.0 → cachekit-0.23.0}/src/cachekit/reliability/async_metrics.py +152 -69
- {cachekit-0.21.0 → cachekit-0.23.0}/src/cachekit/reliability/circuit_breaker.py +97 -27
- {cachekit-0.21.0 → cachekit-0.23.0}/src/cachekit/reliability/load_control.py +60 -49
- {cachekit-0.21.0 → cachekit-0.23.0}/src/cachekit/reliability/metrics_collection.py +15 -6
- {cachekit-0.21.0 → cachekit-0.23.0}/src/cachekit/serializers/__init__.py +2 -2
- {cachekit-0.21.0 → cachekit-0.23.0}/src/cachekit/serializers/arrow_serializer.py +21 -5
- {cachekit-0.21.0 → cachekit-0.23.0}/src/cachekit/serializers/auto_serializer.py +179 -125
- {cachekit-0.21.0 → cachekit-0.23.0}/src/cachekit/serializers/base.py +25 -12
- {cachekit-0.21.0 → cachekit-0.23.0}/src/cachekit/serializers/encryption_wrapper.py +101 -23
- {cachekit-0.21.0 → cachekit-0.23.0}/src/cachekit/serializers/interop_serializer.py +1 -1
- {cachekit-0.21.0 → cachekit-0.23.0}/src/cachekit/serializers/orjson_serializer.py +2 -1
- {cachekit-0.21.0 → cachekit-0.23.0}/src/cachekit/serializers/standard_serializer.py +26 -9
- cachekit-0.23.0/src/cachekit/serializers/wrapper.py +270 -0
- cachekit-0.21.0/src/cachekit/backends/cachekitio/client.py +0 -245
- cachekit-0.21.0/src/cachekit/backends/cachekitio/error_handler.py +0 -134
- cachekit-0.21.0/src/cachekit/backends/redis/__init__.py +0 -12
- cachekit-0.21.0/src/cachekit/backends/redis/provider.py +0 -811
- cachekit-0.21.0/src/cachekit/hiredis_compat.py +0 -98
- cachekit-0.21.0/src/cachekit/logging.py +0 -386
- cachekit-0.21.0/src/cachekit/monitoring/pool_monitor.py +0 -346
- cachekit-0.21.0/src/cachekit/serializers/wrapper.py +0 -169
- {cachekit-0.21.0 → cachekit-0.23.0}/Cargo.toml +0 -0
- {cachekit-0.21.0 → cachekit-0.23.0}/LICENSE +0 -0
- {cachekit-0.21.0 → cachekit-0.23.0}/rust/Makefile +0 -0
- {cachekit-0.21.0 → cachekit-0.23.0}/rust/TEST_EXPANSION_SUMMARY.md +0 -0
- {cachekit-0.21.0 → cachekit-0.23.0}/rust/src/lib.rs +0 -0
- {cachekit-0.21.0 → cachekit-0.23.0}/rust/src/msgpack_bounds.rs +0 -0
- {cachekit-0.21.0 → cachekit-0.23.0}/rust/src/python_bindings.rs +0 -0
- {cachekit-0.21.0 → cachekit-0.23.0}/rust/tsan_suppressions.txt +0 -0
- {cachekit-0.21.0 → cachekit-0.23.0}/src/cachekit/backends/base_config.py +0 -0
- {cachekit-0.21.0 → cachekit-0.23.0}/src/cachekit/backends/cachekitio/__init__.py +0 -0
- {cachekit-0.21.0 → cachekit-0.23.0}/src/cachekit/backends/cachekitio/session.py +0 -0
- {cachekit-0.21.0 → cachekit-0.23.0}/src/cachekit/backends/errors.py +0 -0
- {cachekit-0.21.0 → cachekit-0.23.0}/src/cachekit/backends/file/__init__.py +0 -0
- {cachekit-0.21.0 → cachekit-0.23.0}/src/cachekit/backends/file/config.py +0 -0
- {cachekit-0.21.0 → cachekit-0.23.0}/src/cachekit/backends/memcached/__init__.py +0 -0
- {cachekit-0.21.0 → cachekit-0.23.0}/src/cachekit/backends/memcached/config.py +0 -0
- {cachekit-0.21.0 → cachekit-0.23.0}/src/cachekit/backends/memcached/error_handler.py +0 -0
- {cachekit-0.21.0 → cachekit-0.23.0}/src/cachekit/backends/redis/client.py +0 -0
- {cachekit-0.21.0 → cachekit-0.23.0}/src/cachekit/config/singleton.py +0 -0
- {cachekit-0.21.0 → cachekit-0.23.0}/src/cachekit/decorators/__init__.py +0 -0
- {cachekit-0.21.0 → cachekit-0.23.0}/src/cachekit/decorators/main.py +0 -0
- {cachekit-0.21.0 → cachekit-0.23.0}/src/cachekit/decorators/session.py +0 -0
- {cachekit-0.21.0 → cachekit-0.23.0}/src/cachekit/decorators/stats_context.py +0 -0
- {cachekit-0.21.0 → cachekit-0.23.0}/src/cachekit/decorators/tenant_context.py +0 -0
- {cachekit-0.21.0 → cachekit-0.23.0}/src/cachekit/decorators/utils/__init__.py +0 -0
- {cachekit-0.21.0 → cachekit-0.23.0}/src/cachekit/di.py +0 -0
- {cachekit-0.21.0 → cachekit-0.23.0}/src/cachekit/imports.py +0 -0
- {cachekit-0.21.0 → cachekit-0.23.0}/src/cachekit/key_generator.py +0 -0
- {cachekit-0.21.0 → cachekit-0.23.0}/src/cachekit/monitoring/__init__.py +0 -0
- {cachekit-0.21.0 → cachekit-0.23.0}/src/cachekit/monitoring/protocols.py +0 -0
- {cachekit-0.21.0 → cachekit-0.23.0}/src/cachekit/py.typed +0 -0
- {cachekit-0.21.0 → cachekit-0.23.0}/src/cachekit/reliability/__init__.py +0 -0
- {cachekit-0.21.0 → cachekit-0.23.0}/src/cachekit/reliability/error_classification.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
|
|
@@ -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:
|
|
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>=
|
|
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
|
|
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 #
|
|
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
|
-
> [!
|
|
145
|
-
>
|
|
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
|
-
>
|
|
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/
|
|
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
|
|
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** |
|
|
315
|
-
| **
|
|
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
|
-
#
|
|
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
|
|
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
|
|
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>
|
|
@@ -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
|
-
>
|
|
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/
|
|
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
|
|
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** |
|
|
259
|
-
| **
|
|
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
|
-
#
|
|
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
|
|
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.
|
|
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
|
-
|
|
76
|
-
#
|
|
77
|
-
|
|
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
|
-
|
|
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>=
|
|
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
|
|
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
|