import os
import httpx
from typing import Any
from tenacity import retry, stop_after_attempt, wait_exponential, retry_if_exception_type
class IonError(RuntimeError):
"""Raised when ION returns errors[] in the response body."""
def __init__(self, errors: list[dict[str, Any]]):
self.errors = errors
super().__init__("; ".join(e.get("message", "") for e in errors))
class IonClient:
def __init__(self, base_url: str | None = None, token: str | None = None):
self.base_url = (base_url or os.environ["ION_BASE_URL"]).rstrip("/")
self.token = token or os.environ["ION_TOKEN"]
self._http = httpx.Client(
base_url=self.base_url,
headers={"Authorization": f"Bearer {self.token}"},
timeout=30.0,
)
@retry(
retry=retry_if_exception_type((httpx.HTTPError,)),
stop=stop_after_attempt(3),
wait=wait_exponential(multiplier=1, min=1, max=10),
reraise=True,
)
def _post_graphql(self, query: str, variables: dict[str, Any] | None = None) -> dict[str, Any]:
r = self._http.post(
"/graphql",
json={"query": query, "variables": variables or {}},
)
r.raise_for_status()
payload = r.json()
if payload.get("errors"):
raise IonError(payload["errors"])
return payload["data"]
def query(self, query: str, **variables) -> dict[str, Any]:
return self._post_graphql(query, variables)
def mutate(self, mutation: str, **variables) -> dict[str, Any]:
return self._post_graphql(mutation, variables)
def close(self):
self._http.close()