"""Inbox change-feed envelopes (docs/inbox-change-feed-contract.md, v1).""" from __future__ import annotations import re from datetime import datetime from typing import Any, Literal from pydantic import BaseModel CONTRACT = "statehub.inbox-feed.v1" CURSOR_VERSION = "v1" _CURSOR_RE = re.compile(r"^(v\d+):(\d+)$") class CursorError(ValueError): """Malformed cursor (400).""" class ForeignCursorError(ValueError): """Cursor of another contract version (409).""" def encode_cursor(revision: int) -> str: return f"{CURSOR_VERSION}:{int(revision)}" def decode_cursor(cursor: str) -> int: match = _CURSOR_RE.match(cursor or "") if match is None: raise CursorError(f"malformed cursor {cursor!r}") if match.group(1) != CURSOR_VERSION: raise ForeignCursorError( f"cursor {cursor!r} belongs to another contract version; re-snapshot" ) return int(match.group(2)) class FeedChange(BaseModel): revision: int entity: Literal["message", "receipt", "tombstone"] key: str op: Literal["upsert", "delete"] state: dict[str, Any] class FeedPage(BaseModel): contract: str = CONTRACT source_instance: str | None source_time: datetime high_water: str next_cursor: str has_more: bool compacted_before: str changes: list[FeedChange] class FeedSnapshot(BaseModel): contract: str = CONTRACT source_instance: str | None source_time: datetime cursor: str compacted_before: str messages: list[dict[str, Any]] receipts: list[dict[str, Any]] tombstones: list[dict[str, Any]] count: int content_hash: str