2026-01-11 14:46:41 -08:00
|
|
|
from __future__ import annotations
|
|
|
|
|
|
|
|
|
|
from typing import Any, Dict, Optional
|
2026-05-03 17:29:32 -07:00
|
|
|
import threading
|
2026-01-11 14:46:41 -08:00
|
|
|
|
|
|
|
|
from .HTTP import HTTPClient
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class ApiError(Exception):
|
|
|
|
|
"""Base exception for API errors."""
|
|
|
|
|
pass
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class API:
|
|
|
|
|
"""Base class for API clients using the internal HTTPClient."""
|
|
|
|
|
|
|
|
|
|
def __init__(self, base_url: str, timeout: float = 10.0) -> None:
|
|
|
|
|
self.base_url = str(base_url or "").rstrip("/")
|
|
|
|
|
self.timeout = float(timeout)
|
2026-05-03 17:29:32 -07:00
|
|
|
self._http_client: Optional[HTTPClient] = None
|
|
|
|
|
self._http_client_lock = threading.Lock()
|
|
|
|
|
|
|
|
|
|
def _get_http_client(self) -> HTTPClient:
|
|
|
|
|
"""Return a reusable opened HTTP client for this API instance."""
|
|
|
|
|
client = self._http_client
|
|
|
|
|
if client is not None and getattr(client, "_client", None) is not None:
|
|
|
|
|
return client
|
|
|
|
|
|
|
|
|
|
with self._http_client_lock:
|
|
|
|
|
client = self._http_client
|
|
|
|
|
if client is None:
|
|
|
|
|
client = HTTPClient(timeout=self.timeout)
|
|
|
|
|
self._http_client = client
|
|
|
|
|
if getattr(client, "_client", None) is None:
|
|
|
|
|
client.__enter__()
|
|
|
|
|
return client
|
|
|
|
|
|
|
|
|
|
def close(self) -> None:
|
|
|
|
|
client = self._http_client
|
|
|
|
|
if client is None:
|
|
|
|
|
return
|
|
|
|
|
with self._http_client_lock:
|
|
|
|
|
current = self._http_client
|
|
|
|
|
if current is None:
|
|
|
|
|
return
|
|
|
|
|
try:
|
|
|
|
|
current.__exit__(None, None, None)
|
|
|
|
|
except Exception:
|
|
|
|
|
pass
|
|
|
|
|
self._http_client = None
|
|
|
|
|
|
|
|
|
|
def __del__(self) -> None:
|
|
|
|
|
try:
|
|
|
|
|
self.close()
|
|
|
|
|
except Exception:
|
|
|
|
|
pass
|
2026-01-11 14:46:41 -08:00
|
|
|
|
|
|
|
|
def _get_json(
|
|
|
|
|
self,
|
|
|
|
|
path: str,
|
|
|
|
|
params: Optional[Dict[str, Any]] = None,
|
|
|
|
|
headers: Optional[Dict[str, str]] = None,
|
|
|
|
|
) -> Dict[str, Any]:
|
|
|
|
|
url = f"{self.base_url}/{str(path or '').lstrip('/')}"
|
|
|
|
|
try:
|
2026-05-03 17:29:32 -07:00
|
|
|
client = self._get_http_client()
|
|
|
|
|
response = client.get(url, params=params, headers=headers, allow_redirects=True)
|
|
|
|
|
response.raise_for_status()
|
|
|
|
|
return response.json()
|
2026-01-11 14:46:41 -08:00
|
|
|
except Exception as exc:
|
|
|
|
|
raise ApiError(f"API request failed for {url}: {exc}") from exc
|
|
|
|
|
|
|
|
|
|
def _post_json(
|
|
|
|
|
self,
|
|
|
|
|
path: str,
|
|
|
|
|
json_data: Optional[Dict[str, Any]] = None,
|
|
|
|
|
params: Optional[Dict[str, Any]] = None,
|
|
|
|
|
headers: Optional[Dict[str, str]] = None,
|
|
|
|
|
) -> Dict[str, Any]:
|
|
|
|
|
url = f"{self.base_url}/{str(path or '').lstrip('/')}"
|
|
|
|
|
try:
|
2026-05-03 17:29:32 -07:00
|
|
|
client = self._get_http_client()
|
|
|
|
|
response = client.request(
|
|
|
|
|
"POST",
|
|
|
|
|
url,
|
|
|
|
|
json=json_data,
|
|
|
|
|
params=params,
|
|
|
|
|
headers=headers,
|
|
|
|
|
follow_redirects=True,
|
|
|
|
|
)
|
|
|
|
|
response.raise_for_status()
|
|
|
|
|
return response.json()
|
2026-01-11 14:46:41 -08:00
|
|
|
except Exception as exc:
|
|
|
|
|
raise ApiError(f"API request failed for {url}: {exc}") from exc
|