Fully open-source, drop-in stainless.yml-compatible idiomatic Python SDK generator.
Project description
stainful
The open-source Stainless. Generate an idiomatic Python SDK from an OpenAPI
spec and a stainless.yml — open source, runs locally and in CI, no hosted service.
stainful turns an OpenAPI 3.x spec and a Stainless config into a Python SDK
that reads like it was written by hand — typed models, real error classes,
retries, auto-pagination, streaming, sync and async. It reuses the
stainless.yml format, so if you already have one, you can point stainful at it
as-is.
Features
- 🧬 Typed everything — pydantic v2 models, real discriminated unions from
oneOf - 🔁 Auto-pagination —
for item in client.things.list(): ... - 🛡️ Typed errors —
except RateLimitError:instead of checking status codes - 🔄 Resilient — retries with exponential backoff + jitter,
Retry-After, idempotency keys - 📡 Streaming — typed Server-Sent Events, identical surface in sync and async
- 🎯 Precise optionality —
required,optional, andnullablestay distinct - 🧭 Domain-shaped clients —
client.chat.completions.create(...), not flat stubs - ⚡ Sync + async generated from one model
- 📦 Self-contained output — the generated SDK depends only on
httpx+pydantic
Quickstart
pip install stainful
# generate an idiomatic Python SDK from your OpenAPI spec + stainless.yml
stainful generate --spec openapi.yml --config stainless.yml --out ./sdk
Or from source (and to run the conformance harness):
git clone https://github.com/stainlu/stainful && cd stainful
uv venv && uv pip install -e ".[dev,generated-runtime]"
uv run stainful generate \
--spec examples/onebusaway/openapi.yml \
--config examples/onebusaway/stainless.yml \
--out examples/onebusaway/sdk
That config is a real production stainless.yml; examples/onebusaway/sdk
is checked in and CI fails if regenerating changes a byte (the repo dogfoods
itself — see examples/onebusaway/).
The generated SDK feels like an official client:
from onebusaway import OnebusawaySDK
client = OnebusawaySDK(api_key="...") # or set ONEBUSAWAY_API_KEY
agency = client.agency.retrieve("1") # typed, retried, idiomatic
print(agency.data.entry.name)
Streaming, async, and typed errors work the way you'd expect:
import asyncio
from chat import AsyncChatSDK
from chat import RateLimitError
async def main():
client = AsyncChatSDK(api_key="...")
try:
stream = await client.chat.completions.create(
model="m", messages=[{"role": "user", "content": "hi"}], stream=True
)
async for chunk in stream:
print(chunk.delta, end="")
except RateLimitError as e:
print("rate limited:", e.request_id)
asyncio.run(main())
What you get vs. a mechanical generator
# typical OpenAPI generator # stainful
api = DefaultApi(ApiClient(cfg)) client = OnebusawaySDK()
resp = api.agency_agency_id_json_get(id) agency = client.agency.retrieve(id)
# loosely typed, no retries, no error classes, # typed model, retries, typed errors,
# you hand-write the pagination loop # auto-pagination, request id, async twin
How it works
The pipeline is shown above: an OpenAPI spec and stainless.yml are parsed,
resolved, and lowered into an intermediate representation, which the emitter
renders into a Python SDK over a vendored runtime.
The intermediate representation is a fully-resolved, language-agnostic model:
allOf is merged, oneOf becomes a real tagged union, and optionality is
three-valued. The emitter is a thin renderer over a hand-written runtime, so the
idiomatic behavior lives in audited code rather than per-endpoint templates.
How it compares
| OpenAPI Generator | Fern | Stainless | stainful | |
|---|---|---|---|---|
| Open source | ✅ | ✅ | — | ✅ |
| Runs fully locally, no account | ✅ | ✅ | — | ✅ |
Reads the stainless.yml format |
— | — | ✅ | ✅ |
| Idiomatic output (pagination, typed errors, streaming) | — | ✅ | ✅ | ✅ |
stainful's niche: idiomatic, fully-open, and a drop-in for the Stainless config you may already have. Different tools fit different teams — this one is for people who want that workflow without a hosted service.
Project layout
| Path | What |
|---|---|
src/stainful/config/ |
stainless.yml loader with precise, located diagnostics |
src/stainful/openapi/ |
OpenAPI 3.x loader + cycle-safe $ref / allOf resolver |
src/stainful/ir/ |
the intermediate representation |
src/stainful/emit/ |
the Python emitter |
src/stainful/runtime/ |
the hand-written runtime vendored into generated SDKs |
tests/fixtures/ |
conformance fixtures (OneBusAway, chat) |
Status
Early but working. stainful generates complete sync + async SDKs and its output has been checked against the real Stainless-generated OneBusAway SDK — client class, package, env var, and call shape all match, so existing import lines keep working.
Not yet at full parity: .to_json()/.to_dict() model helpers, a richer raw-response
object, per-file model modules, typed error-body models, and custom_casings.
These are tracked and contributions are welcome.
Roadmap: Python SDK → MCP server from the same model → a second language → docs. One language done well first.
Contributing
PRs welcome — see CONTRIBUTING.md and the
Code of Conduct.
uv run pytest -q
uv run ruff check src tests
License
MIT. The vendored runtime ships inside generated SDKs under the same terms.
Project details
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 stainful-0.2.0.tar.gz.
File metadata
- Download URL: stainful-0.2.0.tar.gz
- Upload date:
- Size: 1.0 MB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: uv/0.11.14 {"installer":{"name":"uv","version":"0.11.14","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"macOS","version":null,"id":null,"libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
2da1a4f6411ce4d62eef58b9a6cea51be530affea605ce3dfea22cb4206ef6e1
|
|
| MD5 |
e2bbb25d50978484c6a1cc4085689f75
|
|
| BLAKE2b-256 |
cbb004e3343b209764b0b94bbb1fb3c0b6ff46373e93b39b4199cc0160f6244a
|
File details
Details for the file stainful-0.2.0-py3-none-any.whl.
File metadata
- Download URL: stainful-0.2.0-py3-none-any.whl
- Upload date:
- Size: 57.0 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: uv/0.11.14 {"installer":{"name":"uv","version":"0.11.14","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"macOS","version":null,"id":null,"libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
350908447b7d5c3a7af729cbf2bdf7927777bdc0228902261ddcfb6062f9e193
|
|
| MD5 |
66ecdf8045c77cd6b18c203eb12412b8
|
|
| BLAKE2b-256 |
c4bdaeb46a43a6df450e80566acfbb3e49d4bbacfec0bdc63cb7cf954e89a1c5
|