Skip to content

Commit bc80fe9

Browse files
authored
refactor: rename SerializationWrapper methods to be backend-agnostic (#28)
Remove Redis-specific naming from the generic serialization wrapper: - wrap_for_redis() → wrap() - unwrap_from_redis() → unwrap() The SerializationWrapper creates a JSON envelope with base64-encoded payload - this format is backend-agnostic and works with any cache backend (Redis, CachekitIO, Memcached, etc.). The "redis" naming was a historical artifact that incorrectly implied Redis coupling. Also updated related docstrings/comments in cache_handler.py to use "cache" instead of "Redis" for consistency. Pre-commit fix: Use venv basedpyright directly with --level error to avoid uv.lock modification and match Makefile behavior.
1 parent c9b0751 commit bc80fe9

File tree

4 files changed

+44
-27
lines changed

4 files changed

+44
-27
lines changed

.pre-commit-config.yaml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,9 +17,10 @@ repos:
1717
- repo: local
1818
hooks:
1919
# Python type checking with basedpyright
20+
# Uses venv directly to avoid uv run modifying uv.lock
2021
- id: basedpyright
2122
name: basedpyright
22-
entry: uv run basedpyright src/
23+
entry: .venv/bin/basedpyright src/ --level error
2324
language: system
2425
types: [python]
2526
pass_filenames: false

src/cachekit/cache_handler.py

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -339,7 +339,7 @@ class CacheSerializationHandler:
339339
- Tenant extraction: For multi-tenant encryption key isolation (FAIL CLOSED)
340340
341341
Modes:
342-
- encryption=False: Direct serialization (plaintext in Redis)
342+
- encryption=False: Direct serialization (plaintext in cache)
343343
- encryption=True, tenant_extractor=None: Single-tenant encrypted (nil UUID)
344344
- encryption=True, tenant_extractor provided: Multi-tenant encrypted (FAIL CLOSED)
345345
@@ -602,7 +602,7 @@ def serialize_data(
602602
kwargs: dict[str, Any] | None = None,
603603
cache_key: str = "",
604604
) -> bytes:
605-
"""Serialize data for Redis storage with optional tenant context for encryption.
605+
"""Serialize data for cache storage with optional tenant context for encryption.
606606
607607
Args:
608608
data: Data to serialize
@@ -612,7 +612,7 @@ def serialize_data(
612612
Required when encryption is enabled to prevent ciphertext substitution.
613613
614614
Returns:
615-
Serialized data wrapped for Redis storage
615+
Serialized data wrapped for cache storage
616616
617617
Raises:
618618
ValueError: If tenant extraction fails in multi-tenant mode (FAIL CLOSED)
@@ -685,7 +685,7 @@ def serialize_data(
685685

686686
# Convert metadata to dict if needed
687687
metadata_dict = metadata.to_dict() if hasattr(metadata, "to_dict") else {}
688-
return SerializationWrapper.wrap_for_redis(serialized_data, metadata_dict, self._serializer_string_name)
688+
return SerializationWrapper.wrap(serialized_data, metadata_dict, self._serializer_string_name)
689689
except ValueError:
690690
# Tenant extraction or cache_key missing - FAIL CLOSED (re-raise, don't catch)
691691
# This is a security violation: encryption requires valid tenant_id and cache_key
@@ -696,10 +696,10 @@ def serialize_data(
696696
raise SerializationError(f"Failed to serialize data with {self.serializer_name}: {e}") from e
697697

698698
def deserialize_data(self, data: str | bytes, cache_key: str = "") -> Any:
699-
"""Deserialize data from Redis storage with cache_key verification.
699+
"""Deserialize data from cache storage with cache_key verification.
700700
701701
Args:
702-
data: Serialized data from Redis (may be encrypted)
702+
data: Serialized data from cache (may be encrypted)
703703
cache_key: Cache key for AAD verification (SECURITY CRITICAL for encrypted data).
704704
Required when data is encrypted to verify ciphertext binding.
705705
@@ -743,8 +743,8 @@ def deserialize_data(self, data: str | bytes, cache_key: str = "") -> Any:
743743
True
744744
"""
745745
try:
746-
# Unwrap Redis data envelope
747-
serialized_data, metadata_dict, serializer_name = SerializationWrapper.unwrap_from_redis(data)
746+
# Unwrap cache data envelope
747+
serialized_data, metadata_dict, serializer_name = SerializationWrapper.unwrap(data)
748748

749749
# Convert metadata
750750
serialization_metadata = _get_cached_serializer_class("metadata", "cachekit.serializers.SerializationMetadata")

src/cachekit/serializers/wrapper.py

Lines changed: 31 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
1-
"""Standard serialization wrapper for Redis storage.
1+
"""Standard serialization wrapper for cache storage.
22
33
This module provides utilities for wrapping and unwrapping data with metadata
4-
for consistent Redis serialization across the codebase.
4+
for consistent serialization across all cache backends.
55
"""
66

77
import base64
@@ -10,41 +10,54 @@
1010

1111

1212
class SerializationWrapper:
13-
"""Standard wrapper/unwrapper for Redis serialization data.
13+
"""Standard wrapper/unwrapper for cache serialization data.
1414
15-
Wraps serialized bytes with JSON envelope containing metadata for Redis storage.
15+
Wraps serialized bytes with JSON envelope containing metadata for cache storage.
1616
The envelope format enables introspection of cached data without deserialization.
1717
18+
This wrapper is backend-agnostic and works with any cache backend (Redis,
19+
CachekitIO, Memcached, etc.).
20+
1821
Examples:
1922
Wrap and unwrap data:
2023
2124
>>> data = b"serialized_bytes"
2225
>>> metadata = {"format": "msgpack", "compressed": True}
23-
>>> wrapped = SerializationWrapper.wrap_for_redis(data, metadata, "auto")
26+
>>> wrapped = SerializationWrapper.wrap(data, metadata, "auto")
2427
>>> isinstance(wrapped, bytes)
2528
True
2629
2730
Unwrap returns original data, metadata, and serializer name:
2831
29-
>>> unwrapped_data, unwrapped_meta, serializer = SerializationWrapper.unwrap_from_redis(wrapped)
32+
>>> unwrapped_data, unwrapped_meta, serializer = SerializationWrapper.unwrap(wrapped)
3033
>>> unwrapped_data == data
3134
True
3235
>>> unwrapped_meta["format"]
3336
'msgpack'
3437
>>> serializer
3538
'auto'
3639
37-
Works with string input (from Redis):
40+
Works with string input (from cache backend):
3841
3942
>>> wrapped_str = wrapped.decode("utf-8")
40-
>>> unwrapped_data, _, _ = SerializationWrapper.unwrap_from_redis(wrapped_str)
43+
>>> unwrapped_data, _, _ = SerializationWrapper.unwrap(wrapped_str)
4144
>>> unwrapped_data == data
4245
True
4346
"""
4447

4548
@staticmethod
46-
def wrap_for_redis(data: bytes, metadata: dict, serializer_name: str, version: str = "2.0") -> bytes:
47-
"""Standard wrapper for Redis storage."""
49+
def wrap(data: bytes, metadata: dict, serializer_name: str, version: str = "2.0") -> bytes:
50+
"""Wrap serialized data with metadata envelope for cache storage.
51+
52+
Args:
53+
data: Serialized bytes to wrap
54+
metadata: Serialization metadata dict (must include "format" key)
55+
serializer_name: Name of serializer used (e.g., "default", "auto")
56+
version: Envelope format version
57+
58+
Returns:
59+
JSON-encoded bytes containing base64 data and metadata
60+
"""
4861
wrapper = {
4962
"data": base64.b64encode(data).decode("ascii"),
5063
"metadata": metadata,
@@ -54,16 +67,19 @@ def wrap_for_redis(data: bytes, metadata: dict, serializer_name: str, version: s
5467
return json.dumps(wrapper, ensure_ascii=False).encode("utf-8")
5568

5669
@staticmethod
57-
def unwrap_from_redis(redis_data: Union[str, bytes]) -> tuple[bytes, dict, str]:
58-
"""Standard unwrapper for Redis data.
70+
def unwrap(wrapped_data: Union[str, bytes]) -> tuple[bytes, dict, str]:
71+
"""Unwrap data envelope from cache storage.
72+
73+
Args:
74+
wrapped_data: JSON envelope (bytes or string) from cache backend
5975
6076
Returns:
6177
tuple: (data_bytes, metadata_dict, serializer_name)
6278
"""
63-
if isinstance(redis_data, bytes):
64-
redis_data = redis_data.decode("utf-8")
79+
if isinstance(wrapped_data, bytes):
80+
wrapped_data = wrapped_data.decode("utf-8")
6581

66-
wrapper = json.loads(redis_data)
82+
wrapper = json.loads(wrapped_data)
6783
data = base64.b64decode(wrapper["data"].encode("ascii"))
6884
metadata = wrapper.get("metadata", {})
6985
serializer_name = wrapper.get("serializer", "unknown")

tests/unit/test_context_leak_regression.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -618,9 +618,9 @@ def ttl_refresh_mock_backend():
618618
# Serialize the value - returns (bytes, SerializationMetadata)
619619
serializer = StandardSerializer()
620620
raw_bytes, metadata = serializer.serialize(42)
621-
# Convert metadata to dict for wrap_for_redis (needs "format" key)
621+
# Convert metadata to dict for wrap (needs "format" key)
622622
metadata_dict = metadata.to_dict()
623-
wrapped_data = SerializationWrapper.wrap_for_redis(
623+
wrapped_data = SerializationWrapper.wrap(
624624
data=raw_bytes,
625625
metadata=metadata_dict, # Must include "format" key from actual metadata
626626
serializer_name="default", # Match StandardSerializer
@@ -810,7 +810,7 @@ def backend_without_ttl():
810810
serializer = StandardSerializer()
811811
raw_bytes, metadata = serializer.serialize(42)
812812
metadata_dict = metadata.to_dict()
813-
wrapped_data = SerializationWrapper.wrap_for_redis(
813+
wrapped_data = SerializationWrapper.wrap(
814814
data=raw_bytes,
815815
metadata=metadata_dict,
816816
serializer_name="default",

0 commit comments

Comments
 (0)