Files
blender-portable-repo/scripts/addons/Rokoko Libraries/python311/gql/client.py
T
2026-03-17 14:58:51 -06:00

1860 lines
65 KiB
Python

import asyncio
import logging
import sys
import time
import warnings
from concurrent.futures import Future
from queue import Queue
from threading import Event, Thread
from typing import (
Any,
AsyncGenerator,
Callable,
Dict,
Generator,
List,
Optional,
Tuple,
TypeVar,
Union,
cast,
overload,
)
import backoff
from anyio import fail_after
from graphql import (
DocumentNode,
ExecutionResult,
GraphQLSchema,
IntrospectionQuery,
build_ast_schema,
parse,
validate,
)
from .graphql_request import GraphQLRequest
from .transport.async_transport import AsyncTransport
from .transport.exceptions import TransportClosed, TransportQueryError
from .transport.local_schema import LocalSchemaTransport
from .transport.transport import Transport
from .utilities import build_client_schema, get_introspection_query_ast
from .utilities import parse_result as parse_result_fn
from .utilities import serialize_variable_values
from .utils import str_first_element
"""
Load the appropriate instance of the Literal type
Note: we cannot use try: except ImportError because of the following mypy issue:
https://github.com/python/mypy/issues/8520
"""
if sys.version_info[:2] >= (3, 8):
from typing import Literal
else:
from typing_extensions import Literal # pragma: no cover
log = logging.getLogger(__name__)
class Client:
"""The Client class is the main entrypoint to execute GraphQL requests
on a GQL transport.
It can take sync or async transports as argument and can either execute
and subscribe to requests itself with the
:func:`execute <gql.client.Client.execute>` and
:func:`subscribe <gql.client.Client.subscribe>` methods
OR can be used to get a sync or async session depending on the
transport type.
To connect to an :ref:`async transport <async_transports>` and get an
:class:`async session <gql.client.AsyncClientSession>`,
use :code:`async with client as session:`
To connect to a :ref:`sync transport <sync_transports>` and get a
:class:`sync session <gql.client.SyncClientSession>`,
use :code:`with client as session:`
"""
def __init__(
self,
schema: Optional[Union[str, GraphQLSchema]] = None,
introspection: Optional[IntrospectionQuery] = None,
transport: Optional[Union[Transport, AsyncTransport]] = None,
fetch_schema_from_transport: bool = False,
introspection_args: Optional[Dict] = None,
execute_timeout: Optional[Union[int, float]] = 10,
serialize_variables: bool = False,
parse_results: bool = False,
batch_interval: float = 0,
batch_max: int = 10,
):
"""Initialize the client with the given parameters.
:param schema: an optional GraphQL Schema for local validation
See :ref:`schema_validation`
:param transport: The provided :ref:`transport <Transports>`.
:param fetch_schema_from_transport: Boolean to indicate that if we want to fetch
the schema from the transport using an introspection query.
:param introspection_args: arguments passed to the
:meth:`gql.utilities.get_introspection_query_ast` method.
:param execute_timeout: The maximum time in seconds for the execution of a
request before a TimeoutError is raised. Only used for async transports.
Passing None results in waiting forever for a response.
:param serialize_variables: whether the variable values should be
serialized. Used for custom scalars and/or enums. Default: False.
:param parse_results: Whether gql will try to parse the serialized output
sent by the backend. Can be used to unserialize custom scalars or enums.
:param batch_interval: Time to wait in seconds for batching requests together.
Batching is disabled (by default) if 0.
:param batch_max: Maximum number of requests in a single batch.
"""
if introspection:
assert (
not schema
), "Cannot provide introspection and schema at the same time."
schema = build_client_schema(introspection)
if isinstance(schema, str):
type_def_ast = parse(schema)
schema = build_ast_schema(type_def_ast)
if transport and fetch_schema_from_transport:
assert (
not schema
), "Cannot fetch the schema from transport if is already provided."
assert not type(transport).__name__ == "AppSyncWebsocketsTransport", (
"fetch_schema_from_transport=True is not allowed "
"for AppSyncWebsocketsTransport "
"because only subscriptions are allowed on the realtime endpoint."
)
if schema and not transport:
transport = LocalSchemaTransport(schema)
# GraphQL schema
self.schema: Optional[GraphQLSchema] = schema
# Answer of the introspection query
self.introspection: Optional[IntrospectionQuery] = introspection
# GraphQL transport chosen
self.transport: Optional[Union[Transport, AsyncTransport]] = transport
# Flag to indicate that we need to fetch the schema from the transport
# On async transports, we fetch the schema before executing the first query
self.fetch_schema_from_transport: bool = fetch_schema_from_transport
self.introspection_args = (
{} if introspection_args is None else introspection_args
)
# Enforced timeout of the execute function (only for async transports)
self.execute_timeout = execute_timeout
self.serialize_variables = serialize_variables
self.parse_results = parse_results
self.batch_interval = batch_interval
self.batch_max = batch_max
@property
def batching_enabled(self):
return self.batch_interval != 0
def validate(self, document: DocumentNode):
""":meta private:"""
assert (
self.schema
), "Cannot validate the document locally, you need to pass a schema."
validation_errors = validate(self.schema, document)
if validation_errors:
raise validation_errors[0]
def _build_schema_from_introspection(self, execution_result: ExecutionResult):
if execution_result.errors:
raise TransportQueryError(
(
"Error while fetching schema: "
f"{str_first_element(execution_result.errors)}\n"
"If you don't need the schema, you can try with: "
'"fetch_schema_from_transport=False"'
),
errors=execution_result.errors,
data=execution_result.data,
extensions=execution_result.extensions,
)
self.introspection = cast(IntrospectionQuery, execution_result.data)
self.schema = build_client_schema(self.introspection)
@overload
def execute_sync(
self,
document: DocumentNode,
variable_values: Optional[Dict[str, Any]] = ...,
operation_name: Optional[str] = ...,
serialize_variables: Optional[bool] = ...,
parse_result: Optional[bool] = ...,
*, # https://github.com/python/mypy/issues/7333#issuecomment-788255229
get_execution_result: Literal[False] = ...,
**kwargs,
) -> Dict[str, Any]:
... # pragma: no cover
@overload
def execute_sync(
self,
document: DocumentNode,
variable_values: Optional[Dict[str, Any]] = ...,
operation_name: Optional[str] = ...,
serialize_variables: Optional[bool] = ...,
parse_result: Optional[bool] = ...,
*,
get_execution_result: Literal[True],
**kwargs,
) -> ExecutionResult:
... # pragma: no cover
@overload
def execute_sync(
self,
document: DocumentNode,
variable_values: Optional[Dict[str, Any]] = ...,
operation_name: Optional[str] = ...,
serialize_variables: Optional[bool] = ...,
parse_result: Optional[bool] = ...,
*,
get_execution_result: bool,
**kwargs,
) -> Union[Dict[str, Any], ExecutionResult]:
... # pragma: no cover
def execute_sync(
self,
document: DocumentNode,
variable_values: Optional[Dict[str, Any]] = None,
operation_name: Optional[str] = None,
serialize_variables: Optional[bool] = None,
parse_result: Optional[bool] = None,
get_execution_result: bool = False,
**kwargs,
) -> Union[Dict[str, Any], ExecutionResult]:
""":meta private:"""
with self as session:
return session.execute(
document,
variable_values=variable_values,
operation_name=operation_name,
serialize_variables=serialize_variables,
parse_result=parse_result,
get_execution_result=get_execution_result,
**kwargs,
)
@overload
def execute_batch_sync(
self,
requests: List[GraphQLRequest],
*,
serialize_variables: Optional[bool] = None,
parse_result: Optional[bool] = None,
get_execution_result: Literal[False],
**kwargs,
) -> List[Dict[str, Any]]:
... # pragma: no cover
@overload
def execute_batch_sync(
self,
requests: List[GraphQLRequest],
*,
serialize_variables: Optional[bool] = None,
parse_result: Optional[bool] = None,
get_execution_result: Literal[True],
**kwargs,
) -> List[ExecutionResult]:
... # pragma: no cover
@overload
def execute_batch_sync(
self,
requests: List[GraphQLRequest],
*,
serialize_variables: Optional[bool] = None,
parse_result: Optional[bool] = None,
get_execution_result: bool,
**kwargs,
) -> Union[List[Dict[str, Any]], List[ExecutionResult]]:
... # pragma: no cover
def execute_batch_sync(
self,
requests: 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(
requests,
serialize_variables=serialize_variables,
parse_result=parse_result,
get_execution_result=get_execution_result,
**kwargs,
)
@overload
async def execute_async(
self,
document: DocumentNode,
variable_values: Optional[Dict[str, Any]] = ...,
operation_name: Optional[str] = ...,
serialize_variables: Optional[bool] = ...,
parse_result: Optional[bool] = ...,
*, # https://github.com/python/mypy/issues/7333#issuecomment-788255229
get_execution_result: Literal[False] = ...,
**kwargs,
) -> Dict[str, Any]:
... # pragma: no cover
@overload
async def execute_async(
self,
document: DocumentNode,
variable_values: Optional[Dict[str, Any]] = ...,
operation_name: Optional[str] = ...,
serialize_variables: Optional[bool] = ...,
parse_result: Optional[bool] = ...,
*,
get_execution_result: Literal[True],
**kwargs,
) -> ExecutionResult:
... # pragma: no cover
@overload
async def execute_async(
self,
document: DocumentNode,
variable_values: Optional[Dict[str, Any]] = ...,
operation_name: Optional[str] = ...,
serialize_variables: Optional[bool] = ...,
parse_result: Optional[bool] = ...,
*,
get_execution_result: bool,
**kwargs,
) -> Union[Dict[str, Any], ExecutionResult]:
... # pragma: no cover
async def execute_async(
self,
document: DocumentNode,
variable_values: Optional[Dict[str, Any]] = None,
operation_name: Optional[str] = None,
serialize_variables: Optional[bool] = None,
parse_result: Optional[bool] = None,
get_execution_result: bool = False,
**kwargs,
) -> Union[Dict[str, Any], ExecutionResult]:
""":meta private:"""
async with self as session:
return await session.execute(
document,
variable_values=variable_values,
operation_name=operation_name,
serialize_variables=serialize_variables,
parse_result=parse_result,
get_execution_result=get_execution_result,
**kwargs,
)
@overload
def execute(
self,
document: DocumentNode,
variable_values: Optional[Dict[str, Any]] = ...,
operation_name: Optional[str] = ...,
serialize_variables: Optional[bool] = ...,
parse_result: Optional[bool] = ...,
*, # https://github.com/python/mypy/issues/7333#issuecomment-788255229
get_execution_result: Literal[False] = ...,
**kwargs,
) -> Dict[str, Any]:
... # pragma: no cover
@overload
def execute(
self,
document: DocumentNode,
variable_values: Optional[Dict[str, Any]] = ...,
operation_name: Optional[str] = ...,
serialize_variables: Optional[bool] = ...,
parse_result: Optional[bool] = ...,
*,
get_execution_result: Literal[True],
**kwargs,
) -> ExecutionResult:
... # pragma: no cover
@overload
def execute(
self,
document: DocumentNode,
variable_values: Optional[Dict[str, Any]] = ...,
operation_name: Optional[str] = ...,
serialize_variables: Optional[bool] = ...,
parse_result: Optional[bool] = ...,
*,
get_execution_result: bool,
**kwargs,
) -> Union[Dict[str, Any], ExecutionResult]:
... # pragma: no cover
def execute(
self,
document: DocumentNode,
variable_values: Optional[Dict[str, Any]] = None,
operation_name: Optional[str] = None,
serialize_variables: Optional[bool] = None,
parse_result: Optional[bool] = None,
get_execution_result: bool = False,
**kwargs,
) -> Union[Dict[str, Any], 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):
# Get the current asyncio event loop
# Or create a new event loop if there isn't one (in a new Thread)
try:
with warnings.catch_warnings():
warnings.filterwarnings(
"ignore", message="There is no current event loop"
)
loop = asyncio.get_event_loop()
except RuntimeError:
loop = asyncio.new_event_loop()
asyncio.set_event_loop(loop)
assert not loop.is_running(), (
"Cannot run client.execute(query) if an asyncio loop is running."
" Use 'await client.execute_async(query)' instead."
)
data = loop.run_until_complete(
self.execute_async(
document,
variable_values=variable_values,
operation_name=operation_name,
serialize_variables=serialize_variables,
parse_result=parse_result,
get_execution_result=get_execution_result,
**kwargs,
)
)
return data
else: # Sync transports
return self.execute_sync(
document,
variable_values=variable_values,
operation_name=operation_name,
serialize_variables=serialize_variables,
parse_result=parse_result,
get_execution_result=get_execution_result,
**kwargs,
)
@overload
def execute_batch(
self,
requests: List[GraphQLRequest],
*,
serialize_variables: Optional[bool] = None,
parse_result: Optional[bool] = None,
get_execution_result: Literal[False],
**kwargs,
) -> List[Dict[str, Any]]:
... # pragma: no cover
@overload
def execute_batch(
self,
requests: List[GraphQLRequest],
*,
serialize_variables: Optional[bool] = None,
parse_result: Optional[bool] = None,
get_execution_result: Literal[True],
**kwargs,
) -> List[ExecutionResult]:
... # pragma: no cover
@overload
def execute_batch(
self,
requests: List[GraphQLRequest],
*,
serialize_variables: Optional[bool] = None,
parse_result: Optional[bool] = None,
get_execution_result: bool,
**kwargs,
) -> Union[List[Dict[str, Any]], List[ExecutionResult]]:
... # pragma: no cover
def execute_batch(
self,
requests: 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 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.
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 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
the context manager to keep a session active.
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(
requests,
serialize_variables=serialize_variables,
parse_result=parse_result,
get_execution_result=get_execution_result,
**kwargs,
)
@overload
def subscribe_async(
self,
document: DocumentNode,
variable_values: Optional[Dict[str, Any]] = ...,
operation_name: Optional[str] = ...,
serialize_variables: Optional[bool] = ...,
parse_result: Optional[bool] = ...,
*,
get_execution_result: Literal[False] = ...,
**kwargs,
) -> AsyncGenerator[Dict[str, Any], None]:
... # pragma: no cover
@overload
def subscribe_async(
self,
document: DocumentNode,
variable_values: Optional[Dict[str, Any]] = ...,
operation_name: Optional[str] = ...,
serialize_variables: Optional[bool] = ...,
parse_result: Optional[bool] = ...,
*,
get_execution_result: Literal[True],
**kwargs,
) -> AsyncGenerator[ExecutionResult, None]:
... # pragma: no cover
@overload
def subscribe_async(
self,
document: DocumentNode,
variable_values: Optional[Dict[str, Any]] = ...,
operation_name: Optional[str] = ...,
serialize_variables: Optional[bool] = ...,
parse_result: Optional[bool] = ...,
*,
get_execution_result: bool,
**kwargs,
) -> Union[
AsyncGenerator[Dict[str, Any], None], AsyncGenerator[ExecutionResult, None]
]:
... # pragma: no cover
async def subscribe_async(
self,
document: DocumentNode,
variable_values: Optional[Dict[str, Any]] = None,
operation_name: Optional[str] = None,
serialize_variables: Optional[bool] = None,
parse_result: Optional[bool] = None,
get_execution_result: bool = False,
**kwargs,
) -> Union[
AsyncGenerator[Dict[str, Any], None], AsyncGenerator[ExecutionResult, None]
]:
""":meta private:"""
async with self as session:
generator = session.subscribe(
document,
variable_values=variable_values,
operation_name=operation_name,
serialize_variables=serialize_variables,
parse_result=parse_result,
get_execution_result=get_execution_result,
**kwargs,
)
async for result in generator:
yield result
@overload
def subscribe(
self,
document: DocumentNode,
variable_values: Optional[Dict[str, Any]] = ...,
operation_name: Optional[str] = ...,
serialize_variables: Optional[bool] = ...,
parse_result: Optional[bool] = ...,
*,
get_execution_result: Literal[False] = ...,
**kwargs,
) -> Generator[Dict[str, Any], None, None]:
... # pragma: no cover
@overload
def subscribe(
self,
document: DocumentNode,
variable_values: Optional[Dict[str, Any]] = ...,
operation_name: Optional[str] = ...,
serialize_variables: Optional[bool] = ...,
parse_result: Optional[bool] = ...,
*,
get_execution_result: Literal[True],
**kwargs,
) -> Generator[ExecutionResult, None, None]:
... # pragma: no cover
@overload
def subscribe(
self,
document: DocumentNode,
variable_values: Optional[Dict[str, Any]] = ...,
operation_name: Optional[str] = ...,
serialize_variables: Optional[bool] = ...,
parse_result: Optional[bool] = ...,
*,
get_execution_result: bool,
**kwargs,
) -> Union[
Generator[Dict[str, Any], None, None], Generator[ExecutionResult, None, None]
]:
... # pragma: no cover
def subscribe(
self,
document: DocumentNode,
variable_values: Optional[Dict[str, Any]] = None,
operation_name: Optional[str] = None,
serialize_variables: Optional[bool] = None,
parse_result: Optional[bool] = None,
*,
get_execution_result: bool = False,
**kwargs,
) -> Union[
Generator[Dict[str, Any], None, None], Generator[ExecutionResult, None, None]
]:
"""Execute a GraphQL subscription with a python generator.
We need an async transport for this functionality.
"""
# Get the current asyncio event loop
# Or create a new event loop if there isn't one (in a new Thread)
try:
with warnings.catch_warnings():
warnings.filterwarnings(
"ignore", message="There is no current event loop"
)
loop = asyncio.get_event_loop()
except RuntimeError:
loop = asyncio.new_event_loop()
asyncio.set_event_loop(loop)
async_generator: Union[
AsyncGenerator[Dict[str, Any], None], AsyncGenerator[ExecutionResult, None]
] = self.subscribe_async(
document,
variable_values=variable_values,
operation_name=operation_name,
serialize_variables=serialize_variables,
parse_result=parse_result,
get_execution_result=get_execution_result,
**kwargs,
)
assert not loop.is_running(), (
"Cannot run client.subscribe(query) if an asyncio loop is running."
" Use 'await client.subscribe_async(query)' instead."
)
try:
while True:
# Note: we need to create a task here in order to be able to close
# the async generator properly on python 3.8
# See https://bugs.python.org/issue38559
generator_task = asyncio.ensure_future(
async_generator.__anext__(), loop=loop
)
result: Union[
Dict[str, Any], ExecutionResult
] = loop.run_until_complete(
generator_task
) # type: ignore
yield result
except StopAsyncIteration:
pass
except (KeyboardInterrupt, Exception, GeneratorExit):
# Graceful shutdown
asyncio.ensure_future(async_generator.aclose(), loop=loop)
generator_task.cancel()
loop.run_until_complete(loop.shutdown_asyncgens())
# Then reraise the exception
raise
async def connect_async(self, reconnecting=False, **kwargs):
r"""Connect asynchronously with the underlying async transport to
produce a session.
That session will be a permanent auto-reconnecting session
if :code:`reconnecting=True`.
If you call this method, you should call the
:meth:`close_async <gql.client.Client.close_async>` method
for cleanup.
:param reconnecting: if True, create a permanent reconnecting session
:param \**kwargs: additional arguments for the
:meth:`ReconnectingAsyncClientSession init method
<gql.client.ReconnectingAsyncClientSession.__init__>`.
"""
assert isinstance(
self.transport, AsyncTransport
), "Only a transport of type AsyncTransport can be used asynchronously"
if reconnecting:
self.session = ReconnectingAsyncClientSession(client=self, **kwargs)
await self.session.start_connecting_task()
else:
await self.transport.connect()
self.session = AsyncClientSession(client=self)
# Get schema from transport if needed
try:
if self.fetch_schema_from_transport and not self.schema:
await self.session.fetch_schema()
except Exception:
# we don't know what type of exception is thrown here because it
# depends on the underlying transport; we just make sure that the
# transport is closed and re-raise the exception
await self.transport.close()
raise
return self.session
async def close_async(self):
"""Close the async transport and stop the optional reconnecting task."""
if isinstance(self.session, ReconnectingAsyncClientSession):
await self.session.stop_connecting_task()
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):
r"""Connect synchronously with the underlying sync transport to
produce a session.
If you call this method, you should call the
:meth:`close_sync <gql.client.Client.close_sync>` method
for cleanup.
"""
assert not isinstance(self.transport, AsyncTransport), (
"Only a sync transport can be used."
" Use 'async with Client(...) as session:' instead"
)
if not hasattr(self, "session"):
self.session = SyncClientSession(client=self)
self.session.connect()
# Get schema from transport if needed
try:
if self.fetch_schema_from_transport and not self.schema:
self.session.fetch_schema()
except Exception:
# we don't know what type of exception is thrown here because it
# depends on the underlying transport; we just make sure that the
# transport is closed and re-raise the exception
self.session.close()
raise
return self.session
def close_sync(self):
"""Close the sync session and the sync transport.
If batching is enabled, this will block until the remaining queries in the
batching queue have been processed.
"""
self.session.close()
def __enter__(self):
return self.connect_sync()
def __exit__(self, *args):
self.close_sync()
class SyncClientSession:
"""An instance of this class is created when using :code:`with` on the client.
It contains the sync method execute to send queries
on a sync transport using the same session.
"""
def __init__(self, client: Client):
""":param client: the :class:`client <gql.client.Client>` used"""
self.client = client
def _execute(
self,
document: DocumentNode,
variable_values: Optional[Dict[str, Any]] = None,
operation_name: Optional[str] = None,
serialize_variables: Optional[bool] = None,
parse_result: Optional[bool] = None,
**kwargs,
) -> 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:
self.client.validate(document)
# Parse variable values for custom scalars if requested
if variable_values is not None:
if serialize_variables or (
serialize_variables is None and self.client.serialize_variables
):
variable_values = serialize_variable_values(
self.client.schema,
document,
variable_values,
operation_name=operation_name,
)
if self.client.batching_enabled:
request = GraphQLRequest(
document,
variable_values=variable_values,
operation_name=operation_name,
)
future_result = self._execute_future(request)
result = future_result.result()
else:
result = self.transport.execute(
document,
variable_values=variable_values,
operation_name=operation_name,
**kwargs,
)
# Unserialize the result if requested
if self.client.schema:
if parse_result or (parse_result is None and self.client.parse_results):
result.data = parse_result_fn(
self.client.schema,
document,
result.data,
operation_name=operation_name,
)
return result
@overload
def execute(
self,
document: DocumentNode,
variable_values: Optional[Dict[str, Any]] = ...,
operation_name: Optional[str] = ...,
serialize_variables: Optional[bool] = ...,
parse_result: Optional[bool] = ...,
*,
get_execution_result: Literal[False] = ...,
**kwargs,
) -> Dict[str, Any]:
... # pragma: no cover
@overload
def execute(
self,
document: DocumentNode,
variable_values: Optional[Dict[str, Any]] = ...,
operation_name: Optional[str] = ...,
serialize_variables: Optional[bool] = ...,
parse_result: Optional[bool] = ...,
*,
get_execution_result: Literal[True],
**kwargs,
) -> ExecutionResult:
... # pragma: no cover
@overload
def execute(
self,
document: DocumentNode,
variable_values: Optional[Dict[str, Any]] = ...,
operation_name: Optional[str] = ...,
serialize_variables: Optional[bool] = ...,
parse_result: Optional[bool] = ...,
*,
get_execution_result: bool,
**kwargs,
) -> Union[Dict[str, Any], ExecutionResult]:
... # pragma: no cover
def execute(
self,
document: DocumentNode,
variable_values: Optional[Dict[str, Any]] = None,
operation_name: Optional[str] = None,
serialize_variables: Optional[bool] = None,
parse_result: Optional[bool] = None,
get_execution_result: bool = False,
**kwargs,
) -> Union[Dict[str, Any], 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
result = self._execute(
document,
variable_values=variable_values,
operation_name=operation_name,
serialize_variables=serialize_variables,
parse_result=parse_result,
**kwargs,
)
# 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 result
return result.data
def _execute_batch(
self,
requests: List[GraphQLRequest],
*,
serialize_variables: Optional[bool] = None,
parse_result: Optional[bool] = None,
validate_document: Optional[bool] = True,
**kwargs,
) -> List[ExecutionResult]:
"""Execute multiple GraphQL requests in a batch, using
the sync transport, returning a list of ExecutionResult objects.
:param requests: 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.
:param parse_result: Whether gql will unserialize the result.
By default use the parse_results argument of the client.
:param validate_document: Whether we still need to validate the document.
The extra arguments are passed to the transport execute method."""
# Validate document
if self.client.schema:
if validate_document:
for req in requests:
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
):
requests = [
req.serialize_variable_values(self.client.schema)
if req.variable_values is not None
else req
for req in requests
]
results = self.transport.execute_batch(requests, **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
@overload
def execute_batch(
self,
requests: List[GraphQLRequest],
*,
serialize_variables: Optional[bool] = None,
parse_result: Optional[bool] = None,
get_execution_result: Literal[False],
**kwargs,
) -> List[Dict[str, Any]]:
... # pragma: no cover
@overload
def execute_batch(
self,
requests: List[GraphQLRequest],
*,
serialize_variables: Optional[bool] = None,
parse_result: Optional[bool] = None,
get_execution_result: Literal[True],
**kwargs,
) -> List[ExecutionResult]:
... # pragma: no cover
@overload
def execute_batch(
self,
requests: List[GraphQLRequest],
*,
serialize_variables: Optional[bool] = None,
parse_result: Optional[bool] = None,
get_execution_result: bool,
**kwargs,
) -> Union[List[Dict[str, Any]], List[ExecutionResult]]:
... # pragma: no cover
def execute_batch(
self,
requests: 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 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
ExecutionResult.
:param requests: 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.
: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(
requests,
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 cast(List[Dict[str, Any]], [result.data for result in results])
def _batch_loop(self) -> None:
"""main loop of the thread used to wait for requests
to execute them in a batch"""
stop_loop = False
while not stop_loop:
# First wait for a first request in from the batch queue
requests_and_futures: List[Tuple[GraphQLRequest, Future]] = []
request_and_future: Tuple[GraphQLRequest, Future] = self.batch_queue.get()
if request_and_future is None:
break
requests_and_futures.append(request_and_future)
# Then wait the requested batch interval except if we already
# have the maximum number of requests in the queue
if self.batch_queue.qsize() < self.client.batch_max - 1:
time.sleep(self.client.batch_interval)
# Then get the requests which had been made during that wait interval
for _ in range(self.client.batch_max - 1):
if self.batch_queue.empty():
break
request_and_future = self.batch_queue.get()
if request_and_future is None:
stop_loop = True
break
requests_and_futures.append(request_and_future)
requests = [request for request, _ in requests_and_futures]
futures = [future for _, future in requests_and_futures]
# Manually execute the requests in a batch
try:
results: List[ExecutionResult] = self._execute_batch(
requests,
serialize_variables=False, # already done
parse_result=False,
validate_document=False,
)
except Exception as exc:
for future in futures:
future.set_exception(exc)
continue
# Fill in the future results
for result, future in zip(results, futures):
future.set_result(result)
# Indicate that the Thread has stopped
self._batch_thread_stopped_event.set()
def _execute_future(
self,
request: GraphQLRequest,
) -> Future:
"""If batching is enabled, this method will put a request in the batching queue
instead of executing it directly so that the requests could be put in a batch.
"""
assert hasattr(self, "batch_queue"), "Batching is not enabled"
assert not self._batch_thread_stop_requested, "Batching thread has been stopped"
future: Future = Future()
self.batch_queue.put((request, future))
return future
def connect(self):
"""Connect the transport and initialize the batch threading loop if batching
is enabled."""
if self.client.batching_enabled:
self.batch_queue: Queue = Queue()
self._batch_thread_stop_requested = False
self._batch_thread_stopped_event = Event()
self._batch_thread = Thread(target=self._batch_loop, daemon=True)
self._batch_thread.start()
self.transport.connect()
def close(self):
"""Close the transport and cleanup the batching thread if batching is enabled.
Will wait until all the remaining requests in the batch processing queue
have been executed.
"""
if hasattr(self, "_batch_thread_stopped_event"):
# Send a None in the queue to indicate that the batching Thread must stop
# after having processed the remaining requests in the queue
self._batch_thread_stop_requested = True
self.batch_queue.put(None)
# Wait for the Thread to stop
self._batch_thread_stopped_event.wait()
self.transport.close()
def fetch_schema(self) -> None:
"""Fetch the GraphQL schema explicitly using introspection.
Don't use this function and instead set the fetch_schema_from_transport
attribute to True"""
introspection_query = get_introspection_query_ast(
**self.client.introspection_args
)
execution_result = self.transport.execute(introspection_query)
self.client._build_schema_from_introspection(execution_result)
@property
def transport(self):
return self.client.transport
class AsyncClientSession:
"""An instance of this class is created when using :code:`async with` on a
:class:`client <gql.client.Client>`.
It contains the async methods (execute, subscribe) to send queries
on an async transport using the same session.
"""
def __init__(self, client: Client):
""":param client: the :class:`client <gql.client.Client>` used"""
self.client = client
async def _subscribe(
self,
document: DocumentNode,
variable_values: Optional[Dict[str, Any]] = None,
operation_name: Optional[str] = None,
serialize_variables: Optional[bool] = None,
parse_result: Optional[bool] = None,
**kwargs,
) -> AsyncGenerator[ExecutionResult, None]:
"""Coroutine to subscribe asynchronously to the provided document AST
asynchronously using the async transport,
returning an async generator producing ExecutionResult objects.
* Validate the query with the schema if provided.
* Serialize the variable_values if requested.
: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 subscribe method."""
# Validate document
if self.client.schema:
self.client.validate(document)
# Parse variable values for custom scalars if requested
if variable_values is not None:
if serialize_variables or (
serialize_variables is None and self.client.serialize_variables
):
variable_values = serialize_variable_values(
self.client.schema,
document,
variable_values,
operation_name=operation_name,
)
# Subscribe to the transport
inner_generator: AsyncGenerator[
ExecutionResult, None
] = self.transport.subscribe(
document,
variable_values=variable_values,
operation_name=operation_name,
**kwargs,
)
# Keep a reference to the inner generator to allow the user to call aclose()
# before a break if python version is too old (pypy3 py 3.6.1)
self._generator = inner_generator
try:
async for result in inner_generator:
if self.client.schema:
if parse_result or (
parse_result is None and self.client.parse_results
):
result.data = parse_result_fn(
self.client.schema,
document,
result.data,
operation_name=operation_name,
)
yield result
finally:
await inner_generator.aclose()
@overload
def subscribe(
self,
document: DocumentNode,
variable_values: Optional[Dict[str, Any]] = ...,
operation_name: Optional[str] = ...,
serialize_variables: Optional[bool] = ...,
parse_result: Optional[bool] = ...,
*,
get_execution_result: Literal[False] = ...,
**kwargs,
) -> AsyncGenerator[Dict[str, Any], None]:
... # pragma: no cover
@overload
def subscribe(
self,
document: DocumentNode,
variable_values: Optional[Dict[str, Any]] = ...,
operation_name: Optional[str] = ...,
serialize_variables: Optional[bool] = ...,
parse_result: Optional[bool] = ...,
*,
get_execution_result: Literal[True],
**kwargs,
) -> AsyncGenerator[ExecutionResult, None]:
... # pragma: no cover
@overload
def subscribe(
self,
document: DocumentNode,
variable_values: Optional[Dict[str, Any]] = ...,
operation_name: Optional[str] = ...,
serialize_variables: Optional[bool] = ...,
parse_result: Optional[bool] = ...,
*,
get_execution_result: bool,
**kwargs,
) -> Union[
AsyncGenerator[Dict[str, Any], None], AsyncGenerator[ExecutionResult, None]
]:
... # pragma: no cover
async def subscribe(
self,
document: DocumentNode,
variable_values: Optional[Dict[str, Any]] = None,
operation_name: Optional[str] = None,
serialize_variables: Optional[bool] = None,
parse_result: Optional[bool] = None,
get_execution_result: bool = False,
**kwargs,
) -> Union[
AsyncGenerator[Dict[str, Any], None], AsyncGenerator[ExecutionResult, None]
]:
"""Coroutine to subscribe asynchronously to the provided document AST
asynchronously using the async 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: yield 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 subscribe method."""
inner_generator: AsyncGenerator[ExecutionResult, None] = self._subscribe(
document,
variable_values=variable_values,
operation_name=operation_name,
serialize_variables=serialize_variables,
parse_result=parse_result,
**kwargs,
)
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(
str_first_element(result.errors),
errors=result.errors,
data=result.data,
extensions=result.extensions,
)
elif result.data is not None:
if get_execution_result:
yield result
else:
yield result.data
finally:
await inner_generator.aclose()
async def _execute(
self,
document: DocumentNode,
variable_values: Optional[Dict[str, Any]] = None,
operation_name: Optional[str] = None,
serialize_variables: Optional[bool] = None,
parse_result: Optional[bool] = None,
**kwargs,
) -> ExecutionResult:
"""Coroutine to execute the provided document AST asynchronously using
the async transport, returning an ExecutionResult object.
* Validate the query with the schema if provided.
* Serialize the variable_values if requested.
: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:
self.client.validate(document)
# Parse variable values for custom scalars if requested
if variable_values is not None:
if serialize_variables or (
serialize_variables is None and self.client.serialize_variables
):
variable_values = serialize_variable_values(
self.client.schema,
document,
variable_values,
operation_name=operation_name,
)
# Execute the query with the transport with a timeout
with fail_after(self.client.execute_timeout):
result = await self.transport.execute(
document,
variable_values=variable_values,
operation_name=operation_name,
**kwargs,
)
# Unserialize the result if requested
if self.client.schema:
if parse_result or (parse_result is None and self.client.parse_results):
result.data = parse_result_fn(
self.client.schema,
document,
result.data,
operation_name=operation_name,
)
return result
@overload
async def execute(
self,
document: DocumentNode,
variable_values: Optional[Dict[str, Any]] = ...,
operation_name: Optional[str] = ...,
serialize_variables: Optional[bool] = ...,
parse_result: Optional[bool] = ...,
*,
get_execution_result: Literal[False] = ...,
**kwargs,
) -> Dict[str, Any]:
... # pragma: no cover
@overload
async def execute(
self,
document: DocumentNode,
variable_values: Optional[Dict[str, Any]] = ...,
operation_name: Optional[str] = ...,
serialize_variables: Optional[bool] = ...,
parse_result: Optional[bool] = ...,
*,
get_execution_result: Literal[True],
**kwargs,
) -> ExecutionResult:
... # pragma: no cover
@overload
async def execute(
self,
document: DocumentNode,
variable_values: Optional[Dict[str, Any]] = ...,
operation_name: Optional[str] = ...,
serialize_variables: Optional[bool] = ...,
parse_result: Optional[bool] = ...,
*,
get_execution_result: bool,
**kwargs,
) -> Union[Dict[str, Any], ExecutionResult]:
... # pragma: no cover
async def execute(
self,
document: DocumentNode,
variable_values: Optional[Dict[str, Any]] = None,
operation_name: Optional[str] = None,
serialize_variables: Optional[bool] = None,
parse_result: Optional[bool] = None,
get_execution_result: bool = False,
**kwargs,
) -> Union[Dict[str, Any], ExecutionResult]:
"""Coroutine to execute the provided document AST asynchronously using
the async 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
result = await self._execute(
document,
variable_values=variable_values,
operation_name=operation_name,
serialize_variables=serialize_variables,
parse_result=parse_result,
**kwargs,
)
# 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 result
return result.data
async def fetch_schema(self) -> None:
"""Fetch the GraphQL schema explicitly using introspection.
Don't use this function and instead set the fetch_schema_from_transport
attribute to True"""
introspection_query = get_introspection_query_ast(
**self.client.introspection_args
)
execution_result = await self.transport.execute(introspection_query)
self.client._build_schema_from_introspection(execution_result)
@property
def transport(self):
return self.client.transport
_CallableT = TypeVar("_CallableT", bound=Callable[..., Any])
_Decorator = Callable[[_CallableT], _CallableT]
class ReconnectingAsyncClientSession(AsyncClientSession):
"""An instance of this class is created when using the
:meth:`connect_async <gql.client.Client.connect_async>` method of the
:class:`Client <gql.client.Client>` class with :code:`reconnecting=True`.
It is used to provide a single session which will reconnect automatically if
the connection fails.
"""
def __init__(
self,
client: Client,
retry_connect: Union[bool, _Decorator] = True,
retry_execute: Union[bool, _Decorator] = True,
):
"""
:param client: the :class:`client <gql.client.Client>` used.
:param retry_connect: Either a Boolean to activate/deactivate the retries
for the connection to the transport OR a backoff decorator to
provide specific retries parameters for the connections.
:param retry_execute: Either a Boolean to activate/deactivate the retries
for the execute method OR a backoff decorator to
provide specific retries parameters for this method.
"""
self.client = client
self._connect_task = None
self._reconnect_request_event = asyncio.Event()
self._connected_event = asyncio.Event()
if retry_connect is True:
# By default, retry again and again, with maximum 60 seconds
# between retries
self.retry_connect = backoff.on_exception(
backoff.expo,
Exception,
max_value=60,
)
elif retry_connect is False:
self.retry_connect = lambda e: e
else:
assert callable(retry_connect)
self.retry_connect = retry_connect
if retry_execute is True:
# By default, retry 5 times, except if we receive a TransportQueryError
self.retry_execute = backoff.on_exception(
backoff.expo,
Exception,
max_tries=5,
giveup=lambda e: isinstance(e, TransportQueryError),
)
elif retry_execute is False:
self.retry_execute = lambda e: e
else:
assert callable(retry_execute)
self.retry_execute = retry_execute
# Creating the _execute_with_retries and _connect_with_retries methods
# using the provided backoff decorators
self._execute_with_retries = self.retry_execute(self._execute_once)
self._connect_with_retries = self.retry_connect(self.transport.connect)
async def _connection_loop(self):
"""Coroutine used for the connection task.
- try to connect to the transport with retries
- send a connected event when the connection has been made
- then wait for a reconnect request to try to connect again
"""
while True:
# Connect to the transport with the retry decorator
# By default it should keep retrying until it connect
await self._connect_with_retries()
# Once connected, set the connected event
self._connected_event.set()
self._connected_event.clear()
# Then wait for the reconnect event
self._reconnect_request_event.clear()
await self._reconnect_request_event.wait()
async def start_connecting_task(self):
"""Start the task responsible to restart the connection
of the transport when requested by an event.
"""
if self._connect_task:
log.warning("connect task already started!")
else:
self._connect_task = asyncio.ensure_future(self._connection_loop())
await self._connected_event.wait()
async def stop_connecting_task(self):
"""Stop the connecting task."""
if self._connect_task is not None:
self._connect_task.cancel()
self._connect_task = None
async def _execute_once(
self,
document: DocumentNode,
variable_values: Optional[Dict[str, Any]] = None,
operation_name: Optional[str] = None,
serialize_variables: Optional[bool] = None,
parse_result: Optional[bool] = None,
**kwargs,
) -> ExecutionResult:
"""Same Coroutine as parent method _execute but requesting a
reconnection if we receive a TransportClosed exception.
"""
try:
answer = await super()._execute(
document,
variable_values=variable_values,
operation_name=operation_name,
serialize_variables=serialize_variables,
parse_result=parse_result,
**kwargs,
)
except TransportClosed:
self._reconnect_request_event.set()
raise
return answer
async def _execute(
self,
document: DocumentNode,
variable_values: Optional[Dict[str, Any]] = None,
operation_name: Optional[str] = None,
serialize_variables: Optional[bool] = None,
parse_result: Optional[bool] = None,
**kwargs,
) -> ExecutionResult:
"""Same Coroutine as parent, but with optional retries
and requesting a reconnection if we receive a TransportClosed exception.
"""
return await self._execute_with_retries(
document,
variable_values=variable_values,
operation_name=operation_name,
serialize_variables=serialize_variables,
parse_result=parse_result,
**kwargs,
)
async def _subscribe(
self,
document: DocumentNode,
variable_values: Optional[Dict[str, Any]] = None,
operation_name: Optional[str] = None,
serialize_variables: Optional[bool] = None,
parse_result: Optional[bool] = None,
**kwargs,
) -> AsyncGenerator[ExecutionResult, None]:
"""Same Async generator as parent method _subscribe but requesting a
reconnection if we receive a TransportClosed exception.
"""
inner_generator: AsyncGenerator[ExecutionResult, None] = super()._subscribe(
document,
variable_values=variable_values,
operation_name=operation_name,
serialize_variables=serialize_variables,
parse_result=parse_result,
**kwargs,
)
try:
async for result in inner_generator:
yield result
except TransportClosed:
self._reconnect_request_event.set()
raise
finally:
await inner_generator.aclose()