Skip to main content

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.

License Python CI PRs welcome

stainful — OpenAPI spec + stainless.yml → resolved IR → idiomatic Python SDK

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-paginationfor item in client.things.list(): ...
  • 🛡️ Typed errorsexcept 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 optionalityrequired, optional, and nullable stay distinct
  • 🧭 Domain-shaped clientsclient.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

stainful-0.2.0.tar.gz (1.0 MB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

stainful-0.2.0-py3-none-any.whl (57.0 kB view details)

Uploaded Python 3

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

Hashes for stainful-0.2.0.tar.gz
Algorithm Hash digest
SHA256 2da1a4f6411ce4d62eef58b9a6cea51be530affea605ce3dfea22cb4206ef6e1
MD5 e2bbb25d50978484c6a1cc4085689f75
BLAKE2b-256 cbb004e3343b209764b0b94bbb1fb3c0b6ff46373e93b39b4199cc0160f6244a

See more details on using hashes here.

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

Hashes for stainful-0.2.0-py3-none-any.whl
Algorithm Hash digest
SHA256 350908447b7d5c3a7af729cbf2bdf7927777bdc0228902261ddcfb6062f9e193
MD5 66ecdf8045c77cd6b18c203eb12412b8
BLAKE2b-256 c4bdaeb46a43a6df450e80566acfbb3e49d4bbacfec0bdc63cb7cf954e89a1c5

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page