Skip to content

Custom Transport

The Transport protocol defines a single send method. By default merchants uses RequestsTransport, but you can inject any HTTP client.

Default Transport

RequestsTransport wraps a requests.Session. You can pass a pre-configured session:

import requests
from requests.adapters import HTTPAdapter, Retry
from merchants import Client, RequestsTransport
from merchants.providers.stripe import StripeProvider

session = requests.Session()
retry = Retry(total=3, backoff_factor=0.5)
session.mount("https://", HTTPAdapter(max_retries=retry))

client = Client(
    provider=StripeProvider(api_key="sk_test_…"),
    transport=RequestsTransport(session=session),
)

Add retries for production

Wrap the default requests.Session with a Retry adapter (as shown above) to handle transient network errors without any changes to the SDK or your provider code.

Custom Transport with httpx

from __future__ import annotations
from typing import Any

import httpx
from merchants.transport import HttpResponse, Transport, TransportError


class HttpxTransport(Transport):
    """Transport backed by httpx.Client."""

    def __init__(self, client: httpx.Client | None = None) -> None:
        self._client = client or httpx.Client()

    def send(
        self,
        method: str,
        url: str,
        *,
        headers: dict[str, str] | None = None,
        json: Any = None,
        params: dict[str, str] | None = None,
        timeout: float = 30.0,
    ) -> HttpResponse:
        try:
            resp = self._client.request(
                method, url,
                headers=headers, json=json, params=params, timeout=timeout,
            )
        except httpx.RequestError as exc:
            raise TransportError(str(exc)) from exc

        try:
            body = resp.json()
        except ValueError:
            body = resp.text

        return HttpResponse(
            status_code=resp.status_code,
            headers=dict(resp.headers),
            body=body,
        )

Use it with any provider:

from merchants import Client
from merchants.providers.dummy import DummyProvider

client = Client(
    provider=DummyProvider(),
    transport=HttpxTransport(httpx.Client(timeout=httpx.Timeout(10.0))),
)

Transport Protocol

Implement the Transport ABC with a single send method:

from abc import ABC, abstractmethod
from typing import Any
from merchants.transport import HttpResponse


class Transport(ABC):
    @abstractmethod
    def send(
        self,
        method: str,
        url: str,
        *,
        headers: dict[str, str] | None = None,
        json: Any = None,
        params: dict[str, str] | None = None,
        timeout: float = 30.0,
    ) -> HttpResponse:
        ...

HttpResponse

The send method must return an HttpResponse:

Field Type Description
status_code int HTTP status code
headers dict[str, str] Response headers
body dict \| list \| str Parsed JSON body or raw string
ok bool True if 200 <= status_code < 300 (computed property)

Low-level Escape Hatch

Use client.request to make arbitrary HTTP calls through the configured transport:

response = client.request("GET", "https://api.stripe.com/v1/balance")
print(response.status_code, response.body)

Transport is shared

client.request uses the same transport (and auth strategy) configured on the Client. This is useful for one-off API calls that are not covered by the provider abstraction.