from __future__ import annotations from contextlib import asynccontextmanager from typing import Any, TYPE_CHECKING from collections.abc import AsyncIterator from yuxi.utils.logging_config import logger if TYPE_CHECKING: from .client import UrbitClient class HttpPokeApiClient: def __init__(self, client: UrbitClient, channel_id: str): self._client = client self._channel_id = channel_id self._closed = False @property def channel_id(self) -> str: return self._channel_id async def poke(self, app: str, mark: str, json_data: dict[str, Any], *, timeout: float = 15.0) -> dict[str, Any]: if self._closed: raise RuntimeError("HttpPokeApiClient is closed") payload = { "action": "poke", "ship": self._client.ship_name, "app": app, "mark": mark, "json": json_data, } r = await self._client.put( f"/~/channel/{self._channel_id}", json=payload, timeout=timeout, ) r.raise_for_status() return r.json() async def scry(self, app: str, path: str) -> dict[str, Any]: if self._closed: raise RuntimeError("HttpPokeApiClient is closed") r = await self._client.get( f"/~/scry/{app}{path}", timeout=10.0, ) r.raise_for_status() return r.json() async def subscribe(self, app: str, path: str) -> None: if self._closed: raise RuntimeError("HttpPokeApiClient is closed") payload = { "action": "subscribe", "ship": self._client.ship_name, "app": app, "path": path, } r = await self._client.put( f"/~/channel/{self._channel_id}", json=payload, timeout=10.0, ) r.raise_for_status() logger.debug(f"[Urbit] HttpPokeApi subscribed: {app}{path} on {self._channel_id}") async def unsubscribe(self) -> None: if self._closed: return payload = { "action": "unsubscribe", "ship": self._client.ship_name, } try: r = await self._client.put( f"/~/channel/{self._channel_id}", json=payload, timeout=5.0, ) r.raise_for_status() except Exception as e: logger.debug(f"[Urbit] HttpPokeApi unsubscribe error on {self._channel_id}: {e}") async def delete(self) -> None: if self._closed: return try: r = await self._client.delete(f"/~/channel/{self._channel_id}") if r.status_code not in (200, 204): logger.debug(f"[Urbit] HttpPokeApi DELETE {self._channel_id} → {r.status_code}") except Exception as e: logger.debug(f"[Urbit] HttpPokeApi DELETE error on {self._channel_id}: {e}") async def close(self) -> None: if self._closed: return await self.unsubscribe() await self.delete() self._closed = True logger.debug(f"[Urbit] HttpPokeApiClient closed: {self._channel_id}") @property def is_closed(self) -> bool: return self._closed @asynccontextmanager async def with_http_poke_api( client: UrbitClient, channel_id: str | None = None, ) -> AsyncIterator[HttpPokeApiClient]: import time cid = channel_id or f"poke-api-{int(time.time() * 1000)}" api = HttpPokeApiClient(client, cid) logger.debug(f"[Urbit] HttpPokeApiClient opened: {cid}") try: yield api finally: try: await api.close() except Exception as e: logger.warning(f"[Urbit] HttpPokeApiClient cleanup error for {cid}: {e}") async def create_http_poke_api(client: UrbitClient, channel_id: str | None = None) -> HttpPokeApiClient: import time cid = channel_id or f"poke-api-{int(time.time() * 1000)}" api = HttpPokeApiClient(client, cid) logger.debug(f"[Urbit] HttpPokeApiClient created: {cid}") return api