Skip to main content

openapi-client-core

Shared runtime library for Python OpenAPI clients

CI PyPI version Python versions License: MIT

Vision

openapi-client-core provides battle-tested patterns for building Python OpenAPI clients. Instead of duplicating retry logic, error handling, and testing utilities across every client, share a common foundation.

Features:

  • 🔄 Composable transport layers (retry, pagination, error logging, auth)
  • 🔁 3 retry strategies (idempotent-only, rate-limit-aware, all-methods)
  • 🛡️ Error handling with RFC 7807 ProblemDetails and null field detection
  • 🔐 Multi-source authentication (param → env → .env → netrc)
  • 🧪 Testing utilities (fixtures, mocks, factories)
  • ⚡ Async-first with httpx

Installation

pip install openapi-client-core

Or with UV:

uv add openapi-client-core

Quick Start

Creating a Resilient Client

from openapi_client_core.transport import create_transport_stack
from openapi_client_core.auth import CredentialResolver
from your_generated_client import Client

# Resolve credentials from multiple sources
resolver = CredentialResolver()
api_key = resolver.resolve(
    param_value=None,  # Will check env vars and .env file
    env_var_name="MY_API_KEY",
    netrc_host="api.example.com",
)

# Create transport stack with retry, error logging, pagination
transport = create_transport_stack(
    base_url="https://api.example.com",
    retry_strategy="rate_limited",  # or "idempotent_only" or "all_methods"
    enable_pagination=True,
    enable_error_logging=True,
    enable_null_field_detection=True,
)

# Initialize your generated client with enhanced transport
client = Client(
    base_url="https://api.example.com",
    token=api_key,
    transport=transport,
)

Using the unwrap() Helper

from openapi_client_core.utils import unwrap
from openapi_client_core.exceptions import NotFoundError, ValidationError

# Raises specific exceptions on error
try:
    data = unwrap(client.get_resource(id=123))
    print(f"Got resource: {data}")
except NotFoundError:
    print("Resource not found")
except ValidationError as e:
    print(f"Validation failed: {e.problem_details}")

# Or return None on error
data = unwrap(client.get_resource(id=123), raise_on_error=False)
if data is None:
    print("Request failed")

Testing Your Client

import pytest
from openapi_client_core.testing import (
    mock_api_credentials,
    create_mock_response,
    create_error_response,
)

def test_client_handles_404(mock_api_credentials):
    """Test client gracefully handles 404 errors."""
    # Fixtures provided by openapi-client-core
    client = MyClient(**mock_api_credentials)

    # Mock a 404 response
    response = create_error_response("404")

    # Test your error handling
    with pytest.raises(NotFoundError):
        unwrap(response)

Why openapi-client-core?

Before

Every OpenAPI client duplicates the same patterns:

# In client A
class RateLimitAwareRetry(Retry):
    # 60 lines of retry logic...

# In client B
class RateLimitAwareRetry(Retry):
    # Same 60 lines duplicated...

# In client C
class RateLimitAwareRetry(Retry):
    # Same 60 lines again...

Problems:

  • ❌ Code duplication (hundreds of lines per client)
  • ❌ Bug fixes require updating every client
  • ❌ New clients don't benefit from lessons learned
  • ❌ Testing utilities re-implemented everywhere

After

Share the battle-tested core:

from openapi_client_core.transport import create_transport_stack

transport = create_transport_stack(
    base_url=base_url,
    retry_strategy="rate_limited",
    enable_pagination=True,
)

Benefits:

  • ✅ 35-40% less code in each client
  • ✅ Shared maintenance: Fix once, benefit everywhere
  • ✅ Battle-tested patterns: Learned from real-world usage
  • ✅ Fast development: New clients in <5 minutes

Core Components

Transport Layers

Composable async HTTP transport middleware:

from openapi_client_core.transport import (
    ErrorLoggingTransport,
    RetryTransport,
    PaginationTransport,
    CustomHeaderAuthTransport,
)

