cachekit 0.10.0__tar.gz → 0.11.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.10.0 → cachekit-0.11.0}/Cargo.lock +1 -1
- {cachekit-0.10.0 → cachekit-0.11.0}/PKG-INFO +5 -4
- {cachekit-0.10.0 → cachekit-0.11.0}/README.md +1 -1
- {cachekit-0.10.0 → cachekit-0.11.0}/pyproject.toml +9 -3
- {cachekit-0.10.0 → cachekit-0.11.0}/rust/Cargo.toml +1 -1
- {cachekit-0.10.0 → cachekit-0.11.0}/rust/README.md +1 -1
- {cachekit-0.10.0 → cachekit-0.11.0}/src/cachekit/__init__.py +1 -1
- {cachekit-0.10.0 → cachekit-0.11.0}/src/cachekit/cache_handler.py +6 -1
- {cachekit-0.10.0 → cachekit-0.11.0}/src/cachekit/serializers/__init__.py +30 -6
- {cachekit-0.10.0 → cachekit-0.11.0}/src/cachekit/serializers/auto_serializer.py +64 -31
- {cachekit-0.10.0 → cachekit-0.11.0}/src/cachekit/serializers/orjson_serializer.py +6 -1
- {cachekit-0.10.0 → cachekit-0.11.0}/Cargo.toml +0 -0
- {cachekit-0.10.0 → cachekit-0.11.0}/LICENSE +0 -0
- {cachekit-0.10.0 → cachekit-0.11.0}/rust/Makefile +0 -0
- {cachekit-0.10.0 → cachekit-0.11.0}/rust/TEST_EXPANSION_SUMMARY.md +0 -0
- {cachekit-0.10.0 → cachekit-0.11.0}/rust/src/lib.rs +0 -0
- {cachekit-0.10.0 → cachekit-0.11.0}/rust/src/python_bindings.rs +0 -0
- {cachekit-0.10.0 → cachekit-0.11.0}/rust/supply-chain/audits.toml +0 -0
- {cachekit-0.10.0 → cachekit-0.11.0}/rust/supply-chain/config.toml +0 -0
- {cachekit-0.10.0 → cachekit-0.11.0}/rust/supply-chain/imports.lock +0 -0
- {cachekit-0.10.0 → cachekit-0.11.0}/rust/tsan_suppressions.txt +0 -0
- {cachekit-0.10.0 → cachekit-0.11.0}/src/cachekit/backends/__init__.py +0 -0
- {cachekit-0.10.0 → cachekit-0.11.0}/src/cachekit/backends/base.py +0 -0
- {cachekit-0.10.0 → cachekit-0.11.0}/src/cachekit/backends/base_config.py +0 -0
- {cachekit-0.10.0 → cachekit-0.11.0}/src/cachekit/backends/cachekitio/__init__.py +0 -0
- {cachekit-0.10.0 → cachekit-0.11.0}/src/cachekit/backends/cachekitio/backend.py +0 -0
- {cachekit-0.10.0 → cachekit-0.11.0}/src/cachekit/backends/cachekitio/client.py +0 -0
- {cachekit-0.10.0 → cachekit-0.11.0}/src/cachekit/backends/cachekitio/config.py +0 -0
- {cachekit-0.10.0 → cachekit-0.11.0}/src/cachekit/backends/cachekitio/error_handler.py +0 -0
- {cachekit-0.10.0 → cachekit-0.11.0}/src/cachekit/backends/cachekitio/session.py +0 -0
- {cachekit-0.10.0 → cachekit-0.11.0}/src/cachekit/backends/errors.py +0 -0
- {cachekit-0.10.0 → cachekit-0.11.0}/src/cachekit/backends/file/__init__.py +0 -0
- {cachekit-0.10.0 → cachekit-0.11.0}/src/cachekit/backends/file/backend.py +0 -0
- {cachekit-0.10.0 → cachekit-0.11.0}/src/cachekit/backends/file/config.py +0 -0
- {cachekit-0.10.0 → cachekit-0.11.0}/src/cachekit/backends/memcached/__init__.py +0 -0
- {cachekit-0.10.0 → cachekit-0.11.0}/src/cachekit/backends/memcached/backend.py +0 -0
- {cachekit-0.10.0 → cachekit-0.11.0}/src/cachekit/backends/memcached/config.py +0 -0
- {cachekit-0.10.0 → cachekit-0.11.0}/src/cachekit/backends/memcached/error_handler.py +0 -0
- {cachekit-0.10.0 → cachekit-0.11.0}/src/cachekit/backends/provider.py +0 -0
- {cachekit-0.10.0 → cachekit-0.11.0}/src/cachekit/backends/redis/__init__.py +0 -0
- {cachekit-0.10.0 → cachekit-0.11.0}/src/cachekit/backends/redis/backend.py +0 -0
- {cachekit-0.10.0 → cachekit-0.11.0}/src/cachekit/backends/redis/client.py +0 -0
- {cachekit-0.10.0 → cachekit-0.11.0}/src/cachekit/backends/redis/config.py +0 -0
- {cachekit-0.10.0 → cachekit-0.11.0}/src/cachekit/backends/redis/error_handler.py +0 -0
- {cachekit-0.10.0 → cachekit-0.11.0}/src/cachekit/backends/redis/provider.py +0 -0
- {cachekit-0.10.0 → cachekit-0.11.0}/src/cachekit/config/__init__.py +0 -0
- {cachekit-0.10.0 → cachekit-0.11.0}/src/cachekit/config/decorator.py +0 -0
- {cachekit-0.10.0 → cachekit-0.11.0}/src/cachekit/config/nested.py +0 -0
- {cachekit-0.10.0 → cachekit-0.11.0}/src/cachekit/config/settings.py +0 -0
- {cachekit-0.10.0 → cachekit-0.11.0}/src/cachekit/config/singleton.py +0 -0
- {cachekit-0.10.0 → cachekit-0.11.0}/src/cachekit/config/validation.py +0 -0
- {cachekit-0.10.0 → cachekit-0.11.0}/src/cachekit/decorators/__init__.py +0 -0
- {cachekit-0.10.0 → cachekit-0.11.0}/src/cachekit/decorators/intent.py +0 -0
- {cachekit-0.10.0 → cachekit-0.11.0}/src/cachekit/decorators/local_wrapper.py +0 -0
- {cachekit-0.10.0 → cachekit-0.11.0}/src/cachekit/decorators/main.py +0 -0
- {cachekit-0.10.0 → cachekit-0.11.0}/src/cachekit/decorators/orchestrator.py +0 -0
- {cachekit-0.10.0 → cachekit-0.11.0}/src/cachekit/decorators/session.py +0 -0
- {cachekit-0.10.0 → cachekit-0.11.0}/src/cachekit/decorators/stats_context.py +0 -0
- {cachekit-0.10.0 → cachekit-0.11.0}/src/cachekit/decorators/tenant_context.py +0 -0
- {cachekit-0.10.0 → cachekit-0.11.0}/src/cachekit/decorators/utils/__init__.py +0 -0
- {cachekit-0.10.0 → cachekit-0.11.0}/src/cachekit/decorators/wrapper.py +0 -0
- {cachekit-0.10.0 → cachekit-0.11.0}/src/cachekit/di.py +0 -0
- {cachekit-0.10.0 → cachekit-0.11.0}/src/cachekit/hash_utils.py +0 -0
- {cachekit-0.10.0 → cachekit-0.11.0}/src/cachekit/health.py +0 -0
- {cachekit-0.10.0 → cachekit-0.11.0}/src/cachekit/hiredis_compat.py +0 -0
- {cachekit-0.10.0 → cachekit-0.11.0}/src/cachekit/imports.py +0 -0
- {cachekit-0.10.0 → cachekit-0.11.0}/src/cachekit/invalidation/__init__.py +0 -0
- {cachekit-0.10.0 → cachekit-0.11.0}/src/cachekit/invalidation/channel.py +0 -0
- {cachekit-0.10.0 → cachekit-0.11.0}/src/cachekit/invalidation/event.py +0 -0
- {cachekit-0.10.0 → cachekit-0.11.0}/src/cachekit/invalidation/redis_channel.py +0 -0
- {cachekit-0.10.0 → cachekit-0.11.0}/src/cachekit/key_generator.py +0 -0
- {cachekit-0.10.0 → cachekit-0.11.0}/src/cachekit/l1_cache.py +0 -0
- {cachekit-0.10.0 → cachekit-0.11.0}/src/cachekit/logging.py +0 -0
- {cachekit-0.10.0 → cachekit-0.11.0}/src/cachekit/monitoring/__init__.py +0 -0
- {cachekit-0.10.0 → cachekit-0.11.0}/src/cachekit/monitoring/correlation_tracking.py +0 -0
- {cachekit-0.10.0 → cachekit-0.11.0}/src/cachekit/monitoring/pool_monitor.py +0 -0
- {cachekit-0.10.0 → cachekit-0.11.0}/src/cachekit/monitoring/protocols.py +0 -0
- {cachekit-0.10.0 → cachekit-0.11.0}/src/cachekit/object_cache.py +0 -0
- {cachekit-0.10.0 → cachekit-0.11.0}/src/cachekit/py.typed +0 -0
- {cachekit-0.10.0 → cachekit-0.11.0}/src/cachekit/reliability/__init__.py +0 -0
- {cachekit-0.10.0 → cachekit-0.11.0}/src/cachekit/reliability/adaptive_timeout.py +0 -0
- {cachekit-0.10.0 → cachekit-0.11.0}/src/cachekit/reliability/async_metrics.py +0 -0
- {cachekit-0.10.0 → cachekit-0.11.0}/src/cachekit/reliability/circuit_breaker.py +0 -0
- {cachekit-0.10.0 → cachekit-0.11.0}/src/cachekit/reliability/error_classification.py +0 -0
- {cachekit-0.10.0 → cachekit-0.11.0}/src/cachekit/reliability/load_control.py +0 -0
- {cachekit-0.10.0 → cachekit-0.11.0}/src/cachekit/reliability/metrics_collection.py +0 -0
- {cachekit-0.10.0 → cachekit-0.11.0}/src/cachekit/reliability/profiles.py +0 -0
- {cachekit-0.10.0 → cachekit-0.11.0}/src/cachekit/serializers/arrow_serializer.py +0 -0
- {cachekit-0.10.0 → cachekit-0.11.0}/src/cachekit/serializers/base.py +0 -0
- {cachekit-0.10.0 → cachekit-0.11.0}/src/cachekit/serializers/encryption_wrapper.py +0 -0
- {cachekit-0.10.0 → cachekit-0.11.0}/src/cachekit/serializers/standard_serializer.py +0 -0
- {cachekit-0.10.0 → cachekit-0.11.0}/src/cachekit/serializers/wrapper.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: cachekit
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.11.0
|
|
4
4
|
Classifier: Development Status :: 3 - Alpha
|
|
5
5
|
Classifier: Intended Audience :: Developers
|
|
6
6
|
Classifier: License :: OSI Approved :: MIT License
|
|
@@ -26,15 +26,16 @@ Requires-Dist: tenacity>=8.0.0
|
|
|
26
26
|
Requires-Dist: prometheus-client>=0.22.1
|
|
27
27
|
Requires-Dist: psutil>=7.0.0
|
|
28
28
|
Requires-Dist: blake3>=1.0.5
|
|
29
|
-
Requires-Dist: msgpack>=1.1
|
|
30
|
-
Requires-Dist: orjson>=3.9.0
|
|
29
|
+
Requires-Dist: msgpack>=1.2.1
|
|
31
30
|
Requires-Dist: xxhash>=3.5.0
|
|
32
31
|
Requires-Dist: httpx[http2]>=0.28.1
|
|
33
32
|
Requires-Dist: numpy>=2.0.2 ; extra == 'data'
|
|
34
33
|
Requires-Dist: pandas>=1.3.0 ; extra == 'data'
|
|
35
34
|
Requires-Dist: pyarrow>=21.0.0 ; extra == 'data'
|
|
35
|
+
Requires-Dist: orjson>=3.9.0 ; extra == 'json'
|
|
36
36
|
Requires-Dist: pymemcache>=4.0.0 ; extra == 'memcached'
|
|
37
37
|
Provides-Extra: data
|
|
38
|
+
Provides-Extra: json
|
|
38
39
|
Provides-Extra: memcached
|
|
39
40
|
License-File: LICENSE
|
|
40
41
|
Summary: Production-ready Redis caching for Python with intelligent reliability features and Rust-powered performance
|
|
@@ -302,7 +303,7 @@ def test_cached_function():
|
|
|
302
303
|
| Serializer | Speed | Use Case |
|
|
303
304
|
|:-----------|:-----:|:---------|
|
|
304
305
|
| **StandardSerializer** | ★★★★☆ | General Python types, NumPy, Pandas |
|
|
305
|
-
| **OrjsonSerializer** | ★★★★★ | JSON APIs (2-5x faster than stdlib) |
|
|
306
|
+
| **OrjsonSerializer** | ★★★★★ | JSON APIs (2-5x faster than stdlib) — requires `cachekit[json]` |
|
|
306
307
|
| **ArrowSerializer** | ★★★★★ | Large DataFrames (6-23x faster for 10K+ rows) |
|
|
307
308
|
| **EncryptionWrapper** | ★★★★☆ | Wraps any serializer with AES-256-GCM |
|
|
308
309
|
|
|
@@ -249,7 +249,7 @@ def test_cached_function():
|
|
|
249
249
|
| Serializer | Speed | Use Case |
|
|
250
250
|
|:-----------|:-----:|:---------|
|
|
251
251
|
| **StandardSerializer** | ★★★★☆ | General Python types, NumPy, Pandas |
|
|
252
|
-
| **OrjsonSerializer** | ★★★★★ | JSON APIs (2-5x faster than stdlib) |
|
|
252
|
+
| **OrjsonSerializer** | ★★★★★ | JSON APIs (2-5x faster than stdlib) — requires `cachekit[json]` |
|
|
253
253
|
| **ArrowSerializer** | ★★★★★ | Large DataFrames (6-23x faster for 10K+ rows) |
|
|
254
254
|
| **EncryptionWrapper** | ★★★★☆ | Wraps any serializer with AES-256-GCM |
|
|
255
255
|
|
|
@@ -4,7 +4,7 @@ build-backend = "maturin"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "cachekit"
|
|
7
|
-
version = "0.
|
|
7
|
+
version = "0.11.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"}
|
|
@@ -64,8 +64,7 @@ dependencies = [
|
|
|
64
64
|
"psutil>=7.0.0",
|
|
65
65
|
# Serialization and hashing
|
|
66
66
|
"blake3>=1.0.5",
|
|
67
|
-
"msgpack>=1.1
|
|
68
|
-
"orjson>=3.9.0",
|
|
67
|
+
"msgpack>=1.2.1",
|
|
69
68
|
"xxhash>=3.5.0",
|
|
70
69
|
# HTTP client for SaaS backend (cachekit.io)
|
|
71
70
|
"httpx[http2]>=0.28.1",
|
|
@@ -78,6 +77,10 @@ data = [
|
|
|
78
77
|
"pandas>=1.3.0",
|
|
79
78
|
"pyarrow>=21.0.0",
|
|
80
79
|
]
|
|
80
|
+
json = [
|
|
81
|
+
# OrjsonSerializer (serializer="orjson") — fast JSON via orjson
|
|
82
|
+
"orjson>=3.9.0",
|
|
83
|
+
]
|
|
81
84
|
memcached = [
|
|
82
85
|
"pymemcache>=4.0.0",
|
|
83
86
|
]
|
|
@@ -228,6 +231,9 @@ dev = [
|
|
|
228
231
|
"numpy>=2.0.2",
|
|
229
232
|
"pandas>=1.3.0",
|
|
230
233
|
"pyarrow>=21.0.0",
|
|
234
|
+
# OrjsonSerializer support — now the [json] optional extra; kept here so the
|
|
235
|
+
# orjson tests, doctests, and markdown-docs still resolve it in dev/CI.
|
|
236
|
+
"orjson>=3.9.0",
|
|
231
237
|
"pytest-xdist>=3.8.0",
|
|
232
238
|
"time-machine>=2.19.0",
|
|
233
239
|
]
|
|
@@ -249,7 +249,7 @@ def test_cached_function():
|
|
|
249
249
|
| Serializer | Speed | Use Case |
|
|
250
250
|
|:-----------|:-----:|:---------|
|
|
251
251
|
| **StandardSerializer** | ★★★★☆ | General Python types, NumPy, Pandas |
|
|
252
|
-
| **OrjsonSerializer** | ★★★★★ | JSON APIs (2-5x faster than stdlib) |
|
|
252
|
+
| **OrjsonSerializer** | ★★★★★ | JSON APIs (2-5x faster than stdlib) — requires `cachekit[json]` |
|
|
253
253
|
| **ArrowSerializer** | ★★★★★ | Large DataFrames (6-23x faster for 10K+ rows) |
|
|
254
254
|
| **EncryptionWrapper** | ★★★★☆ | Wraps any serializer with AES-256-GCM |
|
|
255
255
|
|
|
@@ -32,6 +32,11 @@ if TYPE_CHECKING:
|
|
|
32
32
|
# Python-specific type tags that no other-language SDK can decode.
|
|
33
33
|
CROSS_SDK_SERIALIZER_NAMES = ("default", "std", "standard", "orjson", "arrow")
|
|
34
34
|
|
|
35
|
+
# Serializer-name aliases collapsed to one canonical frame tag so interchangeable names stay
|
|
36
|
+
# cache-compatible: an entry written as 'auto' must read back under 'pythonic' (its documented
|
|
37
|
+
# alias) and vice-versa, instead of a serializer-mismatch that recomputes on every read (#167).
|
|
38
|
+
_SERIALIZER_NAME_ALIASES = {"std": "default", "standard": "default", "pythonic": "auto"}
|
|
39
|
+
|
|
35
40
|
# Global DI container instance with default registrations
|
|
36
41
|
container = DIContainer()
|
|
37
42
|
container.register(LoggerProvider, DefaultLoggerProvider)
|
|
@@ -348,7 +353,7 @@ class CacheSerializationHandler:
|
|
|
348
353
|
# Extract string name for metadata storage (for protocol instances, use class name)
|
|
349
354
|
if isinstance(serializer_name, str):
|
|
350
355
|
# Canonicalize aliases to prevent envelope mismatch on deserialize
|
|
351
|
-
self._serializer_string_name =
|
|
356
|
+
self._serializer_string_name = _SERIALIZER_NAME_ALIASES.get(serializer_name, serializer_name)
|
|
352
357
|
else:
|
|
353
358
|
# Protocol instance - use class name for metadata
|
|
354
359
|
self._serializer_string_name = type(serializer_name).__name__
|
|
@@ -15,11 +15,11 @@ from .base import (
|
|
|
15
15
|
SerializerProtocol,
|
|
16
16
|
)
|
|
17
17
|
from .encryption_wrapper import EncryptionWrapper
|
|
18
|
-
from .orjson_serializer import OrjsonSerializer
|
|
19
18
|
from .standard_serializer import StandardSerializer
|
|
20
19
|
|
|
21
20
|
if TYPE_CHECKING:
|
|
22
21
|
from .arrow_serializer import ArrowSerializer
|
|
22
|
+
from .orjson_serializer import OrjsonSerializer
|
|
23
23
|
|
|
24
24
|
logger = logging.getLogger(__name__)
|
|
25
25
|
|
|
@@ -37,6 +37,20 @@ def _get_arrow_serializer() -> type:
|
|
|
37
37
|
return _ArrowSerializer
|
|
38
38
|
|
|
39
39
|
|
|
40
|
+
# Lazy import for optional OrjsonSerializer (requires orjson from [json] extra)
|
|
41
|
+
_OrjsonSerializer: type | None = None
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def _get_orjson_serializer() -> type:
|
|
45
|
+
"""Lazy-load OrjsonSerializer. Raises ImportError if orjson not installed."""
|
|
46
|
+
global _OrjsonSerializer
|
|
47
|
+
if _OrjsonSerializer is None:
|
|
48
|
+
from .orjson_serializer import OrjsonSerializer
|
|
49
|
+
|
|
50
|
+
_OrjsonSerializer = OrjsonSerializer
|
|
51
|
+
return _OrjsonSerializer
|
|
52
|
+
|
|
53
|
+
|
|
40
54
|
# Validate ByteStorage works correctly
|
|
41
55
|
test_storage = ByteStorage("msgpack")
|
|
42
56
|
test_data = b"test validation data"
|
|
@@ -57,7 +71,7 @@ SERIALIZER_REGISTRY = {
|
|
|
57
71
|
"default": StandardSerializer, # Language-agnostic MessagePack for multi-language caches
|
|
58
72
|
"std": StandardSerializer, # Explicit StandardSerializer alias
|
|
59
73
|
"arrow": None, # Lazy-loaded: requires pyarrow from [data] extra
|
|
60
|
-
"orjson":
|
|
74
|
+
"orjson": None, # Lazy-loaded: requires orjson from [json] extra
|
|
61
75
|
"encrypted": EncryptionWrapper, # StandardSerializer + AES-256-GCM encryption
|
|
62
76
|
}
|
|
63
77
|
|
|
@@ -116,9 +130,11 @@ def get_serializer(name: str, enable_integrity_checking: bool = True) -> Seriali
|
|
|
116
130
|
f"@cache(serializer=MySerializer())"
|
|
117
131
|
)
|
|
118
132
|
|
|
119
|
-
# Get serializer class (lazy-load
|
|
133
|
+
# Get serializer class (lazy-load optional serializers if needed)
|
|
120
134
|
if name == "arrow":
|
|
121
135
|
serializer_class = _get_arrow_serializer()
|
|
136
|
+
elif name == "orjson":
|
|
137
|
+
serializer_class = _get_orjson_serializer()
|
|
122
138
|
else:
|
|
123
139
|
serializer_class = SERIALIZER_REGISTRY[name]
|
|
124
140
|
|
|
@@ -177,9 +193,15 @@ def get_serializer_info() -> dict[str, dict[str, Any]]:
|
|
|
177
193
|
if hasattr(instance, "get_info"):
|
|
178
194
|
info[name].update(instance.get_info()) # type: ignore[attr-defined]
|
|
179
195
|
except ImportError as e:
|
|
196
|
+
# Optional serializer whose backing dependency (pyarrow / orjson) is absent.
|
|
197
|
+
optional_modules = {
|
|
198
|
+
"arrow": ("ArrowSerializer", "cachekit.serializers.arrow_serializer"),
|
|
199
|
+
"orjson": ("OrjsonSerializer", "cachekit.serializers.orjson_serializer"),
|
|
200
|
+
}
|
|
201
|
+
cls, module = optional_modules.get(name, ("Unknown", "unknown"))
|
|
180
202
|
info[name] = {
|
|
181
|
-
"class":
|
|
182
|
-
"module":
|
|
203
|
+
"class": cls,
|
|
204
|
+
"module": module,
|
|
183
205
|
"available": False,
|
|
184
206
|
"error": str(e),
|
|
185
207
|
}
|
|
@@ -194,9 +216,11 @@ def get_serializer_info() -> dict[str, dict[str, Any]]:
|
|
|
194
216
|
|
|
195
217
|
|
|
196
218
|
def __getattr__(name: str) -> Any:
|
|
197
|
-
"""Lazy attribute access for optional ArrowSerializer."""
|
|
219
|
+
"""Lazy attribute access for optional ArrowSerializer / OrjsonSerializer."""
|
|
198
220
|
if name == "ArrowSerializer":
|
|
199
221
|
return _get_arrow_serializer()
|
|
222
|
+
if name == "OrjsonSerializer":
|
|
223
|
+
return _get_orjson_serializer()
|
|
200
224
|
raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
|
|
201
225
|
|
|
202
226
|
|
|
@@ -26,6 +26,7 @@ from typing import TYPE_CHECKING, Any, ClassVar, Optional
|
|
|
26
26
|
from uuid import UUID
|
|
27
27
|
|
|
28
28
|
import msgpack
|
|
29
|
+
import xxhash
|
|
29
30
|
|
|
30
31
|
if TYPE_CHECKING:
|
|
31
32
|
import numpy as np
|
|
@@ -295,7 +296,8 @@ def _auto_object_hook(obj: Any) -> Any:
|
|
|
295
296
|
raise SerializationError("Cannot deserialize numpy array: numpy is not installed")
|
|
296
297
|
if "data" not in obj or "shape" not in obj or "dtype" not in obj:
|
|
297
298
|
raise SerializationError("Invalid ndarray format: missing required fields in cached data")
|
|
298
|
-
|
|
299
|
+
# .copy(): writable result that does not alias the source buffer (the L1-cached bytes on a hit) — #157.
|
|
300
|
+
return np.frombuffer(obj["data"], dtype=obj["dtype"]).reshape(obj["shape"]).copy()
|
|
299
301
|
|
|
300
302
|
return obj
|
|
301
303
|
|
|
@@ -492,15 +494,19 @@ class AutoSerializer:
|
|
|
492
494
|
"""
|
|
493
495
|
# coerce unwrap's zero-copy memoryview; no-op when already bytes (enables .startswith below + Rust retrieve)
|
|
494
496
|
data = bytes(data)
|
|
495
|
-
#
|
|
496
|
-
|
|
497
|
+
# Custom NumPy format — raw [NUMPY_RAW...] or checksummed [8-byte xxHash3-64][NUMPY_RAW...].
|
|
498
|
+
# Detect by structure (like ArrowSerializer) so it is caught here, before the lossy
|
|
499
|
+
# retrieve()/msgpack fallback below — _deserialize_numpy strips + verifies the optional
|
|
500
|
+
# checksum and fails closed on mismatch (#155), even when no metadata is supplied.
|
|
501
|
+
if data.startswith(b"NUMPY_RAW") or (len(data) >= 17 and data[8:17] == b"NUMPY_RAW"):
|
|
497
502
|
return self._deserialize_numpy(data)
|
|
498
503
|
|
|
499
504
|
# Use metadata for format detection if available
|
|
500
505
|
if metadata and hasattr(metadata, "original_type"):
|
|
501
506
|
detected_format = metadata.original_type
|
|
502
507
|
|
|
503
|
-
# For specialized formats, call type-specific deserializers
|
|
508
|
+
# For specialized formats, call type-specific deserializers.
|
|
509
|
+
# _deserialize_numpy strips + verifies the optional xxHash3-64 checksum prefix itself.
|
|
504
510
|
if detected_format == "numpy":
|
|
505
511
|
return self._deserialize_numpy(data)
|
|
506
512
|
elif detected_format == "arrow":
|
|
@@ -513,31 +519,31 @@ class AutoSerializer:
|
|
|
513
519
|
"Install with: pip install 'cachekit[data]'"
|
|
514
520
|
)
|
|
515
521
|
elif detected_format == "dataframe":
|
|
516
|
-
# For DataFrame and Series, we need to handle both Rust envelope and direct data
|
|
517
522
|
if self.enable_integrity_checking and len(data) > 4:
|
|
523
|
+
# Unwrap the ByteStorage envelope. A checksum mismatch (raised by retrieve) fails
|
|
524
|
+
# closed with a clear corruption error instead of being swallowed and re-parsed as
|
|
525
|
+
# raw msgpack, which lost the diagnostic and produced a confusing error (#156).
|
|
526
|
+
# The unpack/build sits OUTSIDE this guard so a genuine post-retrieve error surfaces
|
|
527
|
+
# as itself rather than being mistaken for corruption.
|
|
518
528
|
try:
|
|
519
|
-
# Try Rust envelope first
|
|
520
529
|
original_data, _ = self._byte_storage.retrieve(data)
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
# Direct msgpack data
|
|
530
|
+
except (ValueError, SerializationError) as e:
|
|
531
|
+
raise SerializationError(f"DataFrame integrity check failed (corrupted cache entry): {e}") from e
|
|
532
|
+
unpacked_data = msgpack.unpackb(original_data, **self._msgpack_unpack_opts)
|
|
533
|
+
return self._deserialize_dataframe(unpacked_data)
|
|
534
|
+
# Integrity off: data is direct msgpack (no envelope)
|
|
527
535
|
unpacked_data = msgpack.unpackb(data, **self._msgpack_unpack_opts)
|
|
528
536
|
return self._deserialize_dataframe(unpacked_data)
|
|
529
537
|
elif detected_format == "series":
|
|
530
|
-
# For DataFrame and Series, we need to handle both Rust envelope and direct data
|
|
531
538
|
if self.enable_integrity_checking and len(data) > 4:
|
|
539
|
+
# Same fail-closed contract as the DataFrame branch above (#156).
|
|
532
540
|
try:
|
|
533
|
-
# Try Rust envelope first
|
|
534
541
|
original_data, _ = self._byte_storage.retrieve(data)
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
# Direct msgpack data
|
|
542
|
+
except (ValueError, SerializationError) as e:
|
|
543
|
+
raise SerializationError(f"Series integrity check failed (corrupted cache entry): {e}") from e
|
|
544
|
+
unpacked_data = msgpack.unpackb(original_data, **self._msgpack_unpack_opts)
|
|
545
|
+
return self._deserialize_series(unpacked_data)
|
|
546
|
+
# Integrity off: data is direct msgpack (no envelope)
|
|
541
547
|
unpacked_data = msgpack.unpackb(data, **self._msgpack_unpack_opts)
|
|
542
548
|
return self._deserialize_series(unpacked_data)
|
|
543
549
|
|
|
@@ -603,17 +609,20 @@ class AutoSerializer:
|
|
|
603
609
|
return self._deserialize_numpy(data)
|
|
604
610
|
|
|
605
611
|
def _serialize_numpy(self, arr: np.ndarray) -> bytes: # type: ignore[name-defined]
|
|
606
|
-
"""Serialize NumPy array
|
|
612
|
+
"""Serialize a NumPy array into the ``NUMPY_RAW`` binary format.
|
|
607
613
|
|
|
608
614
|
Requires: numpy installed (HAS_NUMPY=True)
|
|
609
615
|
|
|
610
616
|
Raises:
|
|
611
617
|
RuntimeError: If numpy not installed
|
|
612
618
|
|
|
613
|
-
|
|
614
|
-
-
|
|
615
|
-
|
|
616
|
-
|
|
619
|
+
When ``enable_integrity_checking`` is on (the default / ``@cache``), an 8-byte
|
|
620
|
+
xxHash3-64 checksum is prepended to the ``NUMPY_RAW`` payload with NO compression,
|
|
621
|
+
mirroring ``ArrowSerializer`` ([checksum][payload]). The numpy branch used to return
|
|
622
|
+
these bytes *unchecked*, so a corrupted entry was reconstructed as silently-wrong data
|
|
623
|
+
on read (#155). Compression is deliberately skipped: numpy is large, often-incompressible
|
|
624
|
+
binary, and LZ4 here measured ~100x slower with no size benefit. When integrity is off
|
|
625
|
+
(``@cache.minimal``), the raw payload is returned without a checksum (msgpack-off parity).
|
|
617
626
|
"""
|
|
618
627
|
if not HAS_NUMPY:
|
|
619
628
|
raise RuntimeError("NumPy not installed. Install with: pip install cachekit[data]")
|
|
@@ -627,7 +636,18 @@ class AutoSerializer:
|
|
|
627
636
|
shape_len = len(shape_data).to_bytes(2, byteorder="little")
|
|
628
637
|
|
|
629
638
|
# Combine: header + raw numpy bytes (zero-copy from NumPy)
|
|
630
|
-
|
|
639
|
+
raw = b"NUMPY_RAW" + dtype_len + dtype_str + shape_len + shape_data + arr.tobytes()
|
|
640
|
+
|
|
641
|
+
if self.enable_integrity_checking:
|
|
642
|
+
# Checksum-only envelope: prepend the 8-byte xxHash3-64 of the payload, NO compression.
|
|
643
|
+
# numpy arrays are large, often-incompressible binary; routing them through ByteStorage's
|
|
644
|
+
# LZ4 measured ~100x slower to serialize, ~400x slower to read, and inflated incompressible
|
|
645
|
+
# data ~1.56x. This mirrors ArrowSerializer's [8-byte xxHash3-64][payload] scheme, giving
|
|
646
|
+
# the #155 integrity guarantee at ~0 cost. The read side strips + verifies in
|
|
647
|
+
# _deserialize_numpy. (No LZ4 here means numpy is genuinely uncompressed, so the
|
|
648
|
+
# metadata.compressed=False set in serialize() is correct — sidesteps #166 entirely.)
|
|
649
|
+
return xxhash.xxh3_64_digest(raw) + raw
|
|
650
|
+
return raw
|
|
631
651
|
|
|
632
652
|
def _deserialize_numpy(self, data: bytes) -> np.ndarray:
|
|
633
653
|
"""Deserialize NumPy array from NUMPY_RAW binary format.
|
|
@@ -641,6 +661,16 @@ class AutoSerializer:
|
|
|
641
661
|
if not HAS_NUMPY:
|
|
642
662
|
raise RuntimeError("NumPy not installed. Install with: pip install cachekit[data]")
|
|
643
663
|
|
|
664
|
+
# Strip + verify the optional 8-byte xxHash3-64 checksum prefix written by integrity-on
|
|
665
|
+
# serialization. Detect by structure (like ArrowSerializer): a checksummed entry is
|
|
666
|
+
# [8-byte checksum][NUMPY_RAW...]; a raw entry (integrity-off / legacy) is [NUMPY_RAW...].
|
|
667
|
+
# A mismatch fails closed (#155) — never reconstructs the corrupted array.
|
|
668
|
+
if not data.startswith(b"NUMPY_RAW") and len(data) >= 17 and data[8:17] == b"NUMPY_RAW":
|
|
669
|
+
body = data[8:]
|
|
670
|
+
if xxhash.xxh3_64_digest(body) != data[:8]:
|
|
671
|
+
raise SerializationError("NumPy integrity check failed: xxHash3-64 checksum mismatch (corrupted cache entry)")
|
|
672
|
+
data = body
|
|
673
|
+
|
|
644
674
|
if not data.startswith(b"NUMPY_RAW"):
|
|
645
675
|
raise SerializationError("Invalid NumPy data format - expected NUMPY_RAW header")
|
|
646
676
|
|
|
@@ -666,9 +696,11 @@ class AutoSerializer:
|
|
|
666
696
|
shape.append(dim)
|
|
667
697
|
shape = tuple(shape)
|
|
668
698
|
|
|
669
|
-
# Extract raw numpy bytes and reconstruct
|
|
699
|
+
# Extract raw numpy bytes and reconstruct. .copy() so the result is writable and does
|
|
700
|
+
# not alias the source bytes (the L1-cached buffer on a hit) — see #157. frombuffer alone
|
|
701
|
+
# returns a read-only view aliasing the input.
|
|
670
702
|
raw_bytes = data[offset:]
|
|
671
|
-
arr = np.frombuffer(raw_bytes, dtype=dtype_str)
|
|
703
|
+
arr = np.frombuffer(raw_bytes, dtype=dtype_str).copy()
|
|
672
704
|
return arr.reshape(shape)
|
|
673
705
|
except (ValueError, IndexError, UnicodeDecodeError) as e:
|
|
674
706
|
raise SerializationError(f"Failed to deserialize NumPy array: {e}") from e
|
|
@@ -729,8 +761,8 @@ class AutoSerializer:
|
|
|
729
761
|
columns_data = {}
|
|
730
762
|
for col, col_info in serialized["data"].items():
|
|
731
763
|
if col_info["type"] == "numeric":
|
|
732
|
-
# Reconstruct from NumPy bytes
|
|
733
|
-
arr = np.frombuffer(col_info["data"], dtype=col_info["dtype"])
|
|
764
|
+
# Reconstruct from NumPy bytes; .copy() → writable, non-aliasing column (#157).
|
|
765
|
+
arr = np.frombuffer(col_info["data"], dtype=col_info["dtype"]).copy()
|
|
734
766
|
columns_data[col] = arr
|
|
735
767
|
else:
|
|
736
768
|
# Use object data directly
|
|
@@ -793,7 +825,8 @@ class AutoSerializer:
|
|
|
793
825
|
serialized = msgpack.unpackb(data, **self._msgpack_unpack_opts)
|
|
794
826
|
|
|
795
827
|
if serialized["type"] == "numeric":
|
|
796
|
-
values
|
|
828
|
+
# .copy() → writable Series values that do not alias the source buffer (#157).
|
|
829
|
+
values = np.frombuffer(serialized["data"], dtype=serialized["dtype"]).copy()
|
|
797
830
|
else:
|
|
798
831
|
values = serialized["data"]
|
|
799
832
|
|
|
@@ -14,7 +14,12 @@ from __future__ import annotations
|
|
|
14
14
|
|
|
15
15
|
from typing import Any, ClassVar
|
|
16
16
|
|
|
17
|
-
|
|
17
|
+
# Required dependency: orjson (fail-fast with install hint when the [json] extra is absent)
|
|
18
|
+
try:
|
|
19
|
+
import orjson
|
|
20
|
+
except ImportError as e: # pragma: no cover - only reachable without the [json] extra (behavior tested via subprocess)
|
|
21
|
+
raise ImportError("orjson is not installed. OrjsonSerializer requires the [json] extra: pip install 'cachekit[json]'") from e
|
|
22
|
+
|
|
18
23
|
import xxhash
|
|
19
24
|
|
|
20
25
|
from .base import SerializationError, SerializationFormat, SerializationMetadata
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|