Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion .github/workflows/tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ jobs:
strategy:
max-parallel: 4
matrix:
python-version: ["3.9", "3.10", "3.11", "3.12", "3.13", "pypy3.10"]
python-version: ["3.9", "3.10", "3.11", "3.12", "3.13", "3.14", "pypy3.10"]
os: [ubuntu-24.04, windows-latest]
exclude:
- os: windows-latest
Expand All @@ -22,6 +22,8 @@ jobs:
python-version: "3.11"
- os: windows-latest
python-version: "3.13"
- os: windows-latest
python-version: "3.14"
- os: windows-latest
python-version: "pypy3.10"

Expand Down
24 changes: 18 additions & 6 deletions docs/advanced/async_advanced_usage.rst
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ Async advanced usage
It is possible to send multiple GraphQL queries (query, mutation or subscription) in parallel,
on the same websocket connection, using asyncio tasks.

In order to retry in case of connection failure, we can use the great `backoff`_ module.
In order to retry in case of connection failure, we can use the great `tenacity`_ module.

.. code-block:: python

Expand All @@ -28,10 +28,22 @@ In order to retry in case of connection failure, we can use the great `backoff`_
async for result in session.subscribe(subscription2):
print(result)

# Then create a couroutine which will connect to your API and run all your queries as tasks.
# We use a `backoff` decorator to reconnect using exponential backoff in case of connection failure.

@backoff.on_exception(backoff.expo, Exception, max_time=300)
# Then create a couroutine which will connect to your API and run all your
# queries as tasks. We use a `tenacity` retry decorator to reconnect using
# exponential backoff in case of connection failure.

from tenacity import (
retry,
retry_if_exception_type,
stop_after_delay,
wait_exponential,
)

@retry(
retry=retry_if_exception_type(Exception),
stop=stop_after_delay(300), # max_time in seconds
wait=wait_exponential(),
)
async def graphql_connection():

transport = WebsocketsTransport(url="wss://YOUR_URL")
Expand All @@ -54,4 +66,4 @@ Subscriptions tasks can be stopped at any time by running

task.cancel()

.. _backoff: https://github.com/litl/backoff
.. _tenacity: https://github.com/jd/tenacity
77 changes: 54 additions & 23 deletions docs/advanced/async_permanent_session.rst
Original file line number Diff line number Diff line change
Expand Up @@ -36,19 +36,22 @@ Retries
Connection retries
^^^^^^^^^^^^^^^^^^

With :code:`reconnecting=True`, gql will use the `backoff`_ module to repeatedly try to connect with
exponential backoff and jitter with a maximum delay of 60 seconds by default.
With :code:`reconnecting=True`, gql will use the `tenacity`_ module to repeatedly
try to connect with exponential backoff and jitter with a maximum delay of
60 seconds by default.

You can change the default reconnecting profile by providing your own
backoff decorator to the :code:`retry_connect` argument.
retry decorator (from tenacity) to the :code:`retry_connect` argument.

.. code-block:: python

from tenacity import retry, retry_if_exception_type, wait_exponential

# Here wait maximum 5 minutes between connection retries
retry_connect = backoff.on_exception(
backoff.expo, # wait generator (here: exponential backoff)
Exception, # which exceptions should cause a retry (here: everything)
max_value=300, # max wait time in seconds
retry_connect = retry(
# which exceptions should cause a retry (here: everything)
retry=retry_if_exception_type(Exception),
wait=wait_exponential(max=300), # max wait time in seconds
)
session = await client.connect_async(
reconnecting=True,
Expand All @@ -66,32 +69,49 @@ There is no retry in case of a :code:`TransportQueryError` exception as it indic
the connection to the backend is working correctly.

You can change the default execute retry profile by providing your own
backoff decorator to the :code:`retry_execute` argument.
retry decorator (from tenacity) to the :code:`retry_execute` argument.

.. code-block:: python

from tenacity import (
retry,
retry_if_exception_type,
stop_after_attempt,
wait_exponential,
)

# Here Only 3 tries for execute calls
retry_execute = backoff.on_exception(
backoff.expo,
Exception,
max_tries=3,
retry_execute = retry(
retry=retry_if_exception_type(Exception),
stop=stop_after_attempt(3),
wait=wait_exponential(),
)
session = await client.connect_async(
reconnecting=True,
retry_execute=retry_execute,
)

If you don't want any retry on the execute calls, you can disable the retries with :code:`retry_execute=False`
If you don't want any retry on the execute calls, you can disable the retries
with :code:`retry_execute=False`

.. note::
If you want to retry even with :code:`TransportQueryError` exceptions,
then you need to make your own backoff decorator on your own method:
then you need to make your own retry decorator (from tenacity) on your own method:

.. code-block:: python

@backoff.on_exception(backoff.expo,
Exception,
max_tries=3)
from tenacity import (
retry,
retry_if_exception_type,
stop_after_attempt,
wait_exponential,
)

@retry(
retry=retry_if_exception_type(Exception),
stop=stop_after_attempt(3),
wait=wait_exponential(),
)
async def execute_with_retry(session, query):
return await session.execute(query)

Expand All @@ -100,14 +120,25 @@ Subscription retries

There is no :code:`retry_subscribe` as it is not feasible with async generators.
If you want retries for your subscriptions, then you can do it yourself
with backoff decorators on your methods.
with retry decorators (from tenacity) on your methods.

.. code-block:: python

@backoff.on_exception(backoff.expo,
Exception,
max_tries=3,
giveup=lambda e: isinstance(e, TransportQueryError))
from tenacity import (
retry,
retry_if_exception_type,
retry_unless_exception_type,
stop_after_attempt,
wait_exponential,
)
from gql.transport.exceptions import TransportQueryError

@retry(
retry=retry_if_exception_type(Exception)
& retry_unless_exception_type(TransportQueryError),
stop=stop_after_attempt(3),
wait=wait_exponential(),
)
async def execute_subscription1(session):
async for result in session.subscribe(subscription1):
print(result)
Expand All @@ -123,4 +154,4 @@ Console example
.. literalinclude:: ../code_examples/console_async.py

.. _difficult to manage: https://github.com/graphql-python/gql/issues/179
.. _backoff: https://github.com/litl/backoff
.. _tenacity: https://github.com/jd/tenacity
37 changes: 37 additions & 0 deletions docs/code_examples/http_multipart_async.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import asyncio
import logging

from gql import Client, gql
from gql.transport.http_multipart_transport import HTTPMultipartTransport

logging.basicConfig(level=logging.INFO)


async def main():

transport = HTTPMultipartTransport(url="https://gql-book-server.fly.dev/graphql")

# Using `async with` on the client will start a connection on the transport
# and provide a `session` variable to execute queries on this connection
async with Client(
transport=transport,
) as session:

# Request subscription
subscription = gql(
"""
subscription {
book {
title
author
}
}
"""
)

# Subscribe and receive streaming updates
async for result in session.subscribe(subscription):
print(f"Received: {result}")


asyncio.run(main())
10 changes: 4 additions & 6 deletions docs/code_examples/reconnecting_mutation_http.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import asyncio
import logging

import backoff
from tenacity import retry, retry_if_exception_type, wait_exponential

from gql import Client, gql
from gql.transport.aiohttp import AIOHTTPTransport
Expand All @@ -17,11 +17,9 @@ async def main():

client = Client(transport=transport)

retry_connect = backoff.on_exception(
backoff.expo,
Exception,
max_value=10,
jitter=None,
retry_connect = retry(
retry=retry_if_exception_type(Exception),
wait=wait_exponential(max=10),
)
session = await client.connect_async(reconnecting=True, retry_connect=retry_connect)

Expand Down
10 changes: 4 additions & 6 deletions docs/code_examples/reconnecting_mutation_ws.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import asyncio
import logging

import backoff
from tenacity import retry, retry_if_exception_type, wait_exponential

from gql import Client, gql
from gql.transport.websockets import WebsocketsTransport
Expand All @@ -17,11 +17,9 @@ async def main():

client = Client(transport=transport)

retry_connect = backoff.on_exception(
backoff.expo,
Exception,
max_value=10,
jitter=None,
retry_connect = retry(
retry=retry_if_exception_type(Exception),
wait=wait_exponential(max=10),
)
session = await client.connect_async(reconnecting=True, retry_connect=retry_connect)

Expand Down
1 change: 1 addition & 0 deletions docs/modules/gql.rst
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ Sub-Packages
transport_common_adapters_aiohttp
transport_common_adapters_websockets
transport_exceptions
transport_http_multipart
transport_phoenix_channel_websockets
transport_requests
transport_httpx
Expand Down
7 changes: 7 additions & 0 deletions docs/modules/transport_http_multipart.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
gql.transport.http\_multipart\_transport module
===============================================

.. automodule:: gql.transport.http_multipart_transport
:members:
:undoc-members:
:show-inheritance:
1 change: 1 addition & 0 deletions docs/transports/async_transports.rst
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ Async transports are transports which are using an underlying async library. The

aiohttp
httpx_async
http_multipart
websockets
aiohttp_websockets
phoenix
Expand Down
Loading