cachekit 0.11.0__tar.gz → 0.14.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.0 → cachekit-0.14.0}/Cargo.lock +7 -7
  2. {cachekit-0.11.0 → cachekit-0.14.0}/PKG-INFO +5 -4
  3. {cachekit-0.11.0 → cachekit-0.14.0}/README.md +4 -3
  4. {cachekit-0.11.0 → cachekit-0.14.0}/pyproject.toml +2 -1
  5. {cachekit-0.11.0 → cachekit-0.14.0}/rust/Cargo.toml +2 -2
  6. {cachekit-0.11.0 → cachekit-0.14.0}/rust/README.md +4 -3
  7. {cachekit-0.11.0 → cachekit-0.14.0}/rust/src/lib.rs +7 -2
  8. {cachekit-0.11.0 → cachekit-0.14.0}/rust/src/python_bindings.rs +56 -11
  9. {cachekit-0.11.0 → cachekit-0.14.0}/src/cachekit/__init__.py +17 -3
  10. {cachekit-0.11.0 → cachekit-0.14.0}/src/cachekit/backends/base.py +4 -2
  11. {cachekit-0.11.0 → cachekit-0.14.0}/src/cachekit/backends/cachekitio/backend.py +64 -11
  12. cachekit-0.14.0/src/cachekit/backends/cachekitio/session.py +59 -0
  13. {cachekit-0.11.0 → cachekit-0.14.0}/src/cachekit/backends/file/backend.py +131 -0
  14. {cachekit-0.11.0 → cachekit-0.14.0}/src/cachekit/backends/memcached/backend.py +43 -0
  15. {cachekit-0.11.0 → cachekit-0.14.0}/src/cachekit/backends/provider.py +49 -0
  16. {cachekit-0.11.0 → cachekit-0.14.0}/src/cachekit/backends/redis/backend.py +40 -15
  17. {cachekit-0.11.0 → cachekit-0.14.0}/src/cachekit/backends/redis/client.py +64 -14
  18. {cachekit-0.11.0 → cachekit-0.14.0}/src/cachekit/backends/redis/config.py +13 -0
  19. {cachekit-0.11.0 → cachekit-0.14.0}/src/cachekit/backends/redis/provider.py +17 -6
  20. {cachekit-0.11.0 → cachekit-0.14.0}/src/cachekit/cache_handler.py +553 -49
  21. {cachekit-0.11.0 → cachekit-0.14.0}/src/cachekit/config/decorator.py +44 -8
  22. {cachekit-0.11.0 → cachekit-0.14.0}/src/cachekit/config/nested.py +26 -6
  23. {cachekit-0.11.0 → cachekit-0.14.0}/src/cachekit/config/settings.py +15 -29
  24. {cachekit-0.11.0 → cachekit-0.14.0}/src/cachekit/config/singleton.py +22 -5
  25. {cachekit-0.11.0 → cachekit-0.14.0}/src/cachekit/decorators/intent.py +13 -3
  26. cachekit-0.14.0/src/cachekit/decorators/session.py +133 -0
  27. {cachekit-0.11.0 → cachekit-0.14.0}/src/cachekit/decorators/wrapper.py +755 -84
  28. cachekit-0.14.0/src/cachekit/interop.py +496 -0
  29. {cachekit-0.11.0 → cachekit-0.14.0}/src/cachekit/l1_cache.py +37 -12
  30. cachekit-0.14.0/src/cachekit/object_cache.py +471 -0
  31. {cachekit-0.11.0 → cachekit-0.14.0}/src/cachekit/serializers/__init__.py +5 -1
  32. {cachekit-0.11.0 → cachekit-0.14.0}/src/cachekit/serializers/auto_serializer.py +29 -8
  33. {cachekit-0.11.0 → cachekit-0.14.0}/src/cachekit/serializers/base.py +16 -0
  34. {cachekit-0.11.0 → cachekit-0.14.0}/src/cachekit/serializers/encryption_wrapper.py +72 -21
  35. cachekit-0.14.0/src/cachekit/serializers/interop_serializer.py +64 -0
  36. cachekit-0.11.0/src/cachekit/backends/cachekitio/session.py +0 -164
  37. cachekit-0.11.0/src/cachekit/decorators/session.py +0 -42
  38. cachekit-0.11.0/src/cachekit/object_cache.py +0 -199
  39. {cachekit-0.11.0 → cachekit-0.14.0}/Cargo.toml +0 -0
  40. {cachekit-0.11.0 → cachekit-0.14.0}/LICENSE +0 -0
  41. {cachekit-0.11.0 → cachekit-0.14.0}/rust/Makefile +0 -0
  42. {cachekit-0.11.0 → cachekit-0.14.0}/rust/TEST_EXPANSION_SUMMARY.md +0 -0
  43. {cachekit-0.11.0 → cachekit-0.14.0}/rust/supply-chain/audits.toml +0 -0
  44. {cachekit-0.11.0 → cachekit-0.14.0}/rust/supply-chain/config.toml +0 -0
  45. {cachekit-0.11.0 → cachekit-0.14.0}/rust/supply-chain/imports.lock +0 -0
  46. {cachekit-0.11.0 → cachekit-0.14.0}/rust/tsan_suppressions.txt +0 -0
  47. {cachekit-0.11.0 → cachekit-0.14.0}/src/cachekit/backends/__init__.py +0 -0
  48. {cachekit-0.11.0 → cachekit-0.14.0}/src/cachekit/backends/base_config.py +0 -0
  49. {cachekit-0.11.0 → cachekit-0.14.0}/src/cachekit/backends/cachekitio/__init__.py +0 -0
  50. {cachekit-0.11.0 → cachekit-0.14.0}/src/cachekit/backends/cachekitio/client.py +0 -0
  51. {cachekit-0.11.0 → cachekit-0.14.0}/src/cachekit/backends/cachekitio/config.py +0 -0
  52. {cachekit-0.11.0 → cachekit-0.14.0}/src/cachekit/backends/cachekitio/error_handler.py +0 -0
  53. {cachekit-0.11.0 → cachekit-0.14.0}/src/cachekit/backends/errors.py +0 -0
  54. {cachekit-0.11.0 → cachekit-0.14.0}/src/cachekit/backends/file/__init__.py +0 -0
  55. {cachekit-0.11.0 → cachekit-0.14.0}/src/cachekit/backends/file/config.py +0 -0
  56. {cachekit-0.11.0 → cachekit-0.14.0}/src/cachekit/backends/memcached/__init__.py +0 -0
  57. {cachekit-0.11.0 → cachekit-0.14.0}/src/cachekit/backends/memcached/config.py +0 -0
  58. {cachekit-0.11.0 → cachekit-0.14.0}/src/cachekit/backends/memcached/error_handler.py +0 -0
  59. {cachekit-0.11.0 → cachekit-0.14.0}/src/cachekit/backends/redis/__init__.py +0 -0
  60. {cachekit-0.11.0 → cachekit-0.14.0}/src/cachekit/backends/redis/error_handler.py +0 -0
  61. {cachekit-0.11.0 → cachekit-0.14.0}/src/cachekit/config/__init__.py +0 -0
  62. {cachekit-0.11.0 → cachekit-0.14.0}/src/cachekit/config/validation.py +0 -0
  63. {cachekit-0.11.0 → cachekit-0.14.0}/src/cachekit/decorators/__init__.py +0 -0
  64. {cachekit-0.11.0 → cachekit-0.14.0}/src/cachekit/decorators/local_wrapper.py +0 -0
  65. {cachekit-0.11.0 → cachekit-0.14.0}/src/cachekit/decorators/main.py +0 -0
  66. {cachekit-0.11.0 → cachekit-0.14.0}/src/cachekit/decorators/orchestrator.py +0 -0
  67. {cachekit-0.11.0 → cachekit-0.14.0}/src/cachekit/decorators/stats_context.py +0 -0
  68. {cachekit-0.11.0 → cachekit-0.14.0}/src/cachekit/decorators/tenant_context.py +0 -0
  69. {cachekit-0.11.0 → cachekit-0.14.0}/src/cachekit/decorators/utils/__init__.py +0 -0
  70. {cachekit-0.11.0 → cachekit-0.14.0}/src/cachekit/di.py +0 -0
  71. {cachekit-0.11.0 → cachekit-0.14.0}/src/cachekit/hash_utils.py +0 -0
  72. {cachekit-0.11.0 → cachekit-0.14.0}/src/cachekit/health.py +0 -0
  73. {cachekit-0.11.0 → cachekit-0.14.0}/src/cachekit/hiredis_compat.py +0 -0
  74. {cachekit-0.11.0 → cachekit-0.14.0}/src/cachekit/imports.py +0 -0
  75. {cachekit-0.11.0 → cachekit-0.14.0}/src/cachekit/invalidation/__init__.py +0 -0
  76. {cachekit-0.11.0 → cachekit-0.14.0}/src/cachekit/invalidation/channel.py +0 -0
  77. {cachekit-0.11.0 → cachekit-0.14.0}/src/cachekit/invalidation/event.py +0 -0
  78. {cachekit-0.11.0 → cachekit-0.14.0}/src/cachekit/invalidation/redis_channel.py +0 -0
  79. {cachekit-0.11.0 → cachekit-0.14.0}/src/cachekit/key_generator.py +0 -0
  80. {cachekit-0.11.0 → cachekit-0.14.0}/src/cachekit/logging.py +0 -0
  81. {cachekit-0.11.0 → cachekit-0.14.0}/src/cachekit/monitoring/__init__.py +0 -0
  82. {cachekit-0.11.0 → cachekit-0.14.0}/src/cachekit/monitoring/correlation_tracking.py +0 -0
  83. {cachekit-0.11.0 → cachekit-0.14.0}/src/cachekit/monitoring/pool_monitor.py +0 -0
  84. {cachekit-0.11.0 → cachekit-0.14.0}/src/cachekit/monitoring/protocols.py +0 -0
  85. {cachekit-0.11.0 → cachekit-0.14.0}/src/cachekit/py.typed +0 -0
  86. {cachekit-0.11.0 → cachekit-0.14.0}/src/cachekit/reliability/__init__.py +0 -0
  87. {cachekit-0.11.0 → cachekit-0.14.0}/src/cachekit/reliability/adaptive_timeout.py +0 -0
  88. {cachekit-0.11.0 → cachekit-0.14.0}/src/cachekit/reliability/async_metrics.py +0 -0
  89. {cachekit-0.11.0 → cachekit-0.14.0}/src/cachekit/reliability/circuit_breaker.py +0 -0
  90. {cachekit-0.11.0 → cachekit-0.14.0}/src/cachekit/reliability/error_classification.py +0 -0
  91. {cachekit-0.11.0 → cachekit-0.14.0}/src/cachekit/reliability/load_control.py +0 -0
  92. {cachekit-0.11.0 → cachekit-0.14.0}/src/cachekit/reliability/metrics_collection.py +0 -0
  93. {cachekit-0.11.0 → cachekit-0.14.0}/src/cachekit/reliability/profiles.py +0 -0
  94. {cachekit-0.11.0 → cachekit-0.14.0}/src/cachekit/serializers/arrow_serializer.py +0 -0
  95. {cachekit-0.11.0 → cachekit-0.14.0}/src/cachekit/serializers/orjson_serializer.py +0 -0
  96. {cachekit-0.11.0 → cachekit-0.14.0}/src/cachekit/serializers/standard_serializer.py +0 -0
  97. {cachekit-0.11.0 → cachekit-0.14.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.0"
274
+ version = "0.14.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
  ]
