Source code for genro_treestore.store.serialization

# Copyright 2025 Softwell S.r.l. - Genropy Team
# SPDX-License-Identifier: Apache-2.0

"""TreeStore serialization - TYTX format support.

This module provides functions to serialize and deserialize TreeStore
hierarchies to/from TYTX format. TYTX preserves Python types (Decimal,
date, datetime, time) across serialization, eliminating manual type
conversion on both ends.

Wire Format:
    The serialized format is a dict with:
    - 'rows': List of tuples, one per node in depth-first order
    - 'paths': (optional, compact mode only) Registry mapping codes to paths

    Each row tuple contains:
    (parent, label, tag, value, attr)
    - parent: Parent path string (e.g., 'a.b') or numeric code if compact
    - label: Node's unique key within its parent (e.g., 'div_0')
    - tag: Node type from builder (e.g., 'div') or None
    - value: Node value (None for branches, actual value for leaves)
    - attr: Dict of node attributes

Two Serialization Modes:
    Normal mode (compact=False, default):
        Parent references are full path strings. More readable, compresses
        well with gzip due to repetitive path patterns.

    Compact mode (compact=True):
        Parent references are numeric codes (0, 1, 2...). Smaller without
        compression, but gzip actually makes it larger than normal mode.

    Recommendation:
        - Use normal mode (default) if you'll compress the data
        - Use compact mode only for uncompressed transmission

Datetime Handling:
    TYTX serializes all datetimes as UTC with millisecond precision.
    Naive datetimes are treated as UTC on serialization. On deserialization,
    datetimes are always returned as timezone-aware (UTC).

    For roundtrip comparison, use ``genro_tytx.utils.tytx_equivalent()``
    which handles naive vs aware UTC equivalence.

Requirements:
    Requires the genro-tytx package for type-preserving encoding/decoding.
    Install with: pip install genro-tytx

Example:
    >>> from genro_treestore import TreeStore
    >>> from decimal import Decimal
    >>>
    >>> store = TreeStore()
    >>> store.set_item('invoice.amount', Decimal('1234.56'))
    >>> store.set_item('invoice.paid', False)
    >>>
    >>> # Serialize
    >>> data = store.to_tytx()
    >>>
    >>> # Deserialize - types are preserved
    >>> restored = TreeStore.from_tytx(data)
    >>> restored['invoice.amount']  # Decimal('1234.56'), not string
"""

from __future__ import annotations

from typing import Any, Literal, TYPE_CHECKING

if TYPE_CHECKING:
    from .core import TreeStore


