orchestrata_ultimate.cxp.payload

Reference › orchestrata_ultimate › cxp › payload

PayloadManager reads, writes and checks the payload of a CXPPacket (Spec 004 US-003). JSON payloads (dicts, lists and scalars) are stored on the packet as they are. Binary payloads are stored in a transport envelope, base64-encoded and optionally gzip-compressed, so the packet stays JSON-safe. set_payload chooses the path from the content type (JSON by default, anything else is binary), accepts None, and raises InvalidPacketError if the data type does not match the content type or the size limit is exceeded. get_payload returns the JSON value or decodes an envelope back to bytes, and get_payload_json and get_payload_binary return the string or bytes form. is_json, is_binary, get_content_type and get_payload_size describe the stored payload, and validate_payload_size checks it against separate limits for JSON (10 MB) and binary (100 MB) payloads. Packets are serialized by json_serializer. See cxp and exceptions.

At a glance

Source src/orchestrata_ultimate/cxp/payload.py
Lines of code 533
Classes 1
Functions 0
Imports (internal) orchestrata_ultimate.core.base.traces, orchestrata_ultimate.core.packet, orchestrata_ultimate.cxp.exceptions
Imported by orchestrata_ultimate.cxp
Tests that import it tests/cxp/test_payload.py, tests/cxp/test_payload_extended.py

Class diagram

Class diagram orchestrata_ultimate.cxp.payload

Dependency diagram

Dependency diagram orchestrata_ultimate.cxp.payload

API

Constants

Name Type Value
JSON_MAX_BYTES int 10485760
BINARY_MAX_BYTES int 104857600

Classes

PayloadManager