@@ -954,9 +954,9 @@ checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79"
954
954
 
955
955
  [[package]]
956
956
  name = "memmap2"
957
- version = "0.9.10"
957
+ version = "0.9.11"
958
958
  source = "registry+https://github.com/rust-lang/crates.io-index"
959
- checksum = "714098028fe011992e1c3962653c96b2d578c4b4bce9036e15ff220319b1e0e3"
959
+ checksum = "d1219ed1b7f229ee7104d281dd01d6802fe28bb6e95d292942c4daacdeb798c0"
960
960
  dependencies = [
961
961
  "libc",
962
962
  ]
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: cachekit
3
- Version: 0.11.0
3
+ Version: 0.14.0
4
4
  Classifier: Development Status :: 3 - Alpha
5
5
  Classifier: Intended Audience :: Developers
6
6
  Classifier: License :: OSI Approved :: MIT License
@@ -412,8 +412,9 @@ exposition setup.
412
412
  <summary><strong>Thread Safety Details</strong></summary>
413
413
 
414
414
  **Per-Function Statistics:**
415
- - Statistics tracked per decorated function (shared across all calls)
415
+ - Statistics tracked per function identity (`module.qualname`), shared across all calls and across re-decorations of the same function
416
416
  - Thread-safe via RLock (all methods safe for concurrent access)
