cachekit 0.11.1__tar.gz → 0.15.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 (97) hide show
  1. {cachekit-0.11.1 → cachekit-0.15.0}/Cargo.lock +5 -5
  2. {cachekit-0.11.1 → cachekit-0.15.0}/PKG-INFO +5 -9
  3. {cachekit-0.11.1 → cachekit-0.15.0}/README.md +4 -7
  4. {cachekit-0.11.1 → cachekit-0.15.0}/pyproject.toml +2 -3
  5. {cachekit-0.11.1 → cachekit-0.15.0}/rust/Cargo.toml +2 -2
  6. {cachekit-0.11.1 → cachekit-0.15.0}/rust/README.md +4 -7
  7. {cachekit-0.11.1 → cachekit-0.15.0}/rust/src/lib.rs +7 -2
  8. {cachekit-0.11.1 → cachekit-0.15.0}/rust/src/python_bindings.rs +56 -11
  9. {cachekit-0.11.1 → cachekit-0.15.0}/src/cachekit/__init__.py +17 -4
  10. {cachekit-0.11.1 → cachekit-0.15.0}/src/cachekit/backends/base.py +6 -4
  11. {cachekit-0.11.1 → cachekit-0.15.0}/src/cachekit/backends/cachekitio/backend.py +64 -11
  12. cachekit-0.15.0/src/cachekit/backends/cachekitio/session.py +59 -0
  13. {cachekit-0.11.1 → cachekit-0.15.0}/src/cachekit/backends/file/backend.py +131 -0
  14. {cachekit-0.11.1 → cachekit-0.15.0}/src/cachekit/backends/memcached/backend.py +43 -0
  15. {cachekit-0.11.1 → cachekit-0.15.0}/src/cachekit/backends/provider.py +49 -0
  16. {cachekit-0.11.1 → cachekit-0.15.0}/src/cachekit/backends/redis/backend.py +40 -15
  17. {cachekit-0.11.1 → cachekit-0.15.0}/src/cachekit/backends/redis/client.py +64 -14
  18. {cachekit-0.11.1 → cachekit-0.15.0}/src/cachekit/backends/redis/config.py +13 -0
  19. {cachekit-0.11.1 → cachekit-0.15.0}/src/cachekit/backends/redis/provider.py +17 -6
  20. {cachekit-0.11.1 → cachekit-0.15.0}/src/cachekit/cache_handler.py +560 -54
  21. {cachekit-0.11.1 → cachekit-0.15.0}/src/cachekit/config/__init__.py +0 -2
  22. {cachekit-0.11.1 → cachekit-0.15.0}/src/cachekit/config/decorator.py +48 -29
  23. {cachekit-0.11.1 → cachekit-0.15.0}/src/cachekit/config/nested.py +26 -64
  24. {cachekit-0.11.1 → cachekit-0.15.0}/src/cachekit/config/settings.py +15 -29
  25. {cachekit-0.11.1 → cachekit-0.15.0}/src/cachekit/config/singleton.py +2 -2
  26. {cachekit-0.11.1 → cachekit-0.15.0}/src/cachekit/decorators/intent.py +13 -5
  27. {cachekit-0.11.1 → cachekit-0.15.0}/src/cachekit/decorators/orchestrator.py +0 -43
  28. cachekit-0.15.0/src/cachekit/decorators/session.py +133 -0
  29. {cachekit-0.11.1 → cachekit-0.15.0}/src/cachekit/decorators/wrapper.py +756 -120
  30. cachekit-0.15.0/src/cachekit/interop.py +496 -0
  31. {cachekit-0.11.1 → cachekit-0.15.0}/src/cachekit/l1_cache.py +37 -12
  32. cachekit-0.15.0/src/cachekit/object_cache.py +471 -0
  33. {cachekit-0.11.1 → cachekit-0.15.0}/src/cachekit/reliability/__init__.py +1 -4
  34. {cachekit-0.11.1 → cachekit-0.15.0}/src/cachekit/reliability/circuit_breaker.py +37 -83
  35. {cachekit-0.11.1 → cachekit-0.15.0}/src/cachekit/reliability/metrics_collection.py +1 -2
  36. {cachekit-0.11.1 → cachekit-0.15.0}/src/cachekit/reliability/profiles.py +0 -10
  37. {cachekit-0.11.1 → cachekit-0.15.0}/src/cachekit/serializers/__init__.py +5 -1
  38. {cachekit-0.11.1 → cachekit-0.15.0}/src/cachekit/serializers/auto_serializer.py +29 -8
  39. {cachekit-0.11.1 → cachekit-0.15.0}/src/cachekit/serializers/base.py +16 -0
  40. {cachekit-0.11.1 → cachekit-0.15.0}/src/cachekit/serializers/encryption_wrapper.py +71 -20
  41. cachekit-0.15.0/src/cachekit/serializers/interop_serializer.py +64 -0
  42. cachekit-0.11.1/src/cachekit/backends/cachekitio/session.py +0 -164
  43. cachekit-0.11.1/src/cachekit/decorators/session.py +0 -42
  44. cachekit-0.11.1/src/cachekit/object_cache.py +0 -199
  45. cachekit-0.11.1/src/cachekit/reliability/adaptive_timeout.py +0 -424
  46. {cachekit-0.11.1 → cachekit-0.15.0}/Cargo.toml +0 -0
  47. {cachekit-0.11.1 → cachekit-0.15.0}/LICENSE +0 -0
  48. {cachekit-0.11.1 → cachekit-0.15.0}/rust/Makefile +0 -0
  49. {cachekit-0.11.1 → cachekit-0.15.0}/rust/TEST_EXPANSION_SUMMARY.md +0 -0
  50. {cachekit-0.11.1 → cachekit-0.15.0}/rust/supply-chain/audits.toml +0 -0
  51. {cachekit-0.11.1 → cachekit-0.15.0}/rust/supply-chain/config.toml +0 -0
  52. {cachekit-0.11.1 → cachekit-0.15.0}/rust/supply-chain/imports.lock +0 -0
  53. {cachekit-0.11.1 → cachekit-0.15.0}/rust/tsan_suppressions.txt +0 -0
  54. {cachekit-0.11.1 → cachekit-0.15.0}/src/cachekit/backends/__init__.py +0 -0
  55. {cachekit-0.11.1 → cachekit-0.15.0}/src/cachekit/backends/base_config.py +0 -0
  56. {cachekit-0.11.1 → cachekit-0.15.0}/src/cachekit/backends/cachekitio/__init__.py +0 -0
  57. {cachekit-0.11.1 → cachekit-0.15.0}/src/cachekit/backends/cachekitio/client.py +0 -0
  58. {cachekit-0.11.1 → cachekit-0.15.0}/src/cachekit/backends/cachekitio/config.py +0 -0
  59. {cachekit-0.11.1 → cachekit-0.15.0}/src/cachekit/backends/cachekitio/error_handler.py +0 -0
  60. {cachekit-0.11.1 → cachekit-0.15.0}/src/cachekit/backends/errors.py +0 -0
  61. {cachekit-0.11.1 → cachekit-0.15.0}/src/cachekit/backends/file/__init__.py +0 -0
  62. {cachekit-0.11.1 → cachekit-0.15.0}/src/cachekit/backends/file/config.py +0 -0
  63. {cachekit-0.11.1 → cachekit-0.15.0}/src/cachekit/backends/memcached/__init__.py +0 -0
  64. {cachekit-0.11.1 → cachekit-0.15.0}/src/cachekit/backends/memcached/config.py +0 -0
  65. {cachekit-0.11.1 → cachekit-0.15.0}/src/cachekit/backends/memcached/error_handler.py +0 -0
  66. {cachekit-0.11.1 → cachekit-0.15.0}/src/cachekit/backends/redis/__init__.py +0 -0
  67. {cachekit-0.11.1 → cachekit-0.15.0}/src/cachekit/backends/redis/error_handler.py +0 -0
  68. {cachekit-0.11.1 → cachekit-0.15.0}/src/cachekit/config/validation.py +0 -0
  69. {cachekit-0.11.1 → cachekit-0.15.0}/src/cachekit/decorators/__init__.py +0 -0
  70. {cachekit-0.11.1 → cachekit-0.15.0}/src/cachekit/decorators/local_wrapper.py +0 -0
  71. {cachekit-0.11.1 → cachekit-0.15.0}/src/cachekit/decorators/main.py +0 -0
  72. {cachekit-0.11.1 → cachekit-0.15.0}/src/cachekit/decorators/stats_context.py +0 -0
  73. {cachekit-0.11.1 → cachekit-0.15.0}/src/cachekit/decorators/tenant_context.py +0 -0
  74. {cachekit-0.11.1 → cachekit-0.15.0}/src/cachekit/decorators/utils/__init__.py +0 -0
  75. {cachekit-0.11.1 → cachekit-0.15.0}/src/cachekit/di.py +0 -0
  76. {cachekit-0.11.1 → cachekit-0.15.0}/src/cachekit/hash_utils.py +0 -0
  77. {cachekit-0.11.1 → cachekit-0.15.0}/src/cachekit/health.py +0 -0
  78. {cachekit-0.11.1 → cachekit-0.15.0}/src/cachekit/hiredis_compat.py +0 -0
  79. {cachekit-0.11.1 → cachekit-0.15.0}/src/cachekit/imports.py +0 -0
  80. {cachekit-0.11.1 → cachekit-0.15.0}/src/cachekit/invalidation/__init__.py +0 -0
  81. {cachekit-0.11.1 → cachekit-0.15.0}/src/cachekit/invalidation/channel.py +0 -0
  82. {cachekit-0.11.1 → cachekit-0.15.0}/src/cachekit/invalidation/event.py +0 -0
  83. {cachekit-0.11.1 → cachekit-0.15.0}/src/cachekit/invalidation/redis_channel.py +0 -0
  84. {cachekit-0.11.1 → cachekit-0.15.0}/src/cachekit/key_generator.py +0 -0
  85. {cachekit-0.11.1 → cachekit-0.15.0}/src/cachekit/logging.py +0 -0
  86. {cachekit-0.11.1 → cachekit-0.15.0}/src/cachekit/monitoring/__init__.py +0 -0
  87. {cachekit-0.11.1 → cachekit-0.15.0}/src/cachekit/monitoring/correlation_tracking.py +0 -0
  88. {cachekit-0.11.1 → cachekit-0.15.0}/src/cachekit/monitoring/pool_monitor.py +0 -0
  89. {cachekit-0.11.1 → cachekit-0.15.0}/src/cachekit/monitoring/protocols.py +0 -0
  90. {cachekit-0.11.1 → cachekit-0.15.0}/src/cachekit/py.typed +0 -0
  91. {cachekit-0.11.1 → cachekit-0.15.0}/src/cachekit/reliability/async_metrics.py +0 -0
  92. {cachekit-0.11.1 → cachekit-0.15.0}/src/cachekit/reliability/error_classification.py +0 -0
  93. {cachekit-0.11.1 → cachekit-0.15.0}/src/cachekit/reliability/load_control.py +0 -0
  94. {cachekit-0.11.1 → cachekit-0.15.0}/src/cachekit/serializers/arrow_serializer.py +0 -0
  95. {cachekit-0.11.1 → cachekit-0.15.0}/src/cachekit/serializers/orjson_serializer.py +0 -0
  96. {cachekit-0.11.1 → cachekit-0.15.0}/src/cachekit/serializers/standard_serializer.py +0 -0
  97. {cachekit-0.11.1 → cachekit-0.15.0}/src/cachekit/serializers/wrapper.py +0 -0
