Metadata-Version: 2.4
Name: pactship
Version: 0.1.0
Summary: Lightweight consumer-driven contract testing for microservices APIs
Author-email: JSLEEKR <93jslee@gmail.com>
License: MIT
Keywords: contract-testing,api,microservices,pact,testing
Classifier: Development Status :: 3 - Alpha
Classifier: Intended Audience :: Developers
Classifier: License :: OSI Approved :: MIT License
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Topic :: Software Development :: Testing
Requires-Python: >=3.10
Description-Content-Type: text/markdown
License-File: LICENSE
Requires-Dist: click>=8.0
Requires-Dist: pyyaml>=6.0
Requires-Dist: jsonschema>=4.0
Requires-Dist: rich>=13.0
Requires-Dist: httpx>=0.24
Provides-Extra: dev
Requires-Dist: pytest>=7.0; extra == "dev"
Requires-Dist: pytest-cov>=4.0; extra == "dev"
Requires-Dist: pytest-asyncio>=0.21; extra == "dev"
Requires-Dist: respx>=0.20; extra == "dev"
Dynamic: license-file

<div align="center">

# :handshake: pactship

### Broker-less contract testing for microservices

[![GitHub Stars](https://img.shields.io/github/stars/JSLEEKR/pactship?style=for-the-badge&logo=github&color=yellow)](https://github.com/JSLEEKR/pactship/stargazers)
[![License](https://img.shields.io/badge/license-MIT-blue?style=for-the-badge)](LICENSE)
[![Python](https://img.shields.io/badge/Python-3.10+-3776AB?style=for-the-badge&logo=python&logoColor=white)](https://python.org)
[![Tests](https://img.shields.io/badge/tests-525%20passing-brightgreen?style=for-the-badge)](#)

<br/>

**Define contracts. Verify providers. Catch breaking changes before they ship.**

[Quick Start](#quick-start) | [CLI](#cli-commands) | [Matchers](#matchers) | [API](#programmatic-api) | [Architecture](#architecture)

</div>

---

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

```bash
pip install pactship
```

### Define a Contract (YAML)

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

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

```bash
pactship verify contract.yaml http://localhost:8080
```

### Detect Breaking Changes

```bash
pactship diff old-contract.yaml new-contract.yaml
```

### Publish to Local Broker

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

1. **Define** -- write contracts as YAML/JSON files or build them with the fluent Python DSL
2. **Verify** -- run contracts against a live provider using async HTTP via `httpx`
3. **Diff** -- compare two contract versions to detect breaking vs non-breaking changes
4. **Report** -- get results in JSON, JUnit XML, Markdown, or TAP format

## Features

### Contract Definition
- **Fluent DSL** -- `ContractBuilder` and `InteractionBuilder` with 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 `breaking` or `non-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** -- `CompatibilityMatrix` tracks 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.yaml` or `.pactship.json` project 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

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

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

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

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

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

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

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

```python
from pactship import ServiceGraph

graph = ServiceGraph()
graph.add_contract(contract)
mermaid = graph.to_mermaid()
print(mermaid)
# graph TD
#   order-service --> user-api
```

### Contract Linting

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

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

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

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

```bash
#!/bin/sh
for f in contracts/*.yaml; do
  pactship validate "$f" || exit 1
done
```

## License

MIT
