Skip to main content

Record and replay HTTP interactions for httpx — an async-native VCR for fast, deterministic tests.

Project description

replayx

Record and replay HTTP interactions for httpx. Run your tests fast and offline.

CI PyPI Python versions License: MIT

replayx saves real HTTP responses to a cassette file on the first test run. Every later run reads from the cassette. No network calls. No flaky tests. No slow CI.

import httpx
from replayx import use_cassette

with use_cassette("cassettes/github.json"):
    resp = httpx.get("https://api.github.com/users/octocat")
    assert resp.json()["login"] == "octocat"

The first run records. Later runs replay.

Why I built replayx

vcrpy brought record and replay to Python. vcrpy targets requests and the sync world. I built replayx for modern httpx code.

Feature replayx vcrpy
Async httpx.AsyncClient yes limited
Built for httpx yes through patches
Zero deps beyond httpx yes (JSON) needs PyYAML
Secret redaction for committed cassettes yes partial
Explicit transport API, no patching yes no
Modern typing with py.typed yes no

Install

pip install replayx

Add YAML cassettes:

pip install "replayx[yaml]"

replayx needs Python 3.9 or newer and httpx 0.23 or newer.

Usage

Patch httpx with use_cassette

use_cassette patches httpx for the block. Your existing client code runs without changes. Sync and async both work.

import httpx
from replayx import use_cassette

async def fetch():
    async with httpx.AsyncClient() as client:
        return await client.get("https://api.example.com/data")

with use_cassette("cassettes/data.json"):
    resp = await fetch()

Build a transport yourself

Prefer no patching? Build a transport and pass the transport to your client. Nothing gets monkeypatched.

import httpx
from replayx import Cassette

cassette = Cassette.load("cassettes/data.json", record_mode="once")

with httpx.Client(transport=cassette.sync_transport()) as client:
    resp = client.get("https://api.example.com/data")

cassette.save()

Use cassette.async_transport() with httpx.AsyncClient for async code.

The pytest plugin

The plugin gives each test an auto-named cassette at <test-dir>/cassettes/<test-name>.json.

import httpx

def test_octocat(replayx_cassette):
    with replayx_cassette():
        resp = httpx.get("https://api.github.com/users/octocat")
        assert resp.status_code == 200

Re-record a whole run from the command line:

pytest --replayx-record=all

Set per-test defaults with the marker:

import pytest

@pytest.mark.replayx(match_on=("method", "url", "body"), filter_headers=["authorization"])
def test_create(replayx_cassette):
    with replayx_cassette():
        ...

Record modes

Mode What happens
once (default) Replay an existing cassette. Record everything when no cassette exists. A new request against an existing cassette raises an error.
new_episodes Replay matches and append new interactions.
none Replay only. No network. No writes. Good for CI.
all Always reach the real backend and overwrite the cassette. Use to re-record.
with use_cassette("cassettes/api.json", record_mode="none"):
    ...

Match requests

Requests match on method and url by default. Query order does not affect matching. Change the rules with match_on.

with use_cassette("cassettes/api.json", match_on=("method", "path", "body")):
    ...

Available matchers: method, scheme, host, port, path, query, url (alias uri), headers, body.

Redact secrets

Commit cassettes without leaking credentials. Redaction runs at record time. The live response your code receives stays intact.

with use_cassette(
    "cassettes/api.json",
    filter_headers=["authorization", "set-cookie"],
    filter_query_params=["api_key", "token"],
):
    ...

Use hooks for full control. Return a changed recording, or return None to skip the recording.

from dataclasses import replace

def scrub_body(response):
    return replace(response, body=b'{"token": "REDACTED"}')

with use_cassette("cassettes/api.json", before_record_response=scrub_body):
    ...

Cassette format

Cassettes use plain JSON. YAML works with the yaml extra. Both read well in code review.

{
  "version": 1,
  "recorded_with": "replayx/0.1.0",
  "interactions": [
    {
      "request": { "method": "GET", "url": "https://api.example.com/data", "headers": [], "body": null },
      "response": { "status_code": 200, "headers": [["content-type", "application/json"]], "body": { "text": "{\"ok\":true}" } }
    }
  ]
}

replayx stores binary bodies as base64.

Contribute

I welcome contributions. Set up a dev environment:

git clone https://github.com/mkusiappiah/replayx
cd replayx
pip install -e ".[dev]"
pytest
ruff check .
mypy

Open an issue before large changes.

License

MIT. See LICENSE.

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

replayx-0.2.0.tar.gz (17.9 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

replayx-0.2.0-py3-none-any.whl (18.5 kB view details)

Uploaded Python 3

File details

Details for the file replayx-0.2.0.tar.gz.

File metadata

  • Download URL: replayx-0.2.0.tar.gz
  • Upload date:
  • Size: 17.9 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.12

File hashes

Hashes for replayx-0.2.0.tar.gz
Algorithm Hash digest
SHA256 044cd03c17603b658b17d68a8c7072d49969a05321be6df1ec50baf13f424ede
MD5 9842c63c2f67ea801bdf69b5fdbfcb6e
BLAKE2b-256 87f38ad18e44265c7fb1fec1124d9cc56b4f5c6d7e840ae1ee297dbf02f56739

See more details on using hashes here.

Provenance

The following attestation bundles were made for replayx-0.2.0.tar.gz:

Publisher: publish.yml on mkusiappiah/replayx

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file replayx-0.2.0-py3-none-any.whl.

File metadata

  • Download URL: replayx-0.2.0-py3-none-any.whl
  • Upload date:
  • Size: 18.5 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.12

File hashes

Hashes for replayx-0.2.0-py3-none-any.whl
Algorithm Hash digest
SHA256 8dbf4419950ebb82fd6ca25c7b3d0e9e65c9a02402c7e5cdcfbbc35550eddbe3
MD5 c4e1580d928453e72fb188b91916988d
BLAKE2b-256 a8c28a5bfb3923935356132fcff0153f6fb68157627fcfe2f164e128240baa5c

See more details on using hashes here.

Provenance

The following attestation bundles were made for replayx-0.2.0-py3-none-any.whl:

Publisher: publish.yml on mkusiappiah/replayx

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page