async def execute( schema: GraphQLSchema, query: str, context_value: typing.Any = None, variable_values: typing.Dict[str, typing.Any] = None, operation_name: str = None, ): schema_validation_errors = validate_schema(schema) if schema_validation_errors: return ExecutionResult(data=None, errors=schema_validation_errors) try: document = parse(query) except GraphQLError as error: return ExecutionResult(data=None, errors=[error]) except Exception as error: error = GraphQLError(str(error), original_error=error) return ExecutionResult(data=None, errors=[error]) validation_errors = validate(schema, document) if validation_errors: return ExecutionResult(data=None, errors=validation_errors) return graphql_excute( schema, parse(query), middleware=[DirectivesMiddleware()], variable_values=variable_values, operation_name=operation_name, context_value=context_value, )
def test_format_execution_result(): result = format_execution_result(None) assert result == GraphQLResponse(None, 200) data = {"answer": 42} result = format_execution_result(ExecutionResult(data, None)) assert result == GraphQLResponse({"data": data}, 200) errors = [GraphQLError("bad")] result = format_execution_result(ExecutionResult(None, errors)) assert result == GraphQLResponse({"errors": errors}, 400)
def test_encode_execution_results_batch(): data = {"answer": 42} errors = [GraphQLError("bad")] results = [ExecutionResult(data, None), ExecutionResult(None, errors)] result = encode_execution_results(results, is_batch=True) assert result == ( '[{"data":{"answer":42}},' '{"errors":[{"message":"bad","locations":null,"path":null}]}]', 400, )
async def execute(self, query: str, variable_values=None, timeout=None) -> Tuple[int, ExecutionResult]: if not self.session: self.session = aiohttp.ClientSession() if isinstance(query, DocumentNode) and self.dsl: query = self.dsl.as_string(query) payload = {"query": query, "variables": variable_values or {}} if self.use_json: body = {"json": payload} else: body = {"data": payload} async with self.session.post( self.url, auth=self.auth, headers=self.headers, timeout=timeout or self.timeout, **body, ) as response: result = await response.json() if self.use_json else response.text( ) if "errors" not in result and "data" not in result: raise ValueError(f'Received incompatible response "{result}"') return ( response.status, ExecutionResult(errors=result.get("errors"), data=result.get("data")), )
def try_fast_introspection(schema: GraphQLSchema, query: str) -> Optional[ExecutionResult]: """Compute the GraphQL introspection query if query can be computed fastly. Args: schema: GraphQL schema object, obtained from the graphql library query: string containing the introspection query to be executed on the schema Returns: - GraphQL Execution Result with data = None: there were schema validation errors. - GraphQL ExecutionResult with data != None: fast introspection was successful and computed data can be found under data attribute. - None if the query does not match the set introspection query in this module: the query cannot be computed fastly with this module. """ if _remove_whitespace_from_query( query) != _whitespace_free_introspection_query: return None # Schema validations schema_validation_errors = validate_schema(schema) if schema_validation_errors: return ExecutionResult(data=None, errors=schema_validation_errors) return _execute_fast_introspection_query(schema)
async def send_events(zero_event: ZeroEvent) -> AsyncIterable[bytes]: LOGGER.debug('Streaming subscription started.') try: zero_event.increment() async for val in cancellable_aiter(result, self.cancellation_event, timeout=self.ping_interval): yield encode(val) yield nudge # Give the ASGI server a nudge. except asyncio.CancelledError: LOGGER.debug("Streaming subscription cancelled.") except Exception as error: # pylint: disable=broad-except LOGGER.exception("Streaming subscription failed.") # If the error is not caught the client fetch will fail, however # the status code and headers have already been sent. So rather # than let the fetch fail we send a GraphQL response with no # data and the error and close gracefully. if not isinstance(error, GraphQLError): error = GraphQLError('Execution error', original_error=error) val = ExecutionResult(None, [error]) yield encode(val) yield nudge # Give the ASGI server a nudge. finally: zero_event.decrement() LOGGER.debug("Streaming subscription stopped.")
async def subscribe(self, query, *args, **kwargs): """Execute a GraphQL subscription on the schema asynchronously.""" # Do parsing try: document = parse(query) except GraphQLError as error: return ExecutionResult(data=None, errors=[error]) # Do validation validation_errors = validate(self.graphql_schema, document) if validation_errors: return ExecutionResult(data=None, errors=validation_errors) # Execute the query kwargs = normalize_execute_kwargs(kwargs) return await subscribe(self.graphql_schema, document, *args, **kwargs)
async def execute( self, document: DocumentNode, variable_values: Optional[Dict[str, str]] = None, operation_name: Optional[str] = None, extra_args: Dict[str, Any] = {}, ) -> ExecutionResult: """Execute the provided document AST against the configured remote server. This uses the aiohttp library to perform a HTTP POST request asynchronously to the remote server. The result is sent as an ExecutionResult object. """ query_str = print_ast(document) payload: Dict[str, Any] = { "query": query_str, } if variable_values: payload["variables"] = variable_values if operation_name: payload["operationName"] = operation_name post_args = { "json": payload, } # Pass post_args to aiohttp post method post_args.update(extra_args) if self.session is None: raise TransportClosed("Transport is not connected") async with self.session.post(self.url, ssl=self.ssl, **post_args) as resp: try: result = await resp.json() except Exception: # We raise a TransportServerError if the status code is 400 or higher # We raise a TransportProtocolError in the other cases try: # Raise a ClientResponseError if response status is 400 or higher resp.raise_for_status() except ClientResponseError as e: raise TransportServerError from e raise TransportProtocolError( "Server did not return a GraphQL result") if "errors" not in result and "data" not in result: raise TransportProtocolError( "Server did not return a GraphQL result") return ExecutionResult(errors=result.get("errors"), data=result.get("data"))
def execute_graphql_request( schema: GraphQLSchema, params: GraphQLParams, allow_only_query: bool = False, **kwargs, ): if not params.query: raise HttpQueryError(400, "Must provide query string.") try: document = parse(params.query) except GraphQLError as e: return ExecutionResult(data=None, errors=[e]) except Exception as e: e = GraphQLError(str(e), original_error=e) return ExecutionResult(data=None, errors=[e]) if allow_only_query: operation_ast = get_operation_ast(document, params.operation_name) if operation_ast: operation = operation_ast.operation.value if operation != "query": raise HttpQueryError( 405, f"Can only perform a {operation} operation from a POST request.", headers={"Allow": "POST"}, ) # Note: the schema is not validated here for performance reasons. # This should be done only once when starting the server. validation_errors = validate(schema, document) if validation_errors: return ExecutionResult(data=None, errors=validation_errors) return execute( schema, document, variable_values=params.variables, operation_name=params.operation_name, **kwargs, )
async def execute( schema: GraphQLSchema, query: str, root_value: typing.Any = None, context_value: typing.Any = None, variable_values: typing.Dict[str, typing.Any] = None, middleware: typing.List[Middleware] = None, operation_name: str = None, ): # pragma: no cover schema_validation_errors = validate_schema(schema) if schema_validation_errors: return ExecutionResult(data=None, errors=schema_validation_errors) try: document = parse(query) except GraphQLError as error: return ExecutionResult(data=None, errors=[error]) except Exception as error: error = GraphQLError(str(error), original_error=error) return ExecutionResult(data=None, errors=[error]) validation_errors = validate(schema, document) if validation_errors: return ExecutionResult(data=None, errors=validation_errors) result = graphql_execute( schema, parse(query), root_value=root_value, middleware=middleware or [], variable_values=variable_values, operation_name=operation_name, context_value=context_value, ) if isawaitable(result): result = await typing.cast(typing.Awaitable[ExecutionResult], result) return result
async def _process_subscription(self, id_: Id, result: AsyncIterator) -> AsyncIterator: try: async for val in result: await self._send_execution_result(id_, val) await self.web_socket.send(self._to_message(GQL_COMPLETE, id_)) except asyncio.CancelledError: pass except Exception as error: # pylint: disable=broad-except if not isinstance(error, GraphQLError): error = GraphQLError('Execution error', original_error=error) await self._send_execution_result(id_, ExecutionResult(errors=[error])) await self.web_socket.send(self._to_message(GQL_COMPLETE, id_)) return result
async def start(self, context: ConnectionContext, message: OperationMessage) -> None: if context.user and not context.user.is_authenticated: await self.send_error(context, message.id, {'message': 'Invalid auth credentials.'}) return op_id = message.id # if we already have a subscription with this id, unsubscribe from it first if op_id in context.operations: await self.unsubscribe(context, op_id) payload = message.payload try: doc = parse(payload.query) except Exception as exc: if isinstance(exc, GraphQLError): await self.send_execution_result(context, op_id, ExecutionResult(data=None, errors=[exc])) else: await self.send_error(context, op_id, {'message': str(exc)}) return result_or_iterator = await subscribe( self.schema, doc, variable_values=payload.variables, context_value=context, operation_name=payload.operation_name, ) if isinstance(result_or_iterator, ExecutionResult): result_or_iterator = create_async_iterator([result_or_iterator]) context.operations[op_id] = result_or_iterator async def iter_result(): async for result in result_or_iterator: await self.send_execution_result(context, op_id, result) await self.send_message(context, MessageType.GQL_COMPLETE, op_id=op_id) asyncio.create_task(iter_result())
def _execute_fast_introspection_query( schema: GraphQLSchema) -> ExecutionResult: """Compute the GraphQL introspection query.""" response_types = [] for type_ in __Schema.fields["types"].resolve(schema, None): response_types.append(_get_full_type(type_, schema)) response_directives = [] for directive in __Schema.fields["directives"].resolve(schema, None): response_directives.append(_get_directive(directive)) query_type = __Schema.fields["queryType"].resolve(schema, None) mutation_type = __Schema.fields["mutationType"].resolve(schema, None) subscription_type = __Schema.fields["subscriptionType"].resolve( schema, None) response: Dict[str, Any] = { "queryType": { "name": __Type.fields["name"].resolve(query_type, None) }, "mutationType": ({ "name": __Type.fields["name"].resolve(mutation_type, None) } if mutation_type else None), "subscriptionType": ({ "name": __Type.fields["name"].resolve(subscription_type, None) } if subscription_type else None), "types": response_types, "directives": response_directives, } response_payload = {"__schema": response} return ExecutionResult(data=response_payload, errors=None)
def _parse_answer_graphqlws( self, json_answer: Dict[str, Any] ) -> Tuple[str, Optional[int], Optional[ExecutionResult]]: """Parse the answer received from the server if the server supports the graphql-ws protocol. Returns a list consisting of: - the answer_type (between: 'connection_ack', 'ping', 'pong', 'data', 'error', 'complete') - the answer id (Integer) if received or None - an execution Result if the answer_type is 'data' or None Differences with the apollo websockets protocol (superclass): - the "data" message is now called "next" - the "stop" message is now called "complete" - there is no connection_terminate or connection_error messages - instead of a unidirectional keep-alive (ka) message from server to client, there is now the possibility to send bidirectional ping/pong messages - connection_ack has an optional payload - the 'error' answer type returns a list of errors instead of a single error """ answer_type: str = "" answer_id: Optional[int] = None execution_result: Optional[ExecutionResult] = None try: answer_type = str(json_answer.get("type")) if answer_type in ["next", "error", "complete"]: answer_id = int(str(json_answer.get("id"))) if answer_type == "next" or answer_type == "error": payload = json_answer.get("payload") if answer_type == "next": if not isinstance(payload, dict): raise ValueError("payload is not a dict") if "errors" not in payload and "data" not in payload: raise ValueError( "payload does not contain 'data' or 'errors' fields" ) execution_result = ExecutionResult( errors=payload.get("errors"), data=payload.get("data"), extensions=payload.get("extensions"), ) # Saving answer_type as 'data' to be understood with superclass answer_type = "data" elif answer_type == "error": if not isinstance(payload, list): raise ValueError("payload is not a list") raise TransportQueryError(str(payload[0]), query_id=answer_id, errors=payload) elif answer_type in ["ping", "pong", "connection_ack"]: self.payloads[answer_type] = json_answer.get("payload", None) else: raise ValueError if self.check_keep_alive_task is not None: self._next_keep_alive_message.set() except ValueError as e: raise TransportProtocolError( f"Server did not return a GraphQL result: {json_answer}" ) from e return answer_type, answer_id, execution_result
async def execute( self, document: DocumentNode, variable_values: Optional[Dict[str, str]] = None, operation_name: Optional[str] = None, extra_args: Dict[str, Any] = None, upload_files: bool = False, ) -> ExecutionResult: """Execute the provided document AST against the configured remote server using the current session. This uses the aiohttp library to perform a HTTP POST request asynchronously to the remote server. Don't call this coroutine directly on the transport, instead use :code:`execute` on a client or a session. :param document: the parsed GraphQL request :param variables_values: An optional Dict of variable values :param operation_name: An optional Operation name for the request :param extra_args: additional arguments to send to the aiohttp post method :param upload_files: Set to True if you want to put files in the variable values :returns: an ExecutionResult object. """ query_str = print_ast(document) payload: Dict[str, Any] = { "query": query_str, } if operation_name: payload["operationName"] = operation_name if upload_files: # If the upload_files flag is set, then we need variable_values assert variable_values is not None # If we upload files, we will extract the files present in the # variable_values dict and replace them by null values nulled_variable_values, files = extract_files( variables=variable_values, file_classes=self.file_classes, ) # Save the nulled variable values in the payload payload["variables"] = nulled_variable_values # Prepare aiohttp to send multipart-encoded data data = aiohttp.FormData() # Generate the file map # path is nested in a list because the spec allows multiple pointers # to the same file. But we don't support that. # Will generate something like {"0": ["variables.file"]} file_map = {str(i): [path] for i, path in enumerate(files)} # Enumerate the file streams # Will generate something like {'0': <_io.BufferedReader ...>} file_streams = {str(i): files[path] for i, path in enumerate(files)} # Add the payload to the operations field operations_str = json.dumps(payload) log.debug("operations %s", operations_str) data.add_field( "operations", operations_str, content_type="application/json" ) # Add the file map field file_map_str = json.dumps(file_map) log.debug("file_map %s", file_map_str) data.add_field("map", file_map_str, content_type="application/json") # Add the extracted files as remaining fields for k, v in file_streams.items(): data.add_field(k, v, filename=k) post_args: Dict[str, Any] = {"data": data} else: if variable_values: payload["variables"] = variable_values if log.isEnabledFor(logging.INFO): log.info(">>> %s", json.dumps(payload)) post_args = {"json": payload} # Pass post_args to aiohttp post method if extra_args: post_args.update(extra_args) if self.session is None: raise TransportClosed("Transport is not connected") async with self.session.post(self.url, ssl=self.ssl, **post_args) as resp: try: result = await resp.json() if log.isEnabledFor(logging.INFO): result_text = await resp.text() log.info("<<< %s", result_text) except Exception: # We raise a TransportServerError if the status code is 400 or higher # We raise a TransportProtocolError in the other cases try: # Raise a ClientResponseError if response status is 400 or higher resp.raise_for_status() except ClientResponseError as e: raise TransportServerError(str(e)) from e result_text = await resp.text() raise TransportProtocolError( f"Server did not return a GraphQL result: {result_text}" ) if "errors" not in result and "data" not in result: result_text = await resp.text() raise TransportProtocolError( "Server did not return a GraphQL result: " 'No "data" or "error" keys in answer: ' f"{result_text}" ) return ExecutionResult(errors=result.get("errors"), data=result.get("data"))
def execute( # type: ignore self, document: DocumentNode, variable_values: Optional[Dict[str, Any]] = None, operation_name: Optional[str] = None, timeout: Optional[int] = None, ) -> ExecutionResult: """Execute GraphQL query. Execute the provided document AST against the configured remote server. This uses the requests library to perform a HTTP POST request to the remote server. :param document: GraphQL query as AST Node object. :param variable_values: Dictionary of input parameters (Default: None). :param operation_name: Name of the operation that shall be executed. Only required in multi-operation documents (Default: None). :param timeout: Specifies a default timeout for requests (Default: None). :return: The result of execution. `data` is the result of executing the query, `errors` is null if no errors occurred, and is a non-empty array if an error occurred. """ if not self.session: raise TransportClosed("Transport is not connected") query_str = print_ast(document) payload: Dict[str, Any] = {"query": query_str} if variable_values: payload["variables"] = variable_values if operation_name: payload["operationName"] = operation_name data_key = "json" if self.use_json else "data" post_args = { "headers": self.headers, "auth": self.auth, "cookies": self.cookies, "timeout": timeout or self.default_timeout, "verify": self.verify, data_key: payload, } # Log the payload if log.isEnabledFor(logging.INFO): log.info(">>> %s", json.dumps(payload)) # Pass kwargs to requests post method post_args.update(self.kwargs) # Using the created session to perform requests response = self.session.request( self.method, self.url, **post_args # type: ignore ) try: result = response.json() if log.isEnabledFor(logging.INFO): log.info("<<< %s", response.text) except Exception: # We raise a TransportServerError if the status code is 400 or higher # We raise a TransportProtocolError in the other cases try: # Raise a requests.HTTPerror if response status is 400 or higher response.raise_for_status() except requests.HTTPError as e: raise TransportServerError(str(e)) raise TransportProtocolError( "Server did not return a GraphQL result") if "errors" not in result and "data" not in result: raise TransportProtocolError( "Server did not return a GraphQL result") return ExecutionResult(errors=result.get("errors"), data=result.get("data"))
def _parse_answer( self, answer: str ) -> Tuple[str, Optional[int], Optional[ExecutionResult]]: """Parse the answer received from the server Returns a list consisting of: - the answer_type (between: 'heartbeat', 'data', 'reply', 'error', 'close') - the answer id (Integer) if received or None - an execution Result if the answer_type is 'data' or None """ event: str = "" answer_id: Optional[int] = None answer_type: str = "" execution_result: Optional[ExecutionResult] = None try: json_answer = json.loads(answer) event = str(json_answer.get("event")) if event == "subscription:data": payload = json_answer.get("payload") if not isinstance(payload, dict): raise ValueError("payload is not a dict") subscription_id = str(payload.get("subscriptionId")) try: answer_id = self.subscription_ids_to_query_ids[ subscription_id] except KeyError: raise ValueError( f"subscription '{subscription_id}' has not been registerd" ) result = payload.get("result") if not isinstance(result, dict): raise ValueError("result is not a dict") answer_type = "data" execution_result = ExecutionResult( errors=payload.get("errors"), data=result.get("data")) elif event == "phx_reply": answer_id = int(json_answer.get("ref")) payload = json_answer.get("payload") if not isinstance(payload, dict): raise ValueError("payload is not a dict") status = str(payload.get("status")) if status == "ok": answer_type = "reply" response = payload.get("response") if isinstance(response, dict) and "subscriptionId" in response: subscription_id = str(response.get("subscriptionId")) self.subscription_ids_to_query_ids[ subscription_id] = answer_id elif status == "error": response = payload.get("response") if isinstance(response, dict): if "errors" in response: raise TransportQueryError(str( response.get("errors")), query_id=answer_id) elif "reason" in response: raise TransportQueryError(str( response.get("reason")), query_id=answer_id) raise ValueError("reply error") elif status == "timeout": raise TransportQueryError("reply timeout", query_id=answer_id) elif event == "phx_error": raise TransportServerError("Server error") elif event == "phx_close": answer_type = "close" else: raise ValueError except ValueError as e: raise TransportProtocolError( "Server did not return a GraphQL result") from e return answer_type, answer_id, execution_result
def _parse_answer( self, answer: str ) -> Tuple[str, Optional[int], Optional[ExecutionResult]]: """Parse the answer received from the server Returns a list consisting of: - the answer_type (between: 'data', 'reply', 'complete', 'close') - the answer id (Integer) if received or None - an execution Result if the answer_type is 'data' or None """ event: str = "" answer_id: Optional[int] = None answer_type: str = "" execution_result: Optional[ExecutionResult] = None subscription_id: Optional[str] = None def _get_value(d: Any, key: str, label: str) -> Any: if not isinstance(d, dict): raise ValueError(f"{label} is not a dict") return d.get(key) def _required_value(d: Any, key: str, label: str) -> Any: value = _get_value(d, key, label) if value is None: raise ValueError(f"null {key} in {label}") return value def _required_subscription_id(d: Any, label: str, must_exist: bool = False, must_not_exist=False) -> str: subscription_id = str(_required_value(d, "subscriptionId", label)) if must_exist and (subscription_id not in self.subscriptions): raise ValueError("unregistered subscriptionId") if must_not_exist and (subscription_id in self.subscriptions): raise ValueError("previously registered subscriptionId") return subscription_id def _validate_data_response(d: Any, label: str) -> dict: """Make sure query, mutation or subscription answer conforms. The GraphQL spec says only three keys are permitted. """ if not isinstance(d, dict): raise ValueError(f"{label} is not a dict") keys = set(d.keys()) invalid = keys - {"data", "errors", "extensions"} if len(invalid) > 0: raise ValueError(f"{label} contains invalid items: " + ", ".join(invalid)) return d try: json_answer = json.loads(answer) event = str(_required_value(json_answer, "event", "answer")) if event == "subscription:data": payload = _required_value(json_answer, "payload", "answer") subscription_id = _required_subscription_id(payload, "payload", must_exist=True) result = _validate_data_response(payload.get("result"), "result") answer_type = "data" subscription = self.subscriptions[subscription_id] answer_id = subscription.listener_id execution_result = ExecutionResult( data=result.get("data"), errors=result.get("errors"), extensions=result.get("extensions"), ) elif event == "phx_reply": # Will generate a ValueError if 'ref' is not there # or if it is not an integer answer_id = int(_required_value(json_answer, "ref", "answer")) payload = _required_value(json_answer, "payload", "answer") status = _get_value(payload, "status", "payload") if status == "ok": answer_type = "reply" if answer_id in self.listeners: response = _required_value(payload, "response", "payload") if isinstance(response, dict) and "subscriptionId" in response: # Subscription answer subscription_id = _required_subscription_id( response, "response", must_not_exist=True) self.subscriptions[subscription_id] = Subscription( answer_id) else: # Query or mutation answer # GraphQL spec says only three keys are permitted response = _validate_data_response( response, "response") answer_type = "data" execution_result = ExecutionResult( data=response.get("data"), errors=response.get("errors"), extensions=response.get("extensions"), ) else: ( registered_subscription_id, listener_id, ) = self._find_subscription(answer_id) if registered_subscription_id is not None: # Unsubscription answer response = _required_value(payload, "response", "payload") subscription_id = _required_subscription_id( response, "response") if subscription_id != registered_subscription_id: raise ValueError( "subscription id does not match") answer_type = "complete" answer_id = listener_id elif status == "error": response = payload.get("response") if isinstance(response, dict): if "errors" in response: raise TransportQueryError(str( response.get("errors")), query_id=answer_id) elif "reason" in response: raise TransportQueryError(str( response.get("reason")), query_id=answer_id) raise TransportQueryError("reply error", query_id=answer_id) elif status == "timeout": raise TransportQueryError("reply timeout", query_id=answer_id) else: # missing or unrecognized status, just continue pass elif event == "phx_error": # Sent if the channel has crashed # answer_id will be the "join_ref" for the channel # answer_id = int(json_answer.get("ref")) raise TransportServerError("Server error") elif event == "phx_close": answer_type = "close" else: raise ValueError("unrecognized event") except ValueError as e: log.error(f"Error parsing answer '{answer}': {e!r}") raise TransportProtocolError( f"Server did not return a GraphQL result: {e!s}") from e return answer_type, answer_id, execution_result
def _parse_answer( self, answer: str ) -> Tuple[str, Optional[int], Optional[ExecutionResult]]: """Parse the answer received from the server Returns a list consisting of: - the answer_type (between: 'connection_ack', 'ka', 'connection_error', 'data', 'error', 'complete') - the answer id (Integer) if received or None - an execution Result if the answer_type is 'data' or None """ answer_type: str = "" answer_id: Optional[int] = None execution_result: Optional[ExecutionResult] = None try: json_answer = json.loads(answer) answer_type = str(json_answer.get("type")) if answer_type in ["data", "error", "complete"]: answer_id = int(str(json_answer.get("id"))) if answer_type == "data" or answer_type == "error": payload = json_answer.get("payload") if not isinstance(payload, dict): raise ValueError("payload is not a dict") if answer_type == "data": if "errors" not in payload and "data" not in payload: raise ValueError( "payload does not contain 'data' or 'errors' fields" ) execution_result = ExecutionResult( errors=payload.get("errors"), data=payload.get("data")) elif answer_type == "error": raise TransportQueryError(str(payload), query_id=answer_id, errors=[payload]) elif answer_type == "ka": # KeepAlive message pass elif answer_type == "connection_ack": pass elif answer_type == "connection_error": error_payload = json_answer.get("payload") raise TransportServerError( f"Server error: '{repr(error_payload)}'") else: raise ValueError except ValueError as e: raise TransportProtocolError( "Server did not return a GraphQL result") from e return answer_type, answer_id, execution_result
def test_encode_execution_results(): data = {"answer": 42} errors = [GraphQLError("bad")] results = [ExecutionResult(data, None), ExecutionResult(None, errors)] result = encode_execution_results(results) assert result == ('{"data":{"answer":42}}', 400)
def test_encode_execution_results_not_encoded(): data = {"answer": 42} results = [ExecutionResult(data, None)] result = encode_execution_results(results, encode=lambda r: r) assert result == ({"data": data}, 200)
async def __call__(self, request: Request) -> Response: """ Run the GraphQL query provided. :param request: aiohttp Request :return: aiohttp Response """ request_method = request.method.lower() try: variables = json.loads(request.query.get("variables", "{}")) except json.decoder.JSONDecodeError: return self.error_response("Variables are invalid JSON.") operation_name = request.query.get("operationName") if request_method == "options": return self.process_preflight(request) elif request_method == "post": try: data = await self.parse_body(request) except json.decoder.JSONDecodeError: return self.error_response("POST body sent invalid JSON.") operation_name = data.get("operationName", operation_name) elif request_method == "get": data = {"query": request.query.get("query")} else: return self.error_response( "GraphQL only supports GET and POST requests.", 405, headers={"Allow": "GET, POST"}, ) is_tool = self.is_tool(request) vars_dyn = data.get("variables", {}) or {} variables.update( vars_dyn if isinstance(vars_dyn, dict) else json.loads(vars_dyn) ) query = cast(str, data.get("query")) context = self.get_context(request) invalid = False if is_tool: tool = cast(GraphQLTool, self.tool) return await tool.render(query, variables, operation_name) if not data.get("query"): return self.encode_response( request, ExecutionResult( data=None, errors=[GraphQLError(message="Must provide query string.")], ), invalid=True, ) # Validate Schema schema_validation_errors = validate_schema(self.schema) if schema_validation_errors: # pragma: no cover return self.encode_response( request, ExecutionResult(data=None, errors=schema_validation_errors), invalid=True, ) # Parse try: document = parse(query) op = get_operation_ast(document, operation_name) if op is None: invalid = True else: if request_method == "get" and op.operation != OperationType.QUERY: return self.error_response( "Can only perform a {} operation from a POST request.".format( op.operation.value ), 405, headers={"Allow": "POST"}, ) except GraphQLError as error: return self.encode_response( request, ExecutionResult(data=None, errors=[error]), invalid=True ) except Exception as error: # pragma: no cover error = GraphQLError(str(error), original_error=error) return self.encode_response( request, ExecutionResult(data=None, errors=[error]), invalid=True ) # Validate validation_errors = validate(self.schema, document) if validation_errors: return self.encode_response( request, ExecutionResult(data=None, errors=validation_errors), invalid=True, ) if self.asynchronous: result = self._graphql( self.schema, document=document, variable_values=variables, operation_name=operation_name, root_value=self.root_value, context_value=context, middleware=self.middleware, ) if isawaitable(result): # pragma: no branch result = await cast(Awaitable[ExecutionResult], result) else: result = self._graphql( self.schema, document=document, variable_values=variables, operation_name=operation_name, root_value=self.root_value, context_value=context, middleware=self.middleware, ) return self.encode_response( request, cast(ExecutionResult, result), invalid=invalid )
def execute( # type: ignore self, document: DocumentNode, variable_values: Optional[Dict[str, Any]] = None, operation_name: Optional[str] = None, timeout: Optional[int] = None, extra_args: Dict[str, Any] = None, upload_files: bool = False, ) -> ExecutionResult: """Execute GraphQL query. Execute the provided document AST against the configured remote server. This uses the requests library to perform a HTTP POST request to the remote server. :param document: GraphQL query as AST Node object. :param variable_values: Dictionary of input parameters (Default: None). :param operation_name: Name of the operation that shall be executed. Only required in multi-operation documents (Default: None). :param timeout: Specifies a default timeout for requests (Default: None). :param extra_args: additional arguments to send to the requests post method :param upload_files: Set to True if you want to put files in the variable values :return: The result of execution. `data` is the result of executing the query, `errors` is null if no errors occurred, and is a non-empty array if an error occurred. """ if not self.session: raise TransportClosed("Transport is not connected") query_str = print_ast(document) payload: Dict[str, Any] = {"query": query_str} if operation_name: payload["operationName"] = operation_name post_args = { "headers": self.headers, "auth": self.auth, "cookies": self.cookies, "timeout": timeout or self.default_timeout, "verify": self.verify, } if upload_files: # If the upload_files flag is set, then we need variable_values assert variable_values is not None # If we upload files, we will extract the files present in the # variable_values dict and replace them by null values nulled_variable_values, files = extract_files( variables=variable_values, file_classes=self.file_classes, ) # Save the nulled variable values in the payload payload["variables"] = nulled_variable_values # Add the payload to the operations field operations_str = json.dumps(payload) log.debug("operations %s", operations_str) # Generate the file map # path is nested in a list because the spec allows multiple pointers # to the same file. But we don't support that. # Will generate something like {"0": ["variables.file"]} file_map = {str(i): [path] for i, path in enumerate(files)} # Enumerate the file streams # Will generate something like {'0': <_io.BufferedReader ...>} file_streams = { str(i): files[path] for i, path in enumerate(files) } # Add the file map field file_map_str = json.dumps(file_map) log.debug("file_map %s", file_map_str) fields = {"operations": operations_str, "map": file_map_str} # Add the extracted files as remaining fields for k, v in file_streams.items(): fields[k] = (getattr(v, "name", k), v) # Prepare requests http to send multipart-encoded data data = MultipartEncoder(fields=fields) post_args["data"] = data if post_args["headers"] is None: post_args["headers"] = {} else: post_args["headers"] = {**post_args["headers"]} post_args["headers"]["Content-Type"] = data.content_type else: if variable_values: payload["variables"] = variable_values data_key = "json" if self.use_json else "data" post_args[data_key] = payload # Log the payload if log.isEnabledFor(logging.INFO): log.info(">>> %s", json.dumps(payload)) # Pass kwargs to requests post method post_args.update(self.kwargs) # Pass post_args to requests post method if extra_args: post_args.update(extra_args) # Using the created session to perform requests response = self.session.request( self.method, self.url, **post_args # type: ignore ) self.response_headers = response.headers def raise_response_error(resp: requests.Response, reason: str): # We raise a TransportServerError if the status code is 400 or higher # We raise a TransportProtocolError in the other cases try: # Raise a HTTPError if response status is 400 or higher resp.raise_for_status() except requests.HTTPError as e: raise TransportServerError(str(e), e.response.status_code) from e result_text = resp.text raise TransportProtocolError( f"Server did not return a GraphQL result: " f"{reason}: " f"{result_text}") try: result = response.json() if log.isEnabledFor(logging.INFO): log.info("<<< %s", response.text) except Exception: raise_response_error(response, "Not a JSON answer") if "errors" not in result and "data" not in result: raise_response_error(response, 'No "data" or "errors" keys in answer') return ExecutionResult( errors=result.get("errors"), data=result.get("data"), extensions=result.get("extensions"), )