417
+ - Fork-safe: a forked child starts with zeroed counters and its own session ID
417
418
 
418
419
  ```python
419
420
  from concurrent.futures import ThreadPoolExecutor
@@ -480,8 +481,8 @@ CACHEKIT_MEMCACHED_KEY_PREFIX="myapp:" # Default: "" (none)
480
481
 
481
482
  # Optional Configuration
482
483
  CACHEKIT_DEFAULT_TTL=3600
483
- CACHEKIT_MAX_CHUNK_SIZE_MB=100
484
- CACHEKIT_ENABLE_COMPRESSION=true
484
+ CACHEKIT_MAX_VALUE_SIZE=104857600
485
+ CACHEKIT_ARROW_COMPRESSION=zstd
485
486
  ```
486
487
 
487
488
  > [!NOTE]
@@ -358,8 +358,9 @@ exposition setup.
358
358
  <summary><strong>Thread Safety Details</strong></summary>
359
359
 
360
360
  **Per-Function Statistics:**
361
- - Statistics tracked per decorated function (shared across all calls)
361
+ - Statistics tracked per function identity (`module.qualname`), shared across all calls and across re-decorations of the same function
362
362
  - Thread-safe via RLock (all methods safe for concurrent access)
363
+ - Fork-safe: a forked child starts with zeroed counters and its own session ID
363
364
 
