extras.py 4.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106
  1. from __future__ import annotations
  2. from typing import AsyncIterator, Dict, Iterator, Optional
  3. import httpx
  4. from opencode_ai.api.default import config_get, session_list
  5. from opencode_ai.client import Client
  6. from opencode_ai.types import UNSET, Unset
  7. class OpenCodeClient:
  8. """High-level convenience wrapper around the generated Client.
  9. Provides sensible defaults and a couple of helper methods.
  10. """
  11. def __init__(
  12. self,
  13. base_url: str = "http://localhost:4096",
  14. *,
  15. headers: Optional[Dict[str, str]] = None,
  16. timeout: Optional[float] = None,
  17. verify_ssl: bool | str | httpx.URLTypes | None = True,
  18. ) -> None:
  19. httpx_timeout = None if timeout is None else httpx.Timeout(timeout)
  20. self._client = Client(
  21. base_url=base_url,
  22. headers=headers or {},
  23. timeout=httpx_timeout,
  24. verify_ssl=verify_ssl if isinstance(verify_ssl, bool) else True,
  25. )
  26. @property
  27. def client(self) -> Client:
  28. return self._client
  29. # ---- Convenience wrappers over generated endpoints ----
  30. def list_sessions(self, *, directory: str | Unset = UNSET):
  31. return session_list.sync(client=self._client, directory=directory)
  32. def get_config(self, *, directory: str | Unset = UNSET):
  33. return config_get.sync(client=self._client, directory=directory)
  34. # ---- Server-Sent Events (SSE) streaming ----
  35. def subscribe_events(self, *, directory: str | Unset = UNSET) -> Iterator[dict]:
  36. """Subscribe to /event SSE endpoint and yield parsed JSON events.
  37. This is a blocking generator which yields one event dict per message.
  38. """
  39. client = self._client.get_httpx_client()
  40. params: dict[str, str] = {}
  41. if directory is not UNSET and directory is not None:
  42. params["directory"] = str(directory)
  43. with client.stream("GET", "/event", headers={"Accept": "text/event-stream"}, params=params) as r:
  44. r.raise_for_status()
  45. buf = ""
  46. for line_bytes in r.iter_lines():
  47. line = line_bytes.decode("utf-8") if isinstance(line_bytes, (bytes, bytearray)) else str(line_bytes)
  48. if line.startswith(":"):
  49. # comment/heartbeat
  50. continue
  51. if line == "":
  52. if buf:
  53. # end of event
  54. for part in buf.split("\n"):
  55. if part.startswith("data:"):
  56. data = part[5:].strip()
  57. if data:
  58. try:
  59. yield httpx._models.jsonlib.loads(data) # type: ignore[attr-defined]
  60. except Exception:
  61. # fall back: skip malformed
  62. pass
  63. buf = ""
  64. continue
  65. buf += line + "\n"
  66. async def subscribe_events_async(self, *, directory: str | Unset = UNSET) -> AsyncIterator[dict]:
  67. """Async variant of subscribe_events using httpx.AsyncClient."""
  68. aclient = self._client.get_async_httpx_client()
  69. params: dict[str, str] = {}
  70. if directory is not UNSET and directory is not None:
  71. params["directory"] = str(directory)
  72. async with aclient.stream("GET", "/event", headers={"Accept": "text/event-stream"}, params=params) as r:
  73. r.raise_for_status()
  74. buf = ""
  75. async for line_bytes in r.aiter_lines():
  76. line = line_bytes
  77. if line.startswith(":"):
  78. continue
  79. if line == "":
  80. if buf:
  81. for part in buf.split("\n"):
  82. if part.startswith("data:"):
  83. data = part[5:].strip()
  84. if data:
  85. try:
  86. yield httpx._models.jsonlib.loads(data) # type: ignore[attr-defined]
  87. except Exception:
  88. pass
  89. buf = ""
  90. continue
  91. buf += line + "\n"