"""Generic GraphQL client.""" import logging from typing import Any import requests from connectors.errors import ApiError from infra.throttler import Throttler logger = logging.getLogger(__name__) class GraphQLError(Exception): """Raised when the GraphQL response contains errors.""" def __init__(self, errors: list[dict]): self.errors = errors messages = [e.get('message', str(e)) for e in errors] super().__init__(f'GraphQL errors: {"; ".join(messages)}') class GraphQLClient: """Thin HTTP client for a GraphQL endpoint.""" def __init__( self, url: str, headers: dict[str, str] | None = None, throttler: Throttler | None = None, timeout: int = 30, ): self._url = url self._timeout = timeout self._throttler = throttler self._session = requests.Session() self._session.headers.update( { 'Content-Type': 'application/json', **(headers or {}), } ) def query( self, query: str, variables: dict[str, Any] | None = None, ) -> dict[str, Any]: """Execute a GraphQL query and return the data dict. Raises GraphQLError if the response contains errors. Raises ApiError on transport failures. """ if self._throttler: self._throttler.acquire() payload: dict[str, Any] = {'query': query} if variables: payload['variables'] = variables try: response = self._session.post( self._url, json=payload, timeout=self._timeout, ) except requests.exceptions.RequestException as e: raise ApiError(str(e)) from e if response.status_code != 200: raise ApiError( f'GraphQL request failed: {response.status_code} {response.text}' ) try: body = response.json() except requests.exceptions.JSONDecodeError as e: raise ApiError(f'Invalid JSON in response: {e}') from e if 'errors' in body: raise GraphQLError(body['errors']) return body.get('data', {})