Metadata-Version: 2.4
Name: grpcvcr
Version: 0.1.1
Summary: Record and replay gRPC interactions for testing
Project-URL: Homepage, https://github.com/tboser/grpcvcr
Project-URL: Documentation, https://grpcvcr.readthedocs.io
Project-URL: Repository, https://github.com/tboser/grpcvcr
Project-URL: Changelog, https://github.com/tboser/grpcvcr/releases
Author: Thomas
License-Expression: MIT
License-File: LICENSE
Classifier: Development Status :: 3 - Alpha
Classifier: Framework :: Pytest
Classifier: Intended Audience :: Developers
Classifier: License :: OSI Approved :: MIT License
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Topic :: Software Development :: Libraries :: Python Modules
Classifier: Topic :: Software Development :: Testing
Classifier: Typing :: Typed
Requires-Python: >=3.11
Requires-Dist: grpcio>=1.50.0
Requires-Dist: protobuf>=4.21.0
Requires-Dist: pyyaml>=6.0
Provides-Extra: dev
Requires-Dist: grpcio-tools>=1.50.0; extra == 'dev'
Requires-Dist: mypy>=1.8.0; extra == 'dev'
Requires-Dist: pre-commit>=3.0.0; extra == 'dev'
Requires-Dist: pyright>=1.1.350; extra == 'dev'
Requires-Dist: pytest-asyncio>=0.21.0; extra == 'dev'
Requires-Dist: pytest-cov>=4.0.0; extra == 'dev'
Requires-Dist: pytest-examples>=0.0.14; extra == 'dev'
Requires-Dist: pytest>=7.0.0; extra == 'dev'
Requires-Dist: ruff>=0.4.0; extra == 'dev'
Requires-Dist: types-protobuf>=4.21.0; extra == 'dev'
Requires-Dist: types-pyyaml>=6.0; extra == 'dev'
Provides-Extra: docs
Requires-Dist: griffe-typingdoc>=0.2.0; extra == 'docs'
Requires-Dist: mkdocs-material>=9.5.0; extra == 'docs'
Requires-Dist: mkdocs>=1.6.0; extra == 'docs'
Requires-Dist: mkdocstrings-python>=1.8.0; extra == 'docs'
Description-Content-Type: text/markdown

# grpcvcr

[![CI](https://github.com/tboser/grpcvcr/actions/workflows/ci.yml/badge.svg)](https://github.com/tboser/grpcvcr/actions/workflows/ci.yml)
[![codecov](https://codecov.io/gh/tboser/grpcvcr/graph/badge.svg)](https://codecov.io/gh/tboser/grpcvcr)
[![PyPI version](https://img.shields.io/pypi/v/grpcvcr.svg)](https://pypi.org/project/grpcvcr/)
[![Python versions](https://img.shields.io/pypi/pyversions/grpcvcr.svg)](https://pypi.org/project/grpcvcr/)

**Record and replay gRPC interactions for testing** - like [VCR.py](https://vcrpy.readthedocs.io/) but for gRPC.

## Installation

```bash
pip install grpcvcr
```

## Quick Start

```python
from grpcvcr import recorded_channel, RecordMode

# Record on first run, replay on subsequent runs
with recorded_channel("tests/cassettes/my_test.yaml", "localhost:50051") as channel:
    stub = MyServiceStub(channel)
    response = stub.GetUser(GetUserRequest(id=1))
    assert response.name == "Alice"
```

## Features

- **Record & Replay**: Automatically record gRPC interactions and replay them in tests
- **All RPC Types**: Supports unary, server streaming, client streaming, and bidirectional streaming
- **Async Support**: Full support for `grpc.aio` async clients
- **pytest Integration**: Built-in fixtures and markers for easy test integration
- **Flexible Matching**: Match requests by method, metadata, body, or custom logic
- **Multiple Formats**: Store cassettes as YAML or JSON

## Recording Modes

| Mode | Description |
|------|-------------|
| `NEW_EPISODES` | Play existing, record new (default) |
| `NONE` | Playback only - fail if no match |
| `ALL` | Always record, overwrite existing |
| `ONCE` | Record if cassette missing, then playback |

## Async Support

```python
from grpcvcr import AsyncRecordingChannel, Cassette, RecordMode

cassette = Cassette("test.yaml", record_mode=RecordMode.ALL)

async with AsyncRecordingChannel(cassette, "localhost:50051") as recording:
    stub = MyServiceStub(recording.channel)
    response = await stub.GetUser(GetUserRequest(id=1))
```

## pytest Integration

```python
import pytest
from grpcvcr import RecordMode

@pytest.mark.grpcvcr(cassette="user_test.yaml", record_mode=RecordMode.NONE)
def test_get_user(grpcvcr_cassette):
    from grpcvcr import RecordingChannel

    with RecordingChannel(grpcvcr_cassette, "localhost:50051") as rc:
        stub = MyServiceStub(rc.channel)
        response = stub.GetUser(GetUserRequest(id=1))
        assert response.name == "Alice"
```

Run in record mode:

```bash
pytest --grpcvcr-record=new_episodes
```

Run in strict playback mode (CI):

```bash
pytest --grpcvcr-record=none
```

## Request Matching

```python
from grpcvcr import recorded_channel, MethodMatcher, RequestMatcher, MetadataMatcher

# Match on method + request body
matcher = MethodMatcher() & RequestMatcher()

with recorded_channel("test.yaml", "localhost:50051", match_on=matcher) as channel:
    ...

# Ignore certain metadata keys
matcher = MethodMatcher() & MetadataMatcher(ignore_keys=["x-request-id"])
```

## License

MIT
