From c09ddd1baa2292636c6dd388ae6f9c82029759a6 Mon Sep 17 00:00:00 2001 From: Ignacio Guerrero Date: Mon, 4 Sep 2023 17:22:06 -0300 Subject: [PATCH 01/15] Add `execute_batch` for requests sync transport --- gql/client.py | 178 +++++++++++++++++- gql/transport/data_structures/__init__.py | 3 + .../data_structures/graphql_request.py | 37 ++++ gql/transport/httpx.py | 10 + gql/transport/requests.py | 151 ++++++++++++++- gql/transport/transport.py | 21 +++ 6 files changed, 388 insertions(+), 12 deletions(-) create mode 100644 gql/transport/data_structures/__init__.py create mode 100644 gql/transport/data_structures/graphql_request.py diff --git a/gql/client.py b/gql/client.py index f6302987..459c335f 100644 --- a/gql/client.py +++ b/gql/client.py @@ -8,6 +8,7 @@ Callable, Dict, Generator, + List, Optional, TypeVar, Union, @@ -27,6 +28,8 @@ validate, ) +from gql.transport.data_structures.graphql_request import GraphQLRequest + from .transport.async_transport import AsyncTransport from .transport.exceptions import TransportClosed, TransportQueryError from .transport.local_schema import LocalSchemaTransport @@ -236,6 +239,24 @@ def execute_sync( **kwargs, ) + def execute_batch_sync( + self, + reqs: List[GraphQLRequest], + serialize_variables: Optional[bool] = None, + parse_result: Optional[bool] = None, + get_execution_result: bool = False, + **kwargs, + ) -> Union[List[Dict[str, Any]], List[ExecutionResult]]: + """:meta private:""" + with self as session: + return session.execute_batch( + reqs, + serialize_variables=serialize_variables, + parse_result=parse_result, + get_execution_result=get_execution_result, + **kwargs, + ) + @overload async def execute_async( self, @@ -375,7 +396,6 @@ def execute( """ if isinstance(self.transport, AsyncTransport): - # Get the current asyncio event loop # Or create a new event loop if there isn't one (in a new Thread) try: @@ -418,6 +438,48 @@ def execute( **kwargs, ) + def execute_batch( + self, + reqs: List[GraphQLRequest], + serialize_variables: Optional[bool] = None, + parse_result: Optional[bool] = None, + get_execution_result: bool = False, + **kwargs, + ) -> Union[List[Dict[str, Any]], List[ExecutionResult]]: + """Execute the provided document AST against the remote server using + the transport provided during init. + + This function **WILL BLOCK** until the result is received from the server. + + Either the transport is sync and we execute the query synchronously directly + OR the transport is async and we execute the query in the asyncio loop + (blocking here until answer). + + This method will: + + - connect using the transport to get a session + - execute the GraphQL request on the transport session + - close the session and close the connection to the server + + If you have multiple requests to send, it is better to get your own session + and execute the requests in your session. + + The extra arguments passed in the method will be passed to the transport + execute method. + """ + + if isinstance(self.transport, AsyncTransport): + raise NotImplementedError("Batching is not implemented for async yet.") + + else: # Sync transports + return self.execute_batch_sync( + reqs, + serialize_variables=serialize_variables, + parse_result=parse_result, + get_execution_result=get_execution_result, + **kwargs, + ) + @overload def subscribe_async( self, @@ -476,7 +538,6 @@ async def subscribe_async( ]: """:meta private:""" async with self as session: - generator = session.subscribe( document, variable_values=variable_values, @@ -600,7 +661,6 @@ def subscribe( pass except (KeyboardInterrupt, Exception, GeneratorExit): - # Graceful shutdown asyncio.ensure_future(async_generator.aclose(), loop=loop) @@ -661,11 +721,9 @@ async def close_async(self): await self.transport.close() async def __aenter__(self): - return await self.connect_async() async def __aexit__(self, exc_type, exc, tb): - await self.close_async() def connect_sync(self): @@ -705,7 +763,6 @@ def close_sync(self): self.transport.close() def __enter__(self): - return self.connect_sync() def __exit__(self, *args): @@ -880,6 +937,112 @@ def execute( return result.data + def _execute_batch( + self, + reqs: List[GraphQLRequest], + serialize_variables: Optional[bool] = None, + parse_result: Optional[bool] = None, + **kwargs, + ) -> List[ExecutionResult]: + """Execute the provided document AST synchronously using + the sync transport, returning an ExecutionResult object. + + :param document: GraphQL query as AST Node object. + :param variable_values: Dictionary of input parameters. + :param operation_name: Name of the operation that shall be executed. + :param serialize_variables: whether the variable values should be + serialized. Used for custom scalars and/or enums. + By default use the serialize_variables argument of the client. + :param parse_result: Whether gql will unserialize the result. + By default use the parse_results argument of the client. + + The extra arguments are passed to the transport execute method.""" + + # Validate document + if self.client.schema: + for req in reqs: + self.client.validate(req.document) + + # Parse variable values for custom scalars if requested + if serialize_variables or ( + serialize_variables is None and self.client.serialize_variables + ): + reqs = [ + req.serialize_variable_values(self.client.schema) + if req.variable_values is not None + else req + for req in reqs + ] + + results = self.transport.execute_batch(reqs, **kwargs) + + # Unserialize the result if requested + if self.client.schema: + if parse_result or (parse_result is None and self.client.parse_results): + for result in results: + result.data = parse_result_fn( + self.client.schema, + req.document, + result.data, + operation_name=req.operation_name, + ) + + return results + + def execute_batch( + self, + reqs: List[GraphQLRequest], + serialize_variables: Optional[bool] = None, + parse_result: Optional[bool] = None, + get_execution_result: bool = False, + **kwargs, + ) -> Union[List[Dict[str, Any]], List[ExecutionResult]]: + """Execute the provided document AST synchronously using + the sync transport. + + Raises a TransportQueryError if an error has been returned in + the ExecutionResult. + + :param document: GraphQL query as AST Node object. + :param variable_values: Dictionary of input parameters. + :param operation_name: Name of the operation that shall be executed. + :param serialize_variables: whether the variable values should be + serialized. Used for custom scalars and/or enums. + By default use the serialize_variables argument of the client. + :param parse_result: Whether gql will unserialize the result. + By default use the parse_results argument of the client. + :param get_execution_result: return the full ExecutionResult instance instead of + only the "data" field. Necessary if you want to get the "extensions" field. + + The extra arguments are passed to the transport execute method.""" + + # Validate and execute on the transport + results = self._execute_batch( + reqs, + serialize_variables=serialize_variables, + parse_result=parse_result, + **kwargs, + ) + + for result in results: + # Raise an error if an error is returned in the ExecutionResult object + if result.errors: + raise TransportQueryError( + str_first_element(result.errors), + errors=result.errors, + data=result.data, + extensions=result.extensions, + ) + + assert ( + result.data is not None + ), "Transport returned an ExecutionResult without data or errors" + + if get_execution_result: + return results + + return [result.data for result in results] # type: ignore + def fetch_schema(self) -> None: """Fetch the GraphQL schema explicitly using introspection. @@ -966,7 +1129,6 @@ async def _subscribe( try: async for result in inner_generator: - if self.client.schema: if parse_result or ( parse_result is None and self.client.parse_results @@ -1070,7 +1232,6 @@ async def subscribe( try: # Validate and subscribe on the transport async for result in inner_generator: - # Raise an error if an error is returned in the ExecutionResult object if result.errors: raise TransportQueryError( @@ -1343,7 +1504,6 @@ async def _connection_loop(self): """ while True: - # Connect to the transport with the retry decorator # By default it should keep retrying until it connect await self._connect_with_retries() diff --git a/gql/transport/data_structures/__init__.py b/gql/transport/data_structures/__init__.py new file mode 100644 index 00000000..2fd89849 --- /dev/null +++ b/gql/transport/data_structures/__init__.py @@ -0,0 +1,3 @@ +from .graphql_request import GraphQLRequest + +__all__ = ["GraphQLRequest"] diff --git a/gql/transport/data_structures/graphql_request.py b/gql/transport/data_structures/graphql_request.py new file mode 100644 index 00000000..0d44aeba --- /dev/null +++ b/gql/transport/data_structures/graphql_request.py @@ -0,0 +1,37 @@ +from typing import Any, Dict, Optional + +from attr import dataclass +from graphql import DocumentNode, GraphQLSchema + +from gql.utilities import serialize_variable_values + + +@dataclass(frozen=True) +class GraphQLRequest: + """GraphQL Request to be executed.""" + + document: DocumentNode + """GraphQL query as AST Node object.""" + + variable_values: Optional[Dict[str, Any]] = None + """Dictionary of input parameters (Default: None).""" + + operation_name: Optional[str] = None + """ + Name of the operation that shall be executed. + Only required in multi-operation documents (Default: None). + """ + + def serialize_variable_values(self, schema: GraphQLSchema) -> "GraphQLRequest": + assert self.variable_values + + return GraphQLRequest( + document=self.document, + variable_values=serialize_variable_values( + schema=schema, + document=self.document, + variable_values=self.variable_values, + operation_name=self.operation_name, + ), + operation_name=self.operation_name, + ) diff --git a/gql/transport/httpx.py b/gql/transport/httpx.py index cfc25dc9..a8ff2014 100644 --- a/gql/transport/httpx.py +++ b/gql/transport/httpx.py @@ -17,6 +17,8 @@ import httpx from graphql import DocumentNode, ExecutionResult, print_ast +from gql.transport.data_structures import GraphQLRequest + from ..utils import extract_files from . import AsyncTransport, Transport from .exceptions import ( @@ -229,6 +231,14 @@ def execute( # type: ignore return self._prepare_result(response) + def execute_batch( + self, + reqs: List[GraphQLRequest], + *args, + **kwargs, + ) -> List[ExecutionResult]: + return super().execute_batch(reqs, *args, **kwargs) + def close(self): """Closing the transport by closing the inner session""" if self.client: diff --git a/gql/transport/requests.py b/gql/transport/requests.py index 6b0bb60b..7faa645d 100644 --- a/gql/transport/requests.py +++ b/gql/transport/requests.py @@ -1,7 +1,7 @@ import io import json import logging -from typing import Any, Collection, Dict, Optional, Tuple, Type, Union +from typing import Any, Collection, Dict, List, Optional, Tuple, Type, Union import requests from graphql import DocumentNode, ExecutionResult, print_ast @@ -13,6 +13,7 @@ from gql.transport import Transport from ..utils import extract_files +from .data_structures import GraphQLRequest from .exceptions import ( TransportAlreadyConnected, TransportClosed, @@ -96,9 +97,7 @@ def __init__( self.response_headers = None def connect(self): - if self.session is None: - # Creating a session that can later be re-use to configure custom mechanisms self.session = requests.Session() @@ -275,6 +274,152 @@ def raise_response_error(resp: requests.Response, reason: str): extensions=result.get("extensions"), ) + def execute_batch( # type: ignore + self, + reqs: List[GraphQLRequest], + timeout: Optional[int] = None, + extra_args: Dict[str, Any] = None, + ) -> List[ExecutionResult]: + """Execute GraphQL query. + + Execute the provided document ASTs against the configured remote server. This + uses the requests library to perform a HTTP POST request to the remote server. + + :param reqs: GraphQL requests as an iterable of GraphQLRequest objects. + :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: A list of results of execution. + For every result `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") + + # Using the created session to perform requests + response = self.session.request( + self.method, + self.url, + **self._build_batch_post_args(reqs, timeout, extra_args), # type: ignore + ) + self.response_headers = response.headers + + answers = self._extract_response(response) + + self._validate_answer_is_a_list(answers) + self._validate_num_of_answers_same_as_requests(reqs, answers) + self._validate_every_answer_is_a_dict(answers) + self._validate_data_and_errors_keys_in_answers(answers) + + return [self._answer_to_execution_result(answer) for answer in answers] + + def _answer_to_execution_result(self, result: Dict[str, Any]) -> ExecutionResult: + return ExecutionResult( + errors=result.get("errors"), + data=result.get("data"), + extensions=result.get("extensions"), + ) + + def _validate_answer_is_a_list(self, results: Any) -> None: + if not isinstance(results, list): + self._raise_invalid_result( + str(results), + "Answer is not a list", + ) + + def _validate_data_and_errors_keys_in_answers( + self, results: List[Dict[str, Any]] + ) -> None: + for result in results: + if "errors" not in result and "data" not in result: + self._raise_invalid_result( + str(results), + 'No "data" or "errors" keys in answer', + ) + + def _validate_every_answer_is_a_dict(self, results: List[Dict[str, Any]]) -> None: + for result in results: + if not isinstance(result, dict): + self._raise_invalid_result(str(results), "Not every answer is dict") + + def _validate_num_of_answers_same_as_requests( + self, + reqs: List[GraphQLRequest], + results: List[Dict[str, Any]], + ) -> None: + if len(reqs) != len(results): + self._raise_invalid_result( + str(results), + "Invalid answer length", + ) + + def _raise_invalid_result(self, result_text: str, reason: str) -> None: + raise TransportProtocolError( + f"Server did not return a valid GraphQL result: " + f"{reason}: " + f"{result_text}" + ) + + def _extract_response(self, response: requests.Response) -> Any: + try: + response.raise_for_status() + result = response.json() + + if log.isEnabledFor(logging.INFO): + log.info("<<< %s", response.text) + + except requests.HTTPError as e: + raise TransportServerError(str(e), e.response.status_code) from e + + except Exception: + self._raise_invalid_result(str(response.text), "Not a JSON answer") + + return result + + def _build_batch_post_args( + self, + reqs: List[GraphQLRequest], + timeout: Optional[int] = None, + extra_args: Dict[str, Any] = None, + ) -> Dict[str, Any]: + post_args: Dict[str, Any] = { + "headers": self.headers, + "auth": self.auth, + "cookies": self.cookies, + "timeout": timeout or self.default_timeout, + "verify": self.verify, + } + + data_key = "json" if self.use_json else "data" + post_args[data_key] = [self._build_data(req) for req in reqs] + + # Log the payload + if log.isEnabledFor(logging.INFO): + log.info(">>> %s", json.dumps(post_args[data_key])) + + # 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) + + return post_args + + def _build_data(self, req: GraphQLRequest) -> Dict[str, Any]: + query_str = print_ast(req.document) + payload: Dict[str, Any] = {"query": query_str} + + if req.operation_name: + payload["operationName"] = req.operation_name + + if req.variable_values: + payload["variables"] = req.variable_values + + return payload + def close(self): """Closing the transport by closing the inner session""" if self.session: diff --git a/gql/transport/transport.py b/gql/transport/transport.py index cf5e94da..173147e8 100644 --- a/gql/transport/transport.py +++ b/gql/transport/transport.py @@ -1,7 +1,10 @@ import abc +from typing import List from graphql import DocumentNode, ExecutionResult +from .data_structures import GraphQLRequest + class Transport(abc.ABC): @abc.abstractmethod @@ -17,6 +20,24 @@ def execute(self, document: DocumentNode, *args, **kwargs) -> ExecutionResult: "Any Transport subclass must implement execute method" ) # pragma: no cover + @abc.abstractmethod + def execute_batch( + self, + reqs: List[GraphQLRequest], + *args, + **kwargs, + ) -> List[ExecutionResult]: + """Execute multiple GraphQL requests in batch. + + Execute the provided requests for either a remote or local GraphQL Schema. + + :param reqs: GraphQL requests as an iterable of GraphQLRequest objects. + :return: iterable of ExecutionResult + """ + raise NotImplementedError( + "Any Transport subclass must implement execute_batch method" + ) # pragma: no cover + def connect(self): """Establish a session with the transport.""" pass # pragma: no cover From bca880b029aaee5673fc5a0fd1bb8b060456315c Mon Sep 17 00:00:00 2001 From: Ignacio Guerrero Date: Mon, 4 Sep 2023 17:23:24 -0300 Subject: [PATCH 02/15] Add tests for `execute_batch` of requests transport --- tests/custom_scalars/test_money.py | 80 +++- tests/datastructures/__init__.py | 0 tests/datastructures/test_graphql_request.py | 203 +++++++++ .../fixtures/vcr_cassettes/queries_batch.yaml | 385 ++++++++++++++++++ tests/test_client.py | 73 +++- tests/test_httpx.py | 14 + tests/test_requests_batch.py | 378 +++++++++++++++++ tests/test_transport_batch.py | 152 +++++++ 8 files changed, 1264 insertions(+), 21 deletions(-) create mode 100644 tests/datastructures/__init__.py create mode 100644 tests/datastructures/test_graphql_request.py create mode 100644 tests/fixtures/vcr_cassettes/queries_batch.yaml create mode 100644 tests/test_requests_batch.py create mode 100644 tests/test_transport_batch.py diff --git a/tests/custom_scalars/test_money.py b/tests/custom_scalars/test_money.py index e67a0bcd..e099c371 100644 --- a/tests/custom_scalars/test_money.py +++ b/tests/custom_scalars/test_money.py @@ -3,7 +3,7 @@ from typing import Any, Dict, NamedTuple, Optional import pytest -from graphql import graphql_sync +from graphql import ExecutionResult, graphql_sync from graphql.error import GraphQLError from graphql.language import ValueNode from graphql.pyutils import inspect @@ -21,6 +21,7 @@ from graphql.utilities import value_from_ast_untyped from gql import Client, gql +from gql.transport.data_structures.graphql_request import GraphQLRequest from gql.transport.exceptions import TransportQueryError from gql.utilities import serialize_value, update_schema_scalar, update_schema_scalars @@ -419,24 +420,45 @@ async def make_money_backend(aiohttp_server): from aiohttp import web async def handler(request): - data = await request.json() - source = data["query"] + req_data = await request.json() - try: - variables = data["variables"] - except KeyError: - variables = None + def handle_single(data: Dict[str, Any]) -> ExecutionResult: + source = data["query"] + try: + variables = data["variables"] + except KeyError: + variables = None - result = graphql_sync( - schema, source, variable_values=variables, root_value=root_value - ) + result = graphql_sync( + schema, source, variable_values=variables, root_value=root_value + ) - return web.json_response( - { - "data": result.data, - "errors": [str(e) for e in result.errors] if result.errors else None, - } - ) + return result + + if isinstance(req_data, list): + results = [handle_single(d) for d in req_data] + + return web.json_response( + [ + { + "data": result.data, + "errors": [str(e) for e in result.errors] + if result.errors + else None, + } + for result in results + ] + ) + else: + result = handle_single(req_data) + return web.json_response( + { + "data": result.data, + "errors": [str(e) for e in result.errors] + if result.errors + else None, + } + ) app = web.Application() app.router.add_route("POST", "/", handler) @@ -736,6 +758,32 @@ def test_code(): await run_sync_test(event_loop, server, test_code) +@pytest.mark.asyncio +@pytest.mark.requests +async def test_custom_scalar_serialize_variables_sync_transport_2( + event_loop, aiohttp_server, run_sync_test +): + + server, transport = await make_sync_money_transport(aiohttp_server) + + def test_code(): + with Client(schema=schema, transport=transport, parse_results=True) as session: + + query = gql("query myquery($money: Money) {toEuros(money: $money)}") + + variable_values = {"money": Money(10, "DM")} + + results = session.execute_batch( + [GraphQLRequest(document=query, variable_values=variable_values)], + serialize_variables=True, + ) + + print(f"result = {results!r}") + assert results[0]["toEuros"] == 5 + + await run_sync_test(event_loop, server, test_code) + + def test_serialize_value_with_invalid_type(): with pytest.raises(GraphQLError) as exc_info: diff --git a/tests/datastructures/__init__.py b/tests/datastructures/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/tests/datastructures/test_graphql_request.py b/tests/datastructures/test_graphql_request.py new file mode 100644 index 00000000..f3b940cf --- /dev/null +++ b/tests/datastructures/test_graphql_request.py @@ -0,0 +1,203 @@ +import asyncio +from math import isfinite +from typing import Any, Dict, NamedTuple, Optional + +import pytest +from graphql.error import GraphQLError +from graphql.language import ValueNode +from graphql.pyutils import inspect +from graphql.type import ( + GraphQLArgument, + GraphQLField, + GraphQLFloat, + GraphQLList, + GraphQLNonNull, + GraphQLObjectType, + GraphQLScalarType, + GraphQLSchema, +) +from graphql.utilities import value_from_ast_untyped + +from gql import gql +from gql.transport.data_structures.graphql_request import GraphQLRequest + +from ..conftest import MS + +# Marking all tests in this file with the aiohttp marker +pytestmark = pytest.mark.aiohttp + + +class Money(NamedTuple): + amount: float + currency: str + + +def is_finite(value: Any) -> bool: + """Return true if a value is a finite number.""" + return (isinstance(value, int) and not isinstance(value, bool)) or ( + isinstance(value, float) and isfinite(value) + ) + + +def serialize_money(output_value: Any) -> Dict[str, Any]: + if not isinstance(output_value, Money): + raise GraphQLError("Cannot serialize money value: " + inspect(output_value)) + return output_value._asdict() + + +def parse_money_value(input_value: Any) -> Money: + """Using Money custom scalar from graphql-core tests except here the + input value is supposed to be a dict instead of a Money object.""" + + """ + if isinstance(input_value, Money): + return input_value + """ + + if isinstance(input_value, dict): + amount = input_value.get("amount", None) + currency = input_value.get("currency", None) + + if not is_finite(amount) or not isinstance(currency, str): + raise GraphQLError("Cannot parse money value dict: " + inspect(input_value)) + + return Money(float(amount), currency) + else: + raise GraphQLError("Cannot parse money value: " + inspect(input_value)) + + +def parse_money_literal( + value_node: ValueNode, variables: Optional[Dict[str, Any]] = None +) -> Money: + money = value_from_ast_untyped(value_node, variables) + if variables is not None and ( + # variables are not set when checked with ValuesIOfCorrectTypeRule + not money + or not is_finite(money.get("amount")) + or not isinstance(money.get("currency"), str) + ): + raise GraphQLError("Cannot parse literal money value: " + inspect(money)) + return Money(**money) + + +MoneyScalar = GraphQLScalarType( + name="Money", + serialize=serialize_money, + parse_value=parse_money_value, + parse_literal=parse_money_literal, +) + +root_value = { + "balance": Money(42, "DM"), + "friends_balance": [Money(12, "EUR"), Money(24, "EUR"), Money(150, "DM")], + "countries_balance": { + "Belgium": Money(15000, "EUR"), + "Luxembourg": Money(99999, "EUR"), + }, +} + + +def resolve_balance(root, _info): + return root["balance"] + + +def resolve_friends_balance(root, _info): + return root["friends_balance"] + + +def resolve_countries_balance(root, _info): + return root["countries_balance"] + + +def resolve_belgium_balance(countries_balance, _info): + return countries_balance["Belgium"] + + +def resolve_luxembourg_balance(countries_balance, _info): + return countries_balance["Luxembourg"] + + +def resolve_to_euros(_root, _info, money): + amount = money.amount + currency = money.currency + if not amount or currency == "EUR": + return amount + if currency == "DM": + return amount * 0.5 + raise ValueError("Cannot convert to euros: " + inspect(money)) + + +countriesBalance = GraphQLObjectType( + name="CountriesBalance", + fields={ + "Belgium": GraphQLField( + GraphQLNonNull(MoneyScalar), resolve=resolve_belgium_balance + ), + "Luxembourg": GraphQLField( + GraphQLNonNull(MoneyScalar), resolve=resolve_luxembourg_balance + ), + }, +) + +queryType = GraphQLObjectType( + name="RootQueryType", + fields={ + "balance": GraphQLField(MoneyScalar, resolve=resolve_balance), + "toEuros": GraphQLField( + GraphQLFloat, + args={"money": GraphQLArgument(MoneyScalar)}, + resolve=resolve_to_euros, + ), + "friends_balance": GraphQLField( + GraphQLList(MoneyScalar), resolve=resolve_friends_balance + ), + "countries_balance": GraphQLField( + GraphQLNonNull(countriesBalance), + resolve=resolve_countries_balance, + ), + }, +) + + +def resolve_spent_money(spent_money, _info, **kwargs): + return spent_money + + +async def subscribe_spend_all(_root, _info, money): + while money.amount > 0: + money = Money(money.amount - 1, money.currency) + yield money + await asyncio.sleep(1 * MS) + + +subscriptionType = GraphQLObjectType( + "Subscription", + fields=lambda: { + "spend": GraphQLField( + MoneyScalar, + args={"money": GraphQLArgument(MoneyScalar)}, + subscribe=subscribe_spend_all, + resolve=resolve_spent_money, + ) + }, +) + +schema = GraphQLSchema( + query=queryType, + subscription=subscriptionType, +) + + +def test_serialize_variables_using_money_example(): + req = GraphQLRequest(document=gql("{balance}")) + + money_value = Money(10, "DM") + + req = GraphQLRequest( + document=gql("query myquery($money: Money) {toEuros(money: $money)}"), + variable_values={"money": money_value}, + ) + + req = req.serialize_variable_values(schema) + + assert req.variable_values == {"money": {"amount": 10, "currency": "DM"}} diff --git a/tests/fixtures/vcr_cassettes/queries_batch.yaml b/tests/fixtures/vcr_cassettes/queries_batch.yaml new file mode 100644 index 00000000..0794cc47 --- /dev/null +++ b/tests/fixtures/vcr_cassettes/queries_batch.yaml @@ -0,0 +1,385 @@ +interactions: +- request: + body: '{"query": "query IntrospectionQuery {\n __schema {\n queryType {\n name\n }\n mutationType + {\n name\n }\n subscriptionType {\n name\n }\n types {\n ...FullType\n }\n directives + {\n name\n description\n locations\n args {\n ...InputValue\n }\n }\n }\n}\n\nfragment + FullType on __Type {\n kind\n name\n description\n fields(includeDeprecated: + true) {\n name\n description\n args {\n ...InputValue\n }\n type + {\n ...TypeRef\n }\n isDeprecated\n deprecationReason\n }\n inputFields + {\n ...InputValue\n }\n interfaces {\n ...TypeRef\n }\n enumValues(includeDeprecated: + true) {\n name\n description\n isDeprecated\n deprecationReason\n }\n possibleTypes + {\n ...TypeRef\n }\n}\n\nfragment InputValue on __InputValue {\n name\n description\n type + {\n ...TypeRef\n }\n defaultValue\n}\n\nfragment TypeRef on __Type {\n kind\n name\n ofType + {\n kind\n name\n ofType {\n kind\n name\n ofType {\n kind\n name\n ofType + {\n kind\n name\n ofType {\n kind\n name\n ofType + {\n kind\n name\n ofType {\n kind\n name\n }\n }\n }\n }\n }\n }\n }\n}"}' + headers: + Accept: + - '*/*' + Accept-Encoding: + - gzip, deflate + Connection: + - keep-alive + Content-Length: + - '1417' + Content-Type: + - application/json + Cookie: + - csrftoken=kAyQyUjNOGXZfkKUtWtvUROaFfDe2GBiV7yIRsqs3r2j9aYchRDXTNo3lHp72h5k + User-Agent: + - python-requests/2.24.0 + x-csrftoken: + - kAyQyUjNOGXZfkKUtWtvUROaFfDe2GBiV7yIRsqs3r2j9aYchRDXTNo3lHp72h5k + method: POST + uri: http://127.0.0.1:8000/graphql + response: + body: + string: '{"data":{"__schema":{"queryType":{"name":"Query"},"mutationType":{"name":"Mutation"},"subscriptionType":null,"types":[{"kind":"OBJECT","name":"Query","description":null,"fields":[{"name":"allFilms","description":null,"args":[{"name":"before","description":null,"type":{"kind":"SCALAR","name":"String","ofType":null},"defaultValue":null},{"name":"after","description":null,"type":{"kind":"SCALAR","name":"String","ofType":null},"defaultValue":null},{"name":"first","description":null,"type":{"kind":"SCALAR","name":"Int","ofType":null},"defaultValue":null},{"name":"last","description":null,"type":{"kind":"SCALAR","name":"Int","ofType":null},"defaultValue":null},{"name":"episodeId_Gt","description":null,"type":{"kind":"SCALAR","name":"Float","ofType":null},"defaultValue":null}],"type":{"kind":"OBJECT","name":"FilmConnection","ofType":null},"isDeprecated":false,"deprecationReason":null},{"name":"allSpecies","description":null,"args":[{"name":"before","description":null,"type":{"kind":"SCALAR","name":"String","ofType":null},"defaultValue":null},{"name":"after","description":null,"type":{"kind":"SCALAR","name":"String","ofType":null},"defaultValue":null},{"name":"first","description":null,"type":{"kind":"SCALAR","name":"Int","ofType":null},"defaultValue":null},{"name":"last","description":null,"type":{"kind":"SCALAR","name":"Int","ofType":null},"defaultValue":null},{"name":"name_Startswith","description":null,"type":{"kind":"SCALAR","name":"String","ofType":null},"defaultValue":null},{"name":"name_Contains","description":null,"type":{"kind":"SCALAR","name":"String","ofType":null},"defaultValue":null}],"type":{"kind":"OBJECT","name":"SpecieConnection","ofType":null},"isDeprecated":false,"deprecationReason":null},{"name":"allCharacters","description":null,"args":[{"name":"before","description":null,"type":{"kind":"SCALAR","name":"String","ofType":null},"defaultValue":null},{"name":"after","description":null,"type":{"kind":"SCALAR","name":"String","ofType":null},"defaultValue":null},{"name":"first","description":null,"type":{"kind":"SCALAR","name":"Int","ofType":null},"defaultValue":null},{"name":"last","description":null,"type":{"kind":"SCALAR","name":"Int","ofType":null},"defaultValue":null},{"name":"name","description":null,"type":{"kind":"SCALAR","name":"String","ofType":null},"defaultValue":null}],"type":{"kind":"OBJECT","name":"PersonConnection","ofType":null},"isDeprecated":false,"deprecationReason":null},{"name":"allVehicles","description":null,"args":[{"name":"before","description":null,"type":{"kind":"SCALAR","name":"String","ofType":null},"defaultValue":null},{"name":"after","description":null,"type":{"kind":"SCALAR","name":"String","ofType":null},"defaultValue":null},{"name":"first","description":null,"type":{"kind":"SCALAR","name":"Int","ofType":null},"defaultValue":null},{"name":"last","description":null,"type":{"kind":"SCALAR","name":"Int","ofType":null},"defaultValue":null},{"name":"name_Startswith","description":null,"type":{"kind":"SCALAR","name":"String","ofType":null},"defaultValue":null}],"type":{"kind":"OBJECT","name":"VehicleConnection","ofType":null},"isDeprecated":false,"deprecationReason":null},{"name":"allPlanets","description":null,"args":[{"name":"before","description":null,"type":{"kind":"SCALAR","name":"String","ofType":null},"defaultValue":null},{"name":"after","description":null,"type":{"kind":"SCALAR","name":"String","ofType":null},"defaultValue":null},{"name":"first","description":null,"type":{"kind":"SCALAR","name":"Int","ofType":null},"defaultValue":null},{"name":"last","description":null,"type":{"kind":"SCALAR","name":"Int","ofType":null},"defaultValue":null},{"name":"name","description":null,"type":{"kind":"SCALAR","name":"String","ofType":null},"defaultValue":null}],"type":{"kind":"OBJECT","name":"PlanetConnection","ofType":null},"isDeprecated":false,"deprecationReason":null},{"name":"allStarships","description":null,"args":[{"name":"before","description":null,"type":{"kind":"SCALAR","name":"String","ofType":null},"defaultValue":null},{"name":"after","description":null,"type":{"kind":"SCALAR","name":"String","ofType":null},"defaultValue":null},{"name":"first","description":null,"type":{"kind":"SCALAR","name":"Int","ofType":null},"defaultValue":null},{"name":"last","description":null,"type":{"kind":"SCALAR","name":"Int","ofType":null},"defaultValue":null},{"name":"name_Startswith","description":null,"type":{"kind":"SCALAR","name":"String","ofType":null},"defaultValue":null},{"name":"name_Contains","description":null,"type":{"kind":"SCALAR","name":"String","ofType":null},"defaultValue":null}],"type":{"kind":"OBJECT","name":"StarshipConnection","ofType":null},"isDeprecated":false,"deprecationReason":null},{"name":"allHeroes","description":null,"args":[{"name":"before","description":null,"type":{"kind":"SCALAR","name":"String","ofType":null},"defaultValue":null},{"name":"after","description":null,"type":{"kind":"SCALAR","name":"String","ofType":null},"defaultValue":null},{"name":"first","description":null,"type":{"kind":"SCALAR","name":"Int","ofType":null},"defaultValue":null},{"name":"last","description":null,"type":{"kind":"SCALAR","name":"Int","ofType":null},"defaultValue":null},{"name":"name_Startswith","description":null,"type":{"kind":"SCALAR","name":"String","ofType":null},"defaultValue":null},{"name":"name_Contains","description":null,"type":{"kind":"SCALAR","name":"String","ofType":null},"defaultValue":null}],"type":{"kind":"OBJECT","name":"HeroConnection","ofType":null},"isDeprecated":false,"deprecationReason":null},{"name":"film","description":"The + ID of the object","args":[{"name":"id","description":null,"type":{"kind":"NON_NULL","name":null,"ofType":{"kind":"SCALAR","name":"ID","ofType":null}},"defaultValue":null}],"type":{"kind":"OBJECT","name":"Film","ofType":null},"isDeprecated":false,"deprecationReason":null},{"name":"specie","description":"The + ID of the object","args":[{"name":"id","description":null,"type":{"kind":"NON_NULL","name":null,"ofType":{"kind":"SCALAR","name":"ID","ofType":null}},"defaultValue":null}],"type":{"kind":"OBJECT","name":"Specie","ofType":null},"isDeprecated":false,"deprecationReason":null},{"name":"character","description":"The + ID of the object","args":[{"name":"id","description":null,"type":{"kind":"NON_NULL","name":null,"ofType":{"kind":"SCALAR","name":"ID","ofType":null}},"defaultValue":null}],"type":{"kind":"OBJECT","name":"Person","ofType":null},"isDeprecated":false,"deprecationReason":null},{"name":"vehicle","description":"The + ID of the object","args":[{"name":"id","description":null,"type":{"kind":"NON_NULL","name":null,"ofType":{"kind":"SCALAR","name":"ID","ofType":null}},"defaultValue":null}],"type":{"kind":"OBJECT","name":"Vehicle","ofType":null},"isDeprecated":false,"deprecationReason":null},{"name":"planet","description":"The + ID of the object","args":[{"name":"id","description":null,"type":{"kind":"NON_NULL","name":null,"ofType":{"kind":"SCALAR","name":"ID","ofType":null}},"defaultValue":null}],"type":{"kind":"OBJECT","name":"Planet","ofType":null},"isDeprecated":false,"deprecationReason":null},{"name":"starship","description":"The + ID of the object","args":[{"name":"id","description":null,"type":{"kind":"NON_NULL","name":null,"ofType":{"kind":"SCALAR","name":"ID","ofType":null}},"defaultValue":null}],"type":{"kind":"OBJECT","name":"Starship","ofType":null},"isDeprecated":false,"deprecationReason":null},{"name":"hero","description":"The + ID of the object","args":[{"name":"id","description":null,"type":{"kind":"NON_NULL","name":null,"ofType":{"kind":"SCALAR","name":"ID","ofType":null}},"defaultValue":null}],"type":{"kind":"OBJECT","name":"Hero","ofType":null},"isDeprecated":false,"deprecationReason":null},{"name":"node","description":"The + ID of the object","args":[{"name":"id","description":null,"type":{"kind":"NON_NULL","name":null,"ofType":{"kind":"SCALAR","name":"ID","ofType":null}},"defaultValue":null}],"type":{"kind":"INTERFACE","name":"Node","ofType":null},"isDeprecated":false,"deprecationReason":null},{"name":"viewer","description":null,"args":[],"type":{"kind":"OBJECT","name":"Query","ofType":null},"isDeprecated":false,"deprecationReason":null},{"name":"_debug","description":null,"args":[],"type":{"kind":"OBJECT","name":"DjangoDebug","ofType":null},"isDeprecated":false,"deprecationReason":null}],"inputFields":null,"interfaces":[],"enumValues":null,"possibleTypes":null},{"kind":"OBJECT","name":"FilmConnection","description":null,"fields":[{"name":"pageInfo","description":"Pagination + data for this connection.","args":[],"type":{"kind":"NON_NULL","name":null,"ofType":{"kind":"OBJECT","name":"PageInfo","ofType":null}},"isDeprecated":false,"deprecationReason":null},{"name":"edges","description":"Contains + the nodes in this connection.","args":[],"type":{"kind":"NON_NULL","name":null,"ofType":{"kind":"LIST","name":null,"ofType":{"kind":"OBJECT","name":"FilmEdge","ofType":null}}},"isDeprecated":false,"deprecationReason":null},{"name":"totalCount","description":null,"args":[],"type":{"kind":"SCALAR","name":"Int","ofType":null},"isDeprecated":false,"deprecationReason":null}],"inputFields":null,"interfaces":[],"enumValues":null,"possibleTypes":null},{"kind":"OBJECT","name":"PageInfo","description":"The + Relay compliant `PageInfo` type, containing data necessary to paginate this + connection.","fields":[{"name":"hasNextPage","description":"When paginating + forwards, are there more items?","args":[],"type":{"kind":"NON_NULL","name":null,"ofType":{"kind":"SCALAR","name":"Boolean","ofType":null}},"isDeprecated":false,"deprecationReason":null},{"name":"hasPreviousPage","description":"When + paginating backwards, are there more items?","args":[],"type":{"kind":"NON_NULL","name":null,"ofType":{"kind":"SCALAR","name":"Boolean","ofType":null}},"isDeprecated":false,"deprecationReason":null},{"name":"startCursor","description":"When + paginating backwards, the cursor to continue.","args":[],"type":{"kind":"SCALAR","name":"String","ofType":null},"isDeprecated":false,"deprecationReason":null},{"name":"endCursor","description":"When + paginating forwards, the cursor to continue.","args":[],"type":{"kind":"SCALAR","name":"String","ofType":null},"isDeprecated":false,"deprecationReason":null}],"inputFields":null,"interfaces":[],"enumValues":null,"possibleTypes":null},{"kind":"SCALAR","name":"Boolean","description":"The + `Boolean` scalar type represents `true` or `false`.","fields":null,"inputFields":null,"interfaces":null,"enumValues":null,"possibleTypes":null},{"kind":"SCALAR","name":"String","description":"The + `String` scalar type represents textual data, represented as UTF-8 character + sequences. The String type is most often used by GraphQL to represent free-form + human-readable text.","fields":null,"inputFields":null,"interfaces":null,"enumValues":null,"possibleTypes":null},{"kind":"OBJECT","name":"FilmEdge","description":"A + Relay edge containing a `Film` and its cursor.","fields":[{"name":"node","description":"The + item at the end of the edge","args":[],"type":{"kind":"OBJECT","name":"Film","ofType":null},"isDeprecated":false,"deprecationReason":null},{"name":"cursor","description":"A + cursor for use in pagination","args":[],"type":{"kind":"NON_NULL","name":null,"ofType":{"kind":"SCALAR","name":"String","ofType":null}},"isDeprecated":false,"deprecationReason":null}],"inputFields":null,"interfaces":[],"enumValues":null,"possibleTypes":null},{"kind":"OBJECT","name":"Film","description":"A + single film.","fields":[{"name":"id","description":"The ID of the object.","args":[],"type":{"kind":"NON_NULL","name":null,"ofType":{"kind":"SCALAR","name":"ID","ofType":null}},"isDeprecated":false,"deprecationReason":null},{"name":"title","description":"","args":[],"type":{"kind":"NON_NULL","name":null,"ofType":{"kind":"SCALAR","name":"String","ofType":null}},"isDeprecated":false,"deprecationReason":null},{"name":"episodeId","description":"","args":[],"type":{"kind":"NON_NULL","name":null,"ofType":{"kind":"SCALAR","name":"Int","ofType":null}},"isDeprecated":false,"deprecationReason":null},{"name":"openingCrawl","description":"","args":[],"type":{"kind":"NON_NULL","name":null,"ofType":{"kind":"SCALAR","name":"String","ofType":null}},"isDeprecated":false,"deprecationReason":null},{"name":"director","description":"","args":[],"type":{"kind":"NON_NULL","name":null,"ofType":{"kind":"SCALAR","name":"String","ofType":null}},"isDeprecated":false,"deprecationReason":null},{"name":"releaseDate","description":"","args":[],"type":{"kind":"NON_NULL","name":null,"ofType":{"kind":"SCALAR","name":"Date","ofType":null}},"isDeprecated":false,"deprecationReason":null},{"name":"characters","description":null,"args":[{"name":"before","description":null,"type":{"kind":"SCALAR","name":"String","ofType":null},"defaultValue":null},{"name":"after","description":null,"type":{"kind":"SCALAR","name":"String","ofType":null},"defaultValue":null},{"name":"first","description":null,"type":{"kind":"SCALAR","name":"Int","ofType":null},"defaultValue":null},{"name":"last","description":null,"type":{"kind":"SCALAR","name":"Int","ofType":null},"defaultValue":null},{"name":"name","description":null,"type":{"kind":"SCALAR","name":"String","ofType":null},"defaultValue":null}],"type":{"kind":"OBJECT","name":"PersonConnection","ofType":null},"isDeprecated":false,"deprecationReason":null},{"name":"planets","description":null,"args":[{"name":"before","description":null,"type":{"kind":"SCALAR","name":"String","ofType":null},"defaultValue":null},{"name":"after","description":null,"type":{"kind":"SCALAR","name":"String","ofType":null},"defaultValue":null},{"name":"first","description":null,"type":{"kind":"SCALAR","name":"Int","ofType":null},"defaultValue":null},{"name":"last","description":null,"type":{"kind":"SCALAR","name":"Int","ofType":null},"defaultValue":null},{"name":"name","description":null,"type":{"kind":"SCALAR","name":"String","ofType":null},"defaultValue":null}],"type":{"kind":"OBJECT","name":"PlanetConnection","ofType":null},"isDeprecated":false,"deprecationReason":null},{"name":"starships","description":null,"args":[{"name":"before","description":null,"type":{"kind":"SCALAR","name":"String","ofType":null},"defaultValue":null},{"name":"after","description":null,"type":{"kind":"SCALAR","name":"String","ofType":null},"defaultValue":null},{"name":"first","description":null,"type":{"kind":"SCALAR","name":"Int","ofType":null},"defaultValue":null},{"name":"last","description":null,"type":{"kind":"SCALAR","name":"Int","ofType":null},"defaultValue":null},{"name":"name_Startswith","description":null,"type":{"kind":"SCALAR","name":"String","ofType":null},"defaultValue":null},{"name":"name_Contains","description":null,"type":{"kind":"SCALAR","name":"String","ofType":null},"defaultValue":null}],"type":{"kind":"OBJECT","name":"StarshipConnection","ofType":null},"isDeprecated":false,"deprecationReason":null},{"name":"vehicles","description":null,"args":[{"name":"before","description":null,"type":{"kind":"SCALAR","name":"String","ofType":null},"defaultValue":null},{"name":"after","description":null,"type":{"kind":"SCALAR","name":"String","ofType":null},"defaultValue":null},{"name":"first","description":null,"type":{"kind":"SCALAR","name":"Int","ofType":null},"defaultValue":null},{"name":"last","description":null,"type":{"kind":"SCALAR","name":"Int","ofType":null},"defaultValue":null},{"name":"name_Startswith","description":null,"type":{"kind":"SCALAR","name":"String","ofType":null},"defaultValue":null}],"type":{"kind":"OBJECT","name":"VehicleConnection","ofType":null},"isDeprecated":false,"deprecationReason":null},{"name":"species","description":null,"args":[{"name":"before","description":null,"type":{"kind":"SCALAR","name":"String","ofType":null},"defaultValue":null},{"name":"after","description":null,"type":{"kind":"SCALAR","name":"String","ofType":null},"defaultValue":null},{"name":"first","description":null,"type":{"kind":"SCALAR","name":"Int","ofType":null},"defaultValue":null},{"name":"last","description":null,"type":{"kind":"SCALAR","name":"Int","ofType":null},"defaultValue":null},{"name":"name_Startswith","description":null,"type":{"kind":"SCALAR","name":"String","ofType":null},"defaultValue":null},{"name":"name_Contains","description":null,"type":{"kind":"SCALAR","name":"String","ofType":null},"defaultValue":null}],"type":{"kind":"OBJECT","name":"SpecieConnection","ofType":null},"isDeprecated":false,"deprecationReason":null},{"name":"producers","description":null,"args":[],"type":{"kind":"LIST","name":null,"ofType":{"kind":"SCALAR","name":"String","ofType":null}},"isDeprecated":false,"deprecationReason":null}],"inputFields":null,"interfaces":[{"kind":"INTERFACE","name":"Node","ofType":null}],"enumValues":null,"possibleTypes":null},{"kind":"INTERFACE","name":"Node","description":"An + object with an ID","fields":[{"name":"id","description":"The ID of the object.","args":[],"type":{"kind":"NON_NULL","name":null,"ofType":{"kind":"SCALAR","name":"ID","ofType":null}},"isDeprecated":false,"deprecationReason":null}],"inputFields":null,"interfaces":null,"enumValues":null,"possibleTypes":[{"kind":"OBJECT","name":"Film","ofType":null},{"kind":"OBJECT","name":"Person","ofType":null},{"kind":"OBJECT","name":"Planet","ofType":null},{"kind":"OBJECT","name":"Specie","ofType":null},{"kind":"OBJECT","name":"Hero","ofType":null},{"kind":"OBJECT","name":"Starship","ofType":null},{"kind":"OBJECT","name":"Vehicle","ofType":null}]},{"kind":"SCALAR","name":"ID","description":"The + `ID` scalar type represents a unique identifier, often used to refetch an + object or as key for a cache. The ID type appears in a JSON response as a + String; however, it is not intended to be human-readable. When expected as + an input type, any string (such as `\"4\"`) or integer (such as `4`) input + value will be accepted as an ID.","fields":null,"inputFields":null,"interfaces":null,"enumValues":null,"possibleTypes":null},{"kind":"SCALAR","name":"Int","description":"The + `Int` scalar type represents non-fractional signed whole numeric values. Int + can represent values between -(2^31 - 1) and 2^31 - 1 since represented in + JSON as double-precision floating point numbers specifiedby [IEEE 754](http://en.wikipedia.org/wiki/IEEE_floating_point).","fields":null,"inputFields":null,"interfaces":null,"enumValues":null,"possibleTypes":null},{"kind":"SCALAR","name":"Date","description":"The + `Date` scalar type represents a Date\nvalue as specified by\n[iso8601](https://en.wikipedia.org/wiki/ISO_8601).","fields":null,"inputFields":null,"interfaces":null,"enumValues":null,"possibleTypes":null},{"kind":"OBJECT","name":"PersonConnection","description":null,"fields":[{"name":"pageInfo","description":"Pagination + data for this connection.","args":[],"type":{"kind":"NON_NULL","name":null,"ofType":{"kind":"OBJECT","name":"PageInfo","ofType":null}},"isDeprecated":false,"deprecationReason":null},{"name":"edges","description":"Contains + the nodes in this connection.","args":[],"type":{"kind":"NON_NULL","name":null,"ofType":{"kind":"LIST","name":null,"ofType":{"kind":"OBJECT","name":"PersonEdge","ofType":null}}},"isDeprecated":false,"deprecationReason":null},{"name":"totalCount","description":null,"args":[],"type":{"kind":"SCALAR","name":"Int","ofType":null},"isDeprecated":false,"deprecationReason":null}],"inputFields":null,"interfaces":[],"enumValues":null,"possibleTypes":null},{"kind":"OBJECT","name":"PersonEdge","description":"A + Relay edge containing a `Person` and its cursor.","fields":[{"name":"node","description":"The + item at the end of the edge","args":[],"type":{"kind":"OBJECT","name":"Person","ofType":null},"isDeprecated":false,"deprecationReason":null},{"name":"cursor","description":"A + cursor for use in pagination","args":[],"type":{"kind":"NON_NULL","name":null,"ofType":{"kind":"SCALAR","name":"String","ofType":null}},"isDeprecated":false,"deprecationReason":null}],"inputFields":null,"interfaces":[],"enumValues":null,"possibleTypes":null},{"kind":"OBJECT","name":"Person","description":"An + individual person or character within the Star Wars universe.","fields":[{"name":"id","description":"The + ID of the object.","args":[],"type":{"kind":"NON_NULL","name":null,"ofType":{"kind":"SCALAR","name":"ID","ofType":null}},"isDeprecated":false,"deprecationReason":null},{"name":"name","description":"","args":[],"type":{"kind":"NON_NULL","name":null,"ofType":{"kind":"SCALAR","name":"String","ofType":null}},"isDeprecated":false,"deprecationReason":null},{"name":"height","description":"","args":[],"type":{"kind":"NON_NULL","name":null,"ofType":{"kind":"SCALAR","name":"String","ofType":null}},"isDeprecated":false,"deprecationReason":null},{"name":"mass","description":"","args":[],"type":{"kind":"NON_NULL","name":null,"ofType":{"kind":"SCALAR","name":"String","ofType":null}},"isDeprecated":false,"deprecationReason":null},{"name":"hairColor","description":"","args":[],"type":{"kind":"NON_NULL","name":null,"ofType":{"kind":"SCALAR","name":"String","ofType":null}},"isDeprecated":false,"deprecationReason":null},{"name":"skinColor","description":"","args":[],"type":{"kind":"NON_NULL","name":null,"ofType":{"kind":"SCALAR","name":"String","ofType":null}},"isDeprecated":false,"deprecationReason":null},{"name":"eyeColor","description":"","args":[],"type":{"kind":"NON_NULL","name":null,"ofType":{"kind":"SCALAR","name":"String","ofType":null}},"isDeprecated":false,"deprecationReason":null},{"name":"birthYear","description":"","args":[],"type":{"kind":"NON_NULL","name":null,"ofType":{"kind":"SCALAR","name":"String","ofType":null}},"isDeprecated":false,"deprecationReason":null},{"name":"gender","description":"","args":[],"type":{"kind":"NON_NULL","name":null,"ofType":{"kind":"SCALAR","name":"String","ofType":null}},"isDeprecated":false,"deprecationReason":null},{"name":"homeworld","description":"","args":[],"type":{"kind":"NON_NULL","name":null,"ofType":{"kind":"OBJECT","name":"Planet","ofType":null}},"isDeprecated":false,"deprecationReason":null},{"name":"species","description":null,"args":[{"name":"before","description":null,"type":{"kind":"SCALAR","name":"String","ofType":null},"defaultValue":null},{"name":"after","description":null,"type":{"kind":"SCALAR","name":"String","ofType":null},"defaultValue":null},{"name":"first","description":null,"type":{"kind":"SCALAR","name":"Int","ofType":null},"defaultValue":null},{"name":"last","description":null,"type":{"kind":"SCALAR","name":"Int","ofType":null},"defaultValue":null},{"name":"name_Startswith","description":null,"type":{"kind":"SCALAR","name":"String","ofType":null},"defaultValue":null},{"name":"name_Contains","description":null,"type":{"kind":"SCALAR","name":"String","ofType":null},"defaultValue":null}],"type":{"kind":"OBJECT","name":"SpecieConnection","ofType":null},"isDeprecated":false,"deprecationReason":null},{"name":"films","description":null,"args":[{"name":"before","description":null,"type":{"kind":"SCALAR","name":"String","ofType":null},"defaultValue":null},{"name":"after","description":null,"type":{"kind":"SCALAR","name":"String","ofType":null},"defaultValue":null},{"name":"first","description":null,"type":{"kind":"SCALAR","name":"Int","ofType":null},"defaultValue":null},{"name":"last","description":null,"type":{"kind":"SCALAR","name":"Int","ofType":null},"defaultValue":null},{"name":"episodeId_Gt","description":null,"type":{"kind":"SCALAR","name":"Float","ofType":null},"defaultValue":null}],"type":{"kind":"OBJECT","name":"FilmConnection","ofType":null},"isDeprecated":false,"deprecationReason":null},{"name":"starships","description":null,"args":[{"name":"before","description":null,"type":{"kind":"SCALAR","name":"String","ofType":null},"defaultValue":null},{"name":"after","description":null,"type":{"kind":"SCALAR","name":"String","ofType":null},"defaultValue":null},{"name":"first","description":null,"type":{"kind":"SCALAR","name":"Int","ofType":null},"defaultValue":null},{"name":"last","description":null,"type":{"kind":"SCALAR","name":"Int","ofType":null},"defaultValue":null},{"name":"name_Startswith","description":null,"type":{"kind":"SCALAR","name":"String","ofType":null},"defaultValue":null},{"name":"name_Contains","description":null,"type":{"kind":"SCALAR","name":"String","ofType":null},"defaultValue":null}],"type":{"kind":"OBJECT","name":"StarshipConnection","ofType":null},"isDeprecated":false,"deprecationReason":null},{"name":"vehicles","description":null,"args":[{"name":"before","description":null,"type":{"kind":"SCALAR","name":"String","ofType":null},"defaultValue":null},{"name":"after","description":null,"type":{"kind":"SCALAR","name":"String","ofType":null},"defaultValue":null},{"name":"first","description":null,"type":{"kind":"SCALAR","name":"Int","ofType":null},"defaultValue":null},{"name":"last","description":null,"type":{"kind":"SCALAR","name":"Int","ofType":null},"defaultValue":null},{"name":"name_Startswith","description":null,"type":{"kind":"SCALAR","name":"String","ofType":null},"defaultValue":null}],"type":{"kind":"OBJECT","name":"VehicleConnection","ofType":null},"isDeprecated":false,"deprecationReason":null}],"inputFields":null,"interfaces":[{"kind":"INTERFACE","name":"Node","ofType":null}],"enumValues":null,"possibleTypes":null},{"kind":"OBJECT","name":"Planet","description":"A + large mass, planet or planetoid in the Star Wars Universe,\nat the time of + 0 ABY.","fields":[{"name":"id","description":"The ID of the object.","args":[],"type":{"kind":"NON_NULL","name":null,"ofType":{"kind":"SCALAR","name":"ID","ofType":null}},"isDeprecated":false,"deprecationReason":null},{"name":"name","description":"","args":[],"type":{"kind":"NON_NULL","name":null,"ofType":{"kind":"SCALAR","name":"String","ofType":null}},"isDeprecated":false,"deprecationReason":null},{"name":"rotationPeriod","description":"","args":[],"type":{"kind":"NON_NULL","name":null,"ofType":{"kind":"SCALAR","name":"String","ofType":null}},"isDeprecated":false,"deprecationReason":null},{"name":"orbitalPeriod","description":"","args":[],"type":{"kind":"NON_NULL","name":null,"ofType":{"kind":"SCALAR","name":"String","ofType":null}},"isDeprecated":false,"deprecationReason":null},{"name":"diameter","description":"","args":[],"type":{"kind":"NON_NULL","name":null,"ofType":{"kind":"SCALAR","name":"String","ofType":null}},"isDeprecated":false,"deprecationReason":null},{"name":"gravity","description":"","args":[],"type":{"kind":"NON_NULL","name":null,"ofType":{"kind":"SCALAR","name":"String","ofType":null}},"isDeprecated":false,"deprecationReason":null},{"name":"surfaceWater","description":"","args":[],"type":{"kind":"NON_NULL","name":null,"ofType":{"kind":"SCALAR","name":"String","ofType":null}},"isDeprecated":false,"deprecationReason":null},{"name":"population","description":"","args":[],"type":{"kind":"NON_NULL","name":null,"ofType":{"kind":"SCALAR","name":"String","ofType":null}},"isDeprecated":false,"deprecationReason":null},{"name":"speciesSet","description":null,"args":[{"name":"before","description":null,"type":{"kind":"SCALAR","name":"String","ofType":null},"defaultValue":null},{"name":"after","description":null,"type":{"kind":"SCALAR","name":"String","ofType":null},"defaultValue":null},{"name":"first","description":null,"type":{"kind":"SCALAR","name":"Int","ofType":null},"defaultValue":null},{"name":"last","description":null,"type":{"kind":"SCALAR","name":"Int","ofType":null},"defaultValue":null},{"name":"name_Startswith","description":null,"type":{"kind":"SCALAR","name":"String","ofType":null},"defaultValue":null},{"name":"name_Contains","description":null,"type":{"kind":"SCALAR","name":"String","ofType":null},"defaultValue":null}],"type":{"kind":"OBJECT","name":"SpecieConnection","ofType":null},"isDeprecated":false,"deprecationReason":null},{"name":"films","description":null,"args":[{"name":"before","description":null,"type":{"kind":"SCALAR","name":"String","ofType":null},"defaultValue":null},{"name":"after","description":null,"type":{"kind":"SCALAR","name":"String","ofType":null},"defaultValue":null},{"name":"first","description":null,"type":{"kind":"SCALAR","name":"Int","ofType":null},"defaultValue":null},{"name":"last","description":null,"type":{"kind":"SCALAR","name":"Int","ofType":null},"defaultValue":null},{"name":"episodeId_Gt","description":null,"type":{"kind":"SCALAR","name":"Float","ofType":null},"defaultValue":null}],"type":{"kind":"OBJECT","name":"FilmConnection","ofType":null},"isDeprecated":false,"deprecationReason":null},{"name":"heroes","description":null,"args":[{"name":"before","description":null,"type":{"kind":"SCALAR","name":"String","ofType":null},"defaultValue":null},{"name":"after","description":null,"type":{"kind":"SCALAR","name":"String","ofType":null},"defaultValue":null},{"name":"first","description":null,"type":{"kind":"SCALAR","name":"Int","ofType":null},"defaultValue":null},{"name":"last","description":null,"type":{"kind":"SCALAR","name":"Int","ofType":null},"defaultValue":null},{"name":"name_Startswith","description":null,"type":{"kind":"SCALAR","name":"String","ofType":null},"defaultValue":null},{"name":"name_Contains","description":null,"type":{"kind":"SCALAR","name":"String","ofType":null},"defaultValue":null}],"type":{"kind":"OBJECT","name":"HeroConnection","ofType":null},"isDeprecated":false,"deprecationReason":null},{"name":"residents","description":null,"args":[{"name":"before","description":null,"type":{"kind":"SCALAR","name":"String","ofType":null},"defaultValue":null},{"name":"after","description":null,"type":{"kind":"SCALAR","name":"String","ofType":null},"defaultValue":null},{"name":"first","description":null,"type":{"kind":"SCALAR","name":"Int","ofType":null},"defaultValue":null},{"name":"last","description":null,"type":{"kind":"SCALAR","name":"Int","ofType":null},"defaultValue":null},{"name":"name","description":null,"type":{"kind":"SCALAR","name":"String","ofType":null},"defaultValue":null}],"type":{"kind":"OBJECT","name":"PersonConnection","ofType":null},"isDeprecated":false,"deprecationReason":null},{"name":"climates","description":null,"args":[],"type":{"kind":"LIST","name":null,"ofType":{"kind":"SCALAR","name":"String","ofType":null}},"isDeprecated":false,"deprecationReason":null},{"name":"terrains","description":null,"args":[],"type":{"kind":"LIST","name":null,"ofType":{"kind":"SCALAR","name":"String","ofType":null}},"isDeprecated":false,"deprecationReason":null}],"inputFields":null,"interfaces":[{"kind":"INTERFACE","name":"Node","ofType":null}],"enumValues":null,"possibleTypes":null},{"kind":"OBJECT","name":"SpecieConnection","description":null,"fields":[{"name":"pageInfo","description":"Pagination + data for this connection.","args":[],"type":{"kind":"NON_NULL","name":null,"ofType":{"kind":"OBJECT","name":"PageInfo","ofType":null}},"isDeprecated":false,"deprecationReason":null},{"name":"edges","description":"Contains + the nodes in this connection.","args":[],"type":{"kind":"NON_NULL","name":null,"ofType":{"kind":"LIST","name":null,"ofType":{"kind":"OBJECT","name":"SpecieEdge","ofType":null}}},"isDeprecated":false,"deprecationReason":null},{"name":"totalCount","description":null,"args":[],"type":{"kind":"SCALAR","name":"Int","ofType":null},"isDeprecated":false,"deprecationReason":null}],"inputFields":null,"interfaces":[],"enumValues":null,"possibleTypes":null},{"kind":"OBJECT","name":"SpecieEdge","description":"A + Relay edge containing a `Specie` and its cursor.","fields":[{"name":"node","description":"The + item at the end of the edge","args":[],"type":{"kind":"OBJECT","name":"Specie","ofType":null},"isDeprecated":false,"deprecationReason":null},{"name":"cursor","description":"A + cursor for use in pagination","args":[],"type":{"kind":"NON_NULL","name":null,"ofType":{"kind":"SCALAR","name":"String","ofType":null}},"isDeprecated":false,"deprecationReason":null}],"inputFields":null,"interfaces":[],"enumValues":null,"possibleTypes":null},{"kind":"OBJECT","name":"Specie","description":"A + type of person or character within the Star Wars Universe.","fields":[{"name":"id","description":"The + ID of the object.","args":[],"type":{"kind":"NON_NULL","name":null,"ofType":{"kind":"SCALAR","name":"ID","ofType":null}},"isDeprecated":false,"deprecationReason":null},{"name":"name","description":"","args":[],"type":{"kind":"NON_NULL","name":null,"ofType":{"kind":"SCALAR","name":"String","ofType":null}},"isDeprecated":false,"deprecationReason":null},{"name":"classification","description":"","args":[],"type":{"kind":"NON_NULL","name":null,"ofType":{"kind":"SCALAR","name":"String","ofType":null}},"isDeprecated":false,"deprecationReason":null},{"name":"designation","description":"","args":[],"type":{"kind":"NON_NULL","name":null,"ofType":{"kind":"SCALAR","name":"String","ofType":null}},"isDeprecated":false,"deprecationReason":null},{"name":"averageHeight","description":"","args":[],"type":{"kind":"NON_NULL","name":null,"ofType":{"kind":"SCALAR","name":"String","ofType":null}},"isDeprecated":false,"deprecationReason":null},{"name":"averageLifespan","description":"","args":[],"type":{"kind":"NON_NULL","name":null,"ofType":{"kind":"SCALAR","name":"String","ofType":null}},"isDeprecated":false,"deprecationReason":null},{"name":"homeworld","description":"","args":[],"type":{"kind":"OBJECT","name":"Planet","ofType":null},"isDeprecated":false,"deprecationReason":null},{"name":"language","description":"","args":[],"type":{"kind":"NON_NULL","name":null,"ofType":{"kind":"SCALAR","name":"String","ofType":null}},"isDeprecated":false,"deprecationReason":null},{"name":"people","description":null,"args":[{"name":"before","description":null,"type":{"kind":"SCALAR","name":"String","ofType":null},"defaultValue":null},{"name":"after","description":null,"type":{"kind":"SCALAR","name":"String","ofType":null},"defaultValue":null},{"name":"first","description":null,"type":{"kind":"SCALAR","name":"Int","ofType":null},"defaultValue":null},{"name":"last","description":null,"type":{"kind":"SCALAR","name":"Int","ofType":null},"defaultValue":null},{"name":"name","description":null,"type":{"kind":"SCALAR","name":"String","ofType":null},"defaultValue":null}],"type":{"kind":"OBJECT","name":"PersonConnection","ofType":null},"isDeprecated":false,"deprecationReason":null},{"name":"films","description":null,"args":[{"name":"before","description":null,"type":{"kind":"SCALAR","name":"String","ofType":null},"defaultValue":null},{"name":"after","description":null,"type":{"kind":"SCALAR","name":"String","ofType":null},"defaultValue":null},{"name":"first","description":null,"type":{"kind":"SCALAR","name":"Int","ofType":null},"defaultValue":null},{"name":"last","description":null,"type":{"kind":"SCALAR","name":"Int","ofType":null},"defaultValue":null},{"name":"episodeId_Gt","description":null,"type":{"kind":"SCALAR","name":"Float","ofType":null},"defaultValue":null}],"type":{"kind":"OBJECT","name":"FilmConnection","ofType":null},"isDeprecated":false,"deprecationReason":null},{"name":"eyeColors","description":null,"args":[],"type":{"kind":"LIST","name":null,"ofType":{"kind":"SCALAR","name":"String","ofType":null}},"isDeprecated":false,"deprecationReason":null},{"name":"hairColors","description":null,"args":[],"type":{"kind":"LIST","name":null,"ofType":{"kind":"SCALAR","name":"String","ofType":null}},"isDeprecated":false,"deprecationReason":null},{"name":"skinColors","description":null,"args":[],"type":{"kind":"LIST","name":null,"ofType":{"kind":"SCALAR","name":"String","ofType":null}},"isDeprecated":false,"deprecationReason":null}],"inputFields":null,"interfaces":[{"kind":"INTERFACE","name":"Node","ofType":null}],"enumValues":null,"possibleTypes":null},{"kind":"SCALAR","name":"Float","description":"The + `Float` scalar type represents signed double-precision fractional values as + specified by [IEEE 754](http://en.wikipedia.org/wiki/IEEE_floating_point). + ","fields":null,"inputFields":null,"interfaces":null,"enumValues":null,"possibleTypes":null},{"kind":"OBJECT","name":"HeroConnection","description":null,"fields":[{"name":"pageInfo","description":"Pagination + data for this connection.","args":[],"type":{"kind":"NON_NULL","name":null,"ofType":{"kind":"OBJECT","name":"PageInfo","ofType":null}},"isDeprecated":false,"deprecationReason":null},{"name":"edges","description":"Contains + the nodes in this connection.","args":[],"type":{"kind":"NON_NULL","name":null,"ofType":{"kind":"LIST","name":null,"ofType":{"kind":"OBJECT","name":"HeroEdge","ofType":null}}},"isDeprecated":false,"deprecationReason":null},{"name":"totalCount","description":null,"args":[],"type":{"kind":"SCALAR","name":"Int","ofType":null},"isDeprecated":false,"deprecationReason":null}],"inputFields":null,"interfaces":[],"enumValues":null,"possibleTypes":null},{"kind":"OBJECT","name":"HeroEdge","description":"A + Relay edge containing a `Hero` and its cursor.","fields":[{"name":"node","description":"The + item at the end of the edge","args":[],"type":{"kind":"OBJECT","name":"Hero","ofType":null},"isDeprecated":false,"deprecationReason":null},{"name":"cursor","description":"A + cursor for use in pagination","args":[],"type":{"kind":"NON_NULL","name":null,"ofType":{"kind":"SCALAR","name":"String","ofType":null}},"isDeprecated":false,"deprecationReason":null}],"inputFields":null,"interfaces":[],"enumValues":null,"possibleTypes":null},{"kind":"OBJECT","name":"Hero","description":"A + hero created by fans","fields":[{"name":"id","description":"The ID of the + object.","args":[],"type":{"kind":"NON_NULL","name":null,"ofType":{"kind":"SCALAR","name":"ID","ofType":null}},"isDeprecated":false,"deprecationReason":null},{"name":"name","description":"","args":[],"type":{"kind":"NON_NULL","name":null,"ofType":{"kind":"SCALAR","name":"String","ofType":null}},"isDeprecated":false,"deprecationReason":null},{"name":"homeworld","description":"","args":[],"type":{"kind":"NON_NULL","name":null,"ofType":{"kind":"OBJECT","name":"Planet","ofType":null}},"isDeprecated":false,"deprecationReason":null}],"inputFields":null,"interfaces":[{"kind":"INTERFACE","name":"Node","ofType":null}],"enumValues":null,"possibleTypes":null},{"kind":"OBJECT","name":"StarshipConnection","description":null,"fields":[{"name":"pageInfo","description":"Pagination + data for this connection.","args":[],"type":{"kind":"NON_NULL","name":null,"ofType":{"kind":"OBJECT","name":"PageInfo","ofType":null}},"isDeprecated":false,"deprecationReason":null},{"name":"edges","description":"Contains + the nodes in this connection.","args":[],"type":{"kind":"NON_NULL","name":null,"ofType":{"kind":"LIST","name":null,"ofType":{"kind":"OBJECT","name":"StarshipEdge","ofType":null}}},"isDeprecated":false,"deprecationReason":null},{"name":"totalCount","description":null,"args":[],"type":{"kind":"SCALAR","name":"Int","ofType":null},"isDeprecated":false,"deprecationReason":null}],"inputFields":null,"interfaces":[],"enumValues":null,"possibleTypes":null},{"kind":"OBJECT","name":"StarshipEdge","description":"A + Relay edge containing a `Starship` and its cursor.","fields":[{"name":"node","description":"The + item at the end of the edge","args":[],"type":{"kind":"OBJECT","name":"Starship","ofType":null},"isDeprecated":false,"deprecationReason":null},{"name":"cursor","description":"A + cursor for use in pagination","args":[],"type":{"kind":"NON_NULL","name":null,"ofType":{"kind":"SCALAR","name":"String","ofType":null}},"isDeprecated":false,"deprecationReason":null}],"inputFields":null,"interfaces":[],"enumValues":null,"possibleTypes":null},{"kind":"OBJECT","name":"Starship","description":"A + single transport craft that has hyperdrive capability.","fields":[{"name":"id","description":"The + ID of the object.","args":[],"type":{"kind":"NON_NULL","name":null,"ofType":{"kind":"SCALAR","name":"ID","ofType":null}},"isDeprecated":false,"deprecationReason":null},{"name":"name","description":"","args":[],"type":{"kind":"NON_NULL","name":null,"ofType":{"kind":"SCALAR","name":"String","ofType":null}},"isDeprecated":false,"deprecationReason":null},{"name":"model","description":"","args":[],"type":{"kind":"NON_NULL","name":null,"ofType":{"kind":"SCALAR","name":"String","ofType":null}},"isDeprecated":false,"deprecationReason":null},{"name":"manufacturer","description":"","args":[],"type":{"kind":"NON_NULL","name":null,"ofType":{"kind":"SCALAR","name":"String","ofType":null}},"isDeprecated":false,"deprecationReason":null},{"name":"costInCredits","description":"","args":[],"type":{"kind":"NON_NULL","name":null,"ofType":{"kind":"SCALAR","name":"String","ofType":null}},"isDeprecated":false,"deprecationReason":null},{"name":"length","description":"","args":[],"type":{"kind":"NON_NULL","name":null,"ofType":{"kind":"SCALAR","name":"String","ofType":null}},"isDeprecated":false,"deprecationReason":null},{"name":"maxAtmospheringSpeed","description":"","args":[],"type":{"kind":"NON_NULL","name":null,"ofType":{"kind":"SCALAR","name":"String","ofType":null}},"isDeprecated":false,"deprecationReason":null},{"name":"crew","description":"","args":[],"type":{"kind":"NON_NULL","name":null,"ofType":{"kind":"SCALAR","name":"String","ofType":null}},"isDeprecated":false,"deprecationReason":null},{"name":"passengers","description":"","args":[],"type":{"kind":"NON_NULL","name":null,"ofType":{"kind":"SCALAR","name":"String","ofType":null}},"isDeprecated":false,"deprecationReason":null},{"name":"cargoCapacity","description":"","args":[],"type":{"kind":"NON_NULL","name":null,"ofType":{"kind":"SCALAR","name":"String","ofType":null}},"isDeprecated":false,"deprecationReason":null},{"name":"consumables","description":"","args":[],"type":{"kind":"NON_NULL","name":null,"ofType":{"kind":"SCALAR","name":"String","ofType":null}},"isDeprecated":false,"deprecationReason":null},{"name":"hyperdriveRating","description":"","args":[],"type":{"kind":"NON_NULL","name":null,"ofType":{"kind":"SCALAR","name":"String","ofType":null}},"isDeprecated":false,"deprecationReason":null},{"name":"MGLT","description":"","args":[],"type":{"kind":"NON_NULL","name":null,"ofType":{"kind":"SCALAR","name":"String","ofType":null}},"isDeprecated":false,"deprecationReason":null},{"name":"starshipClass","description":"","args":[],"type":{"kind":"NON_NULL","name":null,"ofType":{"kind":"SCALAR","name":"String","ofType":null}},"isDeprecated":false,"deprecationReason":null},{"name":"pilots","description":null,"args":[{"name":"before","description":null,"type":{"kind":"SCALAR","name":"String","ofType":null},"defaultValue":null},{"name":"after","description":null,"type":{"kind":"SCALAR","name":"String","ofType":null},"defaultValue":null},{"name":"first","description":null,"type":{"kind":"SCALAR","name":"Int","ofType":null},"defaultValue":null},{"name":"last","description":null,"type":{"kind":"SCALAR","name":"Int","ofType":null},"defaultValue":null},{"name":"name","description":null,"type":{"kind":"SCALAR","name":"String","ofType":null},"defaultValue":null}],"type":{"kind":"OBJECT","name":"PersonConnection","ofType":null},"isDeprecated":false,"deprecationReason":null},{"name":"films","description":null,"args":[{"name":"before","description":null,"type":{"kind":"SCALAR","name":"String","ofType":null},"defaultValue":null},{"name":"after","description":null,"type":{"kind":"SCALAR","name":"String","ofType":null},"defaultValue":null},{"name":"first","description":null,"type":{"kind":"SCALAR","name":"Int","ofType":null},"defaultValue":null},{"name":"last","description":null,"type":{"kind":"SCALAR","name":"Int","ofType":null},"defaultValue":null},{"name":"episodeId_Gt","description":null,"type":{"kind":"SCALAR","name":"Float","ofType":null},"defaultValue":null}],"type":{"kind":"OBJECT","name":"FilmConnection","ofType":null},"isDeprecated":false,"deprecationReason":null},{"name":"manufacturers","description":null,"args":[],"type":{"kind":"LIST","name":null,"ofType":{"kind":"SCALAR","name":"String","ofType":null}},"isDeprecated":false,"deprecationReason":null}],"inputFields":null,"interfaces":[{"kind":"INTERFACE","name":"Node","ofType":null}],"enumValues":null,"possibleTypes":null},{"kind":"OBJECT","name":"VehicleConnection","description":null,"fields":[{"name":"pageInfo","description":"Pagination + data for this connection.","args":[],"type":{"kind":"NON_NULL","name":null,"ofType":{"kind":"OBJECT","name":"PageInfo","ofType":null}},"isDeprecated":false,"deprecationReason":null},{"name":"edges","description":"Contains + the nodes in this connection.","args":[],"type":{"kind":"NON_NULL","name":null,"ofType":{"kind":"LIST","name":null,"ofType":{"kind":"OBJECT","name":"VehicleEdge","ofType":null}}},"isDeprecated":false,"deprecationReason":null},{"name":"totalCount","description":null,"args":[],"type":{"kind":"SCALAR","name":"Int","ofType":null},"isDeprecated":false,"deprecationReason":null}],"inputFields":null,"interfaces":[],"enumValues":null,"possibleTypes":null},{"kind":"OBJECT","name":"VehicleEdge","description":"A + Relay edge containing a `Vehicle` and its cursor.","fields":[{"name":"node","description":"The + item at the end of the edge","args":[],"type":{"kind":"OBJECT","name":"Vehicle","ofType":null},"isDeprecated":false,"deprecationReason":null},{"name":"cursor","description":"A + cursor for use in pagination","args":[],"type":{"kind":"NON_NULL","name":null,"ofType":{"kind":"SCALAR","name":"String","ofType":null}},"isDeprecated":false,"deprecationReason":null}],"inputFields":null,"interfaces":[],"enumValues":null,"possibleTypes":null},{"kind":"OBJECT","name":"Vehicle","description":"A + single transport craft that does not have hyperdrive capability","fields":[{"name":"id","description":"The + ID of the object.","args":[],"type":{"kind":"NON_NULL","name":null,"ofType":{"kind":"SCALAR","name":"ID","ofType":null}},"isDeprecated":false,"deprecationReason":null},{"name":"name","description":"","args":[],"type":{"kind":"NON_NULL","name":null,"ofType":{"kind":"SCALAR","name":"String","ofType":null}},"isDeprecated":false,"deprecationReason":null},{"name":"model","description":"","args":[],"type":{"kind":"NON_NULL","name":null,"ofType":{"kind":"SCALAR","name":"String","ofType":null}},"isDeprecated":false,"deprecationReason":null},{"name":"manufacturer","description":"","args":[],"type":{"kind":"NON_NULL","name":null,"ofType":{"kind":"SCALAR","name":"String","ofType":null}},"isDeprecated":false,"deprecationReason":null},{"name":"costInCredits","description":"","args":[],"type":{"kind":"NON_NULL","name":null,"ofType":{"kind":"SCALAR","name":"String","ofType":null}},"isDeprecated":false,"deprecationReason":null},{"name":"length","description":"","args":[],"type":{"kind":"NON_NULL","name":null,"ofType":{"kind":"SCALAR","name":"String","ofType":null}},"isDeprecated":false,"deprecationReason":null},{"name":"maxAtmospheringSpeed","description":"","args":[],"type":{"kind":"NON_NULL","name":null,"ofType":{"kind":"SCALAR","name":"String","ofType":null}},"isDeprecated":false,"deprecationReason":null},{"name":"crew","description":"","args":[],"type":{"kind":"NON_NULL","name":null,"ofType":{"kind":"SCALAR","name":"String","ofType":null}},"isDeprecated":false,"deprecationReason":null},{"name":"passengers","description":"","args":[],"type":{"kind":"NON_NULL","name":null,"ofType":{"kind":"SCALAR","name":"String","ofType":null}},"isDeprecated":false,"deprecationReason":null},{"name":"cargoCapacity","description":"","args":[],"type":{"kind":"NON_NULL","name":null,"ofType":{"kind":"SCALAR","name":"String","ofType":null}},"isDeprecated":false,"deprecationReason":null},{"name":"consumables","description":"","args":[],"type":{"kind":"NON_NULL","name":null,"ofType":{"kind":"SCALAR","name":"String","ofType":null}},"isDeprecated":false,"deprecationReason":null},{"name":"vehicleClass","description":"","args":[],"type":{"kind":"NON_NULL","name":null,"ofType":{"kind":"SCALAR","name":"String","ofType":null}},"isDeprecated":false,"deprecationReason":null},{"name":"pilots","description":null,"args":[{"name":"before","description":null,"type":{"kind":"SCALAR","name":"String","ofType":null},"defaultValue":null},{"name":"after","description":null,"type":{"kind":"SCALAR","name":"String","ofType":null},"defaultValue":null},{"name":"first","description":null,"type":{"kind":"SCALAR","name":"Int","ofType":null},"defaultValue":null},{"name":"last","description":null,"type":{"kind":"SCALAR","name":"Int","ofType":null},"defaultValue":null},{"name":"name","description":null,"type":{"kind":"SCALAR","name":"String","ofType":null},"defaultValue":null}],"type":{"kind":"OBJECT","name":"PersonConnection","ofType":null},"isDeprecated":false,"deprecationReason":null},{"name":"films","description":null,"args":[{"name":"before","description":null,"type":{"kind":"SCALAR","name":"String","ofType":null},"defaultValue":null},{"name":"after","description":null,"type":{"kind":"SCALAR","name":"String","ofType":null},"defaultValue":null},{"name":"first","description":null,"type":{"kind":"SCALAR","name":"Int","ofType":null},"defaultValue":null},{"name":"last","description":null,"type":{"kind":"SCALAR","name":"Int","ofType":null},"defaultValue":null},{"name":"episodeId_Gt","description":null,"type":{"kind":"SCALAR","name":"Float","ofType":null},"defaultValue":null}],"type":{"kind":"OBJECT","name":"FilmConnection","ofType":null},"isDeprecated":false,"deprecationReason":null},{"name":"manufacturers","description":null,"args":[],"type":{"kind":"LIST","name":null,"ofType":{"kind":"SCALAR","name":"String","ofType":null}},"isDeprecated":false,"deprecationReason":null}],"inputFields":null,"interfaces":[{"kind":"INTERFACE","name":"Node","ofType":null}],"enumValues":null,"possibleTypes":null},{"kind":"OBJECT","name":"PlanetConnection","description":null,"fields":[{"name":"pageInfo","description":"Pagination + data for this connection.","args":[],"type":{"kind":"NON_NULL","name":null,"ofType":{"kind":"OBJECT","name":"PageInfo","ofType":null}},"isDeprecated":false,"deprecationReason":null},{"name":"edges","description":"Contains + the nodes in this connection.","args":[],"type":{"kind":"NON_NULL","name":null,"ofType":{"kind":"LIST","name":null,"ofType":{"kind":"OBJECT","name":"PlanetEdge","ofType":null}}},"isDeprecated":false,"deprecationReason":null},{"name":"totalCount","description":null,"args":[],"type":{"kind":"SCALAR","name":"Int","ofType":null},"isDeprecated":false,"deprecationReason":null}],"inputFields":null,"interfaces":[],"enumValues":null,"possibleTypes":null},{"kind":"OBJECT","name":"PlanetEdge","description":"A + Relay edge containing a `Planet` and its cursor.","fields":[{"name":"node","description":"The + item at the end of the edge","args":[],"type":{"kind":"OBJECT","name":"Planet","ofType":null},"isDeprecated":false,"deprecationReason":null},{"name":"cursor","description":"A + cursor for use in pagination","args":[],"type":{"kind":"NON_NULL","name":null,"ofType":{"kind":"SCALAR","name":"String","ofType":null}},"isDeprecated":false,"deprecationReason":null}],"inputFields":null,"interfaces":[],"enumValues":null,"possibleTypes":null},{"kind":"OBJECT","name":"DjangoDebug","description":null,"fields":[{"name":"sql","description":null,"args":[],"type":{"kind":"LIST","name":null,"ofType":{"kind":"OBJECT","name":"DjangoDebugSQL","ofType":null}},"isDeprecated":false,"deprecationReason":null}],"inputFields":null,"interfaces":[],"enumValues":null,"possibleTypes":null},{"kind":"OBJECT","name":"DjangoDebugSQL","description":null,"fields":[{"name":"vendor","description":null,"args":[],"type":{"kind":"SCALAR","name":"String","ofType":null},"isDeprecated":false,"deprecationReason":null},{"name":"alias","description":null,"args":[],"type":{"kind":"SCALAR","name":"String","ofType":null},"isDeprecated":false,"deprecationReason":null},{"name":"sql","description":null,"args":[],"type":{"kind":"SCALAR","name":"String","ofType":null},"isDeprecated":false,"deprecationReason":null},{"name":"duration","description":null,"args":[],"type":{"kind":"SCALAR","name":"Float","ofType":null},"isDeprecated":false,"deprecationReason":null},{"name":"rawSql","description":null,"args":[],"type":{"kind":"SCALAR","name":"String","ofType":null},"isDeprecated":false,"deprecationReason":null},{"name":"params","description":null,"args":[],"type":{"kind":"SCALAR","name":"String","ofType":null},"isDeprecated":false,"deprecationReason":null},{"name":"startTime","description":null,"args":[],"type":{"kind":"SCALAR","name":"Float","ofType":null},"isDeprecated":false,"deprecationReason":null},{"name":"stopTime","description":null,"args":[],"type":{"kind":"SCALAR","name":"Float","ofType":null},"isDeprecated":false,"deprecationReason":null},{"name":"isSlow","description":null,"args":[],"type":{"kind":"SCALAR","name":"Boolean","ofType":null},"isDeprecated":false,"deprecationReason":null},{"name":"isSelect","description":null,"args":[],"type":{"kind":"SCALAR","name":"Boolean","ofType":null},"isDeprecated":false,"deprecationReason":null},{"name":"transId","description":null,"args":[],"type":{"kind":"SCALAR","name":"String","ofType":null},"isDeprecated":false,"deprecationReason":null},{"name":"transStatus","description":null,"args":[],"type":{"kind":"SCALAR","name":"String","ofType":null},"isDeprecated":false,"deprecationReason":null},{"name":"isoLevel","description":null,"args":[],"type":{"kind":"SCALAR","name":"String","ofType":null},"isDeprecated":false,"deprecationReason":null},{"name":"encoding","description":null,"args":[],"type":{"kind":"SCALAR","name":"String","ofType":null},"isDeprecated":false,"deprecationReason":null}],"inputFields":null,"interfaces":[],"enumValues":null,"possibleTypes":null},{"kind":"OBJECT","name":"Mutation","description":null,"fields":[{"name":"createHero","description":null,"args":[{"name":"input","description":null,"type":{"kind":"NON_NULL","name":null,"ofType":{"kind":"INPUT_OBJECT","name":"CreateHeroInput","ofType":null}},"defaultValue":null}],"type":{"kind":"OBJECT","name":"CreateHeroPayload","ofType":null},"isDeprecated":false,"deprecationReason":null}],"inputFields":null,"interfaces":[],"enumValues":null,"possibleTypes":null},{"kind":"OBJECT","name":"CreateHeroPayload","description":null,"fields":[{"name":"hero","description":null,"args":[],"type":{"kind":"OBJECT","name":"Hero","ofType":null},"isDeprecated":false,"deprecationReason":null},{"name":"ok","description":null,"args":[],"type":{"kind":"SCALAR","name":"Boolean","ofType":null},"isDeprecated":false,"deprecationReason":null},{"name":"clientMutationId","description":null,"args":[],"type":{"kind":"SCALAR","name":"String","ofType":null},"isDeprecated":false,"deprecationReason":null}],"inputFields":null,"interfaces":[],"enumValues":null,"possibleTypes":null},{"kind":"INPUT_OBJECT","name":"CreateHeroInput","description":null,"fields":null,"inputFields":[{"name":"name","description":null,"type":{"kind":"NON_NULL","name":null,"ofType":{"kind":"SCALAR","name":"String","ofType":null}},"defaultValue":null},{"name":"homeworldId","description":null,"type":{"kind":"NON_NULL","name":null,"ofType":{"kind":"SCALAR","name":"String","ofType":null}},"defaultValue":null},{"name":"clientMutationId","description":null,"type":{"kind":"SCALAR","name":"String","ofType":null},"defaultValue":null}],"interfaces":null,"enumValues":null,"possibleTypes":null},{"kind":"OBJECT","name":"__Schema","description":"A + GraphQL Schema defines the capabilities of a GraphQL server. It exposes all + available types and directives on the server, as well as the entry points + for query, mutation and subscription operations.","fields":[{"name":"types","description":"A + list of all types supported by this server.","args":[],"type":{"kind":"NON_NULL","name":null,"ofType":{"kind":"LIST","name":null,"ofType":{"kind":"NON_NULL","name":null,"ofType":{"kind":"OBJECT","name":"__Type","ofType":null}}}},"isDeprecated":false,"deprecationReason":null},{"name":"queryType","description":"The + type that query operations will be rooted at.","args":[],"type":{"kind":"NON_NULL","name":null,"ofType":{"kind":"OBJECT","name":"__Type","ofType":null}},"isDeprecated":false,"deprecationReason":null},{"name":"mutationType","description":"If + this server supports mutation, the type that mutation operations will be rooted + at.","args":[],"type":{"kind":"OBJECT","name":"__Type","ofType":null},"isDeprecated":false,"deprecationReason":null},{"name":"subscriptionType","description":"If + this server support subscription, the type that subscription operations will + be rooted at.","args":[],"type":{"kind":"OBJECT","name":"__Type","ofType":null},"isDeprecated":false,"deprecationReason":null},{"name":"directives","description":"A + list of all directives supported by this server.","args":[],"type":{"kind":"NON_NULL","name":null,"ofType":{"kind":"LIST","name":null,"ofType":{"kind":"NON_NULL","name":null,"ofType":{"kind":"OBJECT","name":"__Directive","ofType":null}}}},"isDeprecated":false,"deprecationReason":null}],"inputFields":null,"interfaces":[],"enumValues":null,"possibleTypes":null},{"kind":"OBJECT","name":"__Type","description":"The + fundamental unit of any GraphQL Schema is the type. There are many kinds of + types in GraphQL as represented by the `__TypeKind` enum.\n\nDepending on + the kind of a type, certain fields describe information about that type. Scalar + types provide no information beyond a name and description, while Enum types + provide their values. Object and Interface types provide the fields they describe. + Abstract types, Union and Interface, provide the Object types possible at + runtime. List and NonNull types compose other types.","fields":[{"name":"kind","description":null,"args":[],"type":{"kind":"NON_NULL","name":null,"ofType":{"kind":"ENUM","name":"__TypeKind","ofType":null}},"isDeprecated":false,"deprecationReason":null},{"name":"name","description":null,"args":[],"type":{"kind":"SCALAR","name":"String","ofType":null},"isDeprecated":false,"deprecationReason":null},{"name":"description","description":null,"args":[],"type":{"kind":"SCALAR","name":"String","ofType":null},"isDeprecated":false,"deprecationReason":null},{"name":"fields","description":null,"args":[{"name":"includeDeprecated","description":null,"type":{"kind":"SCALAR","name":"Boolean","ofType":null},"defaultValue":"false"}],"type":{"kind":"LIST","name":null,"ofType":{"kind":"NON_NULL","name":null,"ofType":{"kind":"OBJECT","name":"__Field","ofType":null}}},"isDeprecated":false,"deprecationReason":null},{"name":"interfaces","description":null,"args":[],"type":{"kind":"LIST","name":null,"ofType":{"kind":"NON_NULL","name":null,"ofType":{"kind":"OBJECT","name":"__Type","ofType":null}}},"isDeprecated":false,"deprecationReason":null},{"name":"possibleTypes","description":null,"args":[],"type":{"kind":"LIST","name":null,"ofType":{"kind":"NON_NULL","name":null,"ofType":{"kind":"OBJECT","name":"__Type","ofType":null}}},"isDeprecated":false,"deprecationReason":null},{"name":"enumValues","description":null,"args":[{"name":"includeDeprecated","description":null,"type":{"kind":"SCALAR","name":"Boolean","ofType":null},"defaultValue":"false"}],"type":{"kind":"LIST","name":null,"ofType":{"kind":"NON_NULL","name":null,"ofType":{"kind":"OBJECT","name":"__EnumValue","ofType":null}}},"isDeprecated":false,"deprecationReason":null},{"name":"inputFields","description":null,"args":[],"type":{"kind":"LIST","name":null,"ofType":{"kind":"NON_NULL","name":null,"ofType":{"kind":"OBJECT","name":"__InputValue","ofType":null}}},"isDeprecated":false,"deprecationReason":null},{"name":"ofType","description":null,"args":[],"type":{"kind":"OBJECT","name":"__Type","ofType":null},"isDeprecated":false,"deprecationReason":null}],"inputFields":null,"interfaces":[],"enumValues":null,"possibleTypes":null},{"kind":"ENUM","name":"__TypeKind","description":"An + enum describing what kind of type a given `__Type` is","fields":null,"inputFields":null,"interfaces":null,"enumValues":[{"name":"SCALAR","description":"Indicates + this type is a scalar.","isDeprecated":false,"deprecationReason":null},{"name":"OBJECT","description":"Indicates + this type is an object. `fields` and `interfaces` are valid fields.","isDeprecated":false,"deprecationReason":null},{"name":"INTERFACE","description":"Indicates + this type is an interface. `fields` and `possibleTypes` are valid fields.","isDeprecated":false,"deprecationReason":null},{"name":"UNION","description":"Indicates + this type is a union. `possibleTypes` is a valid field.","isDeprecated":false,"deprecationReason":null},{"name":"ENUM","description":"Indicates + this type is an enum. `enumValues` is a valid field.","isDeprecated":false,"deprecationReason":null},{"name":"INPUT_OBJECT","description":"Indicates + this type is an input object. `inputFields` is a valid field.","isDeprecated":false,"deprecationReason":null},{"name":"LIST","description":"Indicates + this type is a list. `ofType` is a valid field.","isDeprecated":false,"deprecationReason":null},{"name":"NON_NULL","description":"Indicates + this type is a non-null. `ofType` is a valid field.","isDeprecated":false,"deprecationReason":null}],"possibleTypes":null},{"kind":"OBJECT","name":"__Field","description":"Object + and Interface types are described by a list of Fields, each of which has a + name, potentially a list of arguments, and a return type.","fields":[{"name":"name","description":null,"args":[],"type":{"kind":"NON_NULL","name":null,"ofType":{"kind":"SCALAR","name":"String","ofType":null}},"isDeprecated":false,"deprecationReason":null},{"name":"description","description":null,"args":[],"type":{"kind":"SCALAR","name":"String","ofType":null},"isDeprecated":false,"deprecationReason":null},{"name":"args","description":null,"args":[],"type":{"kind":"NON_NULL","name":null,"ofType":{"kind":"LIST","name":null,"ofType":{"kind":"NON_NULL","name":null,"ofType":{"kind":"OBJECT","name":"__InputValue","ofType":null}}}},"isDeprecated":false,"deprecationReason":null},{"name":"type","description":null,"args":[],"type":{"kind":"NON_NULL","name":null,"ofType":{"kind":"OBJECT","name":"__Type","ofType":null}},"isDeprecated":false,"deprecationReason":null},{"name":"isDeprecated","description":null,"args":[],"type":{"kind":"NON_NULL","name":null,"ofType":{"kind":"SCALAR","name":"Boolean","ofType":null}},"isDeprecated":false,"deprecationReason":null},{"name":"deprecationReason","description":null,"args":[],"type":{"kind":"SCALAR","name":"String","ofType":null},"isDeprecated":false,"deprecationReason":null}],"inputFields":null,"interfaces":[],"enumValues":null,"possibleTypes":null},{"kind":"OBJECT","name":"__InputValue","description":"Arguments + provided to Fields or Directives and the input fields of an InputObject are + represented as Input Values which describe their type and optionally a default + value.","fields":[{"name":"name","description":null,"args":[],"type":{"kind":"NON_NULL","name":null,"ofType":{"kind":"SCALAR","name":"String","ofType":null}},"isDeprecated":false,"deprecationReason":null},{"name":"description","description":null,"args":[],"type":{"kind":"SCALAR","name":"String","ofType":null},"isDeprecated":false,"deprecationReason":null},{"name":"type","description":null,"args":[],"type":{"kind":"NON_NULL","name":null,"ofType":{"kind":"OBJECT","name":"__Type","ofType":null}},"isDeprecated":false,"deprecationReason":null},{"name":"defaultValue","description":null,"args":[],"type":{"kind":"SCALAR","name":"String","ofType":null},"isDeprecated":false,"deprecationReason":null}],"inputFields":null,"interfaces":[],"enumValues":null,"possibleTypes":null},{"kind":"OBJECT","name":"__EnumValue","description":"One + possible value for a given Enum. Enum values are unique values, not a placeholder + for a string or numeric value. However an Enum value is returned in a JSON + response as a string.","fields":[{"name":"name","description":null,"args":[],"type":{"kind":"NON_NULL","name":null,"ofType":{"kind":"SCALAR","name":"String","ofType":null}},"isDeprecated":false,"deprecationReason":null},{"name":"description","description":null,"args":[],"type":{"kind":"SCALAR","name":"String","ofType":null},"isDeprecated":false,"deprecationReason":null},{"name":"isDeprecated","description":null,"args":[],"type":{"kind":"NON_NULL","name":null,"ofType":{"kind":"SCALAR","name":"Boolean","ofType":null}},"isDeprecated":false,"deprecationReason":null},{"name":"deprecationReason","description":null,"args":[],"type":{"kind":"SCALAR","name":"String","ofType":null},"isDeprecated":false,"deprecationReason":null}],"inputFields":null,"interfaces":[],"enumValues":null,"possibleTypes":null},{"kind":"OBJECT","name":"__Directive","description":"A + Directive provides a way to describe alternate runtime execution and type + validation behavior in a GraphQL document.\n\nIn some cases, you need to provide + options to alter GraphQL''s execution behavior in ways field arguments will + not suffice, such as conditionally including or skipping a field. Directives + provide this by describing additional information to the executor.","fields":[{"name":"name","description":null,"args":[],"type":{"kind":"NON_NULL","name":null,"ofType":{"kind":"SCALAR","name":"String","ofType":null}},"isDeprecated":false,"deprecationReason":null},{"name":"description","description":null,"args":[],"type":{"kind":"SCALAR","name":"String","ofType":null},"isDeprecated":false,"deprecationReason":null},{"name":"locations","description":null,"args":[],"type":{"kind":"NON_NULL","name":null,"ofType":{"kind":"LIST","name":null,"ofType":{"kind":"NON_NULL","name":null,"ofType":{"kind":"ENUM","name":"__DirectiveLocation","ofType":null}}}},"isDeprecated":false,"deprecationReason":null},{"name":"args","description":null,"args":[],"type":{"kind":"NON_NULL","name":null,"ofType":{"kind":"LIST","name":null,"ofType":{"kind":"NON_NULL","name":null,"ofType":{"kind":"OBJECT","name":"__InputValue","ofType":null}}}},"isDeprecated":false,"deprecationReason":null},{"name":"onOperation","description":null,"args":[],"type":{"kind":"NON_NULL","name":null,"ofType":{"kind":"SCALAR","name":"Boolean","ofType":null}},"isDeprecated":true,"deprecationReason":"Use + `locations`."},{"name":"onFragment","description":null,"args":[],"type":{"kind":"NON_NULL","name":null,"ofType":{"kind":"SCALAR","name":"Boolean","ofType":null}},"isDeprecated":true,"deprecationReason":"Use + `locations`."},{"name":"onField","description":null,"args":[],"type":{"kind":"NON_NULL","name":null,"ofType":{"kind":"SCALAR","name":"Boolean","ofType":null}},"isDeprecated":true,"deprecationReason":"Use + `locations`."}],"inputFields":null,"interfaces":[],"enumValues":null,"possibleTypes":null},{"kind":"ENUM","name":"__DirectiveLocation","description":"A + Directive can be adjacent to many parts of the GraphQL language, a __DirectiveLocation + describes one such possible adjacencies.","fields":null,"inputFields":null,"interfaces":null,"enumValues":[{"name":"QUERY","description":"Location + adjacent to a query operation.","isDeprecated":false,"deprecationReason":null},{"name":"MUTATION","description":"Location + adjacent to a mutation operation.","isDeprecated":false,"deprecationReason":null},{"name":"SUBSCRIPTION","description":"Location + adjacent to a subscription operation.","isDeprecated":false,"deprecationReason":null},{"name":"FIELD","description":"Location + adjacent to a field.","isDeprecated":false,"deprecationReason":null},{"name":"FRAGMENT_DEFINITION","description":"Location + adjacent to a fragment definition.","isDeprecated":false,"deprecationReason":null},{"name":"FRAGMENT_SPREAD","description":"Location + adjacent to a fragment spread.","isDeprecated":false,"deprecationReason":null},{"name":"INLINE_FRAGMENT","description":"Location + adjacent to an inline fragment.","isDeprecated":false,"deprecationReason":null},{"name":"SCHEMA","description":"Location + adjacent to a schema definition.","isDeprecated":false,"deprecationReason":null},{"name":"SCALAR","description":"Location + adjacent to a scalar definition.","isDeprecated":false,"deprecationReason":null},{"name":"OBJECT","description":"Location + adjacent to an object definition.","isDeprecated":false,"deprecationReason":null},{"name":"FIELD_DEFINITION","description":"Location + adjacent to a field definition.","isDeprecated":false,"deprecationReason":null},{"name":"ARGUMENT_DEFINITION","description":"Location + adjacent to an argument definition.","isDeprecated":false,"deprecationReason":null},{"name":"INTERFACE","description":"Location + adjacent to an interface definition.","isDeprecated":false,"deprecationReason":null},{"name":"UNION","description":"Location + adjacent to a union definition.","isDeprecated":false,"deprecationReason":null},{"name":"ENUM","description":"Location + adjacent to an enum definition.","isDeprecated":false,"deprecationReason":null},{"name":"ENUM_VALUE","description":"Location + adjacent to an enum value definition.","isDeprecated":false,"deprecationReason":null},{"name":"INPUT_OBJECT","description":"Location + adjacent to an input object definition.","isDeprecated":false,"deprecationReason":null},{"name":"INPUT_FIELD_DEFINITION","description":"Location + adjacent to an input object field definition.","isDeprecated":false,"deprecationReason":null}],"possibleTypes":null}],"directives":[{"name":"include","description":"Directs + the executor to include this field or fragment only when the `if` argument + is true.","locations":["FIELD","FRAGMENT_SPREAD","INLINE_FRAGMENT"],"args":[{"name":"if","description":"Included + when true.","type":{"kind":"NON_NULL","name":null,"ofType":{"kind":"SCALAR","name":"Boolean","ofType":null}},"defaultValue":null}]},{"name":"skip","description":"Directs + the executor to skip this field or fragment when the `if` argument is true.","locations":["FIELD","FRAGMENT_SPREAD","INLINE_FRAGMENT"],"args":[{"name":"if","description":"Skipped + when true.","type":{"kind":"NON_NULL","name":null,"ofType":{"kind":"SCALAR","name":"Boolean","ofType":null}},"defaultValue":null}]}]}}}' + headers: + Content-Length: + - '69553' + Content-Type: + - application/json + Date: + - Fri, 06 Nov 2020 11:30:21 GMT + Server: + - WSGIServer/0.1 Python/2.7.18 + Set-Cookie: + - csrftoken=kAyQyUjNOGXZfkKUtWtvUROaFfDe2GBiV7yIRsqs3r2j9aYchRDXTNo3lHp72h5k; + expires=Fri, 05-Nov-2021 11:30:21 GMT; Max-Age=31449600; Path=/ + Vary: + - Cookie + X-Frame-Options: + - SAMEORIGIN + status: + code: 200 + message: OK +- request: + body: '[{"query": "{\n myFavoriteFilm: film(id: \"RmlsbToz\") {\n id\n title\n episodeId\n characters(first: + 5) {\n edges {\n node {\n name\n }\n }\n }\n }\n}"}]' + headers: + Accept: + - '*/*' + Accept-Encoding: + - gzip, deflate + Connection: + - keep-alive + Content-Length: + - '204' + Content-Type: + - application/json + Cookie: + - csrftoken=kAyQyUjNOGXZfkKUtWtvUROaFfDe2GBiV7yIRsqs3r2j9aYchRDXTNo3lHp72h5k; + csrftoken=kAyQyUjNOGXZfkKUtWtvUROaFfDe2GBiV7yIRsqs3r2j9aYchRDXTNo3lHp72h5k + User-Agent: + - python-requests/2.24.0 + x-csrftoken: + - kAyQyUjNOGXZfkKUtWtvUROaFfDe2GBiV7yIRsqs3r2j9aYchRDXTNo3lHp72h5k + method: POST + uri: http://127.0.0.1:8000/graphql + response: + body: + string: '[{"data":{"myFavoriteFilm":{"id":"RmlsbToz","title":"Return of the Jedi","episodeId":6,"characters":{"edges":[{"node":{"name":"Luke + Skywalker"}},{"node":{"name":"C-3PO"}},{"node":{"name":"R2-D2"}},{"node":{"name":"Darth + Vader"}},{"node":{"name":"Leia Organa"}}]}}}}]' + headers: + Content-Length: + - '264' + Content-Type: + - application/json + Date: + - Fri, 06 Nov 2020 11:30:21 GMT + Server: + - WSGIServer/0.1 Python/2.7.18 + Set-Cookie: + - csrftoken=kAyQyUjNOGXZfkKUtWtvUROaFfDe2GBiV7yIRsqs3r2j9aYchRDXTNo3lHp72h5k; + expires=Fri, 05-Nov-2021 11:30:21 GMT; Max-Age=31449600; Path=/ + Vary: + - Cookie + X-Frame-Options: + - SAMEORIGIN + status: + code: 200 + message: OK +- request: + body: '[{"query": "query Planet($id: ID!) {\n planet(id: $id) {\n id\n name\n }\n}", + "variables": {"id": "UGxhbmV0OjEw"}}]' + headers: + Accept: + - '*/*' + Accept-Encoding: + - gzip, deflate + Connection: + - keep-alive + Content-Length: + - '123' + Content-Type: + - application/json + Cookie: + - csrftoken=kAyQyUjNOGXZfkKUtWtvUROaFfDe2GBiV7yIRsqs3r2j9aYchRDXTNo3lHp72h5k; + csrftoken=kAyQyUjNOGXZfkKUtWtvUROaFfDe2GBiV7yIRsqs3r2j9aYchRDXTNo3lHp72h5k + User-Agent: + - python-requests/2.24.0 + x-csrftoken: + - kAyQyUjNOGXZfkKUtWtvUROaFfDe2GBiV7yIRsqs3r2j9aYchRDXTNo3lHp72h5k + method: POST + uri: http://127.0.0.1:8000/graphql + response: + body: + string: '[{"data":{"planet":{"id":"UGxhbmV0OjEw","name":"Kamino"}}}]' + headers: + Content-Length: + - '57' + Content-Type: + - application/json + Date: + - Fri, 06 Nov 2020 11:30:21 GMT + Server: + - WSGIServer/0.1 Python/2.7.18 + Set-Cookie: + - csrftoken=kAyQyUjNOGXZfkKUtWtvUROaFfDe2GBiV7yIRsqs3r2j9aYchRDXTNo3lHp72h5k; + expires=Fri, 05-Nov-2021 11:30:21 GMT; Max-Age=31449600; Path=/ + Vary: + - Cookie + X-Frame-Options: + - SAMEORIGIN + status: + code: 200 + message: OK +- request: + body: '[{"query": "query Planet1 {\n planet(id: \"UGxhbmV0OjEw\") {\n id\n name\n }\n}\n\nquery + Planet2 {\n planet(id: \"UGxhbmV0OjEx\") {\n id\n name\n }\n}", "operationName": + "Planet2"}]' + headers: + Accept: + - '*/*' + Accept-Encoding: + - gzip, deflate + Connection: + - keep-alive + Content-Length: + - '197' + Content-Type: + - application/json + Cookie: + - csrftoken=kAyQyUjNOGXZfkKUtWtvUROaFfDe2GBiV7yIRsqs3r2j9aYchRDXTNo3lHp72h5k; + csrftoken=kAyQyUjNOGXZfkKUtWtvUROaFfDe2GBiV7yIRsqs3r2j9aYchRDXTNo3lHp72h5k + User-Agent: + - python-requests/2.24.0 + x-csrftoken: + - kAyQyUjNOGXZfkKUtWtvUROaFfDe2GBiV7yIRsqs3r2j9aYchRDXTNo3lHp72h5k + method: POST + uri: http://127.0.0.1:8000/graphql + response: + body: + string: '[{"data":{"planet":{"id":"UGxhbmV0OjEx","name":"Geonosis"}}}]' + headers: + Content-Length: + - '59' + Content-Type: + - application/json + Date: + - Fri, 06 Nov 2020 11:30:21 GMT + Server: + - WSGIServer/0.1 Python/2.7.18 + Set-Cookie: + - csrftoken=kAyQyUjNOGXZfkKUtWtvUROaFfDe2GBiV7yIRsqs3r2j9aYchRDXTNo3lHp72h5k; + expires=Fri, 05-Nov-2021 11:30:21 GMT; Max-Age=31449600; Path=/ + Vary: + - Cookie + X-Frame-Options: + - SAMEORIGIN + status: + code: 200 + message: OK +- request: + body: '[{"query": "query Planet($id: ID!) {\n planet(id: $id) {\n id\n name\n }\n}"}]' + headers: + Accept: + - '*/*' + Accept-Encoding: + - gzip, deflate + Connection: + - keep-alive + Content-Length: + - '86' + Content-Type: + - application/json + Cookie: + - csrftoken=kAyQyUjNOGXZfkKUtWtvUROaFfDe2GBiV7yIRsqs3r2j9aYchRDXTNo3lHp72h5k; + csrftoken=kAyQyUjNOGXZfkKUtWtvUROaFfDe2GBiV7yIRsqs3r2j9aYchRDXTNo3lHp72h5k + User-Agent: + - python-requests/2.26.0 + authorization: + - xxx-123 + method: POST + uri: http://127.0.0.1:8000/graphql + response: + body: + string: '[{"data":{"planet":{"id":"UGxhbmV0OjEx","name":"Geonosis"}}}]' + headers: + Content-Length: + - '59' + Content-Type: + - application/json + Date: + - Fri, 06 Nov 2020 11:30:21 GMT + Server: + - WSGIServer/0.1 Python/2.7.18 + Set-Cookie: + - csrftoken=kAyQyUjNOGXZfkKUtWtvUROaFfDe2GBiV7yIRsqs3r2j9aYchRDXTNo3lHp72h5k; + expires=Fri, 05-Nov-2021 11:30:21 GMT; Max-Age=31449600; Path=/ + Vary: + - Cookie + X-Frame-Options: + - SAMEORIGIN + status: + code: 200 + message: OK +version: 1 diff --git a/tests/test_client.py b/tests/test_client.py index 8b6575d7..e4c283ae 100644 --- a/tests/test_client.py +++ b/tests/test_client.py @@ -7,6 +7,7 @@ from gql import Client, gql from gql.transport import Transport +from gql.transport.data_structures.graphql_request import GraphQLRequest from gql.transport.exceptions import TransportQueryError with suppress(ModuleNotFoundError): @@ -32,10 +33,33 @@ class RandomTransport(Transport): def execute(self): super(RandomTransport, self).execute(http_transport_query) + def execute_batch(self): + super(RandomTransport, self).execute_batch( + [GraphQLRequest(document=http_transport_query)] + ) + with pytest.raises(NotImplementedError) as exc_info: RandomTransport().execute() assert "Any Transport subclass must implement execute method" == str(exc_info.value) + with pytest.raises(NotImplementedError) as exc_info: + RandomTransport().execute_batch() + assert "Any Transport subclass must implement execute_batch method" == str( + exc_info.value + ) + + +def test_request_async_execute_batch_not_implemented_yet(http_transport_query): + from gql.transport.aiohttp import AIOHTTPTransport + + transport = AIOHTTPTransport(url="http://localhost/") + client = Client(transport=transport) + + with pytest.raises(NotImplementedError) as exc_info: + client.execute_batch([GraphQLRequest(document=gql("{dummy}"))]) + + assert "Batching is not implemented for async yet." == str(exc_info.value) + @pytest.mark.requests @mock.patch("urllib3.connection.HTTPConnection._new_conn") @@ -76,6 +100,17 @@ def test_retries_on_transport(execute_mock): # means you're actually doing 4 calls. assert execute_mock.call_count == expected_retries + 1 + execute_mock.reset_mock() + queries = map(lambda d: GraphQLRequest(document=d), [query, query, query]) + + with client as session: # We're using the client as context manager + with pytest.raises(Exception): + session.execute_batch(queries) + + # This might look strange compared to the previous test, but making 3 retries + # means you're actually doing 4 calls. + assert execute_mock.call_count == expected_retries + 1 + def test_no_schema_exception(): with pytest.raises(AssertionError) as exc_info: @@ -112,6 +147,10 @@ def test_execute_result_error(): client.execute(failing_query) assert 'Cannot query field "id" on type "Continent".' in str(exc_info.value) + with pytest.raises(TransportQueryError) as exc_info: + client.execute_batch([GraphQLRequest(document=failing_query)]) + assert 'Cannot query field "id" on type "Continent".' in str(exc_info.value) + @pytest.mark.online @pytest.mark.requests @@ -127,7 +166,13 @@ def test_http_transport_raise_for_status_error(http_transport_query): ) as client: with pytest.raises(Exception) as exc_info: client.execute(http_transport_query) - assert "400 Client Error: Bad Request for url" in str(exc_info.value) + + assert "400 Client Error: Bad Request for url" in str(exc_info.value) + + with pytest.raises(Exception) as exc_info: + client.execute_batch([GraphQLRequest(document=http_transport_query)]) + + assert "400 Client Error: Bad Request for url" in str(exc_info.value) @pytest.mark.online @@ -143,8 +188,19 @@ def test_http_transport_verify_error(http_transport_query): ) as client: with pytest.warns(Warning) as record: client.execute(http_transport_query) - assert len(record) == 1 - assert "Unverified HTTPS request is being made to host" in str(record[0].message) + + assert len(record) == 1 + assert "Unverified HTTPS request is being made to host" in str( + record[0].message + ) + + with pytest.warns(Warning) as record: + client.execute_batch([GraphQLRequest(document=http_transport_query)]) + + assert len(record) == 1 + assert "Unverified HTTPS request is being made to host" in str( + record[0].message + ) @pytest.mark.online @@ -159,7 +215,10 @@ def test_http_transport_specify_method_valid(http_transport_query): ) ) as client: result = client.execute(http_transport_query) - assert result is not None + assert result is not None + + result = client.execute_batch([GraphQLRequest(document=http_transport_query)]) + assert result is not None @pytest.mark.online @@ -175,7 +234,11 @@ def test_http_transport_specify_method_invalid(http_transport_query): ) as client: with pytest.raises(Exception) as exc_info: client.execute(http_transport_query) - assert "400 Client Error: Bad Request for url" in str(exc_info.value) + assert "400 Client Error: Bad Request for url" in str(exc_info.value) + + with pytest.raises(Exception) as exc_info: + client.execute_batch([GraphQLRequest(document=http_transport_query)]) + assert "400 Client Error: Bad Request for url" in str(exc_info.value) def test_gql(): diff --git a/tests/test_httpx.py b/tests/test_httpx.py index 56a984a4..255a1f5e 100644 --- a/tests/test_httpx.py +++ b/tests/test_httpx.py @@ -3,6 +3,7 @@ import pytest from gql import Client, gql +from gql.transport.data_structures import GraphQLRequest from gql.transport.exceptions import ( TransportAlreadyConnected, TransportClosed, @@ -33,6 +34,19 @@ ) +def test_httpx_execute_batch_is_not_implemented(): + from gql.transport.httpx import HTTPXTransport + + with Client(transport=HTTPXTransport(url="/")) as session: + reqs = [GraphQLRequest(document=gql(query1_str))] + + with pytest.raises(NotImplementedError) as exc_info: + session.execute_batch(reqs) + assert "Any Transport subclass must implement execute_batch method" == str( + exc_info.value + ) + + @pytest.mark.aiohttp @pytest.mark.asyncio async def test_httpx_query(event_loop, aiohttp_server, run_sync_test): diff --git a/tests/test_requests_batch.py b/tests/test_requests_batch.py new file mode 100644 index 00000000..c97f97e0 --- /dev/null +++ b/tests/test_requests_batch.py @@ -0,0 +1,378 @@ +from typing import Mapping + +import pytest + +from gql import Client, gql +from gql.transport.data_structures.graphql_request import GraphQLRequest +from gql.transport.exceptions import ( + TransportClosed, + TransportProtocolError, + TransportQueryError, + TransportServerError, +) + +# Marking all tests in this file with the requests marker +pytestmark = pytest.mark.requests + +query1_str = """ + query getContinents { + continents { + code + name + } + } +""" + +query1_server_answer_list = ( + '[{"data":{"continents":[' + '{"code":"AF","name":"Africa"},{"code":"AN","name":"Antarctica"},' + '{"code":"AS","name":"Asia"},{"code":"EU","name":"Europe"},' + '{"code":"NA","name":"North America"},{"code":"OC","name":"Oceania"},' + '{"code":"SA","name":"South America"}]}}]' +) + + +@pytest.mark.aiohttp +@pytest.mark.asyncio +async def test_requests_query(event_loop, aiohttp_server, run_sync_test): + from aiohttp import web + from gql.transport.requests import RequestsHTTPTransport + + async def handler(request): + return web.Response( + text=query1_server_answer_list, + content_type="application/json", + headers={"dummy": "test1234"}, + ) + + app = web.Application() + app.router.add_route("POST", "/", handler) + server = await aiohttp_server(app) + + url = server.make_url("/") + + def test_code(): + transport = RequestsHTTPTransport(url=url) + + with Client(transport=transport) as session: + + query = [GraphQLRequest(document=gql(query1_str))] + + # Execute query synchronously + results = session.execute_batch(query) + + continents = results[0]["continents"] + + africa = continents[0] + + assert africa["code"] == "AF" + + # Checking response headers are saved in the transport + assert hasattr(transport, "response_headers") + assert isinstance(transport.response_headers, Mapping) + assert transport.response_headers["dummy"] == "test1234" + + await run_sync_test(event_loop, server, test_code) + + +@pytest.mark.aiohttp +@pytest.mark.asyncio +async def test_requests_cookies(event_loop, aiohttp_server, run_sync_test): + from aiohttp import web + from gql.transport.requests import RequestsHTTPTransport + + async def handler(request): + assert "COOKIE" in request.headers + assert "cookie1=val1" == request.headers["COOKIE"] + + return web.Response( + text=query1_server_answer_list, content_type="application/json" + ) + + app = web.Application() + app.router.add_route("POST", "/", handler) + server = await aiohttp_server(app) + + url = server.make_url("/") + + def test_code(): + transport = RequestsHTTPTransport(url=url, cookies={"cookie1": "val1"}) + + with Client(transport=transport) as session: + + query = [GraphQLRequest(document=gql(query1_str))] + + # Execute query synchronously + results = session.execute_batch(query) + + continents = results[0]["continents"] + + africa = continents[0] + + assert africa["code"] == "AF" + + await run_sync_test(event_loop, server, test_code) + + +@pytest.mark.aiohttp +@pytest.mark.asyncio +async def test_requests_error_code_401(event_loop, aiohttp_server, run_sync_test): + from aiohttp import web + from gql.transport.requests import RequestsHTTPTransport + + async def handler(request): + # Will generate http error code 401 + return web.Response( + text='{"error":"Unauthorized","message":"401 Client Error: Unauthorized"}', + content_type="application/json", + status=401, + ) + + app = web.Application() + app.router.add_route("POST", "/", handler) + server = await aiohttp_server(app) + + url = server.make_url("/") + + def test_code(): + transport = RequestsHTTPTransport(url=url) + + with Client(transport=transport) as session: + + query = [GraphQLRequest(document=gql(query1_str))] + + with pytest.raises(TransportServerError) as exc_info: + session.execute_batch(query) + + assert "401 Client Error: Unauthorized" in str(exc_info.value) + + await run_sync_test(event_loop, server, test_code) + + +@pytest.mark.aiohttp +@pytest.mark.asyncio +async def test_requests_error_code_429(event_loop, aiohttp_server, run_sync_test): + from aiohttp import web + from gql.transport.requests import RequestsHTTPTransport + + async def handler(request): + # Will generate http error code 429 + return web.Response( + text=""" + + + Too Many Requests + + +

Too Many Requests

+

I only allow 50 requests per hour to this Web site per + logged in user. Try again soon.

+ +""", + content_type="text/html", + status=429, + headers={"Retry-After": "3600"}, + ) + + app = web.Application() + app.router.add_route("POST", "/", handler) + server = await aiohttp_server(app) + + url = server.make_url("/") + + def test_code(): + transport = RequestsHTTPTransport(url=url) + + with Client(transport=transport) as session: + + query = [GraphQLRequest(document=gql(query1_str))] + + with pytest.raises(TransportServerError) as exc_info: + session.execute_batch(query) + + assert "429, message='Too Many Requests'" in str(exc_info.value) + + # Checking response headers are saved in the transport + assert hasattr(transport, "response_headers") + assert isinstance(transport.response_headers, Mapping) + assert transport.response_headers["Retry-After"] == "3600" + + +@pytest.mark.aiohttp +@pytest.mark.asyncio +async def test_requests_error_code_500(event_loop, aiohttp_server, run_sync_test): + from aiohttp import web + from gql.transport.requests import RequestsHTTPTransport + + async def handler(request): + # Will generate http error code 500 + raise Exception("Server error") + + app = web.Application() + app.router.add_route("POST", "/", handler) + server = await aiohttp_server(app) + + url = server.make_url("/") + + def test_code(): + transport = RequestsHTTPTransport(url=url) + + with Client(transport=transport) as session: + + query = [GraphQLRequest(document=gql(query1_str))] + + with pytest.raises(TransportServerError): + session.execute_batch(query) + + await run_sync_test(event_loop, server, test_code) + + +query1_server_error_answer_list = '[{"errors": ["Error 1", "Error 2"]}]' + + +@pytest.mark.aiohttp +@pytest.mark.asyncio +async def test_requests_error_code(event_loop, aiohttp_server, run_sync_test): + from aiohttp import web + from gql.transport.requests import RequestsHTTPTransport + + async def handler(request): + return web.Response( + text=query1_server_error_answer_list, content_type="application/json" + ) + + app = web.Application() + app.router.add_route("POST", "/", handler) + server = await aiohttp_server(app) + + url = server.make_url("/") + + def test_code(): + transport = RequestsHTTPTransport(url=url) + + with Client(transport=transport) as session: + + query = [GraphQLRequest(document=gql(query1_str))] + + with pytest.raises(TransportQueryError): + session.execute_batch(query) + + await run_sync_test(event_loop, server, test_code) + + +invalid_protocol_responses = [ + "{}", + "qlsjfqsdlkj", + '{"not_data_or_errors": 35}', + "[{}]", + "[qlsjfqsdlkj]", + '[{"not_data_or_errors": 35}]', + "[]", + "[1]", +] + + +@pytest.mark.aiohttp +@pytest.mark.asyncio +@pytest.mark.parametrize("response", invalid_protocol_responses) +async def test_requests_invalid_protocol( + event_loop, aiohttp_server, response, run_sync_test +): + from aiohttp import web + from gql.transport.requests import RequestsHTTPTransport + + async def handler(request): + return web.Response(text=response, content_type="application/json") + + app = web.Application() + app.router.add_route("POST", "/", handler) + server = await aiohttp_server(app) + + url = server.make_url("/") + + def test_code(): + transport = RequestsHTTPTransport(url=url) + + with Client(transport=transport) as session: + + query = [GraphQLRequest(document=gql(query1_str))] + + with pytest.raises(TransportProtocolError): + session.execute_batch(query) + + await run_sync_test(event_loop, server, test_code) + + +@pytest.mark.aiohttp +@pytest.mark.asyncio +async def test_requests_cannot_execute_if_not_connected( + event_loop, aiohttp_server, run_sync_test +): + from aiohttp import web + from gql.transport.requests import RequestsHTTPTransport + + async def handler(request): + return web.Response( + text=query1_server_answer_list, content_type="application/json" + ) + + app = web.Application() + app.router.add_route("POST", "/", handler) + server = await aiohttp_server(app) + + url = server.make_url("/") + + def test_code(): + transport = RequestsHTTPTransport(url=url) + + query = [GraphQLRequest(document=gql(query1_str))] + + with pytest.raises(TransportClosed): + transport.execute_batch(query) + + await run_sync_test(event_loop, server, test_code) + + +query1_server_answer_with_extensions_list = ( + '[{"data":{"continents":[' + '{"code":"AF","name":"Africa"},{"code":"AN","name":"Antarctica"},' + '{"code":"AS","name":"Asia"},{"code":"EU","name":"Europe"},' + '{"code":"NA","name":"North America"},{"code":"OC","name":"Oceania"},' + '{"code":"SA","name":"South America"}]},' + '"extensions": {"key1": "val1"}' + "}]" +) + + +@pytest.mark.aiohttp +@pytest.mark.asyncio +async def test_requests_query_with_extensions( + event_loop, aiohttp_server, run_sync_test +): + from aiohttp import web + from gql.transport.requests import RequestsHTTPTransport + + async def handler(request): + return web.Response( + text=query1_server_answer_with_extensions_list, + content_type="application/json", + ) + + app = web.Application() + app.router.add_route("POST", "/", handler) + server = await aiohttp_server(app) + + url = server.make_url("/") + + def test_code(): + transport = RequestsHTTPTransport(url=url) + + with Client(transport=transport) as session: + + query = [GraphQLRequest(document=gql(query1_str))] + + execution_results = session.execute_batch(query, get_execution_result=True) + + assert execution_results[0].extensions["key1"] == "val1" + + await run_sync_test(event_loop, server, test_code) diff --git a/tests/test_transport_batch.py b/tests/test_transport_batch.py new file mode 100644 index 00000000..88a67ee8 --- /dev/null +++ b/tests/test_transport_batch.py @@ -0,0 +1,152 @@ +import os + +import pytest + +from gql import Client, gql +from gql.transport.data_structures.graphql_request import GraphQLRequest + +# We serve https://github.com/graphql-python/swapi-graphene locally: +URL = "http://127.0.0.1:8000/graphql" + +# Marking all tests in this file with the requests marker +pytestmark = pytest.mark.requests + + +def use_cassette(name): + import vcr + + query_vcr = vcr.VCR( + cassette_library_dir=os.path.join( + os.path.dirname(__file__), "fixtures", "vcr_cassettes" + ), + record_mode="new_episodes", + match_on=["uri", "method", "body"], + ) + + return query_vcr.use_cassette(name + ".yaml") + + +@pytest.fixture +def client(): + import requests + from gql.transport.requests import RequestsHTTPTransport + + with use_cassette("client"): + response = requests.get( + URL, headers={"Host": "swapi.graphene-python.org", "Accept": "text/html"} + ) + response.raise_for_status() + csrf = response.cookies["csrftoken"] + + return Client( + transport=RequestsHTTPTransport( + url=URL, cookies={"csrftoken": csrf}, headers={"x-csrftoken": csrf} + ), + fetch_schema_from_transport=True, + ) + + +def test_hero_name_query(client): + query = gql( + """ + { + myFavoriteFilm: film(id:"RmlsbToz") { + id + title + episodeId + characters(first:5) { + edges { + node { + name + } + } + } + } + } + """ + ) + expected = [ + { + "myFavoriteFilm": { + "id": "RmlsbToz", + "title": "Return of the Jedi", + "episodeId": 6, + "characters": { + "edges": [ + {"node": {"name": "Luke Skywalker"}}, + {"node": {"name": "C-3PO"}}, + {"node": {"name": "R2-D2"}}, + {"node": {"name": "Darth Vader"}}, + {"node": {"name": "Leia Organa"}}, + ] + }, + } + } + ] + with use_cassette("queries_batch"): + results = client.execute_batch([GraphQLRequest(document=query)]) + assert results == expected + + +def test_query_with_variable(client): + query = gql( + """ + query Planet($id: ID!) { + planet(id: $id) { + id + name + } + } + """ + ) + expected = [{"planet": {"id": "UGxhbmV0OjEw", "name": "Kamino"}}] + with use_cassette("queries_batch"): + results = client.execute_batch( + [GraphQLRequest(document=query, variable_values={"id": "UGxhbmV0OjEw"})] + ) + assert results == expected + + +def test_named_query(client): + query = gql( + """ + query Planet1 { + planet(id: "UGxhbmV0OjEw") { + id + name + } + } + query Planet2 { + planet(id: "UGxhbmV0OjEx") { + id + name + } + } + """ + ) + expected = [{"planet": {"id": "UGxhbmV0OjEx", "name": "Geonosis"}}] + with use_cassette("queries_batch"): + results = client.execute_batch( + [GraphQLRequest(document=query, operation_name="Planet2")] + ) + assert results == expected + + +def test_header_query(client): + query = gql( + """ + query Planet($id: ID!) { + planet(id: $id) { + id + name + } + } + """ + ) + expected = [{"planet": {"id": "UGxhbmV0OjEx", "name": "Geonosis"}}] + with use_cassette("queries_batch"): + results = client.execute_batch( + [GraphQLRequest(document=query)], + extra_args={"headers": {"authorization": "xxx-123"}}, + ) + assert results == expected From 10b8cdb45e680b9f47cefa3cf7b639f786830cb8 Mon Sep 17 00:00:00 2001 From: Ignacio Guerrero Date: Mon, 4 Sep 2023 17:24:05 -0300 Subject: [PATCH 03/15] Reformat code examples files --- docs/code_examples/console_async.py | 1 - docs/code_examples/fastapi_async.py | 1 - 2 files changed, 2 deletions(-) diff --git a/docs/code_examples/console_async.py b/docs/code_examples/console_async.py index 5391f7bf..9a5e94e5 100644 --- a/docs/code_examples/console_async.py +++ b/docs/code_examples/console_async.py @@ -2,7 +2,6 @@ import logging from aioconsole import ainput - from gql import Client, gql from gql.transport.aiohttp import AIOHTTPTransport diff --git a/docs/code_examples/fastapi_async.py b/docs/code_examples/fastapi_async.py index 3bedd187..80920252 100644 --- a/docs/code_examples/fastapi_async.py +++ b/docs/code_examples/fastapi_async.py @@ -10,7 +10,6 @@ from fastapi import FastAPI, HTTPException from fastapi.responses import HTMLResponse - from gql import Client, gql from gql.transport.aiohttp import AIOHTTPTransport From 7e134741a4fc83579dbf25b9182e285a3d08ba92 Mon Sep 17 00:00:00 2001 From: Ignacio Guerrero Date: Mon, 4 Sep 2023 17:55:20 -0300 Subject: [PATCH 04/15] Mark a couple of tests using aiohttp --- tests/test_client.py | 4 +++- tests/test_httpx.py | 2 ++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/tests/test_client.py b/tests/test_client.py index e4c283ae..c007f7f7 100644 --- a/tests/test_client.py +++ b/tests/test_client.py @@ -49,7 +49,9 @@ def execute_batch(self): ) -def test_request_async_execute_batch_not_implemented_yet(http_transport_query): +@pytest.mark.aiohttp +@pytest.mark.asyncio +def test_request_async_execute_batch_not_implemented_yet(): from gql.transport.aiohttp import AIOHTTPTransport transport = AIOHTTPTransport(url="http://localhost/") diff --git a/tests/test_httpx.py b/tests/test_httpx.py index 255a1f5e..0e0020fd 100644 --- a/tests/test_httpx.py +++ b/tests/test_httpx.py @@ -34,6 +34,8 @@ ) +@pytest.mark.aiohttp +@pytest.mark.asyncio def test_httpx_execute_batch_is_not_implemented(): from gql.transport.httpx import HTTPXTransport From c01749fdfc03e7aea134b32c541dc9c7406f84ec Mon Sep 17 00:00:00 2001 From: Ignacio Guerrero Date: Mon, 4 Sep 2023 22:01:15 -0300 Subject: [PATCH 05/15] Revert httpx modifications --- gql/transport/httpx.py | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/gql/transport/httpx.py b/gql/transport/httpx.py index a8ff2014..cfc25dc9 100644 --- a/gql/transport/httpx.py +++ b/gql/transport/httpx.py @@ -17,8 +17,6 @@ import httpx from graphql import DocumentNode, ExecutionResult, print_ast -from gql.transport.data_structures import GraphQLRequest - from ..utils import extract_files from . import AsyncTransport, Transport from .exceptions import ( @@ -231,14 +229,6 @@ def execute( # type: ignore return self._prepare_result(response) - def execute_batch( - self, - reqs: List[GraphQLRequest], - *args, - **kwargs, - ) -> List[ExecutionResult]: - return super().execute_batch(reqs, *args, **kwargs) - def close(self): """Closing the transport by closing the inner session""" if self.client: From c48b35e173375b03c371dbb025bd2bf5311de38e Mon Sep 17 00:00:00 2001 From: Ignacio Guerrero Date: Mon, 4 Sep 2023 22:08:03 -0300 Subject: [PATCH 06/15] Revert docs files changes --- docs/code_examples/console_async.py | 1 + docs/code_examples/fastapi_async.py | 1 + 2 files changed, 2 insertions(+) diff --git a/docs/code_examples/console_async.py b/docs/code_examples/console_async.py index 9a5e94e5..5391f7bf 100644 --- a/docs/code_examples/console_async.py +++ b/docs/code_examples/console_async.py @@ -2,6 +2,7 @@ import logging from aioconsole import ainput + from gql import Client, gql from gql.transport.aiohttp import AIOHTTPTransport diff --git a/docs/code_examples/fastapi_async.py b/docs/code_examples/fastapi_async.py index 80920252..3bedd187 100644 --- a/docs/code_examples/fastapi_async.py +++ b/docs/code_examples/fastapi_async.py @@ -10,6 +10,7 @@ from fastapi import FastAPI, HTTPException from fastapi.responses import HTMLResponse + from gql import Client, gql from gql.transport.aiohttp import AIOHTTPTransport From 79fae3f1ae86658b9d4ca7e5eabbbd663abb0843 Mon Sep 17 00:00:00 2001 From: Ignacio Guerrero Date: Mon, 4 Sep 2023 22:33:39 -0300 Subject: [PATCH 07/15] Fix docstrings and typing issues --- gql/__init__.py | 2 ++ gql/client.py | 32 ++++++++++++++------------------ gql/graphql_request.py | 37 +++++++++++++++++++++++++++++++++++++ gql/transport/aiohttp.py | 2 +- gql/transport/requests.py | 14 +++++++------- gql/transport/transport.py | 9 ++++----- 6 files changed, 65 insertions(+), 31 deletions(-) create mode 100644 gql/graphql_request.py diff --git a/gql/__init__.py b/gql/__init__.py index a2449700..8eaa0b7c 100644 --- a/gql/__init__.py +++ b/gql/__init__.py @@ -10,9 +10,11 @@ from .__version__ import __version__ from .client import Client from .gql import gql +from .graphql_request import GraphQLRequest __all__ = [ "__version__", "gql", "Client", + "GraphQLRequest", ] diff --git a/gql/client.py b/gql/client.py index 459c335f..da54fbfe 100644 --- a/gql/client.py +++ b/gql/client.py @@ -28,7 +28,7 @@ validate, ) -from gql.transport.data_structures.graphql_request import GraphQLRequest +from gql.graphql_request import GraphQLRequest from .transport.async_transport import AsyncTransport from .transport.exceptions import TransportClosed, TransportQueryError @@ -446,7 +446,7 @@ def execute_batch( get_execution_result: bool = False, **kwargs, ) -> Union[List[Dict[str, Any]], List[ExecutionResult]]: - """Execute the provided document AST against the remote server using + """Execute the provided requests against the remote server using the transport provided during init. This function **WILL BLOCK** until the result is received from the server. @@ -458,11 +458,11 @@ def execute_batch( This method will: - connect using the transport to get a session - - execute the GraphQL request on the transport session + - execute the GraphQL requests on the transport session - close the session and close the connection to the server - If you have multiple requests to send, it is better to get your own session - and execute the requests in your session. + If you want to perform multiple executions, it is better to use + the context manager to keep a session active. The extra arguments passed in the method will be passed to the transport execute method. @@ -944,12 +944,10 @@ def _execute_batch( parse_result: Optional[bool] = None, **kwargs, ) -> List[ExecutionResult]: - """Execute the provided document AST synchronously using - the sync transport, returning an ExecutionResult object. + """Execute the provided requests synchronously using + the sync transport, returning a list of ExecutionResult objects. - :param document: GraphQL query as AST Node object. - :param variable_values: Dictionary of input parameters. - :param operation_name: Name of the operation that shall be executed. + :param reqs: List of requests that will be executed. :param serialize_variables: whether the variable values should be serialized. Used for custom scalars and/or enums. By default use the serialize_variables argument of the client. @@ -997,15 +995,13 @@ def execute_batch( get_execution_result: bool = False, **kwargs, ) -> Union[List[Dict[str, Any]], List[ExecutionResult]]: - """Execute the provided document AST synchronously using - the sync transport. + """Execute the provided requests synchronously using + the sync transport. This method sends the requests to the server all at once. - Raises a TransportQueryError if an error has been returned in - the ExecutionResult. + Raises a TransportQueryError if an error has been returned in any + ExecutionResult. - :param document: GraphQL query as AST Node object. - :param variable_values: Dictionary of input parameters. - :param operation_name: Name of the operation that shall be executed. + :param reqs: List of requests that will be executed. :param serialize_variables: whether the variable values should be serialized. Used for custom scalars and/or enums. By default use the serialize_variables argument of the client. @@ -1041,7 +1037,7 @@ def execute_batch( if get_execution_result: return results - return [result.data for result in results] # type: ignore + return cast(List[Dict[str, Any]], [result.data for result in results]) def fetch_schema(self) -> None: """Fetch the GraphQL schema explicitly using introspection. diff --git a/gql/graphql_request.py b/gql/graphql_request.py new file mode 100644 index 00000000..b0c68f5c --- /dev/null +++ b/gql/graphql_request.py @@ -0,0 +1,37 @@ +from dataclasses import dataclass +from typing import Any, Dict, Optional + +from graphql import DocumentNode, GraphQLSchema + +from .utilities import serialize_variable_values + + +@dataclass(frozen=True) +class GraphQLRequest: + """GraphQL Request to be executed.""" + + document: DocumentNode + """GraphQL query as AST Node object.""" + + variable_values: Optional[Dict[str, Any]] = None + """Dictionary of input parameters (Default: None).""" + + operation_name: Optional[str] = None + """ + Name of the operation that shall be executed. + Only required in multi-operation documents (Default: None). + """ + + def serialize_variable_values(self, schema: GraphQLSchema) -> "GraphQLRequest": + assert self.variable_values + + return GraphQLRequest( + document=self.document, + variable_values=serialize_variable_values( + schema=schema, + document=self.document, + variable_values=self.variable_values, + operation_name=self.operation_name, + ), + operation_name=self.operation_name, + ) diff --git a/gql/transport/aiohttp.py b/gql/transport/aiohttp.py index 2fd92a72..60f42c94 100644 --- a/gql/transport/aiohttp.py +++ b/gql/transport/aiohttp.py @@ -205,7 +205,7 @@ async def execute( document: DocumentNode, variable_values: Optional[Dict[str, Any]] = None, operation_name: Optional[str] = None, - extra_args: Dict[str, Any] = None, + extra_args: Optional[Dict[str, Any]] = None, upload_files: bool = False, ) -> ExecutionResult: """Execute the provided document AST against the configured remote server diff --git a/gql/transport/requests.py b/gql/transport/requests.py index 7faa645d..4eb6d0ab 100644 --- a/gql/transport/requests.py +++ b/gql/transport/requests.py @@ -10,10 +10,10 @@ from requests.cookies import RequestsCookieJar from requests_toolbelt.multipart.encoder import MultipartEncoder +from gql.graphql_request import GraphQLRequest from gql.transport import Transport from ..utils import extract_files -from .data_structures import GraphQLRequest from .exceptions import ( TransportAlreadyConnected, TransportClosed, @@ -122,7 +122,7 @@ def execute( # type: ignore variable_values: Optional[Dict[str, Any]] = None, operation_name: Optional[str] = None, timeout: Optional[int] = None, - extra_args: Dict[str, Any] = None, + extra_args: Optional[Dict[str, Any]] = None, upload_files: bool = False, ) -> ExecutionResult: """Execute GraphQL query. @@ -278,14 +278,14 @@ def execute_batch( # type: ignore self, reqs: List[GraphQLRequest], timeout: Optional[int] = None, - extra_args: Dict[str, Any] = None, + extra_args: Optional[Dict[str, Any]] = None, ) -> List[ExecutionResult]: """Execute GraphQL query. - Execute the provided document ASTs against the configured remote server. This + Execute the provided requests against the configured remote server. This uses the requests library to perform a HTTP POST request to the remote server. - :param reqs: GraphQL requests as an iterable of GraphQLRequest objects. + :param reqs: GraphQL requests as a list of GraphQLRequest objects. :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 @@ -302,7 +302,7 @@ def execute_batch( # type: ignore response = self.session.request( self.method, self.url, - **self._build_batch_post_args(reqs, timeout, extra_args), # type: ignore + **self._build_batch_post_args(reqs, timeout, extra_args), ) self.response_headers = response.headers @@ -382,7 +382,7 @@ def _build_batch_post_args( self, reqs: List[GraphQLRequest], timeout: Optional[int] = None, - extra_args: Dict[str, Any] = None, + extra_args: Optional[Dict[str, Any]] = None, ) -> Dict[str, Any]: post_args: Dict[str, Any] = { "headers": self.headers, diff --git a/gql/transport/transport.py b/gql/transport/transport.py index 173147e8..4fa4af2b 100644 --- a/gql/transport/transport.py +++ b/gql/transport/transport.py @@ -3,7 +3,7 @@ from graphql import DocumentNode, ExecutionResult -from .data_structures import GraphQLRequest +from gql.graphql_request import GraphQLRequest class Transport(abc.ABC): @@ -20,7 +20,6 @@ def execute(self, document: DocumentNode, *args, **kwargs) -> ExecutionResult: "Any Transport subclass must implement execute method" ) # pragma: no cover - @abc.abstractmethod def execute_batch( self, reqs: List[GraphQLRequest], @@ -31,11 +30,11 @@ def execute_batch( Execute the provided requests for either a remote or local GraphQL Schema. - :param reqs: GraphQL requests as an iterable of GraphQLRequest objects. - :return: iterable of ExecutionResult + :param reqs: GraphQL requests as a list of GraphQLRequest objects. + :return: a list of ExecutionResult objects """ raise NotImplementedError( - "Any Transport subclass must implement execute_batch method" + "This Transport has not implemented the execute_batch method" ) # pragma: no cover def connect(self): From 9f2e0af3db1d877d2d90e65a081f813eb2b7896d Mon Sep 17 00:00:00 2001 From: Ignacio Guerrero Date: Mon, 4 Sep 2023 22:35:38 -0300 Subject: [PATCH 08/15] Test batch with two queries instead of one --- tests/custom_scalars/test_money.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/tests/custom_scalars/test_money.py b/tests/custom_scalars/test_money.py index e099c371..582704b6 100644 --- a/tests/custom_scalars/test_money.py +++ b/tests/custom_scalars/test_money.py @@ -21,7 +21,7 @@ from graphql.utilities import value_from_ast_untyped from gql import Client, gql -from gql.transport.data_structures.graphql_request import GraphQLRequest +from gql.graphql_request import GraphQLRequest from gql.transport.exceptions import TransportQueryError from gql.utilities import serialize_value, update_schema_scalar, update_schema_scalars @@ -763,7 +763,6 @@ def test_code(): async def test_custom_scalar_serialize_variables_sync_transport_2( event_loop, aiohttp_server, run_sync_test ): - server, transport = await make_sync_money_transport(aiohttp_server) def test_code(): @@ -774,12 +773,16 @@ def test_code(): variable_values = {"money": Money(10, "DM")} results = session.execute_batch( - [GraphQLRequest(document=query, variable_values=variable_values)], + [ + GraphQLRequest(document=query, variable_values=variable_values), + GraphQLRequest(document=query, variable_values=variable_values), + ], serialize_variables=True, ) print(f"result = {results!r}") assert results[0]["toEuros"] == 5 + assert results[1]["toEuros"] == 5 await run_sync_test(event_loop, server, test_code) From 786a56c7b7637a7fd39f6deb0bef279a0f2c8f4f Mon Sep 17 00:00:00 2001 From: Ignacio Guerrero Date: Mon, 4 Sep 2023 22:40:13 -0300 Subject: [PATCH 09/15] Update GraphQLRequest project location --- gql/transport/data_structures/__init__.py | 3 -- .../data_structures/graphql_request.py | 37 ------------------- tests/datastructures/__init__.py | 0 .../test_graphql_request.py | 4 +- 4 files changed, 2 insertions(+), 42 deletions(-) delete mode 100644 gql/transport/data_structures/__init__.py delete mode 100644 gql/transport/data_structures/graphql_request.py delete mode 100644 tests/datastructures/__init__.py rename tests/{datastructures => }/test_graphql_request.py (98%) diff --git a/gql/transport/data_structures/__init__.py b/gql/transport/data_structures/__init__.py deleted file mode 100644 index 2fd89849..00000000 --- a/gql/transport/data_structures/__init__.py +++ /dev/null @@ -1,3 +0,0 @@ -from .graphql_request import GraphQLRequest - -__all__ = ["GraphQLRequest"] diff --git a/gql/transport/data_structures/graphql_request.py b/gql/transport/data_structures/graphql_request.py deleted file mode 100644 index 0d44aeba..00000000 --- a/gql/transport/data_structures/graphql_request.py +++ /dev/null @@ -1,37 +0,0 @@ -from typing import Any, Dict, Optional - -from attr import dataclass -from graphql import DocumentNode, GraphQLSchema - -from gql.utilities import serialize_variable_values - - -@dataclass(frozen=True) -class GraphQLRequest: - """GraphQL Request to be executed.""" - - document: DocumentNode - """GraphQL query as AST Node object.""" - - variable_values: Optional[Dict[str, Any]] = None - """Dictionary of input parameters (Default: None).""" - - operation_name: Optional[str] = None - """ - Name of the operation that shall be executed. - Only required in multi-operation documents (Default: None). - """ - - def serialize_variable_values(self, schema: GraphQLSchema) -> "GraphQLRequest": - assert self.variable_values - - return GraphQLRequest( - document=self.document, - variable_values=serialize_variable_values( - schema=schema, - document=self.document, - variable_values=self.variable_values, - operation_name=self.operation_name, - ), - operation_name=self.operation_name, - ) diff --git a/tests/datastructures/__init__.py b/tests/datastructures/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/tests/datastructures/test_graphql_request.py b/tests/test_graphql_request.py similarity index 98% rename from tests/datastructures/test_graphql_request.py rename to tests/test_graphql_request.py index f3b940cf..049d942b 100644 --- a/tests/datastructures/test_graphql_request.py +++ b/tests/test_graphql_request.py @@ -19,9 +19,9 @@ from graphql.utilities import value_from_ast_untyped from gql import gql -from gql.transport.data_structures.graphql_request import GraphQLRequest +from gql.graphql_request import GraphQLRequest -from ..conftest import MS +from .conftest import MS # Marking all tests in this file with the aiohttp marker pytestmark = pytest.mark.aiohttp From afe90ef7bdf6b2604ed1ebbbb57d8cbf044a797a Mon Sep 17 00:00:00 2001 From: Ignacio Guerrero Date: Mon, 4 Sep 2023 22:43:27 -0300 Subject: [PATCH 10/15] Update library locations and reformat --- gql/client.py | 2 +- tests/test_client.py | 5 ++--- tests/test_httpx.py | 16 ---------------- tests/test_requests_batch.py | 3 +-- tests/test_transport_batch.py | 2 +- 5 files changed, 5 insertions(+), 23 deletions(-) diff --git a/gql/client.py b/gql/client.py index da54fbfe..844fd533 100644 --- a/gql/client.py +++ b/gql/client.py @@ -461,7 +461,7 @@ def execute_batch( - execute the GraphQL requests on the transport session - close the session and close the connection to the server - If you want to perform multiple executions, it is better to use + If you want to perform multiple executions, it is better to use the context manager to keep a session active. The extra arguments passed in the method will be passed to the transport diff --git a/tests/test_client.py b/tests/test_client.py index c007f7f7..656edac7 100644 --- a/tests/test_client.py +++ b/tests/test_client.py @@ -5,9 +5,8 @@ import pytest from graphql import build_ast_schema, parse -from gql import Client, gql +from gql import Client, GraphQLRequest, gql from gql.transport import Transport -from gql.transport.data_structures.graphql_request import GraphQLRequest from gql.transport.exceptions import TransportQueryError with suppress(ModuleNotFoundError): @@ -44,7 +43,7 @@ def execute_batch(self): with pytest.raises(NotImplementedError) as exc_info: RandomTransport().execute_batch() - assert "Any Transport subclass must implement execute_batch method" == str( + assert "This Transport has not implemented the execute_batch method" == str( exc_info.value ) diff --git a/tests/test_httpx.py b/tests/test_httpx.py index 0e0020fd..56a984a4 100644 --- a/tests/test_httpx.py +++ b/tests/test_httpx.py @@ -3,7 +3,6 @@ import pytest from gql import Client, gql -from gql.transport.data_structures import GraphQLRequest from gql.transport.exceptions import ( TransportAlreadyConnected, TransportClosed, @@ -34,21 +33,6 @@ ) -@pytest.mark.aiohttp -@pytest.mark.asyncio -def test_httpx_execute_batch_is_not_implemented(): - from gql.transport.httpx import HTTPXTransport - - with Client(transport=HTTPXTransport(url="/")) as session: - reqs = [GraphQLRequest(document=gql(query1_str))] - - with pytest.raises(NotImplementedError) as exc_info: - session.execute_batch(reqs) - assert "Any Transport subclass must implement execute_batch method" == str( - exc_info.value - ) - - @pytest.mark.aiohttp @pytest.mark.asyncio async def test_httpx_query(event_loop, aiohttp_server, run_sync_test): diff --git a/tests/test_requests_batch.py b/tests/test_requests_batch.py index c97f97e0..23ab1254 100644 --- a/tests/test_requests_batch.py +++ b/tests/test_requests_batch.py @@ -2,8 +2,7 @@ import pytest -from gql import Client, gql -from gql.transport.data_structures.graphql_request import GraphQLRequest +from gql import Client, GraphQLRequest, gql from gql.transport.exceptions import ( TransportClosed, TransportProtocolError, diff --git a/tests/test_transport_batch.py b/tests/test_transport_batch.py index 88a67ee8..7ded6702 100644 --- a/tests/test_transport_batch.py +++ b/tests/test_transport_batch.py @@ -3,7 +3,7 @@ import pytest from gql import Client, gql -from gql.transport.data_structures.graphql_request import GraphQLRequest +from gql.graphql_request import GraphQLRequest # We serve https://github.com/graphql-python/swapi-graphene locally: URL = "http://127.0.0.1:8000/graphql" From 8424611151fced9f78b45f9f4bebae83565a3e64 Mon Sep 17 00:00:00 2001 From: Ignacio Guerrero Date: Tue, 5 Sep 2023 14:01:05 -0300 Subject: [PATCH 11/15] Update docstrings --- gql/client.py | 4 ++-- gql/transport/requests.py | 3 +-- gql/transport/transport.py | 2 +- tests/test_client.py | 5 ----- 4 files changed, 4 insertions(+), 10 deletions(-) diff --git a/gql/client.py b/gql/client.py index 844fd533..42ea2742 100644 --- a/gql/client.py +++ b/gql/client.py @@ -446,7 +446,7 @@ def execute_batch( get_execution_result: bool = False, **kwargs, ) -> Union[List[Dict[str, Any]], List[ExecutionResult]]: - """Execute the provided requests against the remote server using + """Execute multiple GraphQL requests in a batch against the remote server using the transport provided during init. This function **WILL BLOCK** until the result is received from the server. @@ -995,7 +995,7 @@ def execute_batch( get_execution_result: bool = False, **kwargs, ) -> Union[List[Dict[str, Any]], List[ExecutionResult]]: - """Execute the provided requests synchronously using + """Execute multiple GraphQL requests in a batch, using the sync transport. This method sends the requests to the server all at once. Raises a TransportQueryError if an error has been returned in any diff --git a/gql/transport/requests.py b/gql/transport/requests.py index 4eb6d0ab..a46bf2e4 100644 --- a/gql/transport/requests.py +++ b/gql/transport/requests.py @@ -280,7 +280,7 @@ def execute_batch( # type: ignore timeout: Optional[int] = None, extra_args: Optional[Dict[str, Any]] = None, ) -> List[ExecutionResult]: - """Execute GraphQL query. + """Execute multiple GraphQL requests in a batch. Execute the provided requests against the configured remote server. This uses the requests library to perform a HTTP POST request to the remote server. @@ -288,7 +288,6 @@ def execute_batch( # type: ignore :param reqs: GraphQL requests as a list of GraphQLRequest objects. :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: A list of results of execution. For every result `data` is the result of executing the query, `errors` is null if no errors occurred, and is a non-empty array diff --git a/gql/transport/transport.py b/gql/transport/transport.py index 4fa4af2b..aaaaf453 100644 --- a/gql/transport/transport.py +++ b/gql/transport/transport.py @@ -26,7 +26,7 @@ def execute_batch( *args, **kwargs, ) -> List[ExecutionResult]: - """Execute multiple GraphQL requests in batch. + """Execute multiple GraphQL requests in a batch. Execute the provided requests for either a remote or local GraphQL Schema. diff --git a/tests/test_client.py b/tests/test_client.py index 656edac7..922359c8 100644 --- a/tests/test_client.py +++ b/tests/test_client.py @@ -32,11 +32,6 @@ class RandomTransport(Transport): def execute(self): super(RandomTransport, self).execute(http_transport_query) - def execute_batch(self): - super(RandomTransport, self).execute_batch( - [GraphQLRequest(document=http_transport_query)] - ) - with pytest.raises(NotImplementedError) as exc_info: RandomTransport().execute() assert "Any Transport subclass must implement execute method" == str(exc_info.value) From a2a711fd37dba571acc511fd9da3de8289761b4f Mon Sep 17 00:00:00 2001 From: Ignacio Guerrero Date: Tue, 5 Sep 2023 14:09:42 -0300 Subject: [PATCH 12/15] Use relative imports when possible --- gql/client.py | 2 +- gql/transport/requests.py | 2 +- gql/transport/transport.py | 2 +- tests/custom_scalars/test_money.py | 3 +-- tests/test_client.py | 5 +++++ tests/test_graphql_request.py | 3 +-- tests/test_transport_batch.py | 3 +-- 7 files changed, 11 insertions(+), 9 deletions(-) diff --git a/gql/client.py b/gql/client.py index 42ea2742..8e5d3c21 100644 --- a/gql/client.py +++ b/gql/client.py @@ -28,7 +28,7 @@ validate, ) -from gql.graphql_request import GraphQLRequest +from .graphql_request import GraphQLRequest from .transport.async_transport import AsyncTransport from .transport.exceptions import TransportClosed, TransportQueryError diff --git a/gql/transport/requests.py b/gql/transport/requests.py index a46bf2e4..aeecc7f9 100644 --- a/gql/transport/requests.py +++ b/gql/transport/requests.py @@ -10,7 +10,7 @@ from requests.cookies import RequestsCookieJar from requests_toolbelt.multipart.encoder import MultipartEncoder -from gql.graphql_request import GraphQLRequest +from ..graphql_request import GraphQLRequest from gql.transport import Transport from ..utils import extract_files diff --git a/gql/transport/transport.py b/gql/transport/transport.py index aaaaf453..a5bd7100 100644 --- a/gql/transport/transport.py +++ b/gql/transport/transport.py @@ -3,7 +3,7 @@ from graphql import DocumentNode, ExecutionResult -from gql.graphql_request import GraphQLRequest +from ..graphql_request import GraphQLRequest class Transport(abc.ABC): diff --git a/tests/custom_scalars/test_money.py b/tests/custom_scalars/test_money.py index 582704b6..374c70e6 100644 --- a/tests/custom_scalars/test_money.py +++ b/tests/custom_scalars/test_money.py @@ -20,8 +20,7 @@ ) from graphql.utilities import value_from_ast_untyped -from gql import Client, gql -from gql.graphql_request import GraphQLRequest +from gql import Client, GraphQLRequest, gql from gql.transport.exceptions import TransportQueryError from gql.utilities import serialize_value, update_schema_scalar, update_schema_scalars diff --git a/tests/test_client.py b/tests/test_client.py index 922359c8..656edac7 100644 --- a/tests/test_client.py +++ b/tests/test_client.py @@ -32,6 +32,11 @@ class RandomTransport(Transport): def execute(self): super(RandomTransport, self).execute(http_transport_query) + def execute_batch(self): + super(RandomTransport, self).execute_batch( + [GraphQLRequest(document=http_transport_query)] + ) + with pytest.raises(NotImplementedError) as exc_info: RandomTransport().execute() assert "Any Transport subclass must implement execute method" == str(exc_info.value) diff --git a/tests/test_graphql_request.py b/tests/test_graphql_request.py index 049d942b..13918adc 100644 --- a/tests/test_graphql_request.py +++ b/tests/test_graphql_request.py @@ -18,8 +18,7 @@ ) from graphql.utilities import value_from_ast_untyped -from gql import gql -from gql.graphql_request import GraphQLRequest +from gql import gql, GraphQLRequest from .conftest import MS diff --git a/tests/test_transport_batch.py b/tests/test_transport_batch.py index 7ded6702..a9b21e6a 100644 --- a/tests/test_transport_batch.py +++ b/tests/test_transport_batch.py @@ -2,8 +2,7 @@ import pytest -from gql import Client, gql -from gql.graphql_request import GraphQLRequest +from gql import Client, GraphQLRequest, gql # We serve https://github.com/graphql-python/swapi-graphene locally: URL = "http://127.0.0.1:8000/graphql" From 5808df8f5659b8a2a05c629640b40b4aa1992364 Mon Sep 17 00:00:00 2001 From: Ignacio Guerrero Date: Tue, 5 Sep 2023 14:10:40 -0300 Subject: [PATCH 13/15] Remove unnecessary method definition from test case --- tests/test_client.py | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/tests/test_client.py b/tests/test_client.py index 656edac7..2fb333a9 100644 --- a/tests/test_client.py +++ b/tests/test_client.py @@ -32,17 +32,14 @@ class RandomTransport(Transport): def execute(self): super(RandomTransport, self).execute(http_transport_query) - def execute_batch(self): - super(RandomTransport, self).execute_batch( - [GraphQLRequest(document=http_transport_query)] - ) - with pytest.raises(NotImplementedError) as exc_info: RandomTransport().execute() + assert "Any Transport subclass must implement execute method" == str(exc_info.value) with pytest.raises(NotImplementedError) as exc_info: - RandomTransport().execute_batch() + RandomTransport().execute_batch([]) + assert "This Transport has not implemented the execute_batch method" == str( exc_info.value ) From 9261194164e5763b7fc15f9447f873028482deb0 Mon Sep 17 00:00:00 2001 From: Ignacio Guerrero Date: Tue, 5 Sep 2023 14:12:24 -0300 Subject: [PATCH 14/15] Reformat code --- gql/client.py | 1 - gql/transport/requests.py | 2 +- tests/test_graphql_request.py | 2 +- 3 files changed, 2 insertions(+), 3 deletions(-) diff --git a/gql/client.py b/gql/client.py index 8e5d3c21..b584add3 100644 --- a/gql/client.py +++ b/gql/client.py @@ -29,7 +29,6 @@ ) from .graphql_request import GraphQLRequest - from .transport.async_transport import AsyncTransport from .transport.exceptions import TransportClosed, TransportQueryError from .transport.local_schema import LocalSchemaTransport diff --git a/gql/transport/requests.py b/gql/transport/requests.py index aeecc7f9..1e464104 100644 --- a/gql/transport/requests.py +++ b/gql/transport/requests.py @@ -10,9 +10,9 @@ from requests.cookies import RequestsCookieJar from requests_toolbelt.multipart.encoder import MultipartEncoder -from ..graphql_request import GraphQLRequest from gql.transport import Transport +from ..graphql_request import GraphQLRequest from ..utils import extract_files from .exceptions import ( TransportAlreadyConnected, diff --git a/tests/test_graphql_request.py b/tests/test_graphql_request.py index 13918adc..4c9e7d76 100644 --- a/tests/test_graphql_request.py +++ b/tests/test_graphql_request.py @@ -18,7 +18,7 @@ ) from graphql.utilities import value_from_ast_untyped -from gql import gql, GraphQLRequest +from gql import GraphQLRequest, gql from .conftest import MS From 1e4c981e54a5e515b0504ef38a2f9c936da4ceb3 Mon Sep 17 00:00:00 2001 From: Ignacio Guerrero Date: Tue, 5 Sep 2023 14:17:16 -0300 Subject: [PATCH 15/15] Update docstrings for private method --- gql/client.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gql/client.py b/gql/client.py index b584add3..326442e0 100644 --- a/gql/client.py +++ b/gql/client.py @@ -943,7 +943,7 @@ def _execute_batch( parse_result: Optional[bool] = None, **kwargs, ) -> List[ExecutionResult]: - """Execute the provided requests synchronously using + """Execute multiple GraphQL requests in a batch, using the sync transport, returning a list of ExecutionResult objects. :param reqs: List of requests that will be executed.