Skip to main content

Fully open-source, drop-in stainless.yml-compatible idiomatic SDK generator — Python and TypeScript from one config.

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.

Migrating from Stainless? Anthropic acquired Stainless and is winding down the hosted SDK generator. stainful is a drop-in continuation path — see docs/migrating-from-stainless.md for the step-by-step (it should be a few minutes).

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
  • 📑 stainful docs — emit a Stainless-style api.md from the same inputs (per-resource sections, Methods lists with verb+path, Mintlify-compatible)
  • 🧰 stainful mcp — emit a Model Context Protocol server (one tool per HTTP method) so Claude / Cline / mcp-cli can call your API as tools

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 emit a Stainless-style api.md doc from the same inputs
stainful docs --spec openapi.yml --config stainless.yml --out ./api.md

# OR emit an MCP server inside the generated SDK
stainful mcp --spec openapi.yml --config stainless.yml --out ./sdk/<pkg>/mcp_server.py

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 (chat / paginated / multipart / binary / webhooks / …)
examples/onebusaway/ committed dogfood — regenerated SDK is bit-stable; CI guards it
examples/openai/ real-world test: the public openai-openapi spec → mypy-clean SDK
docs/ migration guide and other docs

Status

v0.4.0. One stainless.yml → SDK + Mintlify-shaped api.md + MCP server. Verified against the real Stainless-generated SDKs at pinned SHAs in CI:

  • OneBusAway: 29/29 (100%) of Stainless's own OneBusAway/python-sdk test files import unchanged against stainful's output; generated SDK is mypy-clean; regeneration is byte-stable (the repo dogfoods itself).
  • OpenAI: the public openai-openapi spec (162 paths, 983 schemas) generates a mypy-clean SDK — see examples/openai/ and docs/migrating-from-stainless.md for what's verified and what's still on the gap list.

End-to-end behavioral conformance covers: cursor pagination (wire param config-driven — ?after=<last_id> matches openai), anthropic-shape bi-directional pagination (before_idafter_id), SSE streaming with @overload pairs, multipart / file upload, binary download (audio/mpeg, octet-stream), raw binary request bodies (S3-style PUT), typed webhook unwrap (Standard Webhooks scheme), rich APIResponse[T] from with_raw_response.*, typed error-body models (<pkg>.types.shared.ErrorObject auto-detected from the spec), spec- specific page class symbols (SyncTokenPage/SyncNextCursorPage/…), custom_casings, .to_json()/.to_dict() aliases, webhook <BRAND>_WEBHOOK_SECRET env-var fallback.

118 tests, mypy 0 on 253 generated files, ruff clean, CI green on py3.10–3.12.

Known scope boundary: multi-content request bodies (one operation declaring multiple requestBody.content types) still pick the first match — no public Stainless oracle to verify the exact API surface. Documented in the migration guide.

Roadmap: Python SDK → MCP server from the same model → a second language → docs site. One language done well first.

Contributing

PRs welcome — see CONTRIBUTING.md and the Code of Conduct.

git clone https://github.com/stainlu/stainful && cd stainful
uv venv && uv pip install -e ".[dev,generated-runtime]"
uv run pytest -q
uv run ruff check src tests

# regenerate the dogfood SDK (the repo dogfoods itself; CI fails if a
# regeneration changes a byte — see examples/onebusaway/):
uv run stainful generate \
  --spec   examples/onebusaway/openapi.yml \
  --config examples/onebusaway/stainless.yml \
  --out    examples/onebusaway/sdk

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.6.0.tar.gz (1.4 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.6.0-py3-none-any.whl (105.4 kB view details)

Uploaded Python 3

File details

Details for the file stainful-0.6.0.tar.gz.

File metadata

  • Download URL: stainful-0.6.0.tar.gz
  • Upload date:
  • Size: 1.4 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.6.0.tar.gz
Algorithm Hash digest
SHA256 900464e5db48d918889a83e8f8b81de0ff447596867f3b8d9e3e01940d1a01ff
MD5 78732f19e2f5ec559c1be4f9020f7e2e
BLAKE2b-256 b58375fa44ef3131afa735cf73c9bdfc8c4cb5524a28e956330b8b801872bf53

See more details on using hashes here.

File details

Details for the file stainful-0.6.0-py3-none-any.whl.

File metadata

  • Download URL: stainful-0.6.0-py3-none-any.whl
  • Upload date:
  • Size: 105.4 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.6.0-py3-none-any.whl
Algorithm Hash digest
SHA256 1ff785e4c36cdf196a27cb5422f3c037a1244876e5fd201f98bd45c2f0082f0b
MD5 8b3862ea016b6ad86054ced338fb61f3
BLAKE2b-256 98a88d2ac2b103a75e0e55ca7adb13cf14a4607704bfb37a629d6b91edf7a116

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