# Stack them in any order
transport = CustomHeaderAuthTransport(
    headers_dict={"api-key": "..."},
    wrapped_transport=RetryTransport(
        retry_strategy="idempotent_only",
        wrapped_transport=ErrorLoggingTransport(
            enable_null_field_detection=True,
            wrapped_transport=httpx.AsyncHTTPTransport(),
        ),
    ),
)

# Or use the factory for common patterns
from openapi_client_core.transport import create_transport_stack

transport = create_transport_stack(
    base_url="https://api.example.com",
    retry_strategy="rate_limited",
    custom_auth_headers={"api-key": "..."},
)

Retry Strategies

Three retry strategies for different API behaviors:

  1. IdempotentOnlyRetry (safest)

    • Retries only GET, HEAD, OPTIONS on 5xx errors
    • Use when duplicates are dangerous
  2. RateLimitAwareRetry (recommended)

    • Retries all methods on 429 (rate limit)
    • Retries GET, HEAD, PUT, DELETE on 5xx
    • Best for modern REST APIs
  3. AllMethodsRetry (use with caution)

    • Retries everything
    • Only use with idempotency keys

Authentication

Multi-source credential resolution:

from openapi_client_core.auth import CredentialResolver

resolver = CredentialResolver()

# Checks in order: param → env var → .env file → ~/.netrc
api_key = resolver.resolve(
    param_value=None,
    env_var_name="API_KEY",
    netrc_host="api.example.com",
)

Custom header authentication:

from openapi_client_core.transport import CustomHeaderAuthTransport

transport = CustomHeaderAuthTransport(
    headers_dict={
        "api-auth-id": tenant_id,
        "api-auth-signature": tenant_name,
    },
    wrapped_transport=base_transport,
)

Error Handling

Structured exceptions with RFC 7807 ProblemDetails:

from openapi_client_core.errors import (
    APIError,
    UnauthorizedError,   # 401
    ForbiddenError,      # 403
    NotFoundError,       # 404
    ValidationError,     # 422
    BadRequestError,     # 400
    ConflictError,       # 409
    RateLimitError,      # 429
    ServerError,         # 5xx
    raise_for_status,
    detect_null_fields,
)
import httpx

# Automatic RFC 7807 parsing
response = httpx.get("https://api.example.com/users/123")
try:
    raise_for_status(response)
except NotFoundError as e:
    # Access structured error details
    print(e.status_code)  # 404
    print(e.problem_detail.title)  # "Resource Not Found"
    print(e.problem_detail.detail)  # "User with id 123 does not exist"
    print(e.problem_detail.type)  # "https://api.example.com/problems/not-found"

# ValidationError with structured errors
try:
    raise_for_status(response)
except ValidationError as e:
    print(e.validation_errors)  # [{"field": "email", "message": "Invalid"}]

# RateLimitError with retry timing
try:
    raise_for_status(response)
except RateLimitError as e:
    print(f"Rate limited. Retry after {e.retry_after} seconds")

# Detect null fields in response data
data = {"user": {"name": "John", "email": None}}
null_fields = detect_null_fields(data)
print(null_fields)  # ["user.email"]

Null field detection:

from openapi_client_core.errors import detect_null_fields, NullFieldError

# Detect null fields in API response
data = {
    "user": {
        "name": "John",
        "email": None,
        "address": {
            "city": None,
            "street": "123 Main St"
        }
    }
}

null_paths = detect_null_fields(data)
print(null_paths)  # ["user.email", "user.address.city"]

# Raise helpful error for null fields
if null_paths:
    raise NullFieldError(
        message=f"Found {len(null_paths)} null field(s): {null_paths}",
        field_path=null_paths[0]
    )

Testing Utilities

Pre-built fixtures and factories:

