accord-contracts 0.1.1
Add your description here
Requires Python: >=3.13

# Accord

Accord is a Python library for consumer-driven contract testing between web services, you define a Contract once using Pydantic models and an [`@endpoint`](https://git.critchlow.net/brodycritchlow/accord/src/commit/3d7d18a8322b223e2744d8586f778887b90d7a26/core/contract.py#L29-L55) decorator, which then automatically takes care of the rest for you:

- Spins up a mock http-server using [werkzeug](https://github.com/pallets/werkzeug)
- Generates a [pact](https://github.com/pact-foundation) compatible JSON contract file

This ensures both sides of a service boundary stay in sync without requiring a shared test environment.

## Why not Pact?

Pact is definitely the industry-standard library, and if you are working with multiple different languages or environments then it is the better choice. But if your stack is Python-only, Pact comes with a lot of overhead; that you may not need:

- A separate DSL
  - Pact has its own way of defining contracts that lives outside your existing code. Accord uses the Pydantic models you're already writing.
- The broker
  - Pact strongly encourages (and in practice, requires) a Pact broker to share contracts between teams. Accord generates a JSON file and you share it however you want: a shared repo, a CI artifact, or S3.
- Usage time
  - Getting Pact fully set up across two services takes time. Accord is designed to be useful in an afternoon.

Accord **is not** supposed to replace Pact, it is for simplicity and less overhead. In reality, Pact is more useful across the board.

## Learn how to use Accord

Before you learn how to use Accord, here are some simple diagrams that showcase how our Contracts & Systems work.

### Contracts

![Contract Flow](https://mermaid.ink/img/c2VxdWVuY2VEaWFncmFtCiAgICBwYXJ0aWNpcGFudCBEZXYgYXMgRGV2ZWxvcGVyCiAgICBwYXJ0aWNpcGFudCBFbmRwb2ludCBhcyBAZW5kcG9pbnQKICAgIHBhcnRpY2lwYW50IENvbnRyYWN0IGFzIENvbnRyYWN0IENsYXNzCgogICAgRGV2LT4-RW5kcG9pbnQ6IERlY29yYXRlcyBtZXRob2Qgd2l0aCBodHRwX21ldGhvZCBhbmQgcGF0aAogICAgRW5kcG9pbnQtPj5FbmRwb2ludDogRXh0cmFjdHMgcmV0dXJuIHR5cGUgZnJvbSB0eXBlIGhpbnRzCiAgICBFbmRwb2ludC0-PkVuZHBvaW50OiBBdHRhY2hlcyBFbmRwb2ludE1ldGFkYXRhIHRvIGZ1bmN0aW9uCiAgICBEZXYtPj5Db250cmFjdDogRGVmaW5lcyBDb250cmFjdCBzdWJjbGFzcwogICAgQ29udHJhY3QtPj5Db250cmFjdDogX19pbml0X3N1YmNsYXNzX18gcnVucwogICAgQ29udHJhY3QtPj5Db250cmFjdDogU2NhbnMgZm9yIGZ1bmN0aW9ucyB3aXRoIGFjY29yZF9lbmRwb2ludAogICAgQ29udHJhY3QtPj5Db250cmFjdDogUmVnaXN0ZXJzIGVhY2ggaW50byBpbnRlcmFjdGlvbnMgZGljdA==)

### Consumer Flow

![Consumer Flow](https://mermaid.ink/img/c2VxdWVuY2VEaWFncmFtCiAgICBwYXJ0aWNpcGFudCBDb25zdW1lcgogICAgcGFydGljaXBhbnQgTW9ja1NlcnZlciBhcyBBY2NvcmQgTW9ja1NlcnZlcgogICAgcGFydGljaXBhbnQgRmlsZSBhcyBhY2NvcmRzLyouanNvbgoKICAgIENvbnN1bWVyLT4-TW9ja1NlcnZlcjogTW9ja1NlcnZlcihDb250cmFjdCkKICAgIE1vY2tTZXJ2ZXItPj5Nb2NrU2VydmVyOiBSZWdpc3RlciByb3V0ZXMgZnJvbSBpbnRlcmFjdGlvbnMKICAgIE1vY2tTZXJ2ZXItPj5Nb2NrU2VydmVyOiBTdGFydCBXZXJremV1ZyBzZXJ2ZXIgaW4gYmFja2dyb3VuZCB0aHJlYWQKICAgIENvbnN1bWVyLT4-TW9ja1NlcnZlcjogZ2l2ZW4oImdldF91c2VyIikuZXhhbXBsZShVc2VyUmVzcG9uc2UoLi4uKSkKICAgIE1vY2tTZXJ2ZXItPj5Nb2NrU2VydmVyOiBTdG9yZSBleGFtcGxlIGluIGV4YW1wbGVzIGRpY3QKICAgIENvbnN1bWVyLT4-TW9ja1NlcnZlcjogSFRUUCBHRVQgL3VzZXJzLzEKICAgIE1vY2tTZXJ2ZXItPj5Nb2NrU2VydmVyOiBNYXRjaCByb3V0ZSwgbG9vayB1cCBleGFtcGxlCiAgICBNb2NrU2VydmVyLT4-Q29uc3VtZXI6IFJldHVybiBleGFtcGxlIGFzIEpTT04gcmVzcG9uc2UKICAgIENvbnN1bWVyLT4-Q29uc3VtZXI6IEFzc2VydCByZXNwb25zZQogICAgQ29uc3VtZXItPj5Nb2NrU2VydmVyOiBFeGl0IGNvbnRleHQgbWFuYWdlcgogICAgTW9ja1NlcnZlci0-PkZpbGU6IHdyaXRlX2NvbnRyYWN0KCkg4oCUIFBhY3QtY29tcGF0aWJsZSBKU09OCiAgICBNb2NrU2VydmVyLT4-TW9ja1NlcnZlcjogU2h1dGRvd24gc2VydmVy)

### Producer Verification

![Producer Verification Flow](https://mermaid.ink/img/c2VxdWVuY2VEaWFncmFtCiAgICBwYXJ0aWNpcGFudCBGaWxlIGFzIGFjY29yZHMvKi5qc29uCiAgICBwYXJ0aWNpcGFudCBWZXJpZmllciBhcyBBY2NvcmQgQ29udHJhY3RWZXJpZmllcgogICAgcGFydGljaXBhbnQgUHJvdmlkZXIKCiAgICBWZXJpZmllci0-PkZpbGU6IHJlYWRfY29udHJhY3QoKQogICAgRmlsZS0-PlZlcmlmaWVyOiBQYXJzZWRDb250cmFjdCB3aXRoIGludGVyYWN0aW9ucwogICAgbG9vcCBGb3IgZWFjaCBpbnRlcmFjdGlvbgogICAgICAgIFZlcmlmaWVyLT4-UHJvdmlkZXI6IEhUVFAgcmVxdWVzdCB1c2luZyBpbnRlcmFjdGlvbiBtZXRob2QgYW5kIHBhdGgKICAgICAgICBQcm92aWRlci0-PlZlcmlmaWVyOiBBY3R1YWwgcmVzcG9uc2UKICAgICAgICBWZXJpZmllci0-PlZlcmlmaWVyOiBQYXJzZSByZXNwb25zZSBib2R5CiAgICAgICAgbG9vcCBGb3IgZWFjaCBmaWVsZCBpbiBtYXRjaGluZ1J1bGVzCiAgICAgICAgICAgIFZlcmlmaWVyLT4-VmVyaWZpZXI6IE1hcCBKU09OIHR5cGUgdG8gUHl0aG9uIHR5cGUKICAgICAgICAgICAgVmVyaWZpZXItPj5WZXJpZmllcjogaXNpbnN0YW5jZSBjaGVjawogICAgICAgICAgICBhbHQgVHlwZSBtaXNtYXRjaAogICAgICAgICAgICAgICAgVmVyaWZpZXItPj5WZXJpZmllcjogUmFpc2UgVmFsdWVFcnJvcgogICAgICAgICAgICBlbmQKICAgICAgICBlbmQKICAgIGVuZAogICAgVmVyaWZpZXItPj5WZXJpZmllcjogQWxsIGludGVyYWN0aW9ucyB2ZXJpZmllZA==)

Now with all of those flows out of the way, the example may make more sense that we provide here.

A basic contract is defined as such:

```py
from pydantic import BaseModel
from core.contract import Contract, endpoint

class UserResponse(BaseModel):
    id: int
    name: str

class GetUserContract(Contract):
    consumer = "order-service"
    producer = "user-service"

    @endpoint(http_method="GET", path="/users/{id}")
    def get_user(self, user_id: int) -> UserResponse: ...
```

Once we have this contract defined, we use it to spin up a MockServer to test the Consumer:

```py
import httpx
from server.mock import MockServer

with MockServer(GetUserContract) as server:
    server.given("get_user").example(UserResponse(id=1, name="Alice"))

    response = httpx.get("http://127.0.0.1:5000/users/1")
    assert response.status_code == 200
    assert response.json()["name"] == "Alice"
```

This writes the contract to `./accords/` once the ContextManager exits. The other side of the testing flow is **producer**, which you can do as such:

```py
from pathlib import Path
from verification.verifier import ContractVerifier

# This assumes your server is running on 8080

verifier = ContractVerifier(
    contract_path=Path("accords/order-service-user-service.json"),
    base_url="http://127.0.0.1:8080",
)
verifier.verify()
```