Source code for aioafero.v1.conclave.protocol

"""Conclave login and private/public-envelope helpers (no I/O)."""

from __future__ import annotations

from collections.abc import Awaitable, Callable
from dataclasses import dataclass
from typing import Any

from .access import ConclaveAccess

# Server may greet with either key after the opening ``{}``.
HANDSHAKE_FRAME_KEYS: frozenset[str] = frozenset({"hello", "tunnel"})
WELCOME_FRAME_KEY = "welcome"
PUBLIC_INVALIDATE_EVENT = "invalidate"

CONCLAVE_CLIENT_TYPE = "aioafero"
CONCLAVE_CLIENT_VERSION = "1.0.0"
CONCLAVE_PROTOCOL = 2

PrivateEventHandler = Callable[[Any, dict[str, Any]], Awaitable[bool]]
PublicEventHandler = Callable[[Any, dict[str, Any]], Awaitable[bool]]


[docs] @dataclass(frozen=True) class PrivateEvent: """Parsed ``private`` envelope from a Conclave push frame.""" event: str data: dict[str, Any]
[docs] @dataclass(frozen=True) class PublicEvent: """Parsed ``public`` envelope from a Conclave push frame. Inventory changes arrive as ``event="invalidate"`` with a ``data`` block carrying ``kind`` (``add`` / ``remove`` / ``update`` / …) and usually a ``target`` (``metadevices`` / ``devices``). """ event: str data: dict[str, Any]
[docs] def build_login_frame( access: ConclaveAccess, *, client_type: str = CONCLAVE_CLIENT_TYPE, version: str = CONCLAVE_CLIENT_VERSION, protocol: int = CONCLAVE_PROTOCOL, trace: bool = False, ) -> dict[str, Any]: """Build the client ``login`` JSON frame for a :class:`~.access.ConclaveAccess`.""" return { "login": { "channelId": access.channel_id, "accessToken": access.token, "type": client_type, "version": version, "protocol": protocol, "trace": trace, } }
[docs] def server_heartbeat_seconds( handshake: dict[str, Any], welcome: dict[str, Any] ) -> float: """Return the server-advertised heartbeat interval in seconds.""" for envelope in ( welcome.get(WELCOME_FRAME_KEY), handshake.get("hello"), handshake.get("tunnel"), ): if not isinstance(envelope, dict): continue heartbeat = envelope.get("heartbeat") if heartbeat is None: continue try: seconds = float(heartbeat) except (TypeError, ValueError): continue if seconds > 0: return seconds return 60.0
[docs] def parse_private_frame(frame: dict[str, Any]) -> PrivateEvent | None: """Extract a ``private`` event from a decoded Conclave frame. :returns: :class:`PrivateEvent` when ``frame`` carries a well-formed ``private`` block; otherwise ``None``. """ private = frame.get("private") if not isinstance(private, dict): return None event = private.get("event") data = private.get("data") if not isinstance(event, str) or not isinstance(data, dict): return None return PrivateEvent(event=event, data=data)
[docs] def parse_public_frame(frame: dict[str, Any]) -> PublicEvent | None: """Extract a ``public`` event from a decoded Conclave frame. :returns: :class:`PublicEvent` when ``frame`` carries a well-formed ``public`` block; otherwise ``None``. """ public = frame.get("public") if not isinstance(public, dict): return None event = public.get("event") data = public.get("data") if not isinstance(event, str) or not isinstance(data, dict): return None return PublicEvent(event=event, data=data)