import pytest
from openapi_client_core.testing import (
    mock_api_credentials,
    create_mock_response,
    create_error_response,
    create_paginated_mock_handler,
)

@pytest.fixture
def my_client(mock_api_credentials):
    return MyClient(**mock_api_credentials)

def test_pagination(my_client):
    handler = create_paginated_mock_handler([
        [{"id": 1}, {"id": 2}],  # Page 1
        [{"id": 3}, {"id": 4}],  # Page 2
    ])
    # Test with mock handler...

Documentation

Full documentation available at: https://dougborg.github.io/openapi-client-core

Development

Setup

# Clone repository
git clone https://github.com/dougborg/openapi-client-core.git
cd openapi-client-core

# Install UV (if not already installed)
curl -LsSf https://astral.sh/uv/install.sh | sh

# Install dependencies
uv sync --all-extras

Running Tests

# Run all tests
uv run pytest

# Run with coverage
uv run pytest --cov=openapi_client_core --cov-report=term-missing

# Run only unit tests
uv run pytest -m unit

# Run specific test file
uv run pytest tests/unit/test_transport/test_retry.py

Code Quality

# Format code
uv run ruff format .

# Lint code
uv run ruff check .

# Type check
uv run ty check

Documentation

# Build docs locally
uv run mkdocs build

# Serve docs locally
uv run mkdocs serve
# Open http://127.0.0.1:8000

Contributing

Contributions are welcome! Please see CONTRIBUTING.md for guidelines.

Areas for Contribution

  • 🐛 Bug fixes and improvements
  • 📚 Documentation enhancements
  • ✨ New transport middleware patterns
  • 🧪 Additional testing utilities
  • 🎨 Usage examples and tutorials

Clients Using This Library

License

MIT License - see LICENSE for details.

Changelog

See CHANGELOG.md for version history.

Acknowledgments

This library extracts battle-tested patterns from:

Special thanks to the OpenAPI and httpx communities for providing excellent foundations.

Metadata

Release files for openapi-client-core 0.1.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for openapi-client-core 0.1.0
File Size Uploaded Provenance
openapi_client_core-0.1.0.tar.gz 27.8 kB With provenance Details

Built distribution (wheel)

Table of built distributions (wheels) for openapi-client-core 0.1.0
File Interpreter ABI Platform
openapi_client_core-0.1.0-py3-none-any.whl Python 3 none any Details

Total release size: 48.8 kB

Release files / openapi_client_core-0.1.0.tar.gz

Download URL openapi_client_core-0.1.0.tar.gz
Size 27.8 kB
Tags Source
SHA-256 checksum
How to use checksums
744fd1d0bbb72390228ee7e362cb750f2efc6c8b722137b3e0f8baba489c46b6
BLAKE2b-256 checksum
How to use checksums
b568faaa74b5ecfac3cf88dc188d23cea79d072a22f126bc0b38c88c121a0a20
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/6.1.0 CPython/3.13.7

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Nov 18, 2025.

Transparency log

Release files / openapi_client_core-0.1.0-py3-none-any.whl

Download URL openapi_client_core-0.1.0-py3-none-any.whl
Size 20.9 kB
Tags Python 3
SHA-256 checksum
How to use checksums
e8c07b3c311eb4c0045b56b3e3ab8ccc9a553de49a0a754173ab5d84caef4430
BLAKE2b-256 checksum
How to use checksums
793c911a1ae0782fbbc118fef8a3e4a7dff41dbde2abf84a1f88d75109b27940
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/6.1.0 CPython/3.13.7

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Nov 18, 2025.

Transparency log

Security for openapi-client-core 0.1.0

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

Full provenance

All 2 files in this release have been uploaded with provenance metadata.

Select from 2 files

Attestation for openapi_client_core-0.1.0.tar.gz

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Nov 18, 2025.

Transparency log

A note on package security

PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.

Release history Release notifications | RSS feed

This release

0.1.0 This release

2 release files Full provenance

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page