from time import sleep from threading import Lock from gql import gql, Client, GraphQLRequest from gql.transport.requests import RequestsHTTPTransport from gql.transport.exceptions import TransportServerError from app.dd_trace_helpers import get_dd_trace_headers from app.logger import log from app.utils import read_query class GraphQLClient: """GraphQL client.""" def __init__(self, api_client): log.info(f"Initializing GraphQL Client for {api_client.secrets['env']}") self.api_client = api_client self.headers = self._build_headers() self._token_lock = Lock() def _build_headers(self): headers = { "Authorization": f"Bearer {self.api_client.token}", "Orchard-Identity-Id": "62d8bf61-9424-47d1-8571-a5b576c0411d", "Orchard-Profile-Id": "374774", "Orchard-Profile-Type": "InsightsProfile", "apollographql-client-name": "frontend-insights", "cache-control": "no-cache", "content-type": "application/json" } return headers def _build_client(self): dd_headers = get_dd_trace_headers() transport = RequestsHTTPTransport( url=self.api_client.secrets['env'], headers={**self.headers, **dd_headers}, use_json=True ) return Client(transport=transport, fetch_schema_from_transport=False) def execute_query(self, query_str, variables=None): log.info(f"Executing GQL query") # Create a fresh transport and client for each request with self._build_client() as client: request = GraphQLRequest(gql(query_str), variable_values=variables) return client.execute(request) def fetch_data(self, query_name, variables=None, retries=3): query_string = read_query(f'{query_name}.graphql') for attempt in range(retries): try: return self.execute_query(query_string, variables) except TransportServerError as e: # 403 or 401: token expired if e.code in (401, 403): log.warning( f"Got {e.code} — refreshing token (attempt {attempt + 1}/{retries})") with self._token_lock: new_token = self.api_client.fetch_fresh_token() if new_token: self.api_client.update_token(new_token) self.headers = self._build_headers() self.client = self._build_client() else: log.error( "Token refresh failed — aborting GraphQL request.") return None continue # 429: rate limit if e.code == 429: wait = 2 ** attempt log.warning(f"Rate limit (429). Retrying in {wait}s...") sleep(wait) continue raise # unrecoverable except Exception as e: # Generic network/transport retry log.warning( f"GraphQL request failed ({type(e).__name__}), retry {attempt + 1}/{retries}: {e}") sleep(1) continue