"""Base httpx async client for Stock Oracle.""" from __future__ import annotations from typing import Any import httpx from libs.common.retries import with_retry from libs.oracle_client.exceptions import ( OracleClientError, OracleConnectionError, OracleNotFoundError, OracleServerError, OracleTimeoutError, ) class OracleClient: """Async HTTP client wrapping Stock Oracle REST API.""" def __init__(self, base_url: str, timeout: float = 30.0) -> None: self._base_url = base_url.rstrip("/") self._timeout = timeout self._client: httpx.AsyncClient | None = None async def __aenter__(self) -> OracleClient: self._client = httpx.AsyncClient( base_url=self._base_url, timeout=self._timeout, ) return self async def __aexit__(self, *args: Any) -> None: if self._client: await self._client.aclose() self._client = None def _ensure_client(self) -> httpx.AsyncClient: if self._client is None: raise RuntimeError("OracleClient must be used as async context manager.") return self._client @with_retry(max_attempts=3, min_wait=0.1, max_wait=5.0, multiplier=0.1) async def get(self, path: str, params: dict[str, Any] | None = None) -> Any: client = self._ensure_client() try: response = await client.get(path, params=params) except httpx.ConnectError as exc: raise OracleConnectionError(str(exc), source="oracle", entity=path) from exc except httpx.TimeoutException as exc: raise OracleTimeoutError(str(exc), source="oracle", entity=path) from exc return self._handle_response(response, path) @with_retry(max_attempts=3, min_wait=0.1, max_wait=5.0, multiplier=0.1) async def post(self, path: str, json: dict[str, Any] | None = None) -> Any: client = self._ensure_client() try: response = await client.post(path, json=json) except httpx.ConnectError as exc: raise OracleConnectionError(str(exc), source="oracle", entity=path) from exc except httpx.TimeoutException as exc: raise OracleTimeoutError(str(exc), source="oracle", entity=path) from exc return self._handle_response(response, path) def _handle_response(self, response: httpx.Response, path: str) -> Any: if response.status_code == 404: raise OracleNotFoundError( f"Not found: {path}", source="oracle", entity=path, context={"status_code": 404}, ) if response.status_code >= 500: raise OracleServerError( f"Server error {response.status_code}: {path}", source="oracle", entity=path, context={"status_code": response.status_code}, ) if response.status_code >= 400: raise OracleClientError( f"Client error {response.status_code}: {path}", source="oracle", entity=path, context={"status_code": response.status_code}, ) return response.json() async def health_check(self) -> bool: try: data = await self.get("/health") return isinstance(data, dict) and data.get("status") == "ok" except Exception: return False def make_oracle_client() -> OracleClient: from libs.common.config import get_settings s = get_settings() return OracleClient(base_url=s.stock_oracle_url, timeout=float(s.stock_oracle_timeout))