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.
Files changed (94) hide show
  1. {cachekit-0.8.0 → cachekit-0.9.0}/Cargo.lock +1 -1
  2. {cachekit-0.8.0 → cachekit-0.9.0}/PKG-INFO +69 -23
  3. {cachekit-0.8.0 → cachekit-0.9.0}/README.md +67 -20
  4. {cachekit-0.8.0 → cachekit-0.9.0}/pyproject.toml +8 -5
  5. {cachekit-0.8.0 → cachekit-0.9.0}/rust/Cargo.toml +1 -1
  6. {cachekit-0.8.0 → cachekit-0.9.0}/rust/README.md +67 -20
  7. {cachekit-0.8.0 → cachekit-0.9.0}/src/cachekit/__init__.py +3 -2
  8. {cachekit-0.8.0 → cachekit-0.9.0}/src/cachekit/backends/memcached/backend.py +22 -1
  9. {cachekit-0.8.0 → cachekit-0.9.0}/src/cachekit/backends/memcached/config.py +10 -0
  10. {cachekit-0.8.0 → cachekit-0.9.0}/src/cachekit/backends/redis/backend.py +5 -9
  11. {cachekit-0.8.0 → cachekit-0.9.0}/src/cachekit/backends/redis/client.py +2 -2
  12. {cachekit-0.8.0 → cachekit-0.9.0}/src/cachekit/cache_handler.py +85 -35
  13. {cachekit-0.8.0 → cachekit-0.9.0}/src/cachekit/config/decorator.py +8 -3
  14. {cachekit-0.8.0 → cachekit-0.9.0}/src/cachekit/config/nested.py +46 -9
  15. {cachekit-0.8.0 → cachekit-0.9.0}/src/cachekit/config/settings.py +10 -1
  16. {cachekit-0.8.0 → cachekit-0.9.0}/src/cachekit/config/validation.py +3 -2
  17. {cachekit-0.8.0 → cachekit-0.9.0}/src/cachekit/decorators/intent.py +24 -1
  18. {cachekit-0.8.0 → cachekit-0.9.0}/src/cachekit/decorators/local_wrapper.py +2 -1
  19. {cachekit-0.8.0 → cachekit-0.9.0}/src/cachekit/decorators/tenant_context.py +2 -1
  20. {cachekit-0.8.0 → cachekit-0.9.0}/src/cachekit/decorators/wrapper.py +10 -4
  21. {cachekit-0.8.0 → cachekit-0.9.0}/src/cachekit/invalidation/channel.py +2 -1
  22. {cachekit-0.8.0 → cachekit-0.9.0}/src/cachekit/key_generator.py +2 -1
  23. {cachekit-0.8.0 → cachekit-0.9.0}/src/cachekit/l1_cache.py +22 -3
  24. {cachekit-0.8.0 → cachekit-0.9.0}/src/cachekit/object_cache.py +3 -2
  25. {cachekit-0.8.0 → cachekit-0.9.0}/src/cachekit/reliability/circuit_breaker.py +2 -1
  26. {cachekit-0.8.0 → cachekit-0.9.0}/src/cachekit/serializers/__init__.py +2 -0
  27. cachekit-0.9.0/src/cachekit/serializers/arrow_serializer.py +319 -0
  28. {cachekit-0.8.0 → cachekit-0.9.0}/src/cachekit/serializers/auto_serializer.py +37 -9
  29. {cachekit-0.8.0 → cachekit-0.9.0}/src/cachekit/serializers/base.py +39 -1
  30. {cachekit-0.8.0 → cachekit-0.9.0}/src/cachekit/serializers/orjson_serializer.py +4 -1
  31. {cachekit-0.8.0 → cachekit-0.9.0}/src/cachekit/serializers/standard_serializer.py +4 -1
  32. cachekit-0.9.0/src/cachekit/serializers/wrapper.py +139 -0
  33. cachekit-0.8.0/src/cachekit/serializers/arrow_serializer.py +0 -247
  34. cachekit-0.8.0/src/cachekit/serializers/wrapper.py +0 -89
  35. {cachekit-0.8.0 → cachekit-0.9.0}/Cargo.toml +0 -0
  36. {cachekit-0.8.0 → cachekit-0.9.0}/LICENSE +0 -0
  37. {cachekit-0.8.0 → cachekit-0.9.0}/rust/Makefile +0 -0
  38. {cachekit-0.8.0 → cachekit-0.9.0}/rust/TEST_EXPANSION_SUMMARY.md +0 -0
  39. {cachekit-0.8.0 → cachekit-0.9.0}/rust/src/lib.rs +0 -0
  40. {cachekit-0.8.0 → cachekit-0.9.0}/rust/src/python_bindings.rs +0 -0
  41. {cachekit-0.8.0 → cachekit-0.9.0}/rust/supply-chain/audits.toml +0 -0
  42. {cachekit-0.8.0 → cachekit-0.9.0}/rust/supply-chain/config.toml +0 -0
  43. {cachekit-0.8.0 → cachekit-0.9.0}/rust/supply-chain/imports.lock +0 -0
  44. {cachekit-0.8.0 → cachekit-0.9.0}/rust/tsan_suppressions.txt +0 -0
  45. {cachekit-0.8.0 → cachekit-0.9.0}/src/cachekit/backends/__init__.py +0 -0
  46. {cachekit-0.8.0 → cachekit-0.9.0}/src/cachekit/backends/base.py +0 -0
  47. {cachekit-0.8.0 → cachekit-0.9.0}/src/cachekit/backends/base_config.py +0 -0
  48. {cachekit-0.8.0 → cachekit-0.9.0}/src/cachekit/backends/cachekitio/__init__.py +0 -0
  49. {cachekit-0.8.0 → cachekit-0.9.0}/src/cachekit/backends/cachekitio/backend.py +0 -0
  50. {cachekit-0.8.0 → cachekit-0.9.0}/src/cachekit/backends/cachekitio/client.py +0 -0
  51. {cachekit-0.8.0 → cachekit-0.9.0}/src/cachekit/backends/cachekitio/config.py +0 -0
  52. {cachekit-0.8.0 → cachekit-0.9.0}/src/cachekit/backends/cachekitio/error_handler.py +0 -0
  53. {cachekit-0.8.0 → cachekit-0.9.0}/src/cachekit/backends/cachekitio/session.py +0 -0
  54. {cachekit-0.8.0 → cachekit-0.9.0}/src/cachekit/backends/errors.py +0 -0
  55. {cachekit-0.8.0 → cachekit-0.9.0}/src/cachekit/backends/file/__init__.py +0 -0
  56. {cachekit-0.8.0 → cachekit-0.9.0}/src/cachekit/backends/file/backend.py +0 -0
  57. {cachekit-0.8.0 → cachekit-0.9.0}/src/cachekit/backends/file/config.py +0 -0
  58. {cachekit-0.8.0 → cachekit-0.9.0}/src/cachekit/backends/memcached/__init__.py +0 -0
  59. {cachekit-0.8.0 → cachekit-0.9.0}/src/cachekit/backends/memcached/error_handler.py +0 -0
  60. {cachekit-0.8.0 → cachekit-0.9.0}/src/cachekit/backends/provider.py +0 -0
  61. {cachekit-0.8.0 → cachekit-0.9.0}/src/cachekit/backends/redis/__init__.py +0 -0
  62. {cachekit-0.8.0 → cachekit-0.9.0}/src/cachekit/backends/redis/config.py +0 -0
  63. {cachekit-0.8.0 → cachekit-0.9.0}/src/cachekit/backends/redis/error_handler.py +0 -0
  64. {cachekit-0.8.0 → cachekit-0.9.0}/src/cachekit/backends/redis/provider.py +0 -0
  65. {cachekit-0.8.0 → cachekit-0.9.0}/src/cachekit/config/__init__.py +0 -0
  66. {cachekit-0.8.0 → cachekit-0.9.0}/src/cachekit/config/singleton.py +0 -0
  67. {cachekit-0.8.0 → cachekit-0.9.0}/src/cachekit/decorators/__init__.py +0 -0
  68. {cachekit-0.8.0 → cachekit-0.9.0}/src/cachekit/decorators/main.py +0 -0
  69. {cachekit-0.8.0 → cachekit-0.9.0}/src/cachekit/decorators/orchestrator.py +0 -0
  70. {cachekit-0.8.0 → cachekit-0.9.0}/src/cachekit/decorators/session.py +0 -0
  71. {cachekit-0.8.0 → cachekit-0.9.0}/src/cachekit/decorators/stats_context.py +0 -0
  72. {cachekit-0.8.0 → cachekit-0.9.0}/src/cachekit/decorators/utils/__init__.py +0 -0
  73. {cachekit-0.8.0 → cachekit-0.9.0}/src/cachekit/di.py +0 -0
  74. {cachekit-0.8.0 → cachekit-0.9.0}/src/cachekit/hash_utils.py +0 -0
  75. {cachekit-0.8.0 → cachekit-0.9.0}/src/cachekit/health.py +0 -0
  76. {cachekit-0.8.0 → cachekit-0.9.0}/src/cachekit/hiredis_compat.py +0 -0
  77. {cachekit-0.8.0 → cachekit-0.9.0}/src/cachekit/imports.py +0 -0
  78. {cachekit-0.8.0 → cachekit-0.9.0}/src/cachekit/invalidation/__init__.py +0 -0
  79. {cachekit-0.8.0 → cachekit-0.9.0}/src/cachekit/invalidation/event.py +0 -0
  80. {cachekit-0.8.0 → cachekit-0.9.0}/src/cachekit/invalidation/redis_channel.py +0 -0
  81. {cachekit-0.8.0 → cachekit-0.9.0}/src/cachekit/logging.py +0 -0
  82. {cachekit-0.8.0 → cachekit-0.9.0}/src/cachekit/monitoring/__init__.py +0 -0
  83. {cachekit-0.8.0 → cachekit-0.9.0}/src/cachekit/monitoring/correlation_tracking.py +0 -0
  84. {cachekit-0.8.0 → cachekit-0.9.0}/src/cachekit/monitoring/pool_monitor.py +0 -0
  85. {cachekit-0.8.0 → cachekit-0.9.0}/src/cachekit/monitoring/protocols.py +0 -0
  86. {cachekit-0.8.0 → cachekit-0.9.0}/src/cachekit/py.typed +0 -0
  87. {cachekit-0.8.0 → cachekit-0.9.0}/src/cachekit/reliability/__init__.py +0 -0
  88. {cachekit-0.8.0 → cachekit-0.9.0}/src/cachekit/reliability/adaptive_timeout.py +0 -0
  89. {cachekit-0.8.0 → cachekit-0.9.0}/src/cachekit/reliability/async_metrics.py +0 -0
  90. {cachekit-0.8.0 → cachekit-0.9.0}/src/cachekit/reliability/error_classification.py +0 -0
  91. {cachekit-0.8.0 → cachekit-0.9.0}/src/cachekit/reliability/load_control.py +0 -0
  92. {cachekit-0.8.0 → cachekit-0.9.0}/src/cachekit/reliability/metrics_collection.py +0 -0
  93. {cachekit-0.8.0 → cachekit-0.9.0}/src/cachekit/reliability/profiles.py +0 -0
  94. {cachekit-0.8.0 → cachekit-0.9.0}/src/cachekit/serializers/encryption_wrapper.py +0 -0
