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.
- {cachekit-0.11.1 → cachekit-0.15.0}/Cargo.lock +5 -5
- {cachekit-0.11.1 → cachekit-0.15.0}/PKG-INFO +5 -9
- {cachekit-0.11.1 → cachekit-0.15.0}/README.md +4 -7
- {cachekit-0.11.1 → cachekit-0.15.0}/pyproject.toml +2 -3
- {cachekit-0.11.1 → cachekit-0.15.0}/rust/Cargo.toml +2 -2
- {cachekit-0.11.1 → cachekit-0.15.0}/rust/README.md +4 -7
- {cachekit-0.11.1 → cachekit-0.15.0}/rust/src/lib.rs +7 -2
- {cachekit-0.11.1 → cachekit-0.15.0}/rust/src/python_bindings.rs +56 -11
- {cachekit-0.11.1 → cachekit-0.15.0}/src/cachekit/__init__.py +17 -4
- {cachekit-0.11.1 → cachekit-0.15.0}/src/cachekit/backends/base.py +6 -4
- {cachekit-0.11.1 → cachekit-0.15.0}/src/cachekit/backends/cachekitio/backend.py +64 -11
- cachekit-0.15.0/src/cachekit/backends/cachekitio/session.py +59 -0
- {cachekit-0.11.1 → cachekit-0.15.0}/src/cachekit/backends/file/backend.py +131 -0
- {cachekit-0.11.1 → cachekit-0.15.0}/src/cachekit/backends/memcached/backend.py +43 -0
- {cachekit-0.11.1 → cachekit-0.15.0}/src/cachekit/backends/provider.py +49 -0
- {cachekit-0.11.1 → cachekit-0.15.0}/src/cachekit/backends/redis/backend.py +40 -15
- {cachekit-0.11.1 → cachekit-0.15.0}/src/cachekit/backends/redis/client.py +64 -14
- {cachekit-0.11.1 → cachekit-0.15.0}/src/cachekit/backends/redis/config.py +13 -0
- {cachekit-0.11.1 → cachekit-0.15.0}/src/cachekit/backends/redis/provider.py +17 -6
- {cachekit-0.11.1 → cachekit-0.15.0}/src/cachekit/cache_handler.py +560 -54
- {cachekit-0.11.1 → cachekit-0.15.0}/src/cachekit/config/__init__.py +0 -2
- {cachekit-0.11.1 → cachekit-0.15.0}/src/cachekit/config/decorator.py +48 -29
- {cachekit-0.11.1 → cachekit-0.15.0}/src/cachekit/config/nested.py +26 -64
- {cachekit-0.11.1 → cachekit-0.15.0}/src/cachekit/config/settings.py +15 -29
- {cachekit-0.11.1 → cachekit-0.15.0}/src/cachekit/config/singleton.py +2 -2
- {cachekit-0.11.1 → cachekit-0.15.0}/src/cachekit/decorators/intent.py +13 -5
- {cachekit-0.11.1 → cachekit-0.15.0}/src/cachekit/decorators/orchestrator.py +0 -43
- cachekit-0.15.0/src/cachekit/decorators/session.py +133 -0
- {cachekit-0.11.1 → cachekit-0.15.0}/src/cachekit/decorators/wrapper.py +756 -120
- cachekit-0.15.0/src/cachekit/interop.py +496 -0
- {cachekit-0.11.1 → cachekit-0.15.0}/src/cachekit/l1_cache.py +37 -12
- cachekit-0.15.0/src/cachekit/object_cache.py +471 -0
- {cachekit-0.11.1 → cachekit-0.15.0}/src/cachekit/reliability/__init__.py +1 -4
- {cachekit-0.11.1 → cachekit-0.15.0}/src/cachekit/reliability/circuit_breaker.py +37 -83
- {cachekit-0.11.1 → cachekit-0.15.0}/src/cachekit/reliability/metrics_collection.py +1 -2
- {cachekit-0.11.1 → cachekit-0.15.0}/src/cachekit/reliability/profiles.py +0 -10
- {cachekit-0.11.1 → cachekit-0.15.0}/src/cachekit/serializers/__init__.py +5 -1
- {cachekit-0.11.1 → cachekit-0.15.0}/src/cachekit/serializers/auto_serializer.py +29 -8
- {cachekit-0.11.1 → cachekit-0.15.0}/src/cachekit/serializers/base.py +16 -0
- {cachekit-0.11.1 → cachekit-0.15.0}/src/cachekit/serializers/encryption_wrapper.py +71 -20
- cachekit-0.15.0/src/cachekit/serializers/interop_serializer.py +64 -0
- cachekit-0.11.1/src/cachekit/backends/cachekitio/session.py +0 -164
- cachekit-0.11.1/src/cachekit/decorators/session.py +0 -42
- cachekit-0.11.1/src/cachekit/object_cache.py +0 -199
- cachekit-0.11.1/src/cachekit/reliability/adaptive_timeout.py +0 -424
- {cachekit-0.11.1 → cachekit-0.15.0}/Cargo.toml +0 -0
- {cachekit-0.11.1 → cachekit-0.15.0}/LICENSE +0 -0
- {cachekit-0.11.1 → cachekit-0.15.0}/rust/Makefile +0 -0
- {cachekit-0.11.1 → cachekit-0.15.0}/rust/TEST_EXPANSION_SUMMARY.md +0 -0
- {cachekit-0.11.1 → cachekit-0.15.0}/rust/supply-chain/audits.toml +0 -0
- {cachekit-0.11.1 → cachekit-0.15.0}/rust/supply-chain/config.toml +0 -0
- {cachekit-0.11.1 → cachekit-0.15.0}/rust/supply-chain/imports.lock +0 -0
- {cachekit-0.11.1 → cachekit-0.15.0}/rust/tsan_suppressions.txt +0 -0
- {cachekit-0.11.1 → cachekit-0.15.0}/src/cachekit/backends/__init__.py +0 -0
- {cachekit-0.11.1 → cachekit-0.15.0}/src/cachekit/backends/base_config.py +0 -0
- {cachekit-0.11.1 → cachekit-0.15.0}/src/cachekit/backends/cachekitio/__init__.py +0 -0
- {cachekit-0.11.1 → cachekit-0.15.0}/src/cachekit/backends/cachekitio/client.py +0 -0
- {cachekit-0.11.1 → cachekit-0.15.0}/src/cachekit/backends/cachekitio/config.py +0 -0
- {cachekit-0.11.1 → cachekit-0.15.0}/src/cachekit/backends/cachekitio/error_handler.py +0 -0
- {cachekit-0.11.1 → cachekit-0.15.0}/src/cachekit/backends/errors.py +0 -0
- {cachekit-0.11.1 → cachekit-0.15.0}/src/cachekit/backends/file/__init__.py +0 -0
- {cachekit-0.11.1 → cachekit-0.15.0}/src/cachekit/backends/file/config.py +0 -0
- {cachekit-0.11.1 → cachekit-0.15.0}/src/cachekit/backends/memcached/__init__.py +0 -0
- {cachekit-0.11.1 → cachekit-0.15.0}/src/cachekit/backends/memcached/config.py +0 -0
- {cachekit-0.11.1 → cachekit-0.15.0}/src/cachekit/backends/memcached/error_handler.py +0 -0
- {cachekit-0.11.1 → cachekit-0.15.0}/src/cachekit/backends/redis/__init__.py +0 -0
- {cachekit-0.11.1 → cachekit-0.15.0}/src/cachekit/backends/redis/error_handler.py +0 -0
- {cachekit-0.11.1 → cachekit-0.15.0}/src/cachekit/config/validation.py +0 -0
- {cachekit-0.11.1 → cachekit-0.15.0}/src/cachekit/decorators/__init__.py +0 -0
- {cachekit-0.11.1 → cachekit-0.15.0}/src/cachekit/decorators/local_wrapper.py +0 -0
- {cachekit-0.11.1 → cachekit-0.15.0}/src/cachekit/decorators/main.py +0 -0
- {cachekit-0.11.1 → cachekit-0.15.0}/src/cachekit/decorators/stats_context.py +0 -0
- {cachekit-0.11.1 → cachekit-0.15.0}/src/cachekit/decorators/tenant_context.py +0 -0
- {cachekit-0.11.1 → cachekit-0.15.0}/src/cachekit/decorators/utils/__init__.py +0 -0
- {cachekit-0.11.1 → cachekit-0.15.0}/src/cachekit/di.py +0 -0
- {cachekit-0.11.1 → cachekit-0.15.0}/src/cachekit/hash_utils.py +0 -0
- {cachekit-0.11.1 → cachekit-0.15.0}/src/cachekit/health.py +0 -0
- {cachekit-0.11.1 → cachekit-0.15.0}/src/cachekit/hiredis_compat.py +0 -0
- {cachekit-0.11.1 → cachekit-0.15.0}/src/cachekit/imports.py +0 -0
- {cachekit-0.11.1 → cachekit-0.15.0}/src/cachekit/invalidation/__init__.py +0 -0
- {cachekit-0.11.1 → cachekit-0.15.0}/src/cachekit/invalidation/channel.py +0 -0
- {cachekit-0.11.1 → cachekit-0.15.0}/src/cachekit/invalidation/event.py +0 -0
- {cachekit-0.11.1 → cachekit-0.15.0}/src/cachekit/invalidation/redis_channel.py +0 -0
- {cachekit-0.11.1 → cachekit-0.15.0}/src/cachekit/key_generator.py +0 -0
- {cachekit-0.11.1 → cachekit-0.15.0}/src/cachekit/logging.py +0 -0
- {cachekit-0.11.1 → cachekit-0.15.0}/src/cachekit/monitoring/__init__.py +0 -0
- {cachekit-0.11.1 → cachekit-0.15.0}/src/cachekit/monitoring/correlation_tracking.py +0 -0
- {cachekit-0.11.1 → cachekit-0.15.0}/src/cachekit/monitoring/pool_monitor.py +0 -0
- {cachekit-0.11.1 → cachekit-0.15.0}/src/cachekit/monitoring/protocols.py +0 -0
- {cachekit-0.11.1 → cachekit-0.15.0}/src/cachekit/py.typed +0 -0
- {cachekit-0.11.1 → cachekit-0.15.0}/src/cachekit/reliability/async_metrics.py +0 -0
- {cachekit-0.11.1 → cachekit-0.15.0}/src/cachekit/reliability/error_classification.py +0 -0
- {cachekit-0.11.1 → cachekit-0.15.0}/src/cachekit/reliability/load_control.py +0 -0
- {cachekit-0.11.1 → cachekit-0.15.0}/src/cachekit/serializers/arrow_serializer.py +0 -0
- {cachekit-0.11.1 → cachekit-0.15.0}/src/cachekit/serializers/orjson_serializer.py +0 -0
- {cachekit-0.11.1 → cachekit-0.15.0}/src/cachekit/serializers/standard_serializer.py +0 -0
- {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.
|
|
248
|
+
version = "0.3.0"
|
|
249
249
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
250
|
-
checksum = "
|
|
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.
|
|
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.
|
|
471
|
+
version = "0.9.20"
|
|
472
472
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
473
|
-
checksum = "
|
|
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.
|
|
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
|
|
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
|
-
|
|
484
|
-
|
|
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
|
|
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
|
-
|
|
430
|
-
|
|
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.
|
|
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.
|
|
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.
|
|
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
|
|
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
|
-
|
|
430
|
-
|
|
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,
|
|
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
|
|
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
|
-
|
|
42
|
-
|
|
43
|
-
|
|
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
|
-
|
|
58
|
-
|
|
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
|
-
|
|
65
|
-
|
|
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
|
-
|
|
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
|
|
5
|
-
|
|
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.
|
|
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
|
|
123
|
-
- Not supported: HTTP (stateless),
|
|
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
|
|
291
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
+
]
|