cachekit 0.8.0__tar.gz → 0.9.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.8.0 → cachekit-0.9.0}/Cargo.lock +1 -1
- {cachekit-0.8.0 → cachekit-0.9.0}/PKG-INFO +69 -23
- {cachekit-0.8.0 → cachekit-0.9.0}/README.md +67 -20
- {cachekit-0.8.0 → cachekit-0.9.0}/pyproject.toml +8 -5
- {cachekit-0.8.0 → cachekit-0.9.0}/rust/Cargo.toml +1 -1
- {cachekit-0.8.0 → cachekit-0.9.0}/rust/README.md +67 -20
- {cachekit-0.8.0 → cachekit-0.9.0}/src/cachekit/__init__.py +3 -2
- {cachekit-0.8.0 → cachekit-0.9.0}/src/cachekit/backends/memcached/backend.py +22 -1
- {cachekit-0.8.0 → cachekit-0.9.0}/src/cachekit/backends/memcached/config.py +10 -0
- {cachekit-0.8.0 → cachekit-0.9.0}/src/cachekit/backends/redis/backend.py +5 -9
- {cachekit-0.8.0 → cachekit-0.9.0}/src/cachekit/backends/redis/client.py +2 -2
- {cachekit-0.8.0 → cachekit-0.9.0}/src/cachekit/cache_handler.py +85 -35
- {cachekit-0.8.0 → cachekit-0.9.0}/src/cachekit/config/decorator.py +8 -3
- {cachekit-0.8.0 → cachekit-0.9.0}/src/cachekit/config/nested.py +46 -9
- {cachekit-0.8.0 → cachekit-0.9.0}/src/cachekit/config/settings.py +10 -1
- {cachekit-0.8.0 → cachekit-0.9.0}/src/cachekit/config/validation.py +3 -2
- {cachekit-0.8.0 → cachekit-0.9.0}/src/cachekit/decorators/intent.py +24 -1
- {cachekit-0.8.0 → cachekit-0.9.0}/src/cachekit/decorators/local_wrapper.py +2 -1
- {cachekit-0.8.0 → cachekit-0.9.0}/src/cachekit/decorators/tenant_context.py +2 -1
- {cachekit-0.8.0 → cachekit-0.9.0}/src/cachekit/decorators/wrapper.py +10 -4
- {cachekit-0.8.0 → cachekit-0.9.0}/src/cachekit/invalidation/channel.py +2 -1
- {cachekit-0.8.0 → cachekit-0.9.0}/src/cachekit/key_generator.py +2 -1
- {cachekit-0.8.0 → cachekit-0.9.0}/src/cachekit/l1_cache.py +22 -3
- {cachekit-0.8.0 → cachekit-0.9.0}/src/cachekit/object_cache.py +3 -2
- {cachekit-0.8.0 → cachekit-0.9.0}/src/cachekit/reliability/circuit_breaker.py +2 -1
- {cachekit-0.8.0 → cachekit-0.9.0}/src/cachekit/serializers/__init__.py +2 -0
- cachekit-0.9.0/src/cachekit/serializers/arrow_serializer.py +319 -0
- {cachekit-0.8.0 → cachekit-0.9.0}/src/cachekit/serializers/auto_serializer.py +37 -9
- {cachekit-0.8.0 → cachekit-0.9.0}/src/cachekit/serializers/base.py +39 -1
- {cachekit-0.8.0 → cachekit-0.9.0}/src/cachekit/serializers/orjson_serializer.py +4 -1
- {cachekit-0.8.0 → cachekit-0.9.0}/src/cachekit/serializers/standard_serializer.py +4 -1
- cachekit-0.9.0/src/cachekit/serializers/wrapper.py +139 -0
- cachekit-0.8.0/src/cachekit/serializers/arrow_serializer.py +0 -247
- cachekit-0.8.0/src/cachekit/serializers/wrapper.py +0 -89
- {cachekit-0.8.0 → cachekit-0.9.0}/Cargo.toml +0 -0
- {cachekit-0.8.0 → cachekit-0.9.0}/LICENSE +0 -0
- {cachekit-0.8.0 → cachekit-0.9.0}/rust/Makefile +0 -0
- {cachekit-0.8.0 → cachekit-0.9.0}/rust/TEST_EXPANSION_SUMMARY.md +0 -0
- {cachekit-0.8.0 → cachekit-0.9.0}/rust/src/lib.rs +0 -0
- {cachekit-0.8.0 → cachekit-0.9.0}/rust/src/python_bindings.rs +0 -0
- {cachekit-0.8.0 → cachekit-0.9.0}/rust/supply-chain/audits.toml +0 -0
- {cachekit-0.8.0 → cachekit-0.9.0}/rust/supply-chain/config.toml +0 -0
- {cachekit-0.8.0 → cachekit-0.9.0}/rust/supply-chain/imports.lock +0 -0
- {cachekit-0.8.0 → cachekit-0.9.0}/rust/tsan_suppressions.txt +0 -0
- {cachekit-0.8.0 → cachekit-0.9.0}/src/cachekit/backends/__init__.py +0 -0
- {cachekit-0.8.0 → cachekit-0.9.0}/src/cachekit/backends/base.py +0 -0
- {cachekit-0.8.0 → cachekit-0.9.0}/src/cachekit/backends/base_config.py +0 -0
- {cachekit-0.8.0 → cachekit-0.9.0}/src/cachekit/backends/cachekitio/__init__.py +0 -0
- {cachekit-0.8.0 → cachekit-0.9.0}/src/cachekit/backends/cachekitio/backend.py +0 -0
- {cachekit-0.8.0 → cachekit-0.9.0}/src/cachekit/backends/cachekitio/client.py +0 -0
- {cachekit-0.8.0 → cachekit-0.9.0}/src/cachekit/backends/cachekitio/config.py +0 -0
- {cachekit-0.8.0 → cachekit-0.9.0}/src/cachekit/backends/cachekitio/error_handler.py +0 -0
- {cachekit-0.8.0 → cachekit-0.9.0}/src/cachekit/backends/cachekitio/session.py +0 -0
- {cachekit-0.8.0 → cachekit-0.9.0}/src/cachekit/backends/errors.py +0 -0
- {cachekit-0.8.0 → cachekit-0.9.0}/src/cachekit/backends/file/__init__.py +0 -0
- {cachekit-0.8.0 → cachekit-0.9.0}/src/cachekit/backends/file/backend.py +0 -0
- {cachekit-0.8.0 → cachekit-0.9.0}/src/cachekit/backends/file/config.py +0 -0
- {cachekit-0.8.0 → cachekit-0.9.0}/src/cachekit/backends/memcached/__init__.py +0 -0
- {cachekit-0.8.0 → cachekit-0.9.0}/src/cachekit/backends/memcached/error_handler.py +0 -0
- {cachekit-0.8.0 → cachekit-0.9.0}/src/cachekit/backends/provider.py +0 -0
- {cachekit-0.8.0 → cachekit-0.9.0}/src/cachekit/backends/redis/__init__.py +0 -0
- {cachekit-0.8.0 → cachekit-0.9.0}/src/cachekit/backends/redis/config.py +0 -0
- {cachekit-0.8.0 → cachekit-0.9.0}/src/cachekit/backends/redis/error_handler.py +0 -0
- {cachekit-0.8.0 → cachekit-0.9.0}/src/cachekit/backends/redis/provider.py +0 -0
- {cachekit-0.8.0 → cachekit-0.9.0}/src/cachekit/config/__init__.py +0 -0
- {cachekit-0.8.0 → cachekit-0.9.0}/src/cachekit/config/singleton.py +0 -0
- {cachekit-0.8.0 → cachekit-0.9.0}/src/cachekit/decorators/__init__.py +0 -0
- {cachekit-0.8.0 → cachekit-0.9.0}/src/cachekit/decorators/main.py +0 -0
- {cachekit-0.8.0 → cachekit-0.9.0}/src/cachekit/decorators/orchestrator.py +0 -0
- {cachekit-0.8.0 → cachekit-0.9.0}/src/cachekit/decorators/session.py +0 -0
- {cachekit-0.8.0 → cachekit-0.9.0}/src/cachekit/decorators/stats_context.py +0 -0
- {cachekit-0.8.0 → cachekit-0.9.0}/src/cachekit/decorators/utils/__init__.py +0 -0
- {cachekit-0.8.0 → cachekit-0.9.0}/src/cachekit/di.py +0 -0
- {cachekit-0.8.0 → cachekit-0.9.0}/src/cachekit/hash_utils.py +0 -0
- {cachekit-0.8.0 → cachekit-0.9.0}/src/cachekit/health.py +0 -0
- {cachekit-0.8.0 → cachekit-0.9.0}/src/cachekit/hiredis_compat.py +0 -0
- {cachekit-0.8.0 → cachekit-0.9.0}/src/cachekit/imports.py +0 -0
- {cachekit-0.8.0 → cachekit-0.9.0}/src/cachekit/invalidation/__init__.py +0 -0
- {cachekit-0.8.0 → cachekit-0.9.0}/src/cachekit/invalidation/event.py +0 -0
- {cachekit-0.8.0 → cachekit-0.9.0}/src/cachekit/invalidation/redis_channel.py +0 -0
- {cachekit-0.8.0 → cachekit-0.9.0}/src/cachekit/logging.py +0 -0
- {cachekit-0.8.0 → cachekit-0.9.0}/src/cachekit/monitoring/__init__.py +0 -0
- {cachekit-0.8.0 → cachekit-0.9.0}/src/cachekit/monitoring/correlation_tracking.py +0 -0
- {cachekit-0.8.0 → cachekit-0.9.0}/src/cachekit/monitoring/pool_monitor.py +0 -0
- {cachekit-0.8.0 → cachekit-0.9.0}/src/cachekit/monitoring/protocols.py +0 -0
- {cachekit-0.8.0 → cachekit-0.9.0}/src/cachekit/py.typed +0 -0
- {cachekit-0.8.0 → cachekit-0.9.0}/src/cachekit/reliability/__init__.py +0 -0
- {cachekit-0.8.0 → cachekit-0.9.0}/src/cachekit/reliability/adaptive_timeout.py +0 -0
- {cachekit-0.8.0 → cachekit-0.9.0}/src/cachekit/reliability/async_metrics.py +0 -0
- {cachekit-0.8.0 → cachekit-0.9.0}/src/cachekit/reliability/error_classification.py +0 -0
- {cachekit-0.8.0 → cachekit-0.9.0}/src/cachekit/reliability/load_control.py +0 -0
- {cachekit-0.8.0 → cachekit-0.9.0}/src/cachekit/reliability/metrics_collection.py +0 -0
- {cachekit-0.8.0 → cachekit-0.9.0}/src/cachekit/reliability/profiles.py +0 -0
- {cachekit-0.8.0 → cachekit-0.9.0}/src/cachekit/serializers/encryption_wrapper.py +0 -0
|
@@ -1,12 +1,11 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: cachekit
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.9.0
|
|
4
4
|
Classifier: Development Status :: 3 - Alpha
|
|
5
5
|
Classifier: Intended Audience :: Developers
|
|
6
6
|
Classifier: License :: OSI Approved :: MIT License
|
|
7
7
|
Classifier: Operating System :: OS Independent
|
|
8
8
|
Classifier: Programming Language :: Python :: 3
|
|
9
|
-
Classifier: Programming Language :: Python :: 3.9
|
|
10
9
|
Classifier: Programming Language :: Python :: 3.10
|
|
11
10
|
Classifier: Programming Language :: Python :: 3.11
|
|
12
11
|
Classifier: Programming Language :: Python :: 3.12
|
|
@@ -44,7 +43,7 @@ Home-Page: https://github.com/cachekit-io/cachekit-py
|
|
|
44
43
|
Author-email: cachekit Contributors <noreply@cachekit.io>
|
|
45
44
|
Maintainer-email: cachekit Contributors <noreply@cachekit.io>
|
|
46
45
|
License: MIT
|
|
47
|
-
Requires-Python: >=3.
|
|
46
|
+
Requires-Python: >=3.10
|
|
48
47
|
Description-Content-Type: text/markdown; charset=UTF-8; variant=GFM
|
|
49
48
|
Project-URL: Changelog, https://github.com/cachekit-io/cachekit-py/blob/main/CHANGELOG.md
|
|
50
49
|
Project-URL: Documentation, https://github.com/cachekit-io/cachekit-py#readme
|
|
@@ -119,7 +118,17 @@ Or with [uv][uv-url] (recommended):
|
|
|
119
118
|
uv add cachekit
|
|
120
119
|
```
|
|
121
120
|
|
|
122
|
-
### Setup (
|
|
121
|
+
### Setup (choose a backend)
|
|
122
|
+
|
|
123
|
+
cachekit exposes one decorator API over a pluggable backend abstraction. Pick the
|
|
124
|
+
backend that fits your infrastructure — they're peers behind the same `@cache` API:
|
|
125
|
+
|
|
126
|
+
| Backend | Best for | Select with |
|
|
127
|
+
|---------|----------|-------------|
|
|
128
|
+
| Redis | Self-hosted, full control | `REDIS_URL` / `CACHEKIT_REDIS_URL` |
|
|
129
|
+
| CachekitIO | Managed, zero-ops (alpha) | `CACHEKIT_API_KEY` |
|
|
130
|
+
| Memcached | High-throughput, existing infra | `CACHEKIT_MEMCACHED_SERVERS` |
|
|
131
|
+
| File / L1-only | Local dev, tests, no external deps | `CACHEKIT_FILE_CACHE_DIR` / `backend=None` |
|
|
123
132
|
|
|
124
133
|
```bash
|
|
125
134
|
# Run Redis locally or use your existing infrastructure
|
|
@@ -129,7 +138,7 @@ export REDIS_URL="redis://localhost:6379"
|
|
|
129
138
|
```python
|
|
130
139
|
from cachekit import cache
|
|
131
140
|
|
|
132
|
-
@cache #
|
|
141
|
+
@cache # Auto-detects backend (defaults to Redis at localhost)
|
|
133
142
|
def expensive_api_call(user_id: int):
|
|
134
143
|
return fetch_user_data(user_id)
|
|
135
144
|
```
|
|
@@ -207,26 +216,37 @@ def get_user_profile(user_id: int):
|
|
|
207
216
|
return db.fetch_user(user_id)
|
|
208
217
|
```
|
|
209
218
|
|
|
210
|
-
| Feature | `@cache.minimal` | `@cache.
|
|
211
|
-
|
|
212
|
-
| Circuit Breaker | - | ✅ |
|
|
213
|
-
| Adaptive Timeouts | - | ✅ |
|
|
214
|
-
|
|
|
215
|
-
| Integrity Checking | - | ✅
|
|
216
|
-
| Encryption | - | - |
|
|
217
|
-
|
|
|
218
|
-
|
|
|
219
|
+
| Feature | `@cache.minimal` | `@cache.dev` | `@cache.test` | `@cache.production` | `@cache.secure` |
|
|
220
|
+
|:--------|:----------------:|:------------:|:-------------:|:-------------------:|:---------------:|
|
|
221
|
+
| Circuit Breaker | - | ✅ | - | ✅ | ✅ |
|
|
222
|
+
| Adaptive Timeouts | - | ✅ | - | ✅ | ✅ |
|
|
223
|
+
| Backpressure | ✅ | ✅ | - | ✅ | ✅ |
|
|
224
|
+
| Integrity Checking | - | ✅ | - | ✅ | ✅ 🔒 |
|
|
225
|
+
| Encryption | - | - | - | - | ✅ Required |
|
|
226
|
+
| L1 SWR | - | ✅ | - | ✅ | ✅ |
|
|
227
|
+
| L1 Invalidation | - | - | - | ✅ | ✅ |
|
|
228
|
+
| L1 Namespace Index | - | - | - | ✅ | ✅ |
|
|
229
|
+
| Prometheus Metrics | - | - | - | ✅ | ✅ |
|
|
230
|
+
| Tracing | - | ✅ | - | ✅ | ✅ |
|
|
231
|
+
| Structured Logging | - | ✅ | - | ✅ | ✅ |
|
|
232
|
+
| **Use Case** | High throughput | Local debugging | Deterministic tests | Production reliability | Compliance/security |
|
|
233
|
+
|
|
234
|
+
> 🔒 `@cache.secure` forces `integrity_checking=True` — it cannot be overridden.
|
|
235
|
+
>
|
|
236
|
+
> **`@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.
|
|
219
237
|
|
|
220
238
|
<details>
|
|
221
|
-
<summary><strong>Additional Presets</strong></summary>
|
|
239
|
+
<summary><strong>Additional Presets: <code>@cache.dev</code> and <code>@cache.test</code></strong></summary>
|
|
240
|
+
|
|
241
|
+
See the comparison table above for the exact feature set of each preset.
|
|
222
242
|
|
|
223
243
|
```python
|
|
224
|
-
# Development:
|
|
244
|
+
# Development: verbose logging, integrity checks on, Prometheus off
|
|
225
245
|
@cache.dev
|
|
226
246
|
def debug_expensive_call():
|
|
227
247
|
return complex_computation()
|
|
228
248
|
|
|
229
|
-
# Testing: deterministic, no
|
|
249
|
+
# Testing: deterministic, all protections off (no circuit breaker, no backpressure)
|
|
230
250
|
@cache.test
|
|
231
251
|
def test_cached_function():
|
|
232
252
|
return fixed_test_value()
|
|
@@ -356,12 +376,36 @@ See [SECURITY.md][security-url] for vulnerability reporting and detailed documen
|
|
|
356
376
|
|
|
357
377
|
</details>
|
|
358
378
|
|
|
359
|
-
###
|
|
379
|
+
### Monitoring & Observability
|
|
360
380
|
|
|
361
|
-
- **
|
|
381
|
+
- **Per-function statistics** - `cache_info()` on every decorated function, modelled on `functools.lru_cache`
|
|
382
|
+
- **Prometheus metrics** - Recorded by default (your app owns exposition)
|
|
362
383
|
- **Structured logging** - Context-aware with correlation IDs
|
|
363
384
|
- **Health checks** - Comprehensive status endpoints
|
|
364
|
-
|
|
385
|
+
|
|
386
|
+
Every decorated function exposes `cache_info()`, returning a `CacheInfo` named tuple with
|
|
387
|
+
hit/miss counts, the L1/L2 split, and average backend latency:
|
|
388
|
+
|
|
389
|
+
```python
|
|
390
|
+
@cache()
|
|
391
|
+
def get_score(x):
|
|
392
|
+
return x ** 2
|
|
393
|
+
|
|
394
|
+
get_score(2)
|
|
395
|
+
get_score(2) # served from cache
|
|
396
|
+
|
|
397
|
+
info = get_score.cache_info()
|
|
398
|
+
# CacheInfo has 9 fields: hits, misses, l1_hits, l2_hits, maxsize,
|
|
399
|
+
# currsize, l2_avg_latency_ms, last_operation_at, session_id
|
|
400
|
+
assert info.l1_hits + info.l2_hits == info.hits # every hit is L1 or L2
|
|
401
|
+
```
|
|
402
|
+
|
|
403
|
+
`maxsize` and `currsize` are always `None` (the cache lives in an external store, not a
|
|
404
|
+
bounded in-process dict); they exist only for `lru_cache` API parity. See the
|
|
405
|
+
[API Reference](docs/api-reference.md#per-function-statistics-via-cache_info) for the full
|
|
406
|
+
field reference and a sample stats endpoint, and the
|
|
407
|
+
[Prometheus Metrics guide](docs/features/prometheus-metrics.md) for metric names and
|
|
408
|
+
exposition setup.
|
|
365
409
|
|
|
366
410
|
<details>
|
|
367
411
|
<summary><strong>Thread Safety Details</strong></summary>
|
|
@@ -381,8 +425,10 @@ def expensive_func(x):
|
|
|
381
425
|
with ThreadPoolExecutor(max_workers=10) as executor:
|
|
382
426
|
results = list(executor.map(expensive_func, range(100)))
|
|
383
427
|
|
|
384
|
-
|
|
385
|
-
# CacheInfo(hits
|
|
428
|
+
info = expensive_func.cache_info()
|
|
429
|
+
# CacheInfo(hits=..., misses=..., l1_hits=..., l2_hits=...,
|
|
430
|
+
# maxsize=None, currsize=None, l2_avg_latency_ms=...,
|
|
431
|
+
# last_operation_at=..., session_id=...)
|
|
386
432
|
```
|
|
387
433
|
|
|
388
434
|
</details>
|
|
@@ -459,7 +505,7 @@ See [CONTRIBUTING.md][contributing-url] for full development guidelines.
|
|
|
459
505
|
|
|
460
506
|
| Component | Version |
|
|
461
507
|
|:----------|:--------|
|
|
462
|
-
| Python | 3.
|
|
508
|
+
| Python | 3.10+ |
|
|
463
509
|
|
|
464
510
|
---
|
|
465
511
|
|
|
@@ -65,7 +65,17 @@ Or with [uv][uv-url] (recommended):
|
|
|
65
65
|
uv add cachekit
|
|
66
66
|
```
|
|
67
67
|
|
|
68
|
-
### Setup (
|
|
68
|
+
### Setup (choose a backend)
|
|
69
|
+
|
|
70
|
+
cachekit exposes one decorator API over a pluggable backend abstraction. Pick the
|
|
71
|
+
backend that fits your infrastructure — they're peers behind the same `@cache` API:
|
|
72
|
+
|
|
73
|
+
| Backend | Best for | Select with |
|
|
74
|
+
|---------|----------|-------------|
|
|
75
|
+
| Redis | Self-hosted, full control | `REDIS_URL` / `CACHEKIT_REDIS_URL` |
|
|
76
|
+
| CachekitIO | Managed, zero-ops (alpha) | `CACHEKIT_API_KEY` |
|
|
77
|
+
| Memcached | High-throughput, existing infra | `CACHEKIT_MEMCACHED_SERVERS` |
|
|
78
|
+
| File / L1-only | Local dev, tests, no external deps | `CACHEKIT_FILE_CACHE_DIR` / `backend=None` |
|
|
69
79
|
|
|
70
80
|
```bash
|
|
71
81
|
# Run Redis locally or use your existing infrastructure
|
|
@@ -75,7 +85,7 @@ export REDIS_URL="redis://localhost:6379"
|
|
|
75
85
|
```python
|
|
76
86
|
from cachekit import cache
|
|
77
87
|
|
|
78
|
-
@cache #
|
|
88
|
+
@cache # Auto-detects backend (defaults to Redis at localhost)
|
|
79
89
|
def expensive_api_call(user_id: int):
|
|
80
90
|
return fetch_user_data(user_id)
|
|
81
91
|
```
|
|
@@ -153,26 +163,37 @@ def get_user_profile(user_id: int):
|
|
|
153
163
|
return db.fetch_user(user_id)
|
|
154
164
|
```
|
|
155
165
|
|
|
156
|
-
| Feature | `@cache.minimal` | `@cache.
|
|
157
|
-
|
|
158
|
-
| Circuit Breaker | - | ✅ |
|
|
159
|
-
| Adaptive Timeouts | - | ✅ |
|
|
160
|
-
|
|
|
161
|
-
| Integrity Checking | - | ✅
|
|
162
|
-
| Encryption | - | - |
|
|
163
|
-
|
|
|
164
|
-
|
|
|
166
|
+
| Feature | `@cache.minimal` | `@cache.dev` | `@cache.test` | `@cache.production` | `@cache.secure` |
|
|
167
|
+
|:--------|:----------------:|:------------:|:-------------:|:-------------------:|:---------------:|
|
|
168
|
+
| Circuit Breaker | - | ✅ | - | ✅ | ✅ |
|
|
169
|
+
| Adaptive Timeouts | - | ✅ | - | ✅ | ✅ |
|
|
170
|
+
| Backpressure | ✅ | ✅ | - | ✅ | ✅ |
|
|
171
|
+
| Integrity Checking | - | ✅ | - | ✅ | ✅ 🔒 |
|
|
172
|
+
| Encryption | - | - | - | - | ✅ Required |
|
|
173
|
+
| L1 SWR | - | ✅ | - | ✅ | ✅ |
|
|
174
|
+
| L1 Invalidation | - | - | - | ✅ | ✅ |
|
|
175
|
+
| L1 Namespace Index | - | - | - | ✅ | ✅ |
|
|
176
|
+
| Prometheus Metrics | - | - | - | ✅ | ✅ |
|
|
177
|
+
| Tracing | - | ✅ | - | ✅ | ✅ |
|
|
178
|
+
| Structured Logging | - | ✅ | - | ✅ | ✅ |
|
|
179
|
+
| **Use Case** | High throughput | Local debugging | Deterministic tests | Production reliability | Compliance/security |
|
|
180
|
+
|
|
181
|
+
> 🔒 `@cache.secure` forces `integrity_checking=True` — it cannot be overridden.
|
|
182
|
+
>
|
|
183
|
+
> **`@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.
|
|
165
184
|
|
|
166
185
|
<details>
|
|
167
|
-
<summary><strong>Additional Presets</strong></summary>
|
|
186
|
+
<summary><strong>Additional Presets: <code>@cache.dev</code> and <code>@cache.test</code></strong></summary>
|
|
187
|
+
|
|
188
|
+
See the comparison table above for the exact feature set of each preset.
|
|
168
189
|
|
|
169
190
|
```python
|
|
170
|
-
# Development:
|
|
191
|
+
# Development: verbose logging, integrity checks on, Prometheus off
|
|
171
192
|
@cache.dev
|
|
172
193
|
def debug_expensive_call():
|
|
173
194
|
return complex_computation()
|
|
174
195
|
|
|
175
|
-
# Testing: deterministic, no
|
|
196
|
+
# Testing: deterministic, all protections off (no circuit breaker, no backpressure)
|
|
176
197
|
@cache.test
|
|
177
198
|
def test_cached_function():
|
|
178
199
|
return fixed_test_value()
|
|
@@ -302,12 +323,36 @@ See [SECURITY.md][security-url] for vulnerability reporting and detailed documen
|
|
|
302
323
|
|
|
303
324
|
</details>
|
|
304
325
|
|
|
305
|
-
###
|
|
326
|
+
### Monitoring & Observability
|
|
306
327
|
|
|
307
|
-
- **
|
|
328
|
+
- **Per-function statistics** - `cache_info()` on every decorated function, modelled on `functools.lru_cache`
|
|
329
|
+
- **Prometheus metrics** - Recorded by default (your app owns exposition)
|
|
308
330
|
- **Structured logging** - Context-aware with correlation IDs
|
|
309
331
|
- **Health checks** - Comprehensive status endpoints
|
|
310
|
-
|
|
332
|
+
|
|
333
|
+
Every decorated function exposes `cache_info()`, returning a `CacheInfo` named tuple with
|
|
334
|
+
hit/miss counts, the L1/L2 split, and average backend latency:
|
|
335
|
+
|
|
336
|
+
```python
|
|
337
|
+
@cache()
|
|
338
|
+
def get_score(x):
|
|
339
|
+
return x ** 2
|
|
340
|
+
|
|
341
|
+
get_score(2)
|
|
342
|
+
get_score(2) # served from cache
|
|
343
|
+
|
|
344
|
+
info = get_score.cache_info()
|
|
345
|
+
# CacheInfo has 9 fields: hits, misses, l1_hits, l2_hits, maxsize,
|
|
346
|
+
# currsize, l2_avg_latency_ms, last_operation_at, session_id
|
|
347
|
+
assert info.l1_hits + info.l2_hits == info.hits # every hit is L1 or L2
|
|
348
|
+
```
|
|
349
|
+
|
|
350
|
+
`maxsize` and `currsize` are always `None` (the cache lives in an external store, not a
|
|
351
|
+
bounded in-process dict); they exist only for `lru_cache` API parity. See the
|
|
352
|
+
[API Reference](docs/api-reference.md#per-function-statistics-via-cache_info) for the full
|
|
353
|
+
field reference and a sample stats endpoint, and the
|
|
354
|
+
[Prometheus Metrics guide](docs/features/prometheus-metrics.md) for metric names and
|
|
355
|
+
exposition setup.
|
|
311
356
|
|
|
312
357
|
<details>
|
|
313
358
|
<summary><strong>Thread Safety Details</strong></summary>
|
|
@@ -327,8 +372,10 @@ def expensive_func(x):
|
|
|
327
372
|
with ThreadPoolExecutor(max_workers=10) as executor:
|
|
328
373
|
results = list(executor.map(expensive_func, range(100)))
|
|
329
374
|
|
|
330
|
-
|
|
331
|
-
# CacheInfo(hits
|
|
375
|
+
info = expensive_func.cache_info()
|
|
376
|
+
# CacheInfo(hits=..., misses=..., l1_hits=..., l2_hits=...,
|
|
377
|
+
# maxsize=None, currsize=None, l2_avg_latency_ms=...,
|
|
378
|
+
# last_operation_at=..., session_id=...)
|
|
332
379
|
```
|
|
333
380
|
|
|
334
381
|
</details>
|
|
@@ -405,7 +452,7 @@ See [CONTRIBUTING.md][contributing-url] for full development guidelines.
|
|
|
405
452
|
|
|
406
453
|
| Component | Version |
|
|
407
454
|
|:----------|:--------|
|
|
408
|
-
| Python | 3.
|
|
455
|
+
| Python | 3.10+ |
|
|
409
456
|
|
|
410
457
|
---
|
|
411
458
|
|
|
@@ -4,7 +4,7 @@ build-backend = "maturin"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "cachekit"
|
|
7
|
-
version = "0.
|
|
7
|
+
version = "0.9.0"
|
|
8
8
|
description = "Production-ready Redis caching for Python with intelligent reliability features and Rust-powered performance"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
license = {text = "MIT"}
|
|
@@ -36,7 +36,6 @@ classifiers = [
|
|
|
36
36
|
"License :: OSI Approved :: MIT License",
|
|
37
37
|
"Operating System :: OS Independent",
|
|
38
38
|
"Programming Language :: Python :: 3",
|
|
39
|
-
"Programming Language :: Python :: 3.9",
|
|
40
39
|
"Programming Language :: Python :: 3.10",
|
|
41
40
|
"Programming Language :: Python :: 3.11",
|
|
42
41
|
"Programming Language :: Python :: 3.12",
|
|
@@ -51,7 +50,7 @@ classifiers = [
|
|
|
51
50
|
"Framework :: AsyncIO",
|
|
52
51
|
"Typing :: Typed",
|
|
53
52
|
]
|
|
54
|
-
requires-python = ">=3.
|
|
53
|
+
requires-python = ">=3.10"
|
|
55
54
|
dependencies = [
|
|
56
55
|
# Core Redis functionality
|
|
57
56
|
"redis[hiredis]>=4.0.0",
|
|
@@ -101,7 +100,7 @@ include = ["LICENSE", "README.md"]
|
|
|
101
100
|
# Ruff configuration
|
|
102
101
|
[tool.ruff]
|
|
103
102
|
line-length = 129
|
|
104
|
-
target-version = "
|
|
103
|
+
target-version = "py310"
|
|
105
104
|
|
|
106
105
|
[tool.ruff.lint]
|
|
107
106
|
select = [
|
|
@@ -237,7 +236,11 @@ fuzz = [
|
|
|
237
236
|
# Override vulnerable transitive dependencies
|
|
238
237
|
[tool.uv]
|
|
239
238
|
constraint-dependencies = [
|
|
240
|
-
"urllib3>=2.
|
|
239
|
+
"urllib3>=2.7.0",
|
|
241
240
|
"fonttools>=4.60.2",
|
|
242
241
|
"werkzeug>=3.1.4",
|
|
242
|
+
# pip is a dev-only transitive dep (pip-audit -> pip-api -> pip). 26.1.2 fixes
|
|
243
|
+
# PYSEC-2026-196 (entry-point path traversal), GHSA-58qw-9mgm-455v (tar/zip
|
|
244
|
+
# confusion) and GHSA-jp4c-xjxw-mgf9 (self-update import ordering).
|
|
245
|
+
"pip>=26.1.2",
|
|
243
246
|
]
|
|
@@ -65,7 +65,17 @@ Or with [uv][uv-url] (recommended):
|
|
|
65
65
|
uv add cachekit
|
|
66
66
|
```
|
|
67
67
|
|
|
68
|
-
### Setup (
|
|
68
|
+
### Setup (choose a backend)
|
|
69
|
+
|
|
70
|
+
cachekit exposes one decorator API over a pluggable backend abstraction. Pick the
|
|
71
|
+
backend that fits your infrastructure — they're peers behind the same `@cache` API:
|
|
72
|
+
|
|
73
|
+
| Backend | Best for | Select with |
|
|
74
|
+
|---------|----------|-------------|
|
|
75
|
+
| Redis | Self-hosted, full control | `REDIS_URL` / `CACHEKIT_REDIS_URL` |
|
|
76
|
+
| CachekitIO | Managed, zero-ops (alpha) | `CACHEKIT_API_KEY` |
|
|
77
|
+
| Memcached | High-throughput, existing infra | `CACHEKIT_MEMCACHED_SERVERS` |
|
|
78
|
+
| File / L1-only | Local dev, tests, no external deps | `CACHEKIT_FILE_CACHE_DIR` / `backend=None` |
|
|
69
79
|
|
|
70
80
|
```bash
|
|
71
81
|
# Run Redis locally or use your existing infrastructure
|
|
@@ -75,7 +85,7 @@ export REDIS_URL="redis://localhost:6379"
|
|
|
75
85
|
```python
|
|
76
86
|
from cachekit import cache
|
|
77
87
|
|
|
78
|
-
@cache #
|
|
88
|
+
@cache # Auto-detects backend (defaults to Redis at localhost)
|
|
79
89
|
def expensive_api_call(user_id: int):
|
|
80
90
|
return fetch_user_data(user_id)
|
|
81
91
|
```
|
|
@@ -153,26 +163,37 @@ def get_user_profile(user_id: int):
|
|
|
153
163
|
return db.fetch_user(user_id)
|
|
154
164
|
```
|
|
155
165
|
|
|
156
|
-
| Feature | `@cache.minimal` | `@cache.
|
|
157
|
-
|
|
158
|
-
| Circuit Breaker | - | ✅ |
|
|
159
|
-
| Adaptive Timeouts | - | ✅ |
|
|
160
|
-
|
|
|
161
|
-
| Integrity Checking | - | ✅
|
|
162
|
-
| Encryption | - | - |
|
|
163
|
-
|
|
|
164
|
-
|
|
|
166
|
+
| Feature | `@cache.minimal` | `@cache.dev` | `@cache.test` | `@cache.production` | `@cache.secure` |
|
|
167
|
+
|:--------|:----------------:|:------------:|:-------------:|:-------------------:|:---------------:|
|
|
168
|
+
| Circuit Breaker | - | ✅ | - | ✅ | ✅ |
|
|
169
|
+
| Adaptive Timeouts | - | ✅ | - | ✅ | ✅ |
|
|
170
|
+
| Backpressure | ✅ | ✅ | - | ✅ | ✅ |
|
|
171
|
+
| Integrity Checking | - | ✅ | - | ✅ | ✅ 🔒 |
|
|
172
|
+
| Encryption | - | - | - | - | ✅ Required |
|
|
173
|
+
| L1 SWR | - | ✅ | - | ✅ | ✅ |
|
|
174
|
+
| L1 Invalidation | - | - | - | ✅ | ✅ |
|
|
175
|
+
| L1 Namespace Index | - | - | - | ✅ | ✅ |
|
|
176
|
+
| Prometheus Metrics | - | - | - | ✅ | ✅ |
|
|
177
|
+
| Tracing | - | ✅ | - | ✅ | ✅ |
|
|
178
|
+
| Structured Logging | - | ✅ | - | ✅ | ✅ |
|
|
179
|
+
| **Use Case** | High throughput | Local debugging | Deterministic tests | Production reliability | Compliance/security |
|
|
180
|
+
|
|
181
|
+
> 🔒 `@cache.secure` forces `integrity_checking=True` — it cannot be overridden.
|
|
182
|
+
>
|
|
183
|
+
> **`@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.
|
|
165
184
|
|
|
166
185
|
<details>
|
|
167
|
-
<summary><strong>Additional Presets</strong></summary>
|
|
186
|
+
<summary><strong>Additional Presets: <code>@cache.dev</code> and <code>@cache.test</code></strong></summary>
|
|
187
|
+
|
|
188
|
+
See the comparison table above for the exact feature set of each preset.
|
|
168
189
|
|
|
169
190
|
```python
|
|
170
|
-
# Development:
|
|
191
|
+
# Development: verbose logging, integrity checks on, Prometheus off
|
|
171
192
|
@cache.dev
|
|
172
193
|
def debug_expensive_call():
|
|
173
194
|
return complex_computation()
|
|
174
195
|
|
|
175
|
-
# Testing: deterministic, no
|
|
196
|
+
# Testing: deterministic, all protections off (no circuit breaker, no backpressure)
|
|
176
197
|
@cache.test
|
|
177
198
|
def test_cached_function():
|
|
178
199
|
return fixed_test_value()
|
|
@@ -302,12 +323,36 @@ See [SECURITY.md][security-url] for vulnerability reporting and detailed documen
|
|
|
302
323
|
|
|
303
324
|
</details>
|
|
304
325
|
|
|
305
|
-
###
|
|
326
|
+
### Monitoring & Observability
|
|
306
327
|
|
|
307
|
-
- **
|
|
328
|
+
- **Per-function statistics** - `cache_info()` on every decorated function, modelled on `functools.lru_cache`
|
|
329
|
+
- **Prometheus metrics** - Recorded by default (your app owns exposition)
|
|
308
330
|
- **Structured logging** - Context-aware with correlation IDs
|
|
309
331
|
- **Health checks** - Comprehensive status endpoints
|
|
310
|
-
|
|
332
|
+
|
|
333
|
+
Every decorated function exposes `cache_info()`, returning a `CacheInfo` named tuple with
|
|
334
|
+
hit/miss counts, the L1/L2 split, and average backend latency:
|
|
335
|
+
|
|
336
|
+
```python
|
|
337
|
+
@cache()
|
|
338
|
+
def get_score(x):
|
|
339
|
+
return x ** 2
|
|
340
|
+
|
|
341
|
+
get_score(2)
|
|
342
|
+
get_score(2) # served from cache
|
|
343
|
+
|
|
344
|
+
info = get_score.cache_info()
|
|
345
|
+
# CacheInfo has 9 fields: hits, misses, l1_hits, l2_hits, maxsize,
|
|
346
|
+
# currsize, l2_avg_latency_ms, last_operation_at, session_id
|
|
347
|
+
assert info.l1_hits + info.l2_hits == info.hits # every hit is L1 or L2
|
|
348
|
+
```
|
|
349
|
+
|
|
350
|
+
`maxsize` and `currsize` are always `None` (the cache lives in an external store, not a
|
|
351
|
+
bounded in-process dict); they exist only for `lru_cache` API parity. See the
|
|
352
|
+
[API Reference](docs/api-reference.md#per-function-statistics-via-cache_info) for the full
|
|
353
|
+
field reference and a sample stats endpoint, and the
|
|
354
|
+
[Prometheus Metrics guide](docs/features/prometheus-metrics.md) for metric names and
|
|
355
|
+
exposition setup.
|
|
311
356
|
|
|
312
357
|
<details>
|
|
313
358
|
<summary><strong>Thread Safety Details</strong></summary>
|
|
@@ -327,8 +372,10 @@ def expensive_func(x):
|
|
|
327
372
|
with ThreadPoolExecutor(max_workers=10) as executor:
|
|
328
373
|
results = list(executor.map(expensive_func, range(100)))
|
|
329
374
|
|
|
330
|
-
|
|
331
|
-
# CacheInfo(hits
|
|
375
|
+
info = expensive_func.cache_info()
|
|
376
|
+
# CacheInfo(hits=..., misses=..., l1_hits=..., l2_hits=...,
|
|
377
|
+
# maxsize=None, currsize=None, l2_avg_latency_ms=...,
|
|
378
|
+
# last_operation_at=..., session_id=...)
|
|
332
379
|
```
|
|
333
380
|
|
|
334
381
|
</details>
|
|
@@ -405,7 +452,7 @@ See [CONTRIBUTING.md][contributing-url] for full development guidelines.
|
|
|
405
452
|
|
|
406
453
|
| Component | Version |
|
|
407
454
|
|:----------|:--------|
|
|
408
|
-
| Python | 3.
|
|
455
|
+
| Python | 3.10+ |
|
|
409
456
|
|
|
410
457
|
---
|
|
411
458
|
|
|
@@ -68,9 +68,10 @@ Example Usage:
|
|
|
68
68
|
```
|
|
69
69
|
"""
|
|
70
70
|
|
|
71
|
-
__version__ = "0.
|
|
71
|
+
__version__ = "0.9.0"
|
|
72
72
|
|
|
73
|
-
from
|
|
73
|
+
from collections.abc import Callable
|
|
74
|
+
from typing import Any, TypeVar
|
|
74
75
|
|
|
75
76
|
# Configure hiredis compatibility BEFORE any Redis imports
|
|
76
77
|
# This prevents GIL warnings in Python 3.13+ free-threading mode
|
|
@@ -9,6 +9,7 @@ from __future__ import annotations
|
|
|
9
9
|
import time
|
|
10
10
|
from typing import Any, Optional
|
|
11
11
|
|
|
12
|
+
from cachekit.backends.errors import BackendError, BackendErrorType
|
|
12
13
|
from cachekit.backends.memcached.config import MAX_MEMCACHED_TTL, MemcachedBackendConfig
|
|
13
14
|
from cachekit.backends.memcached.error_handler import classify_memcached_error
|
|
14
15
|
|
|
@@ -108,12 +109,32 @@ class MemcachedBackend:
|
|
|
108
109
|
Raises:
|
|
109
110
|
BackendError: If Memcached operation fails.
|
|
110
111
|
"""
|
|
112
|
+
# Guard client-side against oversized items. Memcached rejects items over its
|
|
113
|
+
# item-size limit (default 1 MiB), but with noreply that rejection is never read —
|
|
114
|
+
# the call appears to succeed and the entry is silently never cached. Fail loudly
|
|
115
|
+
# instead, so the caller can compress, shard, or switch backends.
|
|
116
|
+
max_size = self._config.max_item_size_bytes
|
|
117
|
+
if max_size and len(value) > max_size:
|
|
118
|
+
raise BackendError(
|
|
119
|
+
message=(
|
|
120
|
+
f"Value for key {key!r} is {len(value)} bytes, which exceeds the Memcached "
|
|
121
|
+
f"max item size of {max_size} bytes. Memcached cannot store it. Enable "
|
|
122
|
+
f"compression, use a larger-payload backend (Redis/SaaS/File), or raise both "
|
|
123
|
+
f"the server's -I limit and CACHEKIT_MEMCACHED_MAX_ITEM_SIZE_BYTES."
|
|
124
|
+
),
|
|
125
|
+
error_type=BackendErrorType.PERMANENT,
|
|
126
|
+
operation="set",
|
|
127
|
+
key=key,
|
|
128
|
+
)
|
|
129
|
+
|
|
111
130
|
expire = 0
|
|
112
131
|
if ttl is not None and ttl > 0:
|
|
113
132
|
expire = min(ttl, MAX_MEMCACHED_TTL)
|
|
114
133
|
|
|
115
134
|
try:
|
|
116
|
-
|
|
135
|
+
# noreply=False so an oversized/error reply from the server is read and surfaced
|
|
136
|
+
# rather than silently swallowed (HashClient defaults to noreply=True).
|
|
137
|
+
self._client.set(self._prefixed_key(key), value, expire=expire, noreply=False)
|
|
117
138
|
except Exception as exc:
|
|
118
139
|
raise classify_memcached_error(exc, operation="set", key=key) from exc
|
|
119
140
|
|
|
@@ -85,6 +85,16 @@ class MemcachedBackendConfig(BaseBackendConfig):
|
|
|
85
85
|
default="",
|
|
86
86
|
description="Optional prefix for all cache keys",
|
|
87
87
|
)
|
|
88
|
+
max_item_size_bytes: int = Field(
|
|
89
|
+
default=1024 * 1024,
|
|
90
|
+
ge=0,
|
|
91
|
+
description=(
|
|
92
|
+
"Reject values larger than this BEFORE sending to Memcached (0 disables the check). "
|
|
93
|
+
"Memcached's default item-size limit is 1 MiB (server -I flag); oversized items are "
|
|
94
|
+
"rejected by the server, and with noreply that rejection is silent — so cachekit "
|
|
95
|
+
"guards client-side and fails loudly. Raise this only if the server's -I is raised too."
|
|
96
|
+
),
|
|
97
|
+
)
|
|
88
98
|
|
|
89
99
|
@field_validator("servers", mode="after")
|
|
90
100
|
@classmethod
|
|
@@ -107,15 +107,11 @@ class RedisBackend:
|
|
|
107
107
|
try:
|
|
108
108
|
client = self._get_client()
|
|
109
109
|
value = client.get(key)
|
|
110
|
-
#
|
|
111
|
-
#
|
|
112
|
-
#
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
return value.encode("utf-8")
|
|
116
|
-
if isinstance(value, bytes):
|
|
117
|
-
return value
|
|
118
|
-
return None
|
|
110
|
+
# The pool is configured with decode_responses=False (see redis/client.py),
|
|
111
|
+
# so Redis returns raw bytes, or None for a missing key. Cached payloads are
|
|
112
|
+
# binary (LZ4/Arrow/AES ciphertext) and must never be UTF-8 decoded. The
|
|
113
|
+
# isinstance check enforces the bytes|None contract without any str coercion.
|
|
114
|
+
return value if isinstance(value, bytes) else None
|
|
119
115
|
except Exception as e:
|
|
120
116
|
raise BackendError(
|
|
121
117
|
message=f"Redis GET failed: {e}",
|
|
@@ -78,7 +78,7 @@ def get_redis_client() -> redis.Redis:
|
|
|
78
78
|
# Use URL-based connection
|
|
79
79
|
_pool_instance = redis.ConnectionPool.from_url(
|
|
80
80
|
redis_config.redis_url,
|
|
81
|
-
decode_responses=
|
|
81
|
+
decode_responses=False, # cached payloads are raw bytes (LZ4/Arrow/AES) — never UTF-8 decode
|
|
82
82
|
max_connections=redis_config.connection_pool_size,
|
|
83
83
|
)
|
|
84
84
|
|
|
@@ -120,7 +120,7 @@ async def get_async_redis_client() -> redis_async.Redis:
|
|
|
120
120
|
# Use URL-based connection
|
|
121
121
|
_async_pool_instance = redis_async.ConnectionPool.from_url(
|
|
122
122
|
redis_config.redis_url,
|
|
123
|
-
decode_responses=
|
|
123
|
+
decode_responses=False, # cached payloads are raw bytes (LZ4/Arrow/AES) — never UTF-8 decode
|
|
124
124
|
max_connections=redis_config.connection_pool_size,
|
|
125
125
|
)
|
|
126
126
|
|