364
365
  ```python
365
366
  from concurrent.futures import ThreadPoolExecutor
@@ -426,8 +427,8 @@ CACHEKIT_MEMCACHED_KEY_PREFIX="myapp:" # Default: "" (none)
426
427
 
427
428
  # Optional Configuration
428
429
  CACHEKIT_DEFAULT_TTL=3600
429
- CACHEKIT_MAX_CHUNK_SIZE_MB=100
430
- CACHEKIT_ENABLE_COMPRESSION=true
430
+ CACHEKIT_MAX_VALUE_SIZE=104857600
431
+ CACHEKIT_ARROW_COMPRESSION=zstd
431
432
  ```
432
433
 
433
434
  > [!NOTE]
@@ -4,7 +4,7 @@ build-backend = "maturin"
4
4
 
5
5
  [project]
6
6
  name = "cachekit"
7
- version = "0.11.0"
7
+ version = "0.14.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"}
@@ -156,6 +156,7 @@ addopts = [
156
156
  "--doctest-modules", # Validate docstring examples
157
157
  "--doctest-continue-on-failure", # Report all doctest failures, not just first
158
158
  "--markdown-docs", # Validate markdown documentation examples
159
+ "--benchmark-skip", # pytest-benchmark tests run only via --benchmark-only (make benchmark)
159
160
  ]
160
161
  markers = [
161
162
  "asyncio: Async tests using pytest-asyncio",
@@ -1,6 +1,6 @@
1
1
  [package]
2
2
  name = "cachekit-rs"
3
- version = "0.11.0"
3
+ version = "0.14.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 }
@@ -358,8 +358,9 @@ exposition setup.
358
358
  <summary><strong>Thread Safety Details</strong></summary>
359
359
 
360
360
  **Per-Function Statistics:**
361
- - Statistics tracked per decorated function (shared across all calls)
361
+ - Statistics tracked per function identity (`module.qualname`), shared across all calls and across re-decorations of the same function
362
362
  - Thread-safe via RLock (all methods safe for concurrent access)
363
+ - Fork-safe: a forked child starts with zeroed counters and its own session ID
363
364
 
364
365
  ```python
365
366
  from concurrent.futures import ThreadPoolExecutor
@@ -426,8 +427,8 @@ CACHEKIT_MEMCACHED_KEY_PREFIX="myapp:" # Default: "" (none)
426
427
 
427
428
  # Optional Configuration
428
429
  CACHEKIT_DEFAULT_TTL=3600
429
- CACHEKIT_MAX_CHUNK_SIZE_MB=100
430
- CACHEKIT_ENABLE_COMPRESSION=true
430
+ CACHEKIT_MAX_VALUE_SIZE=104857600
431
+ CACHEKIT_ARROW_COMPRESSION=zstd
431
432
  ```
432
433
 
433
434
  > [!NOTE]
@@ -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,8 +1,8 @@
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
 
@@ -68,7 +68,7 @@ Example Usage:
68
68
  ```
69
69
  """
70
70
 
71
- __version__ = "0.11.0"
71
+ __version__ = "0.14.0"
72
72
 
73
73
  from collections.abc import Callable
74
74
  from typing import Any, TypeVar
@@ -98,6 +98,16 @@ from .health import (
98
98
  health_check_handler,
99
99
  )
100
100
 
101
+ # Interop mode (interop/v1): cross-SDK key generation and value codec.
102
+ # The decorator API is @cache(interop="op", namespace="ns"); these helpers are
103
+ # for manual key/value handling (debugging, out-of-band writers, migrations).
104
+ from .interop import (
105
+ InteropError,
106
+ decode_interop_value,
107
+ encode_interop_value,
108
+ generate_interop_key,
109
+ )
110
+
101
111
  # L1/L2 architecture integrated into standard cache interface
102
112
  # No separate imports needed - cache.minimal/.production/.secure handle L1+L2 transparently
103
113
  # Import reliability configuration
@@ -113,9 +123,13 @@ __all__ = [
113
123
  "CacheInfo",
114
124
  "CircuitBreakerConfig",
115
125
  "DecoratorConfig",
126
+ "decode_interop_value",
127
+ "encode_interop_value",
128
+ "generate_interop_key",
116
129
  "get_health_checker",
117
130
  "health_check_handler",
118
131
  "HealthCheckResult",
119
132
  "HealthLevel",
120
133
  "HealthStatus",
134
+ "InteropError",
121
135
  ]
@@ -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):
@@ -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
+ ]