[docs] def to_tytx( store: TreeStore, transport: Literal["json", "msgpack"] | None = None, compact: bool = False, ) -> str | bytes: """Serialize a TreeStore to TYTX format. Converts the entire tree hierarchy into a flat list of row tuples, then encodes it using TYTX which preserves Python types (Decimal, date, datetime, time) in the wire format. Args: store: The TreeStore to serialize. transport: Output format: - None or 'json': JSON string (default). Human-readable, compresses very well with gzip. - 'msgpack': Binary MessagePack bytes. ~30% smaller than JSON before compression, good for bandwidth-constrained scenarios. compact: Serialization mode: - False (default): Parent paths as full strings ('a.b.c'). Recommended when using gzip compression. - True: Parent paths as numeric codes (0, 1, 2...). ~30% smaller uncompressed, but larger after gzip. Returns: str if transport is None or 'json', bytes if 'msgpack'. Raises: ImportError: If genro-tytx package is not installed. Output Format: Normal mode:: {"rows": [ ["", "root", "div", null, {"id": "main"}], ["root", "child_0", "span", "text", {}], ... ]} Compact mode:: {"rows": [ [null, "root", "div", null, {"id": "main"}], [0, "child_0", "span", "text", {}], ... ], "paths": {"0": "root", ...}} Example: >>> store = TreeStore() >>> store.set_item('config.timeout', 30) >>> store.set_item('config.retry', True) >>> >>> # Default JSON >>> json_data = to_tytx(store) >>> >>> # Binary MessagePack >>> msgpack_data = to_tytx(store, transport='msgpack') >>> >>> # Compact mode (smaller without gzip) >>> compact_data = to_tytx(store, compact=True) """ try: from genro_tytx import to_tytx as tytx_encode except ImportError as e: raise ImportError("genro-tytx package required for serialization") from e if compact: paths: dict[int, str] = {} rows = list(store.flattened(path_registry=paths)) # Convert int keys to str for JSON compatibility paths_str = {str(k): v for k, v in paths.items()} return tytx_encode({"rows": rows, "paths": paths_str}, transport=transport) else: rows = list(store.flattened()) return tytx_encode({"rows": rows}, transport=transport)
[docs] def from_tytx( data: str | bytes, transport: Literal["json", "msgpack"] | None = None, builder: Any | None = None, ) -> TreeStore: """Deserialize TreeStore from TYTX format. Reconstructs a complete TreeStore hierarchy from TYTX-encoded data. Automatically detects whether the data uses normal or compact format by checking for the presence of a 'paths' registry. The reconstruction algorithm: 1. Decode TYTX data to get rows and optional path registry 2. For each row in depth-first order: - Resolve parent reference (path string or numeric code) - Create TreeStoreNode with label, tag, value, and attributes - If value is None, create child TreeStore (branch node) - Insert node into parent store Args: data: Serialized data from to_tytx(). - str: If transport is None or 'json' - bytes: If transport is 'msgpack' transport: Input format matching how data was serialized: - None or 'json': Parse as JSON string (default) - 'msgpack': Parse as MessagePack bytes builder: Optional builder instance for the reconstructed store. If provided, enables builder pattern methods on the result. Returns: TreeStore: Fully reconstructed tree with: - All nodes in correct hierarchy - Original values with types preserved (Decimal, date, etc.) - All node attributes restored - Parent-child relationships established Raises: ImportError: If genro-tytx package is not installed. ValueError: If data format is invalid or corrupted. Format Detection: The function automatically handles both serialization modes: Normal mode (no 'paths' key):: {"rows": [["", "root", "div", null, {}], ...]} Compact mode (has 'paths' key):: {"rows": [[null, "root", "div", null, {}], ...], "paths": {"0": "root", ...}} Example: >>> # Basic deserialization >>> store = from_tytx(json_data) >>> store['config.timeout'] 30 >>> # With MessagePack >>> store = from_tytx(msgpack_data, transport='msgpack') >>> # With builder for DOM-like API >>> store = from_tytx(data, builder=HtmlBuilder()) >>> store.div() # Builder methods available >>> # Round-trip preserves types >>> original = TreeStore() >>> original.set_item('price', Decimal('99.99')) >>> data = to_tytx(original) >>> restored = from_tytx(data) >>> restored['price'] # Decimal('99.99'), not float """ try: from genro_tytx import from_tytx as tytx_decode except ImportError as e: raise ImportError("genro-tytx package required for deserialization") from e from .core import TreeStore from .node import TreeStoreNode parsed = tytx_decode(data, transport=transport) rows = parsed["rows"] # Check if compact format (has 'paths' registry) paths_raw = parsed.get("paths") # Convert str keys back to int if present code_to_path: dict[int, str] | None = ( {int(k): v for k, v in paths_raw.items()} if paths_raw else None ) store = TreeStore(builder=builder) # Registry to track created branch stores by path path_to_store: dict[str, TreeStore] = {"": store} for row in rows: parent_ref, label, tag, value, attr = row # Resolve parent path from code if compact format if code_to_path is not None: parent_path = code_to_path.get(parent_ref, "") if parent_ref is not None else "" else: parent_path = parent_ref # Get parent store from registry parent_store = path_to_store.get(parent_path, store) # Build full path for this node full_path = f"{parent_path}.{label}" if parent_path else label # Create node with attributes node = TreeStoreNode(label, attr, value=value, tag=tag) # If value is None, this is a branch - create child store if value is None: child_store = TreeStore(builder=builder) node._value = child_store child_store.parent = node # Register this branch for children path_to_store[full_path] = child_store node.parent = parent_store parent_store._insert_node(node, trigger=False) return store