Source code for gestaltdb.serializers
# =========================================
# 1) Serializer Interfaces and Implementations
# =========================================
import pickle
import json
import base64
def _missing_dependency_error(package_name, install_name=None, feature_name=None):
"""Build a consistent optional dependency error.
Args:
package_name: Import package that is missing.
install_name: Optional package name to show in install commands.
feature_name: Feature that requires the package.
Returns:
ImportError describing how to install the dependency.
Examples:
>>> "msgpack" in str(_missing_dependency_error("msgpack"))
True
"""
install_name = install_name or package_name
feature_name = feature_name or package_name
return ImportError(
f"Missing optional dependency '{package_name}' required for {feature_name}. "
f"Install it with `python -m pip install {install_name}` or `uv add {install_name}`."
)
[docs]
class Serializer:
"""Abstract base for serialization/deserialization."""
[docs]
def serialize(self, obj: dict) -> bytes:
"""Serialize a dictionary-like object to bytes.
Args:
obj: Object to serialize.
Returns:
Serialized bytes.
"""
raise NotImplementedError
[docs]
def deserialize(self, data: bytes) -> dict:
"""Deserialize bytes into a dictionary-like object.
Args:
data: Serialized bytes.
Returns:
Decoded object.
"""
raise NotImplementedError
[docs]
class PickleSerializer(Serializer):
"""Uses Python's pickle for serialization."""
[docs]
def serialize(self, obj: dict) -> bytes:
"""Serialize an object with pickle.
Examples:
>>> PickleSerializer().deserialize(PickleSerializer().serialize({"a": 1}))
{'a': 1}
"""
return pickle.dumps(obj)
[docs]
def deserialize(self, data: bytes) -> dict:
"""Deserialize pickle bytes.
Examples:
>>> PickleSerializer().deserialize(PickleSerializer().serialize({"a": 1}))
{'a': 1}
"""
return pickle.loads(data)
[docs]
class JSONSerializer(Serializer):
"""Uses JSON for serialization."""
[docs]
def serialize(self, obj: dict) -> bytes:
"""Serialize a JSON-compatible object.
Examples:
>>> JSONSerializer().serialize({"a": 1})
b'{"a": 1}'
"""
return json.dumps(obj).encode('utf-8')
[docs]
def deserialize(self, data: bytes) -> dict:
"""Deserialize JSON bytes.
Examples:
>>> JSONSerializer().deserialize(b'{"a": 1}')
{'a': 1}
"""
return json.loads(data.decode('utf-8'))
[docs]
class MessagePackSerializer(Serializer):
"""Uses MessagePack for serialization."""
[docs]
def serialize(self, obj: dict) -> bytes:
"""Serialize an object with MessagePack.
Raises:
ImportError: If the optional ``msgpack`` package is missing.
Examples:
>>> MessagePackSerializer().deserialize(MessagePackSerializer().serialize({"a": 1}))
{'a': 1}
"""
try:
import msgpack
except ImportError as exc:
raise _missing_dependency_error("msgpack", feature_name="MessagePackSerializer") from exc
return msgpack.packb(obj, use_bin_type=True)
[docs]
def deserialize(self, data: bytes) -> dict:
"""Deserialize MessagePack bytes.
Raises:
ImportError: If the optional ``msgpack`` package is missing.
Examples:
>>> MessagePackSerializer().deserialize(MessagePackSerializer().serialize({"a": 1}))
{'a': 1}
"""
try:
import msgpack
except ImportError as exc:
raise _missing_dependency_error("msgpack", feature_name="MessagePackSerializer") from exc
return msgpack.unpackb(data, raw=False)
[docs]
class ProtobufSerializer(Serializer):
"""Uses google.protobuf Struct for JSON-like dictionaries.
Struct does not have native integer or bytes types. This serializer tags those
values before encoding so Python dictionaries round-trip without losing them.
"""
_TYPE_KEY = "__gestaltdb_type__"
_VALUE_KEY = "value"
[docs]
def serialize(self, obj: dict) -> bytes:
"""Serialize a JSON-like dictionary with protobuf Struct.
Args:
obj: Dictionary containing JSON-like values plus tagged ints/bytes.
Returns:
Protobuf binary payload.
Raises:
ImportError: If the optional ``protobuf`` package is missing.
"""
try:
from google.protobuf import json_format, struct_pb2
except ImportError as exc:
raise _missing_dependency_error("protobuf", feature_name="ProtobufSerializer") from exc
message = struct_pb2.Struct()
json_format.ParseDict(self._to_struct_compatible(obj), message)
return message.SerializeToString()
[docs]
def deserialize(self, data: bytes) -> dict:
"""Deserialize protobuf Struct bytes.
Args:
data: Protobuf binary payload.
Returns:
Decoded dictionary.
Raises:
ImportError: If the optional ``protobuf`` package is missing.
"""
try:
from google.protobuf import json_format, struct_pb2
except ImportError as exc:
raise _missing_dependency_error("protobuf", feature_name="ProtobufSerializer") from exc
message = struct_pb2.Struct()
message.ParseFromString(data)
return self._from_struct_compatible(json_format.MessageToDict(message))
def _to_struct_compatible(self, obj):
"""Convert Python-only values into protobuf Struct-compatible values.
Args:
obj: Value to convert recursively.
Returns:
Struct-compatible value.
"""
if isinstance(obj, bytes):
return {
self._TYPE_KEY: "bytes",
self._VALUE_KEY: base64.b64encode(obj).decode("ascii"),
}
if isinstance(obj, int) and not isinstance(obj, bool):
return {
self._TYPE_KEY: "int",
self._VALUE_KEY: str(obj),
}
if isinstance(obj, dict):
return {key: self._to_struct_compatible(value) for key, value in obj.items()}
if isinstance(obj, (list, tuple)):
return [self._to_struct_compatible(value) for value in obj]
return obj
def _from_struct_compatible(self, obj):
"""Restore Python-only values from Struct-compatible tagged values.
Args:
obj: Value to convert recursively.
Returns:
Restored Python value.
"""
if isinstance(obj, dict):
if set(obj) == {self._TYPE_KEY, self._VALUE_KEY}:
value_type = obj[self._TYPE_KEY]
value = obj[self._VALUE_KEY]
if value_type == "bytes":
return base64.b64decode(value.encode("ascii"))
if value_type == "int":
return int(value)
return {key: self._from_struct_compatible(value) for key, value in obj.items()}
if isinstance(obj, list):
return [self._from_struct_compatible(value) for value in obj]
return obj