@@ -271,7 +271,7 @@ dependencies = [
271
271
 
272
272
  [[package]]
273
273
  name = "cachekit-rs"
274
- version = "0.8.0"
274
+ version = "0.9.0"
275
275
  dependencies = [
276
276
  "cachekit-core",
277
277
  "criterion",
@@ -1,12 +1,11 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: cachekit
3
- Version: 0.8.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.9
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 (Redis recommended default)
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 # Uses Redis backend by default
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.production` | `@cache.secure` | `@cache.io()` | `@cache.local()` |
211
- |:--------|:----------------:|:-------------------:|:---------------:|:-------------:|:----------------:|
212
- | Circuit Breaker | - | ✅ | | ✅ | - |
213
- | Adaptive Timeouts | - | ✅ | | ✅ | - |
214
- | Monitoring | - | ✅ Full | Full | ✅ Full | ✅ Basic |
215
- | Integrity Checking | - | ✅ Enabled | ✅ Enforced | ✅ Enabled | - |
216
- | Encryption | - | - | Required | - | - |
217
- | Backend | Redis | Redis | Redis | CachekitIO SaaS | In-process |
218
- | **Use Case** | High throughput | Production reliability | Compliance/security | Managed cloud | Opaque objects |
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: debugging with verbose output
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 randomness
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
- ### Built-in Monitoring
379
+ ### Monitoring & Observability
360
380
 
361
- - **Prometheus metrics** - Production-ready observability
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
- - **Performance tracking** - Built-in latency monitoring
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
- print(expensive_func.cache_info())
385
- # CacheInfo(hits=90, misses=10, maxsize=None, currsize=10)
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.9+ |
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 (Redis recommended default)
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 # Uses Redis backend by default
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.production` | `@cache.secure` | `@cache.io()` | `@cache.local()` |
157
- |:--------|:----------------:|:-------------------:|:---------------:|:-------------:|:----------------:|
158
- | Circuit Breaker | - | ✅ | | ✅ | - |
159
- | Adaptive Timeouts | - | ✅ | | ✅ | - |
160
- | Monitoring | - | ✅ Full | Full | ✅ Full | ✅ Basic |
161
- | Integrity Checking | - | ✅ Enabled | ✅ Enforced | ✅ Enabled | - |
162
- | Encryption | - | - | Required | - | - |
163
- | Backend | Redis | Redis | Redis | CachekitIO SaaS | In-process |
164
- | **Use Case** | High throughput | Production reliability | Compliance/security | Managed cloud | Opaque objects |
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: debugging with verbose output
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 randomness
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
- ### Built-in Monitoring
326
+ ### Monitoring & Observability
306
327
 
307
- - **Prometheus metrics** - Production-ready observability
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
- - **Performance tracking** - Built-in latency monitoring
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
- print(expensive_func.cache_info())
331
- # CacheInfo(hits=90, misses=10, maxsize=None, currsize=10)
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.9+ |
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.8.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.9"
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 = "py39"
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.6.0",
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
  ]
@@ -1,6 +1,6 @@
1
1
  [package]
2
2
  name = "cachekit-rs"
3
- version = "0.8.0"
3
+ version = "0.9.0"
4
4
  edition = "2021"
5
5
  authors = ["cachekit Contributors"]
6
6
  description = "High-performance storage engine for caching with compression and encryption"
@@ -65,7 +65,17 @@ Or with [uv][uv-url] (recommended):
65
65
  uv add cachekit
66
66
  ```
67
67
 
68
- ### Setup (Redis recommended default)
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 # Uses Redis backend by default
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.production` | `@cache.secure` | `@cache.io()` | `@cache.local()` |
157
- |:--------|:----------------:|:-------------------:|:---------------:|:-------------:|:----------------:|
158
- | Circuit Breaker | - | ✅ | | ✅ | - |
159
- | Adaptive Timeouts | - | ✅ | | ✅ | - |
160
- | Monitoring | - | ✅ Full | Full | ✅ Full | ✅ Basic |
161
- | Integrity Checking | - | ✅ Enabled | ✅ Enforced | ✅ Enabled | - |
162
- | Encryption | - | - | Required | - | - |
163
- | Backend | Redis | Redis | Redis | CachekitIO SaaS | In-process |
164
- | **Use Case** | High throughput | Production reliability | Compliance/security | Managed cloud | Opaque objects |
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: debugging with verbose output
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 randomness
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
- ### Built-in Monitoring
326
+ ### Monitoring & Observability
306
327
 
307
- - **Prometheus metrics** - Production-ready observability
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
- - **Performance tracking** - Built-in latency monitoring
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
- print(expensive_func.cache_info())
331
- # CacheInfo(hits=90, misses=10, maxsize=None, currsize=10)
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.9+ |
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.8.0"
71
+ __version__ = "0.9.0"
72
72
 
73
- from typing import Any, Callable, TypeVar
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
- self._client.set(self._prefixed_key(key), value, expire=expire)
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
- # Redis client with decode_responses=True returns str, need bytes
111
- # But get() with binary data returns bytes if decode fails
112
- # For safety, encode if we got str
113
- if value is not None:
114
- if isinstance(value, str):
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=True,
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=True,
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