Source code for olo.spatial.tree_subscription

"""Client wrapper for Spatial.SubscribeTree server streams."""

from collections.abc import Iterator
from dataclasses import dataclass

import grpc

from olo.errors import wrap_rpc_error
from olo.spatial._convert import transform_from_proto
from olo.spatial._tree import build_frame_tree
from olo.spatial.types import FrameTree, Transform


[docs] @dataclass(frozen=True, slots=True) class TreeDelta: """Raw tree change batch from a SubscribeTree stream.""" added: tuple[Transform, ...] updated: tuple[Transform, ...] root_frames: tuple[str, ...] base_frame: str snapshot: bool
[docs] class TreeSubscription: """Context-managed sync iterator over a Spatial.SubscribeTree stream. Default iteration yields a materialized :class:`FrameTree` after each server message. Use :meth:`changes` for the raw delta view. Only one consumer should iterate a subscription at a time. """
[docs] def __init__(self, stream, *, namespace: str = "") -> None: """Wrap a SubscribeTree server stream.""" self._stream = stream self._namespace = namespace self._closed = False self._edges: dict[str, Transform] = {} self._root_frames: set[str] = set() self._base_frame = ""
def _apply(self, response) -> TreeDelta: pre_update_children = set(self._edges) added: list[Transform] = [] updated: list[Transform] = [] if response.is_snapshot: self._edges.clear() self._root_frames = set(response.root_frames) self._base_frame = response.base_frame for message in response.upserts: transform = transform_from_proto(message) self._edges[transform.child_frame_id] = transform added.append(transform) return TreeDelta( added=tuple(added), updated=(), root_frames=tuple(response.root_frames), base_frame=response.base_frame, snapshot=True, ) if response.root_frames: self._root_frames = set(response.root_frames) if response.base_frame: self._base_frame = response.base_frame for message in response.upserts: transform = transform_from_proto(message) if transform.child_frame_id in pre_update_children: updated.append(transform) else: added.append(transform) self._edges[transform.child_frame_id] = transform return TreeDelta( added=tuple(added), updated=tuple(updated), root_frames=tuple(response.root_frames), base_frame=response.base_frame, snapshot=False, ) def _materialized_tree(self) -> FrameTree: return build_frame_tree(self._edges.values(), self._root_frames, self._namespace) def _read_delta(self) -> TreeDelta: if self._closed: raise StopIteration try: response = next(self._stream) except StopIteration: self._closed = True raise except grpc.RpcError as exc: self._closed = True raise wrap_rpc_error(exc) from exc return self._apply(response) def __iter__(self) -> Iterator[FrameTree]: return self def __next__(self) -> FrameTree: self._read_delta() return self._materialized_tree()
[docs] def changes(self) -> Iterator[TreeDelta]: """Iterate raw tree deltas instead of materialized trees.""" while True: yield self._read_delta()
[docs] def close(self) -> None: """Cancel the stream and mark it closed.""" if self._closed: return self._closed = True self._stream.cancel()
def __enter__(self) -> "TreeSubscription": """Enter a context manager.""" return self def __exit__(self, exc_type, exc, tb) -> None: """Close the subscription on context exit.""" self.close()