"""Provides a generic connector for upstream GraphQL services.""" from sgqlc.endpoint.http import HTTPEndpoint import backoff class GraphQLConnector: """Connector to an upstream GraphQL service.""" def __init__(self, url: str, app_name: str, raise_on_error=True): """Create an upstream server instance.""" self.raise_on_error = raise_on_error self.endpoint = HTTPEndpoint( url, base_headers={'apollographql-client-name': app_name} ) self.headers = {} def set_headers(self, headers=None): """Set request headers. Args: headers (dict): headers """ if headers: self.headers.update(headers) def get_headers(self): """Return request headers. Returns: dict """ return self.headers def execute(self, query: str, data: dict): """Execute a call to a GraphQL endpoint. Args: query (str): Query data (dict): JSON formatted params Returns: dict """ response = self.endpoint( query, data, extra_headers=self.get_headers()) if response is None: raise GraphQLError([{'message': 'No response from GraphQL endpoint'}]) errors = response.get('errors') if self.raise_on_error and errors: raise GraphQLError(errors) if 'data' not in response: raise GraphQLError([ {'message': f'Invalid GraphQL response: {response!r}'} ]) return response class GraphQLError(Exception): """Any GraphQL errors.""" def __init__(self, errors): """Init. Args: errors (list): """ self.errors = errors if isinstance(errors, list) else [errors] error = self.errors[-1] if self.errors else {} if not isinstance(error, dict): error = {'message': str(error)} self.message = error.get('message', 'Unknown GraphQL error') self.path = error.get('path') self.code = None self.response = None self.stacktrace = None extensions = error.get('extensions') or {} self.code = extensions.get('code') self.response = extensions.get('response') exception = extensions.get('exception') or {} self.stacktrace = exception.get('stacktrace') """Derive self.message from best available source: GraphQL's body message or top-level message""" body = self.response.get('body') if self.response else None upstream_message = None if isinstance(body, dict): upstream_message = body.get('message') or body.get('error') elif isinstance(body, str) and body: upstream_message = body self.message = ( upstream_message or error.get('message') or 'Unknown GraphQL error' ) super().__init__(self.message) def get_response_status(self): """Return service response status code. Returns: int """ if not self.response: return None return self.response.get('status') def get_response_body(self): """Return service response body. Returns: dict """ if not self.response: return None return self.response.get('body') def get_stacktrace(self): """Return service stacktrace. Returns: list """ return self.stacktrace def get_response(self): """Return service response. Returns: dict """ return self.response class GraphQLBackoffConnector(GraphQLConnector): """Connector to an upstream GraphQL service with backoff.""" @backoff.on_exception( backoff.expo, GraphQLError, max_tries=3, max_time=90, max_value=30, ) def execute(self, query: str, data: dict): """Execute a call to a GraphQL endpoint. Args: query (str): Query data (dict): JSON formatted params Returns: dict """ response = self.endpoint( query, data, extra_headers=self.get_headers()) if response is None: raise GraphQLError([{'message': 'No response from GraphQL endpoint'}]) errors = response.get('errors') if self.raise_on_error and errors: raise GraphQLError(errors) if 'data' not in response: raise GraphQLError([ {'message': f'Invalid GraphQL response: {response!r}'} ]) return response