lodum.bson

  1# SPDX-FileCopyrightText: 2025-present Michael R. Bernstein <zopemaven@gmail.com>
  2#
  3# SPDX-License-Identifier: Apache-2.0
  4try:
  5    import bson
  6except ImportError:
  7    bson = None  # type: ignore
  8from typing import Any, Iterator, Type, TypeVar
  9
 10from .core import Loader, BaseDumper, BaseLoader
 11from .exception import DeserializationError
 12from .internal import dump, load, DEFAULT_MAX_SIZE
 13
 14T = TypeVar("T")
 15
 16# --- Public API ---
 17
 18
 19def dumps(obj: Any) -> bytes:
 20    """
 21    Encodes a Python object to BSON bytes.
 22
 23    Args:
 24        obj: The object to encode. Must be lodum-enabled or a supported type.
 25
 26    Returns:
 27        The BSON-encoded bytes.
 28
 29    Raises:
 30        ImportError: If bson (pymongo) is not installed.
 31    """
 32    if bson is None:
 33        raise ImportError(
 34            "bson (pymongo) is required for BSON serialization. Install it with 'pip install lodum[bson]'."
 35        )
 36    dumper = BsonDumper()
 37    dumped_data = dump(obj, dumper)
 38    # BSON requires a dictionary at the root
 39    if not isinstance(dumped_data, dict):
 40        dumped_data = {"_v": dumped_data}
 41    return bson.encode(dumped_data)
 42
 43
 44def loads(cls: Type[T], bson_bytes: bytes, max_size: int = DEFAULT_MAX_SIZE) -> T:
 45    """
 46    Decodes BSON bytes into a Python object of the specified type.
 47
 48    Args:
 49        cls: The class to instantiate.
 50        bson_bytes: The BSON data to decode.
 51        max_size: Maximum allowed size of the input bytes.
 52
 53    Returns:
 54        An instance of cls populated with the decoded data.
 55
 56    Raises:
 57        DeserializationError: If the input is invalid or exceeds max_size.
 58        ImportError: If bson (pymongo) is not installed.
 59    """
 60    if len(bson_bytes) > max_size:
 61        raise DeserializationError(
 62            f"Input size ({len(bson_bytes)}) exceeds maximum allowed ({max_size})"
 63        )
 64
 65    if bson is None:
 66        raise ImportError(
 67            "bson (pymongo) is required for BSON deserialization. Install it with 'pip install lodum[bson]'."
 68        )
 69    try:
 70        data = bson.decode(bson_bytes)
 71    except Exception as e:
 72        raise DeserializationError(f"Failed to parse BSON: {e}")
 73
 74    # Check if we wrapped a primitive
 75    if "_v" in data and len(data) == 1:
 76        data = data["_v"]
 77
 78    loader = BsonLoader(data)
 79    return load(cls, loader)
 80
 81
 82# --- BSON Dumper Implementation ---
 83
 84
 85class BsonDumper(BaseDumper):
 86    pass
 87
 88
 89# --- BSON Loader Implementation ---
 90
 91
 92class BsonLoader(BaseLoader):
 93    def load_list(self) -> Iterator["Loader"]:
 94        if not isinstance(self._data, list):
 95            raise DeserializationError(
 96                f"Expected list, got {type(self._data).__name__}"
 97            )
 98        return (BsonLoader(item) for item in self._data)
 99
100    def load_dict(self) -> Iterator[tuple[str, "Loader"]]:
101        if not isinstance(self._data, dict):
102            raise DeserializationError(
103                f"Expected dict, got {type(self._data).__name__}"
104            )
105        return ((k, BsonLoader(v)) for k, v in self._data.items())
106
107    def load_bytes_value(self, value: Any) -> bytes:
108        if not isinstance(value, bytes):
109            raise DeserializationError(f"Expected bytes, got {type(value).__name__}")
110        return value
def dumps(obj: Any) -> bytes:
20def dumps(obj: Any) -> bytes:
21    """
22    Encodes a Python object to BSON bytes.
23
24    Args:
25        obj: The object to encode. Must be lodum-enabled or a supported type.
26
27    Returns:
28        The BSON-encoded bytes.
29
30    Raises:
31        ImportError: If bson (pymongo) is not installed.
32    """
33    if bson is None:
34        raise ImportError(
35            "bson (pymongo) is required for BSON serialization. Install it with 'pip install lodum[bson]'."
36        )
37    dumper = BsonDumper()
38    dumped_data = dump(obj, dumper)
39    # BSON requires a dictionary at the root
40    if not isinstance(dumped_data, dict):
41        dumped_data = {"_v": dumped_data}
42    return bson.encode(dumped_data)

