Generate a Python SDK from your OpenAPI spec
Octri reads your OpenAPI document and writes a Python client with typed models and both a blocking and an asyncio surface, publishes it to PyPI under your own package name, and rebuilds it every time the spec changes.
pip install acme-apiThe client they actually get
What your users write once the Python package is installed.
Two call-site shapes come out of the same spec. class-namespaced is the default: a client object whose operations are grouped by resource, which is the surface every widely used SDK ships and the one your own reference docs already assume. functions is the override: no client object, one configure call, and each operation called directly. It is a single setting, per language, and nothing else about the client changes with it.
Every paginated operation gets a _paginated companion that yields items one at a time, and pages are fetched as the loop asks for them. As free functions, the coroutine keeps the plain name and the blocking twin carries a _sync suffix.
Published under your own PyPI account. The distribution name is what users type after pip install, and it is normalised, so acme_api and acme-api are the same name to the index.
import os
from acme_api import Acme, AsyncAcme, ClientAuthConfig, ClientConfig
config = ClientConfig(
base_url="https://api.acme.com/v1",
auth=ClientAuthConfig(bearer=os.environ["ACME_TOKEN"]),
)
# Acme blocks, so it works in a script, a Django view or a notebook.
acme = Acme(config)
for user in acme.users.list_paginated(limit=100):
print(user.id)
# AsyncAcme is the same surface on asyncio.
async_acme = AsyncAcme(config)
invoice = await async_acme.invoices.create(customer_id="cus_123", amount_cents=4900)from acme_api import (
ClientAuthConfig,
ClientConfig,
configure_client,
list_users_paginated,
)
configure_client(ClientConfig(
base_url="https://api.acme.com/v1",
auth=ClientAuthConfig(bearer=os.environ["ACME_TOKEN"]),
))
async for user in list_users_paginated(limit=100):
print(user.id)The package at a glance
What a developer checks before adding the Python package to a project.
- Runs on
- Python 3.10+
- Transport
- httpx, for both the sync and async clients
- Runtime dependencies
- httpx, pydantic 2 and typing-extensions
- Models
- pydantic by default, dataclass or TypedDict on request
- Pagination
- Iterator on Acme, async iterator on AsyncAcme
- Ships to
- PyPI, with py.typed for mypy and pyright
When a call fails
Each common status is its own subclass, so except clauses read like the cases they handle. When your spec declares an error model, the decoded body is on err.data for an isinstance check.
from acme_api.errors import SdkHttpError, SdkRateLimitError
try:
invoice = acme.invoices.create(customer_id="cus_123", amount_cents=4900)
except SdkRateLimitError:
# 429: retries with backoff already ran. Hand it to your job queue.
enqueue_retry()
except SdkHttpError as err:
log.error("acme call failed", status=err.status_code, request_id=err.request_id)What you decide for Python
Set project-wide, or per language. The data model is the one Python users feel.
Data model
pydantic (default), dataclass, typeddictpydantic is the default because it is the response model typed Python already standardises on. It validates and coerces at the boundary, so a response that violates your spec fails loudly at the call. dataclass drops the dependency; typeddict is plain dicts with hints and no runtime cost.
Sync methods
include (default), omitIncluded, every operation has a blocking twin and the package exports both Acme and AsyncAcme. Omit them to publish an asyncio-only package.
Client style
class-namespaced (default), class, namespaced, functionsA client object grouped by resource is the default. Free functions are the override.
Argument style
positional (default), objectPlain named parameters, which is how Python is actually called. A wide operation is promoted to a request object automatically, so you only pay for the ceremony where it buys something.
Method naming and namespacing work the same in every language: operations are grouped by your spec’s tags or by path, and the namespace word is dropped from the method name unless you ask to keep the operationId whole.
pydantic is the default, and it is a dependency
Runtime validation catches spec drift at the boundary, which is worth a lot when your API and your SDK ship on different days, and it is why pydantic is the default: it is what typed Python codebases already reach for. It is also a dependency and a per-response cost your consumers did not choose. If your users are a FastAPI shop it is free. If you are shipping to people who keep their dependency trees short, switch the data model to dataclass before the first release.
pydantic>=2.0- Still possible: Validate each response at the boundary
- Not possible: Ship with zero runtime dependencies
- Not possible: Switch to dataclass without a major release
The same in every generated client
The parts nobody wants to hand-write, and the parts a hand-written client usually skips.
- Retries with exponential backoff and jitter, on the statuses that mean "try again", with a per-attempt timeout.
- Idempotency keys on the methods that need them, so a retried write does not become two.
- Auth wired in from your spec’s security scheme, read from wherever your users keep secrets.
- Typed errors split by cause: a failed constraint, an HTTP status, a network failure and a timeout are four different things.
- Pagination that follows whichever contract your spec declares, cursor, offset, page number or next URL, without prefetching the collection.
- Server-sent events as a native stream, for the operations that stream.
- Error reporting to Octri’s monitoring, pre-wired and switched off until a consumer opts in.
Point it at your spec
Upload an OpenAPI or Swagger document, paste a URL, or connect the repository it lives in so a merge to main updates it.
Read the audit
The spec is scored out of 10 on what a generator can do with it, with every missing schema and colliding operationId named. 8 of the fourteen rules carry a button that writes the fix.
Shape the client
Client style, argument style, namespacing, auth, retries and pagination, set once for the project and overridden per language or per endpoint.
Generate and read it
The package is generated, compiled and handed to you as source you can read before anyone installs it.
Publish, then keep publishing
Release to the registry under your own account. Every later spec change regenerates the client, and the version number comes from your own document.
Or in another language
One spec, ten languages. The other nine are generated from the same document, with the same settings, at the same time.
Questions
Both, by default. The package exports Acme, which blocks, and AsyncAcme, which returns coroutines, over the same namespaces and method names. Switch sync methods off and only the asyncio surface ships.