OPEN SOURCE
software/src/protos/serialization.py
Read on this site and download the files you need.
Nichijoyaku was created using AI. A complete review by human biblical-language and biblical-studies specialists has not been established. Read alongside established translations and original-language resources.
← Back to the open-source guide
1"""Serialization utilities for Protocol Buffer-like messages.23This module provides msgpack-based serialization for proto dataclasses,4with custom handling for datetime objects and hash computation for5reproducibility tracking.6"""78from __future__ import annotations910import hashlib11from dataclasses import asdict, is_dataclass12from datetime import datetime, timezone13from enum import IntEnum14from typing import Any, TypeVar1516import msgpack1718T = TypeVar("T")192021def _encode_value(obj: Any) -> Any:22"""Recursively encode a value for msgpack serialization.2324Args:25obj: Value to encode.2627Returns:28Msgpack-compatible value.29"""30if isinstance(obj, datetime):31return {"__datetime__": True, "iso": obj.isoformat()}32elif isinstance(obj, IntEnum):33return int(obj)34elif is_dataclass(obj) and not isinstance(obj, type):35return {k: _encode_value(v) for k, v in asdict(obj).items()}36elif isinstance(obj, dict):37return {k: _encode_value(v) for k, v in obj.items()}38elif isinstance(obj, (list, tuple)):39return [_encode_value(item) for item in obj]40else:41return obj424344def _decode_value(obj: Any, expected_type: type | None = None) -> Any:45"""Recursively decode a value from msgpack deserialization.4647Args:48obj: Value to decode.49expected_type: Optional expected type for reconstruction.5051Returns:52Decoded value.53"""54if isinstance(obj, dict):55if obj.get("__datetime__"):56iso_str = obj["iso"]57# Handle both aware and naive datetimes58dt = datetime.fromisoformat(iso_str)59# Ensure timezone-aware60if dt.tzinfo is None:61dt = dt.replace(tzinfo=timezone.utc)62return dt63return {k: _decode_value(v) for k, v in obj.items()}64elif isinstance(obj, list):65return [_decode_value(item) for item in obj]66else:67return obj686970def serialize_proto(proto: Any) -> bytes:71"""Serialize a proto dataclass to msgpack bytes.7273Args:74proto: A dataclass instance to serialize.7576Returns:77Msgpack-encoded bytes.7879Raises:80TypeError: If proto is not a dataclass instance.81"""82if not is_dataclass(proto) or isinstance(proto, type):83raise TypeError(f"Expected dataclass instance, got {type(proto).__name__}")8485encoded = _encode_value(proto)86return msgpack.packb(encoded, use_bin_type=True)878889def deserialize_proto(data: bytes, proto_class: type[T]) -> T:90"""Deserialize msgpack bytes to a proto dataclass.9192Args:93data: Msgpack-encoded bytes.94proto_class: The dataclass type to instantiate.9596Returns:97Instance of proto_class populated with decoded data.9899Raises:100TypeError: If proto_class is not a dataclass.101ValueError: If data cannot be deserialized.102"""103if not is_dataclass(proto_class):104raise TypeError(f"Expected dataclass type, got {proto_class}")105106try:107decoded = msgpack.unpackb(data, raw=False)108except Exception as e:109raise ValueError(f"Failed to decode msgpack data: {e}") from e110111# Recursively decode values112decoded = _decode_value(decoded)113114# Reconstruct the dataclass115return _reconstruct_dataclass(decoded, proto_class)116117118def _reconstruct_dataclass(data: dict, proto_class: type[T]) -> T:119"""Reconstruct a dataclass from decoded data.120121Args:122data: Decoded dictionary data.123proto_class: The dataclass type to instantiate.124125Returns:126Instance of proto_class.127"""128import dataclasses129import types130import typing131132if not isinstance(data, dict):133raise ValueError(f"Expected dict, got {type(data).__name__}")134135# Get field types for nested reconstruction, with type hints resolved136type_hints = typing.get_type_hints(proto_class)137fields = {f.name: f for f in dataclasses.fields(proto_class)}138kwargs = {}139140for name, value in data.items():141if name not in fields:142continue143144# Use resolved type hints instead of field.type to handle forward refs145field_type = type_hints.get(name, fields[name].type)146147# Handle string type annotations that couldn't be resolved148if isinstance(field_type, str):149kwargs[name] = value150continue151152# Handle Optional types and Union types (X | None)153origin = typing.get_origin(field_type)154args = typing.get_args(field_type)155156# Check for Union types (typing.Union or types.UnionType for X | Y syntax)157is_union = origin is typing.Union or isinstance(field_type, types.UnionType)158if is_union:159# Get the actual type from Optional[X] or X | None160# A Union always has args, and at least one is not NoneType161# (`None | None` collapses to NoneType, which is not a Union).162if args: # pragma: no branch163# Filter out NoneType164non_none_types = [t for t in args if t is not type(None)]165if non_none_types: # pragma: no branch166field_type = non_none_types[0]167# Re-evaluate origin for the actual type168origin = typing.get_origin(field_type)169args = typing.get_args(field_type)170171# Recursively reconstruct nested dataclasses172if value is not None and is_dataclass(field_type) and isinstance(value, dict):173value = _reconstruct_dataclass(value, field_type)174elif origin is list and isinstance(value, list) and value:175# Check if it's a list of dataclasses176if args:177list_item_type = args[0]178if is_dataclass(list_item_type):179value = [180_reconstruct_dataclass(item, list_item_type)181if isinstance(item, dict)182else item183for item in value184]185186kwargs[name] = value187188return proto_class(**kwargs)189190191def compute_proto_hash(proto: Any) -> str:192"""Compute a SHA256 hash of a serialized proto.193194The hash is computed from the msgpack-serialized bytes, providing195a deterministic identifier for the proto contents.196197Args:198proto: A dataclass instance to hash.199200Returns:201Hex-encoded SHA256 hash string.202203Raises:204TypeError: If proto is not a dataclass instance.205"""206serialized = serialize_proto(proto)207return hashlib.sha256(serialized).hexdigest()208