Encodes a Python object to BSON bytes.

Args: obj: The object to encode. Must be lodum-enabled or a supported type.

Returns: The BSON-encoded bytes.

Raises: ImportError: If bson (pymongo) is not installed.

def loads(cls: Type[~T], bson_bytes: bytes, max_size: int = 10485760) -> ~T:
45def loads(cls: Type[T], bson_bytes: bytes, max_size: int = DEFAULT_MAX_SIZE) -> T:
46    """
47    Decodes BSON bytes into a Python object of the specified type.
48
49    Args:
50        cls: The class to instantiate.
51        bson_bytes: The BSON data to decode.
52        max_size: Maximum allowed size of the input bytes.
53
54    Returns:
55        An instance of cls populated with the decoded data.
56
57    Raises:
58        DeserializationError: If the input is invalid or exceeds max_size.
59        ImportError: If bson (pymongo) is not installed.
60    """
61    if len(bson_bytes) > max_size:
62        raise DeserializationError(
63            f"Input size ({len(bson_bytes)}) exceeds maximum allowed ({max_size})"
64        )
65
66    if bson is None:
67        raise ImportError(
68            "bson (pymongo) is required for BSON deserialization. Install it with 'pip install lodum[bson]'."
69        )
70    try:
71        data = bson.decode(bson_bytes)
72    except Exception as e:
73        raise DeserializationError(f"Failed to parse BSON: {e}")
74
75    # Check if we wrapped a primitive
76    if "_v" in data and len(data) == 1:
77        data = data["_v"]
78
79    loader = BsonLoader(data)
80    return load(cls, loader)

Decodes BSON bytes into a Python object of the specified type.

Args: cls: The class to instantiate. bson_bytes: The BSON data to decode. max_size: Maximum allowed size of the input bytes.

Returns: An instance of cls populated with the decoded data.

Raises: DeserializationError: If the input is invalid or exceeds max_size. ImportError: If bson (pymongo) is not installed.

class BsonDumper(lodum.core.BaseDumper):
86class BsonDumper(BaseDumper):
87    pass

Base implementation of the Dumper protocol to reduce duplication.

class BsonLoader(lodum.core.BaseLoader):
 93class BsonLoader(BaseLoader):
 94    def load_list(self) -> Iterator["Loader"]:
 95        if not isinstance(self._data, list):
 96            raise DeserializationError(
 97                f"Expected list, got {type(self._data).__name__}"
 98            )
 99        return (BsonLoader(item) for item in self._data)
100
101    def load_dict(self) -> Iterator[tuple[str, "Loader"]]:
102        if not isinstance(self._data, dict):
103            raise DeserializationError(
104                f"Expected dict, got {type(self._data).__name__}"
105            )
106        return ((k, BsonLoader(v)) for k, v in self._data.items())
107
108    def load_bytes_value(self, value: Any) -> bytes:
109        if not isinstance(value, bytes):
110            raise DeserializationError(f"Expected bytes, got {type(value).__name__}")
111        return value

Base implementation of the Loader protocol to reduce duplication.

def load_list(self) -> Iterator[lodum.core.Loader]:
94    def load_list(self) -> Iterator["Loader"]:
95        if not isinstance(self._data, list):
96            raise DeserializationError(
97                f"Expected list, got {type(self._data).__name__}"
98            )
99        return (BsonLoader(item) for item in self._data)
def load_dict(self) -> Iterator[tuple[str, lodum.core.Loader]]:
101    def load_dict(self) -> Iterator[tuple[str, "Loader"]]:
102        if not isinstance(self._data, dict):
103            raise DeserializationError(
104                f"Expected dict, got {type(self._data).__name__}"
105            )
106        return ((k, BsonLoader(v)) for k, v in self._data.items())
def load_bytes_value(self, value: Any) -> bytes:
108    def load_bytes_value(self, value: Any) -> bytes:
109        if not isinstance(value, bytes):
110            raise DeserializationError(f"Expected bytes, got {type(value).__name__}")
111        return value