Metadata-Version: 2.4
Name: replayx
Version: 0.4.3
Summary: Record and replay HTTP and HTTPS interactions for httpx. An async-native VCR for fast, deterministic tests.
Project-URL: Homepage, https://mkusiappiah.github.io/replayx/
Project-URL: Documentation, https://mkusiappiah.github.io/replayx/documentation.html
Project-URL: Repository, https://github.com/mkusiappiah/replayx
Project-URL: Issues, https://github.com/mkusiappiah/replayx/issues
Project-URL: Changelog, https://github.com/mkusiappiah/replayx/blob/main/CHANGELOG.md
Author-email: Michael Kusi-Appiah <appiah.michael@yahoo.com>
License: MIT License
        
        Copyright (c) 2026 Michael Kusi-Appiah
        
        Permission is hereby granted, free of charge, to any person obtaining a copy
        of this software and associated documentation files (the "Software"), to deal
        in the Software without restriction, including without limitation the rights
        to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
        copies of the Software, and to permit persons to whom the Software is
        furnished to do so, subject to the following conditions:
        
        The above copyright notice and this permission notice shall be included in all
        copies or substantial portions of the Software.
        
        THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
        IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
        FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
        AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
        LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
        OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
        SOFTWARE.
License-File: LICENSE
Keywords: async,asyncio,http,httpx,mock,pytest,record,replay,testing,vcr
Classifier: Development Status :: 4 - Beta
Classifier: Framework :: Pytest
Classifier: Intended Audience :: Developers
Classifier: License :: OSI Approved :: MIT License
Classifier: Operating System :: OS Independent
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.9
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Topic :: Software Development :: Testing
Classifier: Topic :: Software Development :: Testing :: Mocking
Classifier: Typing :: Typed
Requires-Python: >=3.9
Requires-Dist: httpx>=0.23
Provides-Extra: dev
Requires-Dist: mypy>=1.8; extra == 'dev'
Requires-Dist: pytest-asyncio>=0.23; extra == 'dev'
Requires-Dist: pytest>=7.4; extra == 'dev'
Requires-Dist: pyyaml>=6.0; extra == 'dev'
Requires-Dist: ruff>=0.5; extra == 'dev'
Requires-Dist: types-pyyaml; extra == 'dev'
Provides-Extra: yaml
Requires-Dist: pyyaml>=6.0; extra == 'yaml'
Description-Content-Type: text/markdown

# replayx

Record and replay HTTP and HTTPS interactions for [httpx](https://www.python-httpx.org/). Run your tests fast and offline.

[![CI](https://github.com/mkusiappiah/replayx/actions/workflows/ci.yml/badge.svg)](https://github.com/mkusiappiah/replayx/actions/workflows/ci.yml)
[![PyPI](https://img.shields.io/pypi/v/replayx)](https://pypi.org/project/replayx/)
[![Python versions](https://img.shields.io/pypi/pyversions/replayx)](https://pypi.org/project/replayx/)
[![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](LICENSE)

replayx saves real HTTP and HTTPS 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.

```python
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 |

## HTTPS and TLS

replayx works with `https://` URLs the same as `http://`. replayx hooks into httpx at the transport layer, below TLS, so the scheme is never a special case.

- On record, httpx makes the real TLS request and replayx captures the result.
- On replay, replayx serves the response from the cassette with no network and no TLS handshake, so certificates and expiry do not matter.
- Matching tracks the scheme and the correct default port (443 for https, 80 for http), so https and http to the same host stay distinct.

replayx hooks httpx, so requests made through other clients (requests, urllib, aiohttp) are not intercepted.

## Install

```bash
pip install replayx
```

Add YAML cassettes:

```bash
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.

```python
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.

```python
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`.

```python
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:

```bash
pytest --replayx-record=all
```

Set per-test defaults with the marker:

```python
import pytest

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

## Inline stubs, no recording

Sometimes you want to define responses in code instead of recording them. use_stubs patches httpx and serves responses from routes you declare.

```python
import httpx
from replayx import use_stubs

with use_stubs() as router:
    router.get("https://api.example.com/users").respond(json=[{"id": 1}])
    router.post("https://api.example.com/users").respond(201, json={"id": 2})

    with httpx.Client() as client:
        assert client.get("https://api.example.com/users").json() == [{"id": 1}]
```

A request that matches no route raises `UnhandledStubError`, so unmocked calls show up at once. Routes match on method plus scheme, host, port, and path. The query string is ignored. Each route counts its calls:

```python
with use_stubs() as router:
    route = router.get("https://api.example.com/ping").respond(text="pong")
    ...
    assert route.call_count == 1
```

## 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. |

```python
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.

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

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

### GraphQL requests

GraphQL sends every operation as a POST to one URL, so raw body matching breaks on formatting. The graphql matcher compares the operation name, the variables, and the query with whitespace collapsed.

```python
with use_cassette("cassettes/api.json", match_on=("method", "url", "graphql")):
    ...
```

## Redact secrets

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

```python
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.

```python
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.

```json
{
  "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:

```bash
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](LICENSE).