@@ -245,9 +245,9 @@ checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33"
245
245
 
246
246
  [[package]]
247
247
  name = "cachekit-core"
248
- version = "0.2.1"
248
+ version = "0.3.0"
249
249
  source = "registry+https://github.com/rust-lang/crates.io-index"
250
- checksum = "01870e86fa79ca9ee521b5b9c2bcff8428eab95fb343f75d35a6fce159bb1577"
250
+ checksum = "9ee6235f73aefb0dc66b9cd0d333b9da928c8192e4234357a739fe756c2f8f23"
251
251
  dependencies = [
252
252
  "aes",
253
253
  "aes-gcm",
@@ -271,7 +271,7 @@ dependencies = [
271
271
 
272
272
  [[package]]
273
273
  name = "cachekit-rs"
274
- version = "0.11.1"
274
+ version = "0.15.0"
275
275
  dependencies = [
276
276
  "cachekit-core",
277
277
  "criterion",
@@ -468,9 +468,9 @@ dependencies = [
468
468
 
469
469
  [[package]]
470
470
  name = "crossbeam-epoch"
471
- version = "0.9.18"
471
+ version = "0.9.20"
472
472
  source = "registry+https://github.com/rust-lang/crates.io-index"
473
- checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e"
473
+ checksum = "2d6914041f254d6e9176c01941b21115dcfb7089e55135a35411081bd106ef3f"
474
474
  dependencies = [
475
475
  "crossbeam-utils",
476
476
  ]
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: cachekit
3
- Version: 0.11.1
3
+ Version: 0.15.0
4
4
  Classifier: Development Status :: 3 - Alpha
5
5
  Classifier: Intended Audience :: Developers
6
6
  Classifier: License :: OSI Approved :: MIT License
@@ -22,7 +22,6 @@ Classifier: Typing :: Typed
22
22
  Requires-Dist: redis[hiredis]>=4.0.0
23
23
  Requires-Dist: pydantic>=2.0.0
24
24
  Requires-Dist: pydantic-settings>=2.0.0
25
- Requires-Dist: tenacity>=8.0.0
26
25
  Requires-Dist: prometheus-client>=0.22.1
27
26
  Requires-Dist: psutil>=7.0.0
28
27
  Requires-Dist: blake3>=1.0.5
@@ -101,7 +100,6 @@ That's it. You get:
101
100
  | **Prometheus metrics** | Built-in observability |
102
101
  | **MessagePack serialization** | Efficient with optional compression |
103
102
  | **Zero-knowledge encryption** | Client-side AES-256-GCM |
104
- | **Adaptive timeouts** | Auto-tune to system load |
105
103
 
106
104
  ---
107
105
 
@@ -220,7 +218,6 @@ def get_user_profile(user_id: int):
220
218
  | Feature | `@cache.minimal` | `@cache.dev` | `@cache.test` | `@cache.production` | `@cache.secure` |
221
219
  |:--------|:----------------:|:------------:|:-------------:|:-------------------:|:---------------:|
222
220
  | Circuit Breaker | - | ✅ | - | ✅ | ✅ |
223
- | Adaptive Timeouts | - | ✅ | - | ✅ | ✅ |
224
221
  | Backpressure | ✅ | ✅ | - | ✅ | ✅ |
225
222
  | Integrity Checking | - | ✅ | - | ✅ | ✅ 🔒 |
226
223
  | Encryption | - | - | - | - | ✅ Required |
@@ -412,8 +409,9 @@ exposition setup.
412
409
  <summary><strong>Thread Safety Details</strong></summary>
413
410
 
414
411
  **Per-Function Statistics:**
415
- - Statistics tracked per decorated function (shared across all calls)
412
+ - Statistics tracked per function identity (`module.qualname`), shared across all calls and across re-decorations of the same function
416
413
  - Thread-safe via RLock (all methods safe for concurrent access)
414
+ - Fork-safe: a forked child starts with zeroed counters and its own session ID
417
415
 
418
416
  ```python
419
417
  from concurrent.futures import ThreadPoolExecutor
@@ -455,7 +453,6 @@ info = expensive_func.cache_info()
455
453
  | [Distributed Locking][distributed-locking-url] | Cache stampede prevention |
456
454
  | [Prometheus Metrics][prometheus-url] | Built-in observability |
457
455
  | [Zero-Knowledge Encryption][encryption-url] | Client-side security |
458
- | [Adaptive Timeouts][adaptive-timeouts-url] | Auto-tune to infrastructure |
459
456
 
460
457
  ---
461
458
 
@@ -480,8 +477,8 @@ CACHEKIT_MEMCACHED_KEY_PREFIX="myapp:" # Default: "" (none)
480
477
 
481
478
  # Optional Configuration
482
479
  CACHEKIT_DEFAULT_TTL=3600
483
- CACHEKIT_MAX_CHUNK_SIZE_MB=100
484
- CACHEKIT_ENABLE_COMPRESSION=true
480
+ CACHEKIT_MAX_VALUE_SIZE=104857600
481
+ CACHEKIT_ARROW_COMPRESSION=zstd
485
482
  ```
486
483
 
487
484
  > [!NOTE]
@@ -538,7 +535,6 @@ MIT License - see [LICENSE][license-file-url] for details.
538
535
  [distributed-locking-url]: docs/features/distributed-locking.md
539
536
  [prometheus-url]: docs/features/prometheus-metrics.md
540
537
  [encryption-url]: docs/features/zero-knowledge-encryption.md
541
- [adaptive-timeouts-url]: docs/features/adaptive-timeouts.md
542
538
  [contributing-url]: CONTRIBUTING.md
543
539
  [license-file-url]: LICENSE
544
540
  [github-url]: https://github.com/cachekit-io/cachekit-py
@@ -47,7 +47,6 @@ That's it. You get:
47
47
  | **Prometheus metrics** | Built-in observability |
48
48
  | **MessagePack serialization** | Efficient with optional compression |
49
49
  | **Zero-knowledge encryption** | Client-side AES-256-GCM |
50
- | **Adaptive timeouts** | Auto-tune to system load |
51
50
 
52
51
  ---
53
52
 
@@ -166,7 +165,6 @@ def get_user_profile(user_id: int):
166
165
  | Feature | `@cache.minimal` | `@cache.dev` | `@cache.test` | `@cache.production` | `@cache.secure` |
167
166
  |:--------|:----------------:|:------------:|:-------------:|:-------------------:|:---------------:|
168
167
  | Circuit Breaker | - | ✅ | - | ✅ | ✅ |
169
- | Adaptive Timeouts | - | ✅ | - | ✅ | ✅ |
170
168
  | Backpressure | ✅ | ✅ | - | ✅ | ✅ |
171
169
  | Integrity Checking | - | ✅ | - | ✅ | ✅ 🔒 |
172
170
  | Encryption | - | - | - | - | ✅ Required |
@@ -358,8 +356,9 @@ exposition setup.
358
356
  <summary><strong>Thread Safety Details</strong></summary>
359
357
 
360
358
  **Per-Function Statistics:**
361
- - Statistics tracked per decorated function (shared across all calls)
359
+ - Statistics tracked per function identity (`module.qualname`), shared across all calls and across re-decorations of the same function
362
360
  - Thread-safe via RLock (all methods safe for concurrent access)
361
+ - Fork-safe: a forked child starts with zeroed counters and its own session ID
363
362
 
364
363
  ```python
365
364
  from concurrent.futures import ThreadPoolExecutor
@@ -401,7 +400,6 @@ info = expensive_func.cache_info()
401
400
  | [Distributed Locking][distributed-locking-url] | Cache stampede prevention |
402
401
  | [Prometheus Metrics][prometheus-url] | Built-in observability |
403
402
  | [Zero-Knowledge Encryption][encryption-url] | Client-side security |
404
- | [Adaptive Timeouts][adaptive-timeouts-url] | Auto-tune to infrastructure |
405
403
 
406
404
  ---
407
405
 
@@ -426,8 +424,8 @@ CACHEKIT_MEMCACHED_KEY_PREFIX="myapp:" # Default: "" (none)
426
424
 
427
425
  # Optional Configuration
428
426
  CACHEKIT_DEFAULT_TTL=3600
429
- CACHEKIT_MAX_CHUNK_SIZE_MB=100
430
- CACHEKIT_ENABLE_COMPRESSION=true
427
+ CACHEKIT_MAX_VALUE_SIZE=104857600
428
+ CACHEKIT_ARROW_COMPRESSION=zstd
431
429
  ```
432
430
 
433
431
  > [!NOTE]
@@ -484,7 +482,6 @@ MIT License - see [LICENSE][license-file-url] for details.
484
482
  [distributed-locking-url]: docs/features/distributed-locking.md
485
483
  [prometheus-url]: docs/features/prometheus-metrics.md
486
484
  [encryption-url]: docs/features/zero-knowledge-encryption.md
487
- [adaptive-timeouts-url]: docs/features/adaptive-timeouts.md
488
485
  [contributing-url]: CONTRIBUTING.md
489
486
  [license-file-url]: LICENSE
490
487
  [github-url]: https://github.com/cachekit-io/cachekit-py
@@ -4,7 +4,7 @@ build-backend = "maturin"
4
4
 
5
5
  [project]
6
6
  name = "cachekit"
7
- version = "0.11.1"
7
+ version = "0.15.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"}
@@ -57,8 +57,6 @@ dependencies = [
57
57
  # Configuration and validation
58
58
  "pydantic>=2.0.0",
59
59
  "pydantic-settings>=2.0.0",
60
- # Error handling
61
- "tenacity>=8.0.0",
62
60
  # Monitoring and observability
63
61
  "prometheus-client>=0.22.1",
64
62
  "psutil>=7.0.0",
@@ -156,6 +154,7 @@ addopts = [
156
154
  "--doctest-modules", # Validate docstring examples
157
155
  "--doctest-continue-on-failure", # Report all doctest failures, not just first
158
156
  "--markdown-docs", # Validate markdown documentation examples
157
+ "--benchmark-skip", # pytest-benchmark tests run only via --benchmark-only (make benchmark)
159
158
  ]
160
159
  markers = [
161
160
  "asyncio: Async tests using pytest-asyncio",
@@ -1,6 +1,6 @@
1
1
  [package]
2
2
  name = "cachekit-rs"
3
- version = "0.11.1"
3
+ version = "0.15.0"
4
4
  edition = "2021"
5
5
  authors = ["cachekit Contributors"]
6
6
  description = "High-performance storage engine for caching with compression and encryption"
@@ -20,7 +20,7 @@ crate-type = ["cdylib", "rlib"]
20
20
 
21
21
  [dependencies]
22
22
  # Compression, checksums, encryption (https://crates.io/crates/cachekit-core)
23
- cachekit-core = { version = "0.2.0", features = ["compression", "checksum", "messagepack", "encryption"] }
23
+ cachekit-core = { version = "0.3.0", features = ["compression", "checksum", "messagepack", "encryption"] }
24
24
 
25
25
  # Python integration - optional for Rust-only builds
26
26
  pyo3 = { workspace = true, optional = true }
@@ -47,7 +47,6 @@ That's it. You get:
47
47
  | **Prometheus metrics** | Built-in observability |
48
48
  | **MessagePack serialization** | Efficient with optional compression |
49
49
  | **Zero-knowledge encryption** | Client-side AES-256-GCM |
50
- | **Adaptive timeouts** | Auto-tune to system load |
51
50
 
52
51
  ---
53
52
 
@@ -166,7 +165,6 @@ def get_user_profile(user_id: int):
166
165
  | Feature | `@cache.minimal` | `@cache.dev` | `@cache.test` | `@cache.production` | `@cache.secure` |
167
166
  |:--------|:----------------:|:------------:|:-------------:|:-------------------:|:---------------:|
168
167
  | Circuit Breaker | - | ✅ | - | ✅ | ✅ |
169
- | Adaptive Timeouts | - | ✅ | - | ✅ | ✅ |
170
168
  | Backpressure | ✅ | ✅ | - | ✅ | ✅ |
171
169
  | Integrity Checking | - | ✅ | - | ✅ | ✅ 🔒 |
172
170
  | Encryption | - | - | - | - | ✅ Required |
@@ -358,8 +356,9 @@ exposition setup.
358
356
  <summary><strong>Thread Safety Details</strong></summary>
359
357
 
360
358
  **Per-Function Statistics:**
361
- - Statistics tracked per decorated function (shared across all calls)
359
+ - Statistics tracked per function identity (`module.qualname`), shared across all calls and across re-decorations of the same function
362
360
  - Thread-safe via RLock (all methods safe for concurrent access)
361
+ - Fork-safe: a forked child starts with zeroed counters and its own session ID
363
362
 
364
363
  ```python
365
364
  from concurrent.futures import ThreadPoolExecutor
@@ -401,7 +400,6 @@ info = expensive_func.cache_info()
401
400
  | [Distributed Locking][distributed-locking-url] | Cache stampede prevention |
402
401
  | [Prometheus Metrics][prometheus-url] | Built-in observability |
403
402
  | [Zero-Knowledge Encryption][encryption-url] | Client-side security |
404
- | [Adaptive Timeouts][adaptive-timeouts-url] | Auto-tune to infrastructure |
405
403
 
406
404
  ---
407
405
 
@@ -426,8 +424,8 @@ CACHEKIT_MEMCACHED_KEY_PREFIX="myapp:" # Default: "" (none)
426
424
 
427
425
  # Optional Configuration
428
426
  CACHEKIT_DEFAULT_TTL=3600
429
- CACHEKIT_MAX_CHUNK_SIZE_MB=100
430
- CACHEKIT_ENABLE_COMPRESSION=true
427
+ CACHEKIT_MAX_VALUE_SIZE=104857600
428
+ CACHEKIT_ARROW_COMPRESSION=zstd
431
429
  ```
432
430
 
433
431
  > [!NOTE]
@@ -484,7 +482,6 @@ MIT License - see [LICENSE][license-file-url] for details.
484
482
  [distributed-locking-url]: docs/features/distributed-locking.md
485
483
  [prometheus-url]: docs/features/prometheus-metrics.md
486
484
  [encryption-url]: docs/features/zero-knowledge-encryption.md
487
- [adaptive-timeouts-url]: docs/features/adaptive-timeouts.md
488
485
  [contributing-url]: CONTRIBUTING.md
489
486
  [license-file-url]: LICENSE
490
487
  [github-url]: https://github.com/cachekit-io/cachekit-py
@@ -30,6 +30,11 @@ fn _rust_serializer(_py: Python<'_>, m: &Bound<'_, PyModule>) -> PyResult<()> {
30
30
  // Add byte storage class
31
31
  m.add_class::<python_bindings::PyByteStorage>()?;
32
32
 
33
+ // Standalone integrity primitive — registered unconditionally (usable with
34
+ // the checksum feature alone; must not vanish when encryption is off)
35
+ m.add_function(wrap_pyfunction!(python_bindings::checksum_py, m)?)?;
36
+ m.add_function(wrap_pyfunction!(python_bindings::verify_checksum_py, m)?)?;
37
+
33
38
  // Add encryption functionality if feature is enabled
34
39
  #[cfg(feature = "encryption")]
35
40
  {
@@ -43,13 +48,13 @@ fn _rust_serializer(_py: Python<'_>, m: &Bound<'_, PyModule>) -> PyResult<()> {
43
48
  #[cfg(feature = "encryption")]
44
49
  m.add(
45
50
  "__description__",
46
- "Raw byte storage with LZ4 compression, Blake3 checksums, and zero-knowledge encryption",
51
+ "Raw byte storage with LZ4 compression, xxHash3-64 checksums, and zero-knowledge encryption",
47
52
  )?;
48
53
 
49
54
  #[cfg(not(feature = "encryption"))]
50
55
  m.add(
51
56
  "__description__",
52
- "Raw byte storage layer with LZ4 compression and Blake3 checksums",
57
+ "Raw byte storage layer with LZ4 compression and xxHash3-64 checksums",
53
58
  )?;
54
59
 
55
60
  Ok(())
@@ -4,6 +4,7 @@
4
4
  //! All business logic is delegated to cachekit-core.
5
5
 
6
6
  use cachekit_core::ByteStorage;
7
+ use pyo3::buffer::PyBuffer;
7
8
  use pyo3::exceptions::PyValueError;
8
9
  use pyo3::prelude::*;
9
10
  use pyo3::types::PyBytes;
@@ -38,9 +39,11 @@ impl PyByteStorage {
38
39
  /// Returns:
39
40
  /// Bytes: Serialized StorageEnvelope
40
41
  pub fn store(&self, py: Python, data: &[u8], format: Option<String>) -> PyResult<Py<PyBytes>> {
41
- let envelope_bytes = self
42
- .inner
43
- .store(data, format)
42
+ // Detach from the GIL: LZ4 + xxh3 on a large payload otherwise blocks every
43
+ // Python thread for the full compression duration (cachekit-core#45).
44
+ // Sound: `data` borrows an immutable `bytes` buffer kept alive by this call.
45
+ let envelope_bytes = py
46
+ .detach(|| self.inner.store(data, format))
44
47
  .map_err(|e| PyValueError::new_err(format!("Storage failed: {}", e)))?;
45
48
 
46
49
  Ok(PyBytes::new(py, &envelope_bytes).into())
@@ -53,22 +56,23 @@ impl PyByteStorage {
53
56
  ///
54
57
  /// Returns:
55
58
  /// Tuple[bytes, str]: (original_data, format_identifier)
56
- pub fn retrieve(&self, envelope_bytes: &[u8]) -> PyResult<(Vec<u8>, String)> {
57
- self.inner
58
- .retrieve(envelope_bytes)
59
+ pub fn retrieve(&self, py: Python, envelope_bytes: &[u8]) -> PyResult<(Vec<u8>, String)> {
60
+ // Detach from the GIL for decompression + checksum (see store()).
61
+ py.detach(|| self.inner.retrieve(envelope_bytes))
59
62
  .map_err(|e| PyValueError::new_err(format!("Retrieval failed: {}", e)))
60
63
  }
61
64
 
62
65
  /// Get compression ratio for given data
63
- pub fn estimate_compression(&self, data: &[u8]) -> PyResult<f64> {
64
- self.inner
65
- .estimate_compression(data)
66
+ pub fn estimate_compression(&self, py: Python, data: &[u8]) -> PyResult<f64> {
67
+ // Full-payload LZ4 pass — same GIL-blocking profile as store().
68
+ py.detach(|| self.inner.estimate_compression(data))
66
69
  .map_err(|e| PyValueError::new_err(format!("Compression estimation failed: {}", e)))
67
70
  }
68
71
 
69
72
  /// Validate envelope without extracting data
70
- pub fn validate(&self, envelope_bytes: &[u8]) -> PyResult<bool> {
71
- Ok(self.inner.validate(envelope_bytes))
73
+ pub fn validate(&self, py: Python, envelope_bytes: &[u8]) -> PyResult<bool> {
74
+ // Full decompression + checksum under the hood — same GIL-blocking profile.
75
+ Ok(py.detach(|| self.inner.validate(envelope_bytes)))
72
76
  }
73
77
 
74
78
  /// Get security limits for clients
@@ -340,6 +344,47 @@ pub fn key_fingerprint_py(key: &[u8]) -> Vec<u8> {
340
344
  key_fingerprint(key).to_vec()
341
345
  }
342
346
 
347
+ /// Compute the standalone xxHash3-64 checksum of `data` (8 bytes, big-endian).
348
+ ///
349
+ /// Accepts any buffer-protocol object — `bytes`, `bytearray`, `memoryview`,
350
+ /// Arrow buffers — so a serializer holding its payload as a `memoryview`
351
+ /// (e.g. Arrow IPC) can hash it directly, without forcing a `bytes` copy.
352
+ ///
353
+ /// NON-cryptographic: detects corruption, not tampering. For tamper-resistance
354
+ /// use @cache.secure (AES-256-GCM), never this checksum. Produces the exact
355
+ /// bytes embedded in every StorageEnvelope, without the LZ4 compression
356
+ /// overhead — for serializers where compression is ineffective (Arrow IPC, JSON).
357
+ #[pyfunction]
358
+ #[pyo3(name = "checksum")]
359
+ pub fn checksum_py(py: Python, data: PyBuffer<u8>) -> PyResult<Py<PyBytes>> {
360
+ let data = data.to_vec(py)?;
361
+ Ok(PyBytes::new(py, &cachekit_core::checksum(&data)).into())
362
+ }
363
+
364
+ /// Verify `data` against an expected 8-byte xxHash3-64 checksum.
365
+ ///
366
+ /// Both arguments accept any buffer-protocol object (`bytes`, `bytearray`,
367
+ /// `memoryview`, …) — the Arrow verify path slices a `memoryview` (`mv[8:]`),
368
+ /// so a bytes-only signature would break the moment a serializer moves onto
369
+ /// this FFI.
370
+ ///
371
+ /// NON-cryptographic: detects corruption, not tampering (see `checksum`).
372
+ /// Raises ValueError if `expected` is not exactly 8 bytes — a truncated
373
+ /// checksum must fail loudly, never return a wrong verdict.
374
+ #[pyfunction]
375
+ #[pyo3(name = "verify_checksum")]
376
+ pub fn verify_checksum_py(
377
+ py: Python,
378
+ data: PyBuffer<u8>,
379
+ expected: PyBuffer<u8>,
380
+ ) -> PyResult<bool> {
381
+ let expected: [u8; 8] = expected.to_vec(py)?.try_into().map_err(|v: Vec<u8>| {
382
+ PyValueError::new_err(format!("expected must be exactly 8 bytes, got {}", v.len()))
383
+ })?;
384
+ let data = data.to_vec(py)?;
385
+ Ok(cachekit_core::verify_checksum(&data, &expected))
386
+ }
387
+
343
388
  /// Register encryption module with Python
344
389
  #[cfg(feature = "encryption")]
345
390
  pub fn register_encryption_module(m: &Bound<'_, PyModule>) -> PyResult<()> {
@@ -1,15 +1,14 @@
1
1
  r"""cachekit - Caching decorator for Python applications.
2
2
 
3
3
  A robust, production-ready Python library that provides intelligent caching
4
- capabilities with advanced features like chunked data handling, multi-serialization
5
- support, distributed locking, and automatic corruption detection.
4
+ capabilities with advanced features like multi-serialization support,
5
+ distributed locking, and automatic corruption detection.
6
6
 
7
7
  Supports pluggable backends: Redis (default), CachekitIO SaaS, File, and custom.
8
8
 
9
9
  Key Features:
10
10
  - **Intelligent @cache decorator** with auto-detection and intent-based optimization
11
11
  - **Circuit breaker protection** against cascading failures
12
- - **Adaptive timeout adjustment** based on historical backend latency patterns
13
12
  - **Backpressure control** to prevent backend overload
14
13
  - **Connection pooling** for optimized performance
15
14
  - **Health check methods** for comprehensive monitoring
@@ -68,7 +67,7 @@ Example Usage:
68
67
  ```
69
68
  """
70
69
 
71
- __version__ = "0.11.1"
70
+ __version__ = "0.15.0"
72
71
 
73
72
  from collections.abc import Callable
74
73
  from typing import Any, TypeVar
@@ -98,6 +97,16 @@ from .health import (
98
97
  health_check_handler,
99
98
  )
100
99
 
100
+ # Interop mode (interop/v1): cross-SDK key generation and value codec.
101
+ # The decorator API is @cache(interop="op", namespace="ns"); these helpers are
102
+ # for manual key/value handling (debugging, out-of-band writers, migrations).
103
+ from .interop import (
104
+ InteropError,
105
+ decode_interop_value,
106
+ encode_interop_value,
107
+ generate_interop_key,
108
+ )
109
+
101
110
  # L1/L2 architecture integrated into standard cache interface
102
111
  # No separate imports needed - cache.minimal/.production/.secure handle L1+L2 transparently
103
112
  # Import reliability configuration
@@ -113,9 +122,13 @@ __all__ = [
113
122
  "CacheInfo",
114
123
  "CircuitBreakerConfig",
115
124
  "DecoratorConfig",
125
+ "decode_interop_value",
126
+ "encode_interop_value",
127
+ "generate_interop_key",
116
128
  "get_health_checker",
117
129
  "health_check_handler",
118
130
  "HealthCheckResult",
119
131
  "HealthLevel",
120
132
  "HealthStatus",
133
+ "InteropError",
121
134
  ]
@@ -119,8 +119,10 @@ class TTLInspectableBackend(Protocol):
119
119
  TTL refresh for frequently-accessed keys.
120
120
 
121
121
  Not all backends support this capability:
122
- - Supported: Redis, PostgreSQL, DynamoDB, SQLite, FileSystem
123
- - Not supported: HTTP (stateless), Memcached (limited), S3 (limited)
122
+ - Supported: Redis, CachekitIO, File, PostgreSQL, DynamoDB, SQLite
123
+ - Not supported: HTTP (stateless), S3 (limited)
124
+ - Memcached: partial — implements ``refresh_ttl`` (via ``touch``) but NOT ``get_ttl``
125
+ (no protocol command to read remaining TTL), so it is not a full TTLInspectableBackend.
124
126
 
125
127
  Example:
126
128
  >>> # TTL inspection pattern (async context):
@@ -287,8 +289,8 @@ class TimeoutConfigurableBackend(Protocol):
287
289
  """Optional protocol for per-operation timeout configuration.
288
290
 
289
291
  Backends implementing this protocol allow fine-grained timeout control
290
- per operation. This enables features like adaptive timeouts that adjust
291
- based on operation latency.
292
+ per operation, so a caller can set an explicit timeout for a single
293
+ operation where the backend supports it.
292
294
 
293
295
  All backends support some timeout mechanism, but granularity varies:
294
296
  - Per-operation: HTTP, DynamoDB, PostgreSQL
@@ -34,6 +34,20 @@ _logger = get_structured_logger(__name__)
34
34
  # preferring the header. See protocol spec/saas-api.md (DELETE .../lock).
35
35
  LOCK_ID_HEADER = "X-CacheKit-Lock-Id"
36
36
 
37
+ # Protocol-canonical TTL header (spec/saas-api.md). The legacy X-TTL is sent
38
+ # alongside it until the dual-reading server (saas#245) is deployed everywhere;
39
+ # sending both is value-identical and safe against either server generation.
40
+ # TODO(LAB-381 follow-up): drop X-TTL once saas#245 is live in prod.
41
+ TTL_HEADER = "X-CacheKit-TTL"
42
+ LEGACY_TTL_HEADER = "X-TTL"
43
+
44
+ # Stale-while-revalidate (LAB-381, spec/saas-api.md#stale-while-revalidate).
45
+ # STALE_TTL_HEADER rides PUTs to open a stale-grace window past the fresh TTL;
46
+ # FRESHNESS_HEADER labels every GET/HEAD 200 as fresh|stale. Pre-SWR servers
47
+ # ignore the former and never emit the latter.
48
+ STALE_TTL_HEADER = "X-CacheKit-Stale-TTL"
49
+ FRESHNESS_HEADER = "X-CacheKit-Freshness"
50
+
37
51
 
38
52
  def _inject_metrics_headers(stats: _FunctionStats | None) -> dict[str, str]:
39
53
  """Extract cache metrics and format as HTTP headers.
@@ -335,22 +349,62 @@ class CachekitIOBackend:
335
349
  return None
336
350
  raise
337
351
 
338
- def set(self, key: str, value: bytes, ttl: int | None = None) -> None:
352
+ @staticmethod
353
+ def _is_stale(response: httpx.Response) -> bool:
354
+ """Map the X-CacheKit-Freshness header to staleness (spec/saas-api.md).
355
+
356
+ Absent header = fresh (pre-SWR server); unrecognized value = stale
357
+ (revalidation is the conservative action). Tokens are lowercase and
358
+ case-sensitive per spec.
359
+ """
360
+ value = response.headers.get(FRESHNESS_HEADER)
361
+ return value is not None and value != "fresh"
362
+
363
+ def get_with_freshness(self, key: str) -> tuple[bytes, bool] | None:
364
+ """Retrieve value plus its SWR freshness (sync).
365
+
366
+ Returns:
367
+ ``(value, is_stale)`` on a hit — ``is_stale`` is True only for an
368
+ entry in its stale-grace window (LAB-381) — or None on a miss.
369
+
370
+ Raises:
371
+ BackendError: If operation fails (network, auth, etc.)
372
+ """
373
+ try:
374
+ response = self._request_sync("GET", key)
375
+ return response.content, self._is_stale(response)
376
+ except BackendError as exc:
377
+ if exc.original_exception and isinstance(exc.original_exception, httpx.HTTPStatusError):
378
+ if exc.original_exception.response.status_code == 404:
379
+ return None
380
+ raise
381
+
382
+ def set(self, key: str, value: bytes, ttl: int | None = None, stale_ttl: int | None = None) -> None:
339
383
  """Store value in cache (sync).
340
384
 
341
385
  Args:
342
386
  key: Cache key
343
387
  value: Bytes to cache
344
388
  ttl: Time-to-live in seconds (optional)
389
+ stale_ttl: Stale-grace window in seconds past the fresh TTL
390
+ (LAB-381 SWR). Only honoured alongside an explicit ``ttl``;
391
+ pre-SWR servers ignore it.
345
392
 
346
393
  Raises:
347
394
  BackendError: If operation fails
348
395
  """
349
- headers = {}
350
- if ttl is not None:
351
- headers["X-TTL"] = str(ttl)
396
+ self._request_sync("PUT", key, content=value, headers=self._set_headers(ttl, stale_ttl))
352
397
 
353
- self._request_sync("PUT", key, content=value, headers=headers)
398
+ @staticmethod
399
+ def _set_headers(ttl: int | None, stale_ttl: int | None) -> dict[str, str]:
400
+ """PUT timing headers: canonical + legacy TTL (dual-send until saas#245 deploys), stale window."""
401
+ headers: dict[str, str] = {}
402
+ if ttl is not None:
403
+ headers[TTL_HEADER] = str(ttl)
404
+ headers[LEGACY_TTL_HEADER] = str(ttl)
405
+ if stale_ttl is not None and stale_ttl > 0 and ttl is not None:
406
+ headers[STALE_TTL_HEADER] = str(stale_ttl)
407
+ return headers
354
408
 
355
409
  def delete(self, key: str) -> bool:
356
410
  """Delete key from cache (sync).
@@ -457,22 +511,21 @@ class CachekitIOBackend:
457
511
  return None
458
512
  raise
459
513
 
460
- async def set_async(self, key: str, value: bytes, ttl: int | None = None) -> None:
514
+ async def set_async(self, key: str, value: bytes, ttl: int | None = None, stale_ttl: int | None = None) -> None:
461
515
  """Store value in cache (async).
462
516
 
463
517
  Args:
464
518
  key: Cache key
465
519
  value: Bytes to cache
466
520
  ttl: Time-to-live in seconds (optional)
521
+ stale_ttl: Stale-grace window in seconds past the fresh TTL
522
+ (LAB-381 SWR). Only honoured alongside an explicit ``ttl``;
523
+ pre-SWR servers ignore it.
467
524
 
468
525
  Raises:
469
526
  BackendError: If operation fails
470
527
  """
471
- headers = {}
472
- if ttl is not None:
473
- headers["X-TTL"] = str(ttl)
474
-
475
- await self._request_async("PUT", key, content=value, headers=headers)
528
+ await self._request_async("PUT", key, content=value, headers=self._set_headers(ttl, stale_ttl))
476
529
 
477
530
  async def delete_async(self, key: str) -> bool:
478
531
  """Delete key from cache (async).
@@ -0,0 +1,59 @@
1
+ """Session headers for cachekit.io requests.
2
+
3
+ Process session identity (UUID + start timestamp, PID-aware) lives in
4
+ :mod:`cachekit.decorators.session` — the single source of truth shared
5
+ with the decorator stats tracker. This module only assembles the SaaS
6
+ HTTP headers from it.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ import threading
12
+
13
+ from cachekit.decorators.session import get_session_id, get_session_start_ms
14
+
15
+ # Thread-local storage for header dict caching
16
+ _thread_local = threading.local()
17
+
18
+
19
+ def get_session_headers() -> dict[str, str]:
20
+ """Get session headers for cachekit.io requests.
21
+
22
+ Returns a dictionary containing X-CacheKit-Session-ID and
23
+ X-CacheKit-Session-Start headers needed for cachekit.io API requests.
24
+
25
+ The returned dict is cached in thread-local storage to avoid repeated
26
+ dictionary allocations and revalidated against the current session ID,
27
+ so a process restart or fork (PID change) transparently regenerates it.
28
+ A fresh copy is returned on each call to prevent accidental mutation of
29
+ cached data while maintaining efficiency.
30
+
31
+ Returns:
32
+ dict[str, str]: Headers dict with keys:
33
+ - X-CacheKit-Session-ID: Process session UUID
34
+ - X-CacheKit-Session-Start: Session start milliseconds
35
+
36
+ Example:
37
+ >>> headers = get_session_headers()
38
+ >>> "X-CacheKit-Session-ID" in headers
39
+ True
40
+ >>> "X-CacheKit-Session-Start" in headers
41
+ True
42
+ >>> headers["X-CacheKit-Session-Start"].isdigit()
43
+ True
44
+ """
45
+ session_id = get_session_id()
46
+ cached = getattr(_thread_local, "headers", None)
47
+ if cached is None or cached["X-CacheKit-Session-ID"] != session_id:
48
+ _thread_local.headers = {
49
+ "X-CacheKit-Session-ID": session_id,
50
+ "X-CacheKit-Session-Start": str(get_session_start_ms()),
51
+ }
52
+ return dict(_thread_local.headers)
53
+
54
+
55
+ __all__ = [
56
+ "get_session_id",
57
+ "get_session_start_ms",
58
+ "get_session_headers",
59
+ ]