Decorators: traces(spec='004', story='US-003', reqs=['REQ-004-US-003-001', 'REQ-004-US-003-008', 'REQ-004-US-003-018', 'REQ-004-US-… · Line 62

Manages CXP packet payloads: access, type detection, validation.

Method Signature Summary
get_payload get_payload(packet: CXPPacket) -> Any Return the payload from a packet.
get_payload_json get_payload_json(packet: CXPPacket) -> str Return the payload as a JSON string.
get_payload_binary get_payload_binary(packet: CXPPacket) -> bytes Return the payload as bytes.
set_payload set_payload(packet: CXPPacket, data: Any, content_type: str = 'application/json', compress: bool = False) -> None Set the payload on a packet with validation.
is_json is_json(packet: CXPPacket) -> bool Return True if the payload is a JSON-type value (dict, list, scalar).
is_binary is_binary(packet: CXPPacket) -> bool Return True if the payload is binary data.
get_content_type get_content_type(packet: CXPPacket) -> str Detect and return the content type of the payload.
get_payload_size get_payload_size(packet: CXPPacket) -> int Return the size of the payload in bytes.
validate_payload_size validate_payload_size(packet: CXPPacket) -> bool Check whether the payload is within size limits.
_set_json_payload _set_json_payload(packet: CXPPacket, data: Any) -> None Validate and store a JSON payload on the packet.
_set_binary_payload _set_binary_payload(packet: CXPPacket, data: Any, content_type: str, compress: bool) -> None Validate and store a binary payload as a transport envelope.
_is_json_content_type staticmethod _is_json_content_type(content_type: str) -> bool Return True if the content type indicates JSON data.

Source

Permalink: https://gitlab.com/MSamy/orchestrata-ultimate-architecture/-/blob/a4cbc2bdf/src/orchestrata_ultimate/cxp/payload.py

# mypy: ignore-errors
"""
CXP Payload Manager.

Handles payload access, content-type detection, size validation,
and JSON/binary serialization for CXP packets.

Spec: 004-cxp, Section US-003
Author: Mohamed

Payload Limits:
    - JSON payloads: max 10 MB (10_485_760 bytes)
    - Binary payloads: max 100 MB (104_857_600 bytes)

Binary payloads are base64-encoded for transport since CXP packets
serialize to JSON. The PayloadManager transparently handles encoding
and decoding.
"""

# Spec: 004 - CXP Protocol

import base64
import gzip
import json
from typing import Any

from orchestrata_ultimate.core.base.traces import traces
from orchestrata_ultimate.core.packet import CXPPacket
from orchestrata_ultimate.cxp.exceptions import InvalidPacketError

__all__ = [
    "PayloadManager",
    "JSON_MAX_BYTES",
    "BINARY_MAX_BYTES",
]


# ============================================================================
# Constants
# ============================================================================

JSON_MAX_BYTES: int = 10_485_760  # 10 MB
BINARY_MAX_BYTES: int = 104_857_600  # 100 MB


# ============================================================================
# PayloadManager
# ============================================================================


@traces(
    spec="004",
    story="US-003",
    reqs=[
        "REQ-004-US-003-001",
        "REQ-004-US-003-008",
        "REQ-004-US-003-018",
        "REQ-004-US-003-020",
        "REQ-004-US-003-024",
    ],
)
class PayloadManager:
    """Manages CXP packet payloads: access, type detection, validation.

    Provides a clean interface for reading/writing payloads on CXPPacket
    instances, with content-type detection and size enforcement per
    Spec 004 Section US-003.

    Usage::

        pm = PayloadManager()

        # Set a JSON payload
        packet = CXPPacket(from_agent="U:Ox", to_agent="U:Cd", subject="task")
        pm.set_payload(packet, {"action": "build", "target": "api"})

        # Read it back
        data = pm.get_payload(packet)       # -> dict
        raw = pm.get_payload_json(packet)    # -> JSON string

        # Binary payload
        pm.set_payload(packet, b"\x89PNG...", content_type="application/octet-stream")
        raw_bytes = pm.get_payload_binary(packet)  # -> bytes
    """

    # ------------------------------------------------------------------
    # Read methods
    # ------------------------------------------------------------------

    @traces(spec="004", story="US-003", reqs=["REQ-004-US-003-020"])
    def get_payload(self, packet: CXPPacket) -> Any:
        """Return the payload from a packet.

        For JSON payloads, returns the dict/list/scalar directly.
        For binary payloads stored as base64 strings (transport form),
        decodes and returns the raw bytes.

        Args:
            packet: The CXP packet to read from.

        Returns:
            The payload value. Type depends on what was stored:
            dict/list for JSON, bytes for binary, or None if empty.
        """
        payload = packet.payload
        if payload is None:
            return None

        # If payload is already bytes, return as-is
        if isinstance(payload, (bytes, bytearray)):
            return bytes(payload)

        # If payload is a dict with our binary transport envelope,
        # decode the base64 content back to bytes
        if isinstance(payload, dict) and payload.get("_binary_transport"):
            encoded = payload.get("data", "")
            compressed = payload.get("compressed", False)
            raw = base64.b64decode(encoded)
            if compressed:
                raw = gzip.decompress(raw)
            return raw

        return payload

    @traces(
        spec="004",
        story="US-003",
        reqs=["REQ-004-US-003-021", "REQ-004-US-003-023"],
    )
    def get_payload_json(self, packet: CXPPacket) -> str:
        """Return the payload as a JSON string.

        Args:
            packet: The CXP packet to read from.

        Returns:
            JSON string representation of the payload.

        Raises:
            InvalidPacketError: If the payload cannot be serialized to JSON
                (e.g. raw bytes without the transport envelope).
        """
        payload = packet.payload
        if payload is None:
            return "null"

        # Binary transport envelope is JSON-serializable as-is
        if isinstance(payload, (bytes, bytearray)):
            raise InvalidPacketError(
                "Cannot serialize raw binary payload to JSON. "
                "Use set_payload() with content_type='application/octet-stream' "
                "to wrap binary data in a transport envelope.",
                field="payload",
            )

        try:
            return json.dumps(payload, default=str, ensure_ascii=False)
        except (TypeError, ValueError) as exc:
            raise InvalidPacketError(
                f"Payload is not JSON-serializable: {exc}",
                field="payload",
            ) from exc

    @traces(
        spec="004",
        story="US-003",
        reqs=["REQ-004-US-003-022"],
    )
    def get_payload_binary(self, packet: CXPPacket) -> bytes:
        """Return the payload as bytes.

        For binary payloads, returns the raw bytes (decoding base64
        transport envelope if necessary).
        For JSON payloads, returns the UTF-8 encoded JSON string.

        Args:
            packet: The CXP packet to read from.

        Returns:
            Bytes representation of the payload.
        """
        payload = packet.payload
        if payload is None:
            return b""

        # Already raw bytes
        if isinstance(payload, (bytes, bytearray)):
            return bytes(payload)

        # Binary transport envelope -> decode
        if isinstance(payload, dict) and payload.get("_binary_transport"):
            encoded = payload.get("data", "")
            compressed = payload.get("compressed", False)
            raw = base64.b64decode(encoded)
            if compressed:
                raw = gzip.decompress(raw)
            return raw

        # JSON payload -> encode to UTF-8 bytes
        json_str = json.dumps(payload, default=str, ensure_ascii=False)
        return json_str.encode("utf-8")

    # ------------------------------------------------------------------
    # Write methods
    # ------------------------------------------------------------------

    @traces(
        spec="004",
        story="US-003",
        reqs=["REQ-004-US-003-002", "REQ-004-US-003-003"],
    )
    def set_payload(
        self,
        packet: CXPPacket,
        data: Any,
        content_type: str = "application/json",
        compress: bool = False,
    ) -> None:
        """Set the payload on a packet with validation.

        For JSON content types, the data is stored directly (dict, list, etc.).
        For binary content types, the data (bytes) is base64-encoded and
        wrapped in a transport envelope for JSON-safe serialization.

        Args:
            packet: The CXP packet to modify.
            data: The payload data. Must be bytes for binary content types,
                  or a JSON-serializable value for JSON content types.
            content_type: MIME type. Defaults to "application/json".
                Use "application/octet-stream" or any non-JSON type for binary.
            compress: If True, gzip-compress binary payloads before base64
                encoding. Ignored for JSON payloads.

        Raises:
            InvalidPacketError: If the payload exceeds size limits or
                the data type doesn't match the content type.
        """
        if data is None:
            packet.payload = None
            return

        is_binary = not self._is_json_content_type(content_type)

        if is_binary:
            self._set_binary_payload(packet, data, content_type, compress)
        else:
            self._set_json_payload(packet, data)

    # ------------------------------------------------------------------
    # Type detection
    # ------------------------------------------------------------------

    @traces(
        spec="004",
        story="US-003",
        reqs=["REQ-004-US-003-024"],
    )
    def is_json(self, packet: CXPPacket) -> bool:
        """Return True if the payload is a JSON-type value (dict, list, scalar).

        Binary transport envelopes are NOT considered JSON payloads even
        though they are technically dicts.

        Args:
            packet: The CXP packet to inspect.

        Returns:
            True if the payload is a dict (non-binary-envelope), list,
            str, int, float, or bool. False otherwise.
        """
        payload = packet.payload
        if payload is None:
            return False

        if isinstance(payload, (bytes, bytearray)):
            return False

        if isinstance(payload, dict) and payload.get("_binary_transport"):
            return False

        return isinstance(payload, (dict, list, str, int, float, bool))

    @traces(
        spec="004",
        story="US-003",
        reqs=["REQ-004-US-003-024"],
    )
    def is_binary(self, packet: CXPPacket) -> bool:
        """Return True if the payload is binary data.

        This includes both raw bytes and base64 transport envelopes.

        Args:
            packet: The CXP packet to inspect.

        Returns:
            True if the payload is bytes, bytearray, or a binary
            transport envelope dict.
        """
        payload = packet.payload
        if payload is None:
            return False

        if isinstance(payload, (bytes, bytearray)):
            return True

        if isinstance(payload, dict) and payload.get("_binary_transport"):
            return True

        return False

    @traces(spec="004", story="US-003", reqs=["REQ-004-US-003-012"])
    def get_content_type(self, packet: CXPPacket) -> str:
        """Detect and return the content type of the payload.

        Args:
            packet: The CXP packet to inspect.

        Returns:
            MIME type string. Returns "application/json" for JSON payloads,
            the stored content type for binary transport envelopes, or
            "application/octet-stream" for raw bytes.
            Returns "application/x-empty" for None payloads.
        """
        payload = packet.payload
        if payload is None:
            return "application/x-empty"

        if isinstance(payload, (bytes, bytearray)):
            return "application/octet-stream"

        if isinstance(payload, dict) and payload.get("_binary_transport"):
            return payload.get("content_type", "application/octet-stream")

        return "application/json"

    # ------------------------------------------------------------------
    # Size methods
    # ------------------------------------------------------------------

    @traces(
        spec="004", story="US-003", reqs=["REQ-004-US-003-007", "REQ-004-US-003-011"]
    )
    def get_payload_size(self, packet: CXPPacket) -> int:
        """Return the size of the payload in bytes.

        For JSON payloads, this is the byte length of the UTF-8 encoded
        JSON string. For binary payloads, this is the raw byte count
        (before base64 encoding, after decompression if applicable).

        Args:
            packet: The CXP packet to measure.

        Returns:
            Size in bytes, or 0 for None payloads.
        """
        payload = packet.payload
        if payload is None:
            return 0

        if isinstance(payload, (bytes, bytearray)):
            return len(payload)

        if isinstance(payload, dict) and payload.get("_binary_transport"):
            # Report the decoded size, not the base64 size
            encoded = payload.get("data", "")
            compressed = payload.get("compressed", False)
            raw = base64.b64decode(encoded)
            if compressed:
                raw = gzip.decompress(raw)
            return len(raw)

        # JSON payload: measure the serialized form
        json_str = json.dumps(payload, default=str, ensure_ascii=False)
        return len(json_str.encode("utf-8"))

    @traces(
        spec="004",
        story="US-001",
        reqs=[
            "REQ-004-US-003-007",
            "REQ-004-US-003-011",
            "REQ-004-US-003-018",
        ],
    )
    def validate_payload_size(self, packet: CXPPacket) -> bool:
        """Check whether the payload is within size limits.

        JSON payloads must be <= JSON_MAX_BYTES (10 MB).
        Binary payloads must be <= BINARY_MAX_BYTES (100 MB).

        Args:
            packet: The CXP packet to validate.

        Returns:
            True if the payload is within the allowed size limit,
            False otherwise. Returns True for None payloads.
        """
        payload = packet.payload
        if payload is None:
            return True

        size = self.get_payload_size(packet)

        if self.is_binary(packet):
            return size <= BINARY_MAX_BYTES

        return size <= JSON_MAX_BYTES

    # ------------------------------------------------------------------
    # Private helpers
    # ------------------------------------------------------------------

    @traces(spec="004", story="US-003", reqs=["REQ-004-US-003-016"])
    def _set_json_payload(self, packet: CXPPacket, data: Any) -> None:
        """Validate and store a JSON payload on the packet.

        Args:
            packet: Target packet.
            data: JSON-serializable value.

        Raises:
            InvalidPacketError: If data is bytes or exceeds JSON_MAX_BYTES.
        """
        if isinstance(data, (bytes, bytearray)):
            raise InvalidPacketError(
                "Cannot store bytes as JSON payload. "
                "Use content_type='application/octet-stream' for binary data.",
                field="payload",
            )

        # Verify it is serializable and measure size
        try:
            json_str = json.dumps(data, default=str, ensure_ascii=False)
        except (TypeError, ValueError) as exc:
            raise InvalidPacketError(
                f"Payload is not JSON-serializable: {exc}",
                field="payload",
            ) from exc

        size = len(json_str.encode("utf-8"))
        if size > JSON_MAX_BYTES:
            raise InvalidPacketError(
                f"JSON payload size {size:,} bytes exceeds limit of "
                f"{JSON_MAX_BYTES:,} bytes (10 MB).",
                field="payload",
                details={"size": size, "limit": JSON_MAX_BYTES},
            )

        packet.payload = data

    @traces(
        spec="004",
        story="US-003",
        reqs=[
            "REQ-004-US-003-008",
            "REQ-004-US-003-013",
            "REQ-004-US-003-017",
        ],
    )
    def _set_binary_payload(
        self,
        packet: CXPPacket,
        data: Any,
        content_type: str,
        compress: bool,
    ) -> None:
        """Validate and store a binary payload as a transport envelope.

        Binary data is base64-encoded and wrapped in a dict envelope so
        the overall CXP packet remains JSON-serializable.

        Args:
            packet: Target packet.
            data: Must be bytes or bytearray.
            content_type: MIME type to store in the envelope.
            compress: If True, gzip-compress before base64 encoding.

        Raises:
            InvalidPacketError: If data is not bytes or exceeds BINARY_MAX_BYTES.
        """
        if not isinstance(data, (bytes, bytearray)):
            raise InvalidPacketError(
                f"Binary content type '{content_type}' requires bytes data, "
                f"got {type(data).__name__}.",
                field="payload",
            )

        raw = bytes(data)

        if len(raw) > BINARY_MAX_BYTES:
            raise InvalidPacketError(
                f"Binary payload size {len(raw):,} bytes exceeds limit of "
                f"{BINARY_MAX_BYTES:,} bytes (100 MB).",
                field="payload",
                details={"size": len(raw), "limit": BINARY_MAX_BYTES},
            )

        if compress:
            transport_bytes = gzip.compress(raw)
        else:
            transport_bytes = raw

        encoded = base64.b64encode(transport_bytes).decode("ascii")

        packet.payload = {
            "_binary_transport": True,
            "content_type": content_type,
            "data": encoded,
            "compressed": compress,
            "original_size": len(raw),
        }

    @staticmethod
    @traces(spec="004", story="US-003", reqs=["REQ-004-US-003-012"])
    def _is_json_content_type(content_type: str) -> bool:
        """Return True if the content type indicates JSON data.

        Args:
            content_type: MIME type string.

        Returns:
            True for 'application/json' and variants like
            'application/vnd.api+json'.
        """
        ct = content_type.strip().lower()
        if ct == "application/json":
            return True
        if ct.startswith("application/") and ct.endswith("+json"):
            return True
        if ct == "text/json":
            return True
        return False