openapi-client-core
Shared runtime library for Python OpenAPI clients

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
resolver = CredentialResolver()
api_key = resolver.resolve(
param_value=None,
env_var_name="MY_API_KEY",
netrc_host="api.example.com",
)
transport = create_transport_stack(
base_url="https://api.example.com",
retry_strategy="rate_limited",
enable_pagination=True,
enable_error_logging=True,
enable_null_field_detection=True,
)
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
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}")
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."""
client = MyClient(**mock_api_credentials)
response = create_error_response("404")
with pytest.raises(NotFoundError):
unwrap(response)
Why openapi-client-core?
Before
Every OpenAPI client duplicates the same patterns:
class RateLimitAwareRetry(Retry):
class RateLimitAwareRetry(Retry):
class RateLimitAwareRetry(Retry):
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,
)
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(),
),
),
)
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:
-
IdempotentOnlyRetry (safest)
- Retries only GET, HEAD, OPTIONS on 5xx errors
- Use when duplicates are dangerous
-
RateLimitAwareRetry (recommended)
- Retries all methods on 429 (rate limit)
- Retries GET, HEAD, PUT, DELETE on 5xx
- Best for modern REST APIs
-
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()
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,
ForbiddenError,
NotFoundError,
ValidationError,
BadRequestError,
ConflictError,
RateLimitError,
ServerError,
raise_for_status,
detect_null_fields,
)
import httpx
response = httpx.get("https://api.example.com/users/123")
try:
raise_for_status(response)
except NotFoundError as e:
print(e.status_code)
print(e.problem_detail.title)
print(e.problem_detail.detail)
print(e.problem_detail.type)
try:
raise_for_status(response)
except ValidationError as e:
print(e.validation_errors)
try:
raise_for_status(response)
except RateLimitError as e:
print(f"Rate limited. Retry after {e.retry_after} seconds")
data = {"user": {"name": "John", "email": None}}
null_fields = detect_null_fields(data)
print(null_fields)
Null field detection:
from openapi_client_core.errors import detect_null_fields, NullFieldError
data = {
"user": {
"name": "John",
"email": None,
"address": {
"city": None,
"street": "123 Main St"
}
}
}
null_paths = detect_null_fields(data)
print(null_paths)
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}],
[{"id": 3}, {"id": 4}],
])
Documentation
Full documentation available at: https://dougborg.github.io/openapi-client-core
Development
Setup
git clone https://github.com/dougborg/openapi-client-core.git
cd openapi-client-core
curl -LsSf https://astral.sh/uv/install.sh | sh
uv sync --all-extras
Running Tests
uv run pytest
uv run pytest --cov=openapi_client_core --cov-report=term-missing
uv run pytest -m unit
uv run pytest tests/unit/test_transport/test_retry.py
Code Quality
uv run ruff format .
uv run ruff check .
uv run ty check
Documentation
uv run mkdocs build
uv run mkdocs serve
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.