Source code for axio_sse.event

"""One event and the JSON object inside it."""

from __future__ import annotations

import json
import logging
from dataclasses import dataclass
from typing import Any

log = logging.getLogger("axio.sse")


[docs] class Payload(dict[str, Any]): """The JSON object inside one event, read by path. ``payload.number("message", "usage", "input_tokens")`` walks the path and gives the default wherever a step is missing, null, or the wrong type — which is what an optional provider field is. It is a ``dict``, so ``payload["x"]``, ``in``, and ``json.dumps`` all still work. The four readers exist so a handler carries no ``Any`` and no chain of ``.get({})``. """ __slots__ = () def _at(self, keys: tuple[str, ...]) -> Any: found: Any = self for key in keys: if not isinstance(found, dict): return None found = found.get(key) return found
[docs] def string(self, *keys: str, default: str = "") -> str: """The string at this path, or the default where the provider sent none.""" found = self._at(keys) return found if isinstance(found, str) else default
[docs] def number(self, *keys: str, default: int = 0) -> int: """The whole number at this path, or the default where the provider sent none.""" found = self._at(keys) # bool is an int in Python. A true/false field must not read here as 1 or 0. return found if isinstance(found, int) and not isinstance(found, bool) else default
[docs] def obj(self, *keys: str) -> Payload: """The object at this path, empty where there is none, so a path can be walked in steps.""" found = self._at(keys) return Payload(found) if isinstance(found, dict) else Payload()
[docs] def objs(self, *keys: str) -> list[Payload]: """Every object in the list at this path. A missing list reads as no objects.""" found = self._at(keys) if not isinstance(found, list): return [] return [Payload(one) for one in found if isinstance(one, dict)]
class MalformedPayload(ValueError): """An event that carried data no reader can act on. Raised rather than skipped: the stream said this event mattered, and there is no way to continue reading it that does not report a partial turn as a whole one. """
[docs] @dataclass(frozen=True, slots=True) class Event: """One dispatched event, with the four fields the format defines.""" data: str = "" #: What the ``event:`` field carried, empty where the stream sent none. event: str = "" #: The stream position for a client that reconnects, not an id of this event. id: str = "" retry: int | None = None @property def name(self) -> str: """The event's type. An unnamed event is of type ``message``, which the format defines. Dispatched on the raw field instead, an ``@on("message")`` handler never runs for the ordinary unnamed event, and a strict read rejects it as unknown. """ return self.event or "message"
[docs] def payload(self) -> Payload | None: """This event's JSON object, or None where the event carries no data at all. Data that will not parse raises. Skipped instead, a text or tool-call event whose JSON arrived corrupt was dropped, the completion event after it still reported success, and the caller got a short answer or half a tool's arguments with nothing saying anything was lost. A sentinel such as ``[DONE]`` is data too, and reaches here as junk. Name it in ``until``, which ends the stream before it is read. """ if not self.data: return None try: got = json.loads(self.data) except json.JSONDecodeError as exc: log.error("payload is not JSON: %.80s", self.data) raise MalformedPayload(f"event {self.name!r} carries data that is not JSON: {exc}") from exc if not isinstance(got, dict): log.error("payload is not an object: %.80s", self.data) raise MalformedPayload(f"event {self.name!r} carries {type(got).__name__} and not an object") return Payload(got)