Skip to main content

rath

codecov PyPI version Maintenance Maintainer PyPI pyversions PyPI status PyPI download month Ruff

Rath is a transport-agnostic GraphQL client for Python focused on composability. Built on Pydantic v2 and Koil, it works seamlessly in both async and sync code.

Inspired by Apollo Client, Rath composes request logic out of links — small, chainable middleware objects that transform or forward an operation until a terminating link dispatches it over the network. Need auth headers, retries, schema validation, persisted queries, or a websocket transport for subscriptions? Each is just another link you drop into the chain.

Installation

pip install rath

Or with uv:

uv add rath

Quickstart

from rath import Rath
from rath.links.aiohttp import AIOHttpLink

link = AIOHttpLink(endpoint_url="https://countries.trevorblades.com/")

with Rath(link=link) as rath:
    result = rath.query(
        """
        query {
            countries {
                native
                capital
            }
        }
        """
    )
    print(result.data)  # GraphQLResult — the payload lives on .data

Features

  • Pluggable transports — aiohttp, httpx and websockets out of the box.
  • Composable links — build your request pipeline from small, reusable pieces.
  • Async-first, sync-friendly — call aquery/asubscribe from async code, or query/subscribe from sync code (powered by Koil).
  • Auth with automatic refresh — re-fetch the token and retry on a 401/403.
  • Schema validation — validate operations locally before they hit the wire.
  • Automatic Persisted Queries (APQ) — send a hash instead of the full document.
  • Typed operations — pairs beautifully with turms generated Pydantic models.
  • First-class testing — mock terminating links so unit tests never touch the network.

Table of contents

A Rath client is configured with a single terminating link — the link that actually sends the operation over the network (e.g. AIOHttpLink, HttpxLink, GraphQLWSLink).

To add behaviour, you wrap the terminating link with continuation links using compose(). Operations flow left → right through the chain; results flow back right → left:

operation ─▶ AuthTokenLink ─▶ RetryLink ─▶ AIOHttpLink ─▶ server
result    ◀────────────────────────────────────────────◀
from rath import Rath
from rath.links import compose
from rath.links.auth import ComposedAuthLink
from rath.links.retry import RetryLink
from rath.links.aiohttp import AIOHttpLink

link = compose(
    ComposedAuthLink(token_loader=...),   # add the Bearer header
    RetryLink(maximum_retry_attempts=3),  # retry on failure
    AIOHttpLink(endpoint_url="..."),      # terminating link
)

rath = Rath(link=link)

Rath(link=...) also accepts a plain list — it is composed for you, and the last element must be a terminating link:

rath = Rath(link=[auth, retry, AIOHttpLink(endpoint_url="...")])

Async usage

Rath is built for async; the sync API above is just a thin wrapper. In async code use aquery, aquery_operation and asubscribe:

import asyncio
from rath import Rath
from rath.links.aiohttp import AIOHttpLink

link = AIOHttpLink(endpoint_url="https://countries.trevorblades.com/")


async def main():
    async with Rath(link=link) as rath:
        result = await rath.aquery(
            """
            query {
                countries {
                    native
                    capital
                }
            }
            """
        )
        print(result.data)


asyncio.run(main())

Variables and headers

Pass variables and per-operation headers directly to query/aquery:

with Rath(link=link) as rath:
    result = rath.query(
        """
        query Country($code: ID!) {
            country(code: $code) {
                name
                capital
            }
        }
        """,
        variables={"code": "DE"},
        headers={"X-Tenant": "acme"},
    )
    print(result.data["country"])

Handling errors

When the server returns GraphQL errors, Rath raises a GraphQLException carrying the messages and the offending operation:

from rath.operation import GraphQLException

with Rath(link=link) as rath:
    try:
        result = rath.query("query { doesNotExist }")
    except GraphQLException as exc:
        print("GraphQL error:", exc.message)
        print("Operation:", exc.operation.operation_name)

Authentication with refresh

ComposedAuthLink injects a Bearer token (returned by token_loader) into every request. If the terminating link reports an authentication error (e.g. a 403), the token_refresher is called and the operation is retried — up to maximum_refresh_attempts times.

from rath import Rath
from rath.links import compose
from rath.links.auth import ComposedAuthLink
from rath.links.aiohttp import AIOHttpLink


async def aload_token() -> str:
    return "SERVER_TOKEN"


async def arefresh_token() -> str:
    # e.g. exchange a refresh token for a fresh access token
    return "REFRESHED_TOKEN"


auth = ComposedAuthLink(token_loader=aload_token, token_refresher=arefresh_token)
http = AIOHttpLink(endpoint_url="https://countries.trevorblades.com/")

with Rath(link=compose(auth, http)) as rath:
    result = rath.query("query { countries { capital } }")
    print(result.data)

Subscriptions and transport splitting

Different operation types often want different transports: websockets for subscriptions, plain HTTP for queries and mutations (so they stay cacheable). split routes each operation to one of two links based on a predicate.

from graphql import OperationType
from rath import Rath
from rath.links import compose, split
from rath.links.auth import ComposedAuthLink
from rath.links.aiohttp import AIOHttpLink
from rath.links.graphql_ws import GraphQLWSLink


async def aload_token() -> str:
    return "SERVER_TOKEN"


auth = ComposedAuthLink(token_loader=aload_token)
http = AIOHttpLink(endpoint_url="https://api.example.com/graphql")
ws = GraphQLWSLink(ws_endpoint_url="wss://api.example.com/graphql")

# Predicate True  -> first link (http)
# Predicate False -> second link (ws)
transport = split(http, ws, lambda op: op.node.operation != OperationType.SUBSCRIPTION)

with Rath(link=compose(auth, transport)) as rath:
    # queries and mutations go over HTTP
    print(rath.query("query { countries { capital } }").data)

    # subscriptions go over the websocket link
    for event in rath.subscribe("subscription { newCountry { capital } }"):
        print(event.data)

Note: op.node.operation is a graphql-core OperationType enum — compare against OperationType.SUBSCRIPTION, not the string "subscription".

Retries

RetryLink retries an operation when the terminating link fails (handy for flaky connections and dropped subscriptions):

from rath.links import compose
from rath.links.retry import RetryLink
from rath.links.aiohttp import AIOHttpLink

link = compose(
    RetryLink(maximum_retry_attempts=5, sleep_interval=1),  # wait 1s between attempts
    AIOHttpLink(endpoint_url="https://countries.trevorblades.com/"),
)

Timeouts

TimeoutLink aborts an operation that takes longer than timeout seconds:

from rath.links import compose
from rath.links.timeout import TimeoutLink
from rath.links.aiohttp import AIOHttpLink

link = compose(
    TimeoutLink(timeout=10),  # seconds
    AIOHttpLink(endpoint_url="https://countries.trevorblades.com/"),
)

Schema validation

ValidatingLink validates each operation against a GraphQL schema before it is sent, so malformed queries fail fast with a clear error. Provide the schema as a DSL string, a glob of .graphql files, or let it introspect the remote schema.

from rath.links import compose
from rath.links.validate import ValidatingLink
from rath.links.aiohttp import AIOHttpLink

link = compose(
    ValidatingLink(schema_glob="schemas/**/*.graphql"),
    AIOHttpLink(endpoint_url="https://countries.trevorblades.com/"),
)

Automatic Persisted Queries (APQ)

ApqLink sends a SHA-256 hash of the query instead of the full document. If the server hasn't seen the query yet it transparently retries with the full text. This shrinks request payloads and plays well with CDN caching.

from rath.links import compose
from rath.links.apq import ApqLink
from rath.links.aiohttp import AIOHttpLink

link = compose(
    ApqLink(),
    AIOHttpLink(endpoint_url="https://countries.trevorblades.com/"),
)

Testing without a server

The rath.links.testing package provides terminating links that resolve operations locally, so unit tests never hit the network. AsyncMockLink takes a dict of resolvers keyed by field name; each resolver receives the Operation and returns the value for that field:

from rath import Rath
from rath.links.testing.mock import AsyncMockLink
from rath.operation import Operation


async def resolve_countries(operation: Operation):
    return [{"capital": "Berlin", "native": "Deutschland"}]


link = AsyncMockLink(query_resolver={"countries": resolve_countries})

with Rath(link=link) as rath:
    result = rath.query("query { countries { native capital } }")
    assert result.data == {"countries": [{"capital": "Berlin", "native": "Deutschland"}]}

Use AsyncStatefulMockLink when you also need to mock subscriptions, and AssertLink together with compose() to assert that upstream links transformed an operation as expected.

Typed operations with turms

Looking for fully typed operations for your GraphQL API? Rath pairs with turms, a code generator that turns your .graphql documents into typed, Pydantic-powered functions:

import asyncio
from examples.api.schema import aget_capsules  # turms-generated
from rath import Rath
from rath.links import compose
from rath.links.auth import ComposedAuthLink
from rath.links.aiohttp import AIOHttpLink


async def aload_token() -> str:
    return ""


rath = Rath(
    link=compose(
        ComposedAuthLink(token_loader=aload_token),
        AIOHttpLink(endpoint_url="https://api.spacex.land/graphql/"),
    ),
)


async def main():
    # Entering the client registers it as the current Rath, so turms-generated
    # functions can find it without passing the client explicitly.
    async with rath:
        capsules = await aget_capsules()  # fully typed result
        print(capsules)


asyncio.run(main())

This repository contains an example turms-generated client (examples/) querying the public SpaceX API, plus a sample of the generated code.

Link Purpose
AIOHttpLink HTTP transport via aiohttp (with multi-part file upload support)
HttpxLink HTTP transport via httpx
GraphQLWSLink WebSocket transport (graphql-ws protocol), with reconnection
SubscriptionTransportWsLink Legacy subscriptions-transport-ws protocol
SplitLink (split) Route operations to different terminating links by type
ComposedAuthLink / AuthTokenLink Bearer-token insertion with automatic refresh
RetryLink Retry failed operations with optional back-off
TimeoutLink Abort operations that exceed a deadline
ValidatingLink Validate operations against a schema before sending
ApqLink Automatic Persisted Queries
FileExtraction Extract (nested) file-like objects from variables for upload
Testing links AsyncMockLink, AsyncStatefulMockLink, AssertLink, …

License

See LICENSE.

Metadata

Release files for rath 4.1.2

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for rath 4.1.2
File Size Uploaded
rath-4.1.2.tar.gz 46.3 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for rath 4.1.2
File Interpreter ABI Platform
rath-4.1.2-py3-none-any.whl Python 3 none any Details

Total release size: 111.5 kB

Release files / rath-4.1.2.tar.gz

Download URL rath-4.1.2.tar.gz
Size 46.3 kB
Tags Source
SHA-256 checksum
How to use checksums
2b3e482dd5906d09d1536e40f73ca9c7716e5356eb3f1fe18502b76bb428204f
BLAKE2b-256 checksum
How to use checksums
cb4cbf0a743f97c933b7032c986922525f23dab532e81646982062ae0e12d038
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via uv/0.12.23 {"installer":{"name":"uv","version":"0.12.23","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}

Release files / rath-4.1.2-py3-none-any.whl

Download URL rath-4.1.2-py3-none-any.whl
Size 65.2 kB
Tags Python 3
SHA-256 checksum
How to use checksums
7ee5e28bf687aa6877684b23465fa274dab3da93274414104bbdbcf1064e1155
BLAKE2b-256 checksum
How to use checksums
70448a10d17526f7b25353039f596f8b4e13f9ce423cc831a202dfe1f5b9af26
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via uv/0.12.23 {"installer":{"name":"uv","version":"0.12.23","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}

Release history Release notifications | RSS feed

This release

4.1.2 This release

2 release files

4.1.1

2 release files

4.0.2

2 release files

4.0.1

2 release files

4.0.0

2 release files

3.12.0

2 release files

3.9.1

2 release files

3.9.0

2 release files

3.8.0

2 release files

3.7.0

2 release files

3.6.0

2 release files

3.5.1

2 release files

3.5.0

2 release files

3.4.0

2 release files

3.3.0

2 release files

3.2.0

2 release files

3.1.0

2 release files

3.0.0

2 release files

2.0.0

2 release files

1.1.1

2 release files

1.1.0

2 release files

1.0.0

2 release files

0.5.1

2 release files

0.5.0

2 release files

0.4.7

2 release files

0.4.6

2 release files

0.4.5

2 release files

0.4.4

2 release files

0.4.3

2 release files

0.4.2

2 release files

0.4.1

2 release files

0.4.0

2 release files

0.3.6

2 release files

0.3.5

2 release files

0.3.4

2 release files

0.3.3

2 release files

0.3.2

2 release files

0.3.1

2 release files

0.3.0

2 release files

0.2.10

2 release files

0.2.9

2 release files

0.2.8

2 release files

0.2.7

2 release files

0.2.6

2 release files

0.2.5

2 release files

0.2.4

2 release files

0.1.35

2 release files

0.1.34

2 release files

0.1.33

2 release files

0.1.32

2 release files

0.1.31

2 release files

0.1.30

2 release files

0.1.29

2 release files

0.1.28

2 release files

0.1.27

2 release files

0.1.25

2 release files

0.1.17

2 release files

0.1.16

2 release files

0.1.15

2 release files

0.1.14

2 release files

0.1.13

2 release files

0.1.12

2 release files

0.1.11

2 release files

0.1.10

2 release files

0.1.9

2 release files

0.1.8

2 release files

0.1.7

2 release files

0.1.6

2 release files

0.1.5

2 release files

0.1.4

2 release files

0.1.3

2 release files

0.1.2

2 release files

0.1.1

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page