Lightweight consumer-driven contract testing for microservices APIs
Project description
:handshake: pactship
Broker-less contract testing for microservices
Define contracts. Verify providers. Catch breaking changes before they ship.
Quick Start | CLI | Matchers | API | Architecture
Why This Exists
Microservices break in production when providers change their APIs without telling consumers. The consumer expects GET /users/1 to return { id, name, email } -- the provider ships a rename from name to full_name and three downstream services crash at 2 AM.
Existing contract testing tools solve this -- but they bring heavyweight infrastructure with them. Pact JVM needs a broker server. Spring Cloud Contract requires a JVM toolchain. Both demand CI/CD plumbing that takes longer to set up than the contracts themselves.
pactship is a zero-infrastructure, file-based contract testing tool for Python. Write contracts in YAML or JSON, verify them against live providers with async HTTP, diff versions to detect breaking changes, and store everything locally -- no broker server needed, no background processes, no Docker containers.
- No infrastructure -- contracts live as files in your repo, verified locally or in CI
- Fluent Python DSL -- build contracts programmatically with type-checked builders
- 16 matcher types -- from exact match to regex, UUID, email, ISO dates, nullable, and range
- Breaking change detection -- diff two contract versions with breaking/non-breaking classification
Requirements
- Python 3.10+
- Dependencies:
click,pyyaml,jsonschema,rich,httpx
Quick Start
pip install pactship
Define a Contract (YAML)
consumer: order-service
provider: user-api
interactions:
- description: Get user by ID
request:
method: GET
path: /users/1
response:
status: 200
body:
id: 1
name: Alice
email: alice@example.com
Define a Contract (Python DSL)
from pactship import ContractBuilder, InteractionBuilder
from pactship import like, email_match, integer_match
contract = (
ContractBuilder("order-service", "user-api")
.add_interaction(
InteractionBuilder("Get user by ID")
.given("user 1 exists")
.with_request("GET", "/users/1")
.will_respond_with(200)
.with_response_body(
{"id": 1, "name": "Alice", "email": "alice@example.com"},
matchers={
"body.id": integer_match(),
"body.name": like("string"),
"body.email": email_match(),
},
)
.build()
)
.build()
)
Verify Against a Provider
pactship verify contract.yaml http://localhost:8080
Detect Breaking Changes
pactship diff old-contract.yaml new-contract.yaml
Publish to Local Broker
pactship publish contract.yaml --broker-dir .pactship
pactship list --broker-dir .pactship
How It Works
Define Verify Diff Report
────────── ────────── ────────── ──────────
YAML/JSON → Provider → Version A → Breaking
or DSL Verification vs B changes
(async HTTP) classified
- Define -- write contracts as YAML/JSON files or build them with the fluent Python DSL
- Verify -- run contracts against a live provider using async HTTP via
httpx - Diff -- compare two contract versions to detect breaking vs non-breaking changes
- Report -- get results in JSON, JUnit XML, Markdown, or TAP format
Features
Contract Definition
- Fluent DSL --
ContractBuilderandInteractionBuilderwith method chaining - YAML and JSON -- read and write contracts in both formats via
load_contract/save_contract - Provider states -- define preconditions with
.given("user 1 exists") - Request/response specs -- method, path, headers, query params, body, status code
Matcher System (16 Types)
- Type matching --
like("string")matches any string,integer_match()matches any int - Regex patterns --
regex(r"\d{3}-\d{4}")for custom format validation - Structural matchers --
array_like(min_len),each_like(example)for arrays - Domain matchers --
email_match(),uuid_match(),iso_date(),iso_datetime() - Constraint matchers --
range_match(0, 100),any_of(["a", "b"]),nullable("string")
Verification
- Async HTTP verification -- verify contracts against running providers using
httpx - Mock provider -- in-process mock for consumer-side testing without a real server
- Request matching -- method, path, headers, query params validated against spec
- Matcher evaluation -- response body verified against all declared matchers
- Provider state setup -- optional setup URL for test data preparation
- Timeout configuration -- per-verification timeout control
Breaking Change Detection
- Contract diffing --
diff_contracts(old, new)returns a structured diff report - Change classification -- each change tagged as
breakingornon-breaking - Interaction-level diff -- detects added, removed, and modified interactions
- Field-level diff -- tracks changes to individual request/response fields
Local Broker
- Filesystem-based storage -- contracts stored as files in a configurable directory
- Versioning -- publish contracts with version numbers, retrieve specific versions
- Verification history -- track which provider versions were verified against which contracts
- No server needed -- everything runs locally, works offline, no network dependency
Contract Linting
- REST best practices -- validate path naming, HTTP method usage, status codes
- Custom lint rules -- extensible linting with severity-based issue reporting
- Pre-publish validation -- catch contract quality issues before sharing
OpenAPI Import
- OpenAPI 3.x conversion -- convert OpenAPI specs to pactship contracts automatically
- Path and method extraction -- generates interactions from OpenAPI path definitions
- Response schema mapping -- maps OpenAPI response schemas to pactship response specs
Code Generation
- CRUD generator --
generate_crud_contract()creates full CRUD contracts from resource specs - Endpoint generator --
generate_from_endpoints()builds contracts from endpoint definitions - Customizable templates -- configure generated interactions per HTTP method
Service Graph
- Dependency visualization -- build service dependency graphs from contract sets
- Mermaid diagram output -- generate Mermaid diagrams for documentation
- Cycle detection -- identify circular dependencies between services
Compatibility Matrix
- Version tracking --
CompatibilityMatrixtracks which consumer/provider versions work together - Matrix queries -- check compatibility between specific version pairs
- History management -- add, query, and export compatibility records
Reporting
- JSON reports -- structured verification results as JSON
- JUnit XML -- integrate with CI systems expecting JUnit format
- Markdown reports -- human-readable reports for PR comments
- TAP output -- Test Anything Protocol for pipeline integration
Configuration
- File-based config --
.pactship.yamlor.pactship.jsonproject configuration - Environment variables --
PACTSHIP_BROKER_DIR,PACTSHIP_TIMEOUT, etc. - Priority ordering -- env vars override file config, file config overrides defaults
Statistics
- Method distribution -- analyze HTTP method usage across contracts
- Path coverage -- track which API paths are covered by contracts
- Complexity metrics -- measure contract complexity and matcher density
Lifecycle Hooks
- Before/after verification -- run custom logic around verification cycles
- Setup/teardown -- provider state preparation and cleanup
- Hook registration -- register hooks via the
HookRegistry
Contract Transformation
- Path rewriting -- transform contract paths for different environments
- Header injection -- add/modify headers across all interactions
- Body transformation -- apply transforms to request/response bodies
Filtering
- Interaction filters -- filter by HTTP method, path pattern, or description
- Tag-based filtering -- filter contracts by metadata tags
- Composable filters -- combine multiple filters with AND/OR logic
CLI Commands
# Validate a contract file
pactship validate contract.yaml
# Verify against a live provider
pactship verify contract.yaml http://localhost:8080 \
--timeout 30 \
--header "Authorization:Bearer token" \
--setup-url http://localhost:8080/_setup \
--output report.json
# Diff two contract versions
pactship diff v1/contract.yaml v2/contract.yaml
# Publish to local broker
pactship publish contract.yaml \
--broker-dir .pactship \
--version 1.0.0 \
--tag production
# List contracts in broker
pactship list --broker-dir .pactship
# Convert between formats
pactship convert contract.yaml contract.json
| Command | Description |
|---|---|
pactship validate <file> |
Validate contract file syntax and structure |
pactship verify <file> <url> |
Verify contract against a running provider |
pactship diff <old> <new> |
Compare two contract versions for breaking changes |
pactship publish <file> |
Publish contract to local filesystem broker |
pactship list |
List all contracts stored in the broker |
pactship convert <in> <out> |
Convert between YAML and JSON formats |
Matchers
| Matcher | Description | Example |
|---|---|---|
exact(value) |
Exact value match | exact("hello") |
like(type) |
Type-based match | like("string") |
regex(pattern) |
Regex pattern match | regex(r"\d{3}-\d{4}") |
range_match(min, max) |
Numeric range constraint | range_match(0, 100) |
array_like(min_len) |
Array with minimum length | array_like(1) |
each_like(example) |
Each element matches structure | each_like({"id": 0}) |
any_of(values) |
One of allowed values | any_of(["a", "b"]) |
nullable(type) |
Null or specified type | nullable("string") |
iso_date() |
ISO 8601 date string | iso_date() |
iso_datetime() |
ISO 8601 datetime string | iso_datetime() |
uuid_match() |
UUID v4 format | uuid_match() |
email_match() |
Email address format | email_match() |
integer_match() |
Integer value | integer_match() |
decimal_match() |
Decimal number | decimal_match() |
boolean_match() |
Boolean value | boolean_match() |
string_match() |
String value | string_match() |
Programmatic API
Contract Building
from pactship import ContractBuilder, InteractionBuilder
from pactship import like, regex, integer_match, email_match
contract = (
ContractBuilder("order-service", "user-api")
.with_metadata({"version": "1.0.0"})
.add_interaction(
InteractionBuilder("Get user by ID")
.given("user 1 exists")
.with_request("GET", "/users/1")
.with_request_header("Accept", "application/json")
.will_respond_with(200)
.with_response_header("Content-Type", "application/json")
.with_response_body(
{"id": 1, "name": "Alice", "email": "alice@example.com"},
matchers={
"body.id": integer_match(),
"body.name": like("string"),
"body.email": email_match(),
},
)
.build()
)
.build()
)
Contract I/O
from pactship import save_contract, load_contract
# Save to YAML or JSON (auto-detected from extension)
save_contract(contract, "contracts/user-api.yaml")
# Load from file
loaded = load_contract("contracts/user-api.yaml")
Verification
from pactship import ProviderVerifier, MockProvider
# Verify against a live provider
verifier = ProviderVerifier(base_url="http://localhost:8080", timeout=30.0)
report = await verifier.verify(contract)
print(f"Passed: {report.success}")
for result in report.results:
print(f" {result.interaction}: {'PASS' if result.passed else 'FAIL'}")
# Use mock provider for consumer testing
mock = MockProvider(contract)
response = mock.handle_request("GET", "/users/1")
assert response.status == 200
Breaking Change Detection
from pactship import diff_contracts
diff = diff_contracts(old_contract, new_contract)
print(f"Breaking changes: {diff.has_breaking_changes}")
for change in diff.changes:
print(f" [{change.change_type}] {change.description}")
Local Broker
from pactship import ContractBroker
broker = ContractBroker(broker_dir=".pactship")
broker.publish(contract, version="1.0.0", tags=["production"])
contracts = broker.list_contracts()
specific = broker.get_contract("order-service", "user-api", version="1.0.0")
OpenAPI Import
from pactship.openapi import openapi_to_contracts
contracts = openapi_to_contracts("openapi.yaml", consumer="my-service")
for contract in contracts:
save_contract(contract, f"contracts/{contract.provider}.yaml")
Service Graph
from pactship import ServiceGraph
graph = ServiceGraph()
graph.add_contract(contract)
mermaid = graph.to_mermaid()
print(mermaid)
# graph TD
# order-service --> user-api
Contract Linting
from pactship import lint_contract
result = lint_contract(contract)
print(f"Passed: {result.passed}")
for issue in result.issues:
print(f" [{issue.severity}] {issue.rule}: {issue.message}")
Reporting
from pactship.reporting import (
report_json,
report_junit,
report_markdown,
report_tap,
)
# Generate reports in multiple formats
json_report = report_json(verification_report)
junit_xml = report_junit(verification_report)
markdown = report_markdown(verification_report)
tap_output = report_tap(verification_report)
Statistics
from pactship.stats import contract_stats
stats = contract_stats(contract)
print(f"Methods: {stats['method_distribution']}")
print(f"Paths: {stats['path_count']}")
print(f"Matchers: {stats['matcher_count']}")
Architecture
pactship/
__init__.py # Public API exports (54 symbols)
models.py # Core data models (Contract, Interaction, Matcher, etc.)
dsl.py # Fluent builder DSL (ContractBuilder, InteractionBuilder)
matchers.py # 16 matcher types (exact, like, regex, range, etc.)
validator.py # Contract structure validation
verifier.py # Async HTTP provider verification + MockProvider
contract_io.py # YAML/JSON serialization and deserialization
schema.py # JSON Schema generation from contracts
diff.py # Contract version diffing with change classification
broker.py # Filesystem-based contract broker with versioning
cli.py # Click CLI (validate, verify, diff, publish, list, convert)
config.py # File + env var configuration loading
generator.py # CRUD and endpoint-based contract generation
graph.py # Service dependency graph with Mermaid output
matrix.py # Consumer/provider compatibility matrix
openapi.py # OpenAPI 3.x to pactship contract conversion
linter.py # Contract linting with REST best practice rules
reporting.py # Multi-format reports (JSON, JUnit, Markdown, TAP)
stats.py # Contract statistics and complexity metrics
hooks.py # Lifecycle hook registry (before/after verification)
transform.py # Contract transformation (paths, headers, bodies)
filters.py # Interaction filtering (method, path, tags)
Data Flow
┌─────────────┐
│ YAML/JSON │
│ Contract │
└──────┬──────┘
│
┌────────────┼────────────┐
│ │ │
┌─────▼─────┐ ┌───▼───┐ ┌─────▼─────┐
│ Validator │ │ Diff │ │ Linter │
└─────┬─────┘ └───┬───┘ └─────┬─────┘
│ │ │
┌─────▼─────┐ ┌───▼───┐ ┌─────▼─────┐
│ Verifier │ │Report │ │ Issues │
│(async HTTP)│ │ │ │ │
└─────┬─────┘ └───────┘ └───────────┘
│
┌─────▼─────┐
│ Report │
│JSON/JUnit │
│ MD / TAP │
└───────────┘
CI/CD Integration
GitHub Actions
name: Contract Tests
on: [push, pull_request]
jobs:
contracts:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: "3.12"
- run: pip install pactship
- run: |
for f in contracts/*.yaml; do
pactship validate "$f"
done
- run: pactship diff contracts/v1.yaml contracts/v2.yaml || true
Pre-commit Hook
#!/bin/sh
for f in contracts/*.yaml; do
pactship validate "$f" || exit 1
done
License
MIT
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file pactship-0.1.0.tar.gz.
File metadata
- Download URL: pactship-0.1.0.tar.gz
- Upload date:
- Size: 72.1 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.14.3
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
f1352479d9e4e158012d704009ee072f8cf912ef8c367d358a805a7720a4e54d
|
|
| MD5 |
85697f7e91dfb6df21da01184f682d46
|
|
| BLAKE2b-256 |
6298b20e76cfe3a430da0e4de40e42799ac95404cb1f871aa41f7ffdf4f7b400
|
File details
Details for the file pactship-0.1.0-py3-none-any.whl.
File metadata
- Download URL: pactship-0.1.0-py3-none-any.whl
- Upload date:
- Size: 47.0 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.14.3
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
5946b00d854060db18842a6b0c08643158572d92310c1c92eac5b765582c1765
|
|
| MD5 |
81308bbd496302377f28a4f9d4dcef9b
|
|
| BLAKE2b-256 |
6c1c10cbc295935ccbdf8455fca518527974e7ff14a221ec01d6ba12eeec5bd8
|