Skip to content

_validation

_validation

Shared validation helpers for frozen dataclass models.

Private module — not part of the public API. Used exclusively by __post_init__ methods in sibling model modules to enforce runtime type constraints, null-byte safety, and deep immutability.

Functions

validate_instance

validate_instance(
    value: Any, expected: type, name: str
) -> None

Raise TypeError if value is not an instance of expected.

Source code in src/bigbrotr/models/_validation.py
def validate_instance(value: Any, expected: type, name: str) -> None:
    """Raise ``TypeError`` if *value* is not an instance of *expected*."""
    if not isinstance(value, expected):
        article = "an" if expected.__name__[0] in "AEIOUaeiou" else "a"
        raise TypeError(f"{name} must be {article} {expected.__name__}, got {type(value).__name__}")

validate_timestamp

validate_timestamp(value: Any, name: str) -> None

Raise if value is not a non-negative int (bool excluded).

Source code in src/bigbrotr/models/_validation.py
def validate_timestamp(value: Any, name: str) -> None:
    """Raise if *value* is not a non-negative ``int`` (``bool`` excluded)."""
    if isinstance(value, bool) or not isinstance(value, int):
        raise TypeError(f"{name} must be an int, got {type(value).__name__}")
    if value < 0:
        raise ValueError(f"{name} must be non-negative")

validate_str_no_null

validate_str_no_null(value: Any, name: str) -> None

Raise if value is not a str or contains null bytes.

Source code in src/bigbrotr/models/_validation.py
def validate_str_no_null(value: Any, name: str) -> None:
    """Raise if *value* is not a ``str`` or contains null bytes."""
    if not isinstance(value, str):
        raise TypeError(f"{name} must be a str, got {type(value).__name__}")
    if "\x00" in value:
        raise ValueError(f"{name} contains null bytes")

validate_str_not_empty

validate_str_not_empty(value: Any, name: str) -> None

Raise if value is not a non-empty str without null bytes.

Source code in src/bigbrotr/models/_validation.py
def validate_str_not_empty(value: Any, name: str) -> None:
    """Raise if *value* is not a non-empty ``str`` without null bytes."""
    validate_str_no_null(value, name)
    if not value:
        raise ValueError(f"{name} must not be empty")

validate_mapping

validate_mapping(value: Any, name: str) -> None

Raise TypeError if value is not a Mapping.

Source code in src/bigbrotr/models/_validation.py
def validate_mapping(value: Any, name: str) -> None:
    """Raise ``TypeError`` if *value* is not a ``Mapping``."""
    if not isinstance(value, Mapping):
        raise TypeError(f"{name} must be a Mapping, got {type(value).__name__}")

sanitize_data

sanitize_data(
    obj: Any,
    name: str,
    *,
    max_depth: int = _DEFAULT_MAX_DEPTH,
    _depth: int = 0,
) -> Any

Recursively normalize an object for deterministic JSON serialization.

  • Removes None values and empty containers ({}, []).
  • Sorts dictionary keys for consistent ordering.
  • Rejects strings and dict keys containing null bytes (PostgreSQL incompatible).
  • Non-serializable types are replaced with None.

Parameters:

  • obj (Any) –

    The value to sanitize.

  • name (str) –

    Field name for error messages.

  • max_depth (int, default: _DEFAULT_MAX_DEPTH ) –

    Maximum recursion depth (defaults to 50).

  • _depth (int, default: 0 ) –

    Current recursion depth (internal use).

Returns:

  • Any

    The sanitized object, or None for unserializable values.

Raises:

  • ValueError

    If any string value or dict key contains null bytes.

Source code in src/bigbrotr/models/_validation.py
def sanitize_data(
    obj: Any,
    name: str,
    *,
    max_depth: int = _DEFAULT_MAX_DEPTH,
    _depth: int = 0,
) -> Any:
    """Recursively normalize an object for deterministic JSON serialization.

    * Removes ``None`` values and empty containers (``{}``, ``[]``).
    * Sorts dictionary keys for consistent ordering.
    * Rejects strings and dict keys containing null bytes (PostgreSQL incompatible).
    * Non-serializable types are replaced with ``None``.

    Args:
        obj: The value to sanitize.
        name: Field name for error messages.
        max_depth: Maximum recursion depth (defaults to 50).
        _depth: Current recursion depth (internal use).

    Returns:
        The sanitized object, or ``None`` for unserializable values.

    Raises:
        ValueError: If any string value or dict key contains null bytes.
    """
    if _depth > max_depth:
        return None

    if (
        obj is None
        or isinstance(obj, bool | int)
        or (isinstance(obj, float) and math.isfinite(obj))
    ):
        return obj

    if isinstance(obj, str):
        if "\x00" in obj:
            raise ValueError(f"{name} contains null bytes")
        return obj

    if isinstance(obj, Mapping):
        result: dict[str, Any] = {}
        for key in sorted(k for k in obj if isinstance(k, str)):
            if "\x00" in key:
                raise ValueError(f"{name} key contains null bytes")
            v = sanitize_data(obj[key], name, max_depth=max_depth, _depth=_depth + 1)
            if _is_empty(v):
                continue
            result[key] = v
        return result

    if isinstance(obj, list):
        result_list: list[Any] = []
        for item in obj:
            v = sanitize_data(item, name, max_depth=max_depth, _depth=_depth + 1)
            if _is_empty(v):
                continue
            result_list.append(v)
        return result_list

    if isinstance(obj, tuple):
        raise TypeError(f"{name} contains a tuple; use list for JSON-serializable sequences")

    return None

deep_freeze

deep_freeze(obj: Any) -> Any

Recursively wrap dicts with MappingProxyType to prevent mutation.

Source code in src/bigbrotr/models/_validation.py
def deep_freeze(obj: Any) -> Any:
    """Recursively wrap dicts with ``MappingProxyType`` to prevent mutation."""
    if isinstance(obj, dict):
        return MappingProxyType({k: deep_freeze(v) for k, v in obj.items()})
    if isinstance(obj, list):
        return tuple(deep_freeze(item) for item in obj)
    return obj