Skip to main content

🦦 OtterAPI

A cute otter that turns OpenAPI & Swagger specs into type-safe, async Python clients — powered by httpx and Pydantic.

OtterAPI is a sleek Python library that transforms OpenAPI specifications into clean, type-safe client code with Pydantic models and httpx-based HTTP clients.

✨ Features

  • Type-Safe Code Generation - Generates Pydantic models and fully typed endpoint functions
  • Sync & Async Support - Generate both synchronous and asynchronous API clients
  • OpenAPI 3.x Support - Full support for OpenAPI 3.0, 3.1, and 3.2 specifications
  • Strict or Lenient Validation - Fail fast on malformed specs, or drop stray fields with --lenient (vendor x-* extensions always preserved)
  • Module Splitting - Organize large APIs into multiple organized files
  • Pagination - Auto-detect or configure offset, cursor, page, and link-header pagination
  • DataFrame Conversion - Generate pandas/polars DataFrame methods for list endpoints
  • File Export - Generate CSV, TSV, JSONL, and Parquet streaming helpers
  • Response Unwrapping - Transparently unwrap envelope-style responses
  • Customizable Client - Generated client class with configurable base URL, timeout, and headers
  • Environment Variable Support - Use ${VAR} or ${VAR:-default} syntax in config files

🚀 Quick Start

Installation

pip install otterapi

Basic Usage

  1. Create an otter.yml configuration file:
documents:
  - source: https://petstore3.swagger.io/api/v3/openapi.json
    output: petstore_client
  1. Generate the client:
otter generate
  1. Use the generated code:
from petstore_client import get_pet_by_id, async_get_pet_by_id

# Synchronous usage
pet = get_pet_by_id(pet_id=123)

# Asynchronous usage
import asyncio
pet = asyncio.run(async_get_pet_by_id(pet_id=123))

📝 Configuration

Config File Locations

OtterAPI looks for configuration in this order:

  1. Path passed via otter generate -c <path>
  2. otter.yaml or otter.yml in the current directory
  3. otter.json in the current directory
  4. [tool.otterapi] section in pyproject.toml
  5. OTTER_SOURCE and OTTER_OUTPUT environment variables

Config File Formats

YAML (recommended):

documents:
  - source: https://api.example.com/openapi.json
    output: ./client

pyproject.toml:

[tool.otterapi]
[[tool.otterapi.documents]]
source = "https://api.example.com/openapi.json"
output = "./client"

JSON:

{
  "documents": [
    { "source": "https://api.example.com/openapi.json", "output": "./client" }
  ]
}

Top-Level Options

These sit at the root of your config file, outside of documents:.

Option Type Default Description
documents list — List of OpenAPI documents to process (required)
generate_endpoints bool true Whether to generate endpoint functions
format_output bool true Format generated code with ruff/black
validate_output bool true Validate generated code: syntax, plus every referenced name resolving to an import or definition
create_py_typed bool true Create py.typed marker files
lenient bool false Drop unrecognized, invalid fields instead of failing (see Strict vs Lenient Validation)
format_output: true
validate_output: true
create_py_typed: true

documents:
  - source: https://api.example.com/openapi.json
    output: ./client

format_output only controls cosmetic formatting (line wrapping, quote style). The import block of every generated module is computed from the names the module actually references, so generated code never carries unused imports — with or without ruff/black installed. validate_output checks the other direction: generation fails rather than emitting a module that references a name nothing imports or defines.

Document Options

Each entry under documents: supports these fields:

Option Type Default Description
source string — URL or file path to the OpenAPI spec (required)
output string — Output directory for generated code (required)
base_url string from spec Override the base URL defined in the spec
models_file string models.py Filename for generated models
endpoints_file string endpoints.py Filename for generated endpoints (no-split mode)
models_import_path string null Override the import path used for models in endpoints
generate_async bool true Generate async endpoint functions
generate_sync bool true Generate sync endpoint functions
client_class_name string from API title Override the generated client class name
function_naming operation_id | path operation_id How to name endpoint functions. path derives names from the HTTP method and URL path — use it for specs that reuse one operationId across many paths
include_paths list null Glob patterns — only matching paths are generated
exclude_paths list null Glob patterns — matching paths are skipped (applied after include_paths)
target_python 3.10 … 3.14 3.10 Oldest Python the generated code has to run on. 3.12+ emits PEP 695 type parameters in the runtime helpers
pydantic_version 1 | 2 2 Target Pydantic version for generated models and helpers

Function Naming

By default, endpoint function names come from each operation's operationId. Some specs (notably Swashbuckle/.NET output) reuse a single operationId across many distinct paths, which produces a pile of numerically-suffixed duplicates like feed_changes, feed_changes_2, … feed_changes_43. Set function_naming: path to derive names from the HTTP method and URL path instead, giving distinct, descriptive names:

documents:
  - source: https://api.example.com/swagger.json
    output: ./client
    function_naming: path   # get_v1_feed_trial_changes, get_v1_feed_hcp_profile_changes, ...

Path Filtering

documents:
  - source: https://api.example.com/openapi.json
    output: ./client
    include_paths:
      - /api/v2/**       # only v2 endpoints
    exclude_paths:
      - /internal/*      # skip internal endpoints
      - /admin/**        # skip admin endpoints

Patterns follow standard glob syntax (* = single segment, ** = any depth).

Target Python Version

Generated code defaults to running on Python 3.10, so the runtime helpers (_pagination.py, _concurrency.py) declare their generics with module-level TypeVars:

T = TypeVar('T')

def paginate_offset(..., extract_items: Callable[[Any], list[T]]) -> list[T]:

That is the only spelling that works before 3.12, but a project whose linter is configured for a newer Python flags it as legacy (ruff's UP047). If the generated client only has to run on 3.12+, say so and the same helpers are emitted with PEP 695 type parameters:

documents:
  - source: https://api.example.com/openapi.json
    output: ./client
    target_python: "3.12"
def paginate_offset[T](..., extract_items: Callable[[Any], list[T]]) -> list[T]:

The setting describes the generated code — OtterAPI itself still runs on any supported interpreter, including when it emits 3.12-only syntax.

Environment Variable Support

Any string value in a config file can reference environment variables:

documents:
  - source: ${API_SPEC_URL}
    output: ${OUTPUT_DIR:-./client}
    base_url: ${BASE_URL:-https://api.example.com}

You can also configure a single document entirely via environment variables (no config file needed):

Variable Description
OTTER_SOURCE Path or URL to the OpenAPI spec
OTTER_OUTPUT Output directory
OTTER_BASE_URL Base URL override
OTTER_MODELS_FILE Models filename
OTTER_ENDPOINTS_FILE Endpoints filename

🔎 Strict vs Lenient Validation

By default, OtterAPI validates specs strictly: an unrecognized or structurally-invalid field aborts generation with a clear error, catching malformed specs early.

Some real-world specs (especially auto-generated Swagger 2.0 documents) carry stray, non-standard keys. Set lenient: true — or pass --lenient — to drop those fields instead of failing. Each dropped key is reported as a warning.

lenient: true

documents:
  - source: https://api.example.com/openapi.json
    output: ./client
otter generate --lenient

Vendor extensions are always preserved. OpenAPI specification extensions (x-*) are kept in both strict and lenient mode. The only exception is the type-discriminated security-scheme objects (apiKey, http, oauth2, openIdConnect), which still reject unknown keys so their variant stays unambiguous.


📦 Module Splitting

For large APIs, OtterAPI can split generated code into multiple organized modules.

Enabling Module Splitting

documents:
  - source: https://api.example.com/openapi.json
    output: ./client
    module_split:
      enabled: true
      strategy: tag

Splitting Strategies

tag — Split by OpenAPI Tags

module_split:
  enabled: true
  strategy: tag
  min_endpoints: 1

Endpoints tagged ["Users"] go to users.py, ["Orders"] to orders.py, etc.

path — Split by URL Path

module_split:
  enabled: true
  strategy: path
  path_depth: 1
  global_strip_prefixes:
    - /api/v1
    - /api/v2

/api/v1/users/123 → users.py, /api/v1/orders/456 → orders.py.

custom — Explicit Module Mapping

module_split:
  enabled: true
  strategy: custom
  module_map:
    users:
      - /users
      - /users/*
      - /users/**
    orders:
      - /orders/*
    health:
      - /health
      - /ready

hybrid — Combined Strategy (Default)

Tries module_map first, then tags, then path:

module_split:
  enabled: true
  strategy: hybrid
  module_map:
    health:
      - /health
      - /ready

none — All to Fallback

module_split:
  enabled: true
  strategy: none
  fallback_module: api

Module Map Patterns

Pattern Matches
/users Exact path only
/users/* One additional segment
/users/** Any depth below /users
/v?/users Single wildcard character

Nested Module Maps

module_split:
  enabled: true
  strategy: custom
  module_map:
    identity:
      users:
        - /users/*
      auth:
        - /auth/*
        - /login
        - /logout
    billing:
      invoices:
        - /invoices/*
      payments:
        - /payments/*

Advanced Module Definition

module_split:
  enabled: true
  strategy: custom
  module_map:
    v2_api:
      paths:
        - /v2/**
      strip_prefix: /v2
      description: "API v2 endpoints"
      file_name: v2.py          # override the generated filename
      modules:
        users:
          paths:
            - /users/*

Module Split Options Reference

Option Type Default Description
enabled bool false Enable module splitting
strategy string hybrid none, path, tag, hybrid, custom
fallback_module string common Module for unmatched endpoints
min_endpoints int 2 Minimum endpoints per module before consolidating into fallback
flat_structure bool false Flat files instead of nested directories
path_depth int 1 Path segments used for path strategy (1–5)
global_strip_prefixes list /api, /api/v1, /api/v2, /api/v3 Prefixes stripped from all paths before matching
module_map object {} Custom module-to-path mappings
split_models bool false Generate per-module model files instead of one shared models.py
shared_models_module string _models Module name for shared models when split_models is true

Output Structure

Flat (flat_structure: false, default):

client/
├── __init__.py
├── models.py
├── _client.py
├── client.py
├── users.py
├── orders.py
└── health.py

Nested (flat_structure: false with nested module_map):

client/
├── __init__.py
├── models.py
├── _client.py
├── client.py
├── identity/
│   ├── __init__.py
│   ├── users.py
│   └── auth.py
└── billing/
    ├── __init__.py
    └── invoices.py

📄 Pagination

OtterAPI generates iterator-based pagination methods alongside standard endpoint functions. Paginated endpoints get an _iter suffix variant that yields items one by one, handling page-fetching automatically.

Enabling Pagination

documents:
  - source: https://api.example.com/openapi.json
    output: ./client
    pagination:
      enabled: true

Auto-Detection

When auto_detect: true (the default), OtterAPI inspects each endpoint's parameters. If it finds a matching pair for any pagination style, it generates pagination methods automatically — no per-endpoint config needed.

pagination:
  enabled: true
  auto_detect: true           # default
  default_style: offset       # default; used when auto-detected

Pagination Styles

Style Required Parameters Description
offset offset + limit Offset/limit (e.g. ?offset=0&limit=100)
cursor cursor + limit Cursor-based (e.g. ?cursor=abc&limit=100)
page page + per_page Page number (e.g. ?page=2&per_page=50)
link — RFC 5988 Link header

Global Pagination Options

Option Type Default Description
enabled bool false Enable pagination generation
auto_detect bool true Auto-detect pagination from parameter names
default_style string offset Fallback style when auto-detecting
default_page_size int 100 Default page size for iteration
default_data_path string null Default JSON path to items array in response
default_total_path string null Default JSON path to total count in response
default_offset_param string offset Param name used for offset detection
default_limit_param string limit Param name used for limit detection
default_cursor_param string cursor Param name used for cursor detection
default_page_param string page Param name used for page detection
default_per_page_param string per_page Param name used for per-page detection
endpoints object {} Per-endpoint overrides

Per-Endpoint Pagination Options

pagination:
  enabled: true
  endpoints:
    list_users:
      style: cursor
      cursor_param: next_token
      limit_param: max_results
      data_path: data.users
      total_path: meta.total
      next_cursor_path: meta.next_token
      default_page_size: 50
      max_page_size: 200
    get_orders:
      enabled: false            # disable pagination for this endpoint
Option Type Default Description
enabled bool|null inherits Override whether to generate pagination
style string inherits offset, cursor, page, link
offset_param string inherits Name of offset parameter
limit_param string inherits Name of limit parameter
cursor_param string inherits Name of cursor parameter
page_param string inherits Name of page parameter
per_page_param string inherits Name of per-page parameter
data_path string inherits JSON path to items array in response
total_path string inherits JSON path to total count
next_cursor_path string null JSON path to next cursor value
total_pages_path string null JSON path to total page count
default_page_size int inherits Default page size
max_page_size int null Maximum allowed page size

Usage

from client import list_users_iter, async_list_users_iter

# Iterate all users (handles pagination automatically)
for user in list_users_iter():
    print(user.name)

# Async variant
async for user in async_list_users_iter():
    print(user.name)

📊 DataFrame Conversion

When enabled, list-returning endpoints get additional methods that return pandas or polars DataFrames directly.

Enabling DataFrame Methods

documents:
  - source: https://api.example.com/openapi.json
    output: ./client
    dataframe:
      enabled: true
      pandas: true      # generate _df methods (default: true)
      polars: true      # generate _pl methods (default: false)

Generated Methods

Original Pandas Polars
get_users() get_users_df() get_users_pl()
async_get_users() async_get_users_df() async_get_users_pl()

Usage

from client import list_pets_df, list_pets_pl

pdf = list_pets_df("available")
plf = list_pets_pl("available")

# Override the data extraction path at call time
df = list_pets_df("available", path="response.data.pets")

Nested Response Paths

dataframe:
  enabled: true
  pandas: true
  default_path: data.items       # default for all endpoints
  endpoints:
    get_users:
      path: data.users           # override for this endpoint
    get_analytics:
      path: response.events
      polars: true
      pandas: false

DataFrame Options

Option Type Default Description
enabled bool false Enable DataFrame generation
pandas bool true Generate _df (pandas) methods
polars bool false Generate _pl (polars) methods
default_path string null Default JSON path to extract data
include_all bool true Generate for all list-returning endpoints
endpoints object {} Per-endpoint overrides

Per-Endpoint DataFrame Options

Option Type Default Description
enabled bool|null inherits Override whether to generate methods
path string inherits JSON path to extract data
pandas bool|null inherits Override pandas generation
polars bool|null inherits Override polars generation

💾 File Export

When enabled, list-returning endpoints get streaming export helpers for writing responses directly to files (local or remote via UPath).

Enabling Export

documents:
  - source: https://api.example.com/openapi.json
    output: ./client
    export:
      enabled: true
      formats:
        - csv
        - jsonl

Parquet support requires the pyarrow extra:

pip install otterapi[parquet]

Export Options

Option Type Default Description
enabled bool false Enable export helper generation
formats list [csv, jsonl] Default formats: csv, tsv, jsonl, parquet
default_path string null Default JSON path to extract list data
include_all bool true Generate helpers for all list-returning endpoints
batch_size int 1000 Batch size used when streaming pages to disk
endpoints object {} Per-endpoint overrides

Per-Endpoint Export Options

export:
  enabled: true
  formats: [csv, jsonl]
  endpoints:
    list_users:
      formats: [parquet]        # only parquet for this endpoint
      path: data.users
    get_events:
      enabled: false            # disable export for this endpoint
Option Type Default Description
enabled bool|null inherits Override whether to generate helpers
path string inherits JSON path to extract data
formats list inherits Override formats for this endpoint

Usage

from client import export_list_users_csv, export_list_users_parquet

# Export directly to a file
export_list_users_csv("output/users.csv")
export_list_users_parquet("s3://my-bucket/users.parquet")  # UPath remote targets work too

🔓 Response Unwrapping

For APIs that wrap all responses in an envelope (e.g. {"data": {...}, "meta": {...}}), response unwrapping makes endpoints return just the inner data automatically.

Enabling Response Unwrap

documents:
  - source: https://api.example.com/openapi.json
    output: ./client
    response_unwrap:
      enabled: true
      data_path: data          # default path (default: "data")

Per-Endpoint Overrides

response_unwrap:
  enabled: true
  data_path: data
  endpoints:
    get_user:
      data_path: result.user   # this endpoint uses a different path
    get_raw_data:
      enabled: false           # don't unwrap this endpoint

Response Unwrap Options

Option Type Default Description
enabled bool false Enable response unwrapping
data_path string data Default JSON path to extract from all responses
endpoints object {} Per-endpoint overrides

Per-Endpoint Options

Option Type Default Description
enabled bool|null inherits Override whether to unwrap
data_path string inherits JSON path for this endpoint

📖 Using Generated Code

Direct Function Imports

from client import get_user, create_user, list_orders

# Async versions are prefixed with 'async_'
from client import async_get_user, async_create_user, async_list_orders

# Sync
user = get_user(user_id=123)

# Async
import asyncio

async def main():
    user = await async_get_user(user_id=123)

asyncio.run(main())

Module-Specific Imports (with splitting)

from client.users import get_user, create_user
from client.orders import list_orders, get_order

Using the Client Class

from client import Client

client = Client(
    base_url='https://api.example.com',
    timeout=30.0,
    headers={'Authorization': 'Bearer your-token'},
)

user = client.get_user(user_id=123)

async def main():
    user = await client.async_get_user(user_id=123)

Working with Models

from client.models import User, CreateUserRequest

new_user = CreateUserRequest(name='John Doe', email='john@example.com')
user = create_user(body=new_user)
print(user.id, user.email)

🔐 Authentication

Every scheme under components.securitySchemes becomes a keyword-only parameter on the generated client, applied automatically to every request.

# spec
components:
  securitySchemes:
    api_key:
      type: apiKey
      name: X-API-Key
      in: header
    bearerAuth:
      type: http
      scheme: bearer
from client import Client

client = Client(api_key='...', bearer_auth='...')
user = get_user(user_id=123, client=client)
Scheme Parameter type Sent as
apiKey in header str the header named in the spec
apiKey in query str the query parameter named in the spec
apiKey in cookie str appended to the Cookie header
http / bearer str Authorization: Bearer <token>
http / basic tuple[str, str] Authorization: Basic <base64>
oauth2, openIdConnect str Authorization: Bearer <token> — you supply an already-obtained token

Schemes OtterAPI does not recognize are skipped with a warning rather than failing generation.

Credentials from the environment

Omit a credential and the client reads it from <env_prefix>_<SCHEME_NAME>, upper-cased — so the api_key scheme above falls back to OTTER_API_KEY. Set env_prefix per document when one project generates several clients:

documents:
  - source: https://api.example.com/openapi.json
    output: ./client
    auth:
      env_prefix: MYAPI    # -> MYAPI_API_KEY

When a credential already lives in a variable the prefix rule would never derive, name it outright with env_vars. Keys are scheme names from components.securitySchemes (the generated parameter name works too), and only the schemes you name are affected — the rest keep the derived default:

documents:
  - source: https://api.example.com/openapi.json
    output: ./client
    auth:
      env_prefix: MYAPI
      env_vars:
        api_key: STRIPE_SECRET_KEY   # this one exactly
        # bearerAuth still falls back to MYAPI_BEARER_AUTH

A constructor argument always beats the environment. Basic auth needs two values, so it has no environment fallback at all.

Auth Options

Option Type Default Description
enabled bool true Generate credential parameters from securitySchemes
env_prefix string OTTER Prefix for the environment variables consulted when an argument is omitted. Empty string drops the prefix
env_vars map {} Exact environment variable per scheme, keyed by scheme name. Overrides env_prefix for the schemes it names

Customizing

_apply_auth runs before the _before_request hook, so the hook can still override or refresh whatever it set — useful for rotating an OAuth token:

class MyClient(Client):
    def _before_request(self, request):
        if self._token_expired():
            request['headers']['Authorization'] = f'Bearer {self._refresh()}'
        return request

Set auth.enabled: false to opt out entirely and wire authentication by hand in the user-owned client.py.


📤 Request Bodies

Bodies are serialized with model_dump(mode='json', by_alias=True, exclude_unset=True): JSON mode so datetime, UUID and Decimal fields reach the wire as strings rather than raising, and aliases so fields whose spec name is not a Python identifier keep their wire name.

exclude_unset means only the fields you actually set are sent:

place_order(body=Order(id=7))     # -> {"id": 7}

Most APIs read an explicit null as "clear this field", so omitting the fields you never touched is the safer default — and it is what makes partial updates work. If an endpoint genuinely needs those nulls, a PUT that replaces a whole resource for instance, turn it off:

documents:
  - source: https://api.example.com/openapi.json
    output: ./client
    request_body:
      exclude_unset: false
place_order(body=Order(id=7))     # -> {"id": 7, "shipDate": null, "note": null}

Flattening the body into parameters

By default a POST/PUT/PATCH takes the body as one model:

place_order(body=Order(id=7, quantity=2))

Set flatten and the model's fields become parameters instead:

documents:
  - source: https://api.example.com/openapi.json
    output: ./client
    request_body:
      flatten: true
place_order(id=7, quantity=2)

Required fields stay positional so you cannot forget them; the rest are keyword-only and default to NOTSET. Fields inherited through allOf are included. The request that goes on the wire is identical either way — flattening changes the signature, not the protocol.

NOTSET vs None

A nullable field has two distinct "empty" states, and one default cannot carry both. Omitted fields therefore default to NOTSET, which leaves None meaning what it says:

place_order(id=7)                 # -> {"id": 7}
place_order(id=7, note=None)      # -> {"id": 7, "note": null}
place_order(id=7, note=NOTSET)    # -> {"id": 7}   (same as omitting)

NOTSET is exported from the generated package, so you can pass it from a conditional rather than branching on the call itself:

from myclient import NOTSET, place_order

place_order(id=7, note=note if include_note else NOTSET)

It is a single-member enum, so type checkers treat it as a literal: inside the client, value is not NOTSET narrows int | None | NotSet to int | None. It is also falsy, so if value: reads as "was anything given".

Two more details worth knowing:

  • A body field can collide with a path or query parameter. The parameter keeps its name and the body field gains a _body suffix, so a spec with both a status query param and a status body field generates add_note(status_body, *, status=None). The payload still uses status.
  • Only object-shaped JSON bodies can flatten. An array body, a bare string, or a multipart upload has nothing to spread; those keep the single body= parameter and log why.

Per-path overrides

paths overrides the document setting for matching URL paths, using the same glob syntax as include_paths. When several patterns match, the longest wins, so a specific path beats the wildcard covering it:

request_body:
  flatten: true
  paths:
    /reports/**:                 # a big nested body reads better as a model
      flatten: false
    /reports/summary:            # …except this one
      flatten: true
    /users/{id}:
      exclude_unset: false       # this PUT replaces the whole resource

Request Body Options

Option Type Default Description
exclude_unset bool true Send only the fields the caller set, rather than an explicit null for every untouched optional field
flatten bool false Spread the body model's fields into individual parameters instead of one body= argument
paths map {} Per-path overrides keyed by a glob over the URL path; longest match wins

Each path override accepts exclude_unset and flatten; anything omitted inherits the document setting.


🔧 CLI Reference

# Generate from default config (otter.yml, otter.yaml, or pyproject.toml)
otter generate

# Generate from specific config file
otter generate -c my-config.yml

# Generate without a config file
otter generate -s ./api.yaml -o ./client

# Override the base URL — required when a file-loaded spec's servers are
# relative (e.g. `url: /api/v3`), and applies to every document in a config
otter generate -s ./api.yaml -o ./client -b https://api.example.com

# Tolerate malformed specs (drop unknown/invalid fields with a warning)
otter generate --lenient

# Initialize a new config file
otter init

# Validate configuration
otter validate

Generation is all-or-nothing: if a run fails partway through, the output directory is restored to exactly what it was before, so a failure never leaves a half-written client behind. Files you own inside it — client.py and anything else you keep there — survive both a failed run and a successful regeneration.


🐍 Programmatic API

from otterapi.codegen import Codegen
from otterapi.config import DocumentConfig, PaginationConfig, ExportConfig

config = DocumentConfig(
    source='https://petstore3.swagger.io/api/v3/openapi.json',
    output='./client',
    pagination=PaginationConfig(enabled=True),
    export=ExportConfig(enabled=True, formats=['csv', 'parquet']),
)
Codegen(config).generate()

❗ Error Handling

Generated clients raise a typed exception hierarchy rooted at BaseAPIError:

from my_client import (
    Client, list_users,
    BaseAPIError,        # catches every API error
    ClientError,         # all 4xx
    ServerError,         # all 5xx
    NotFoundError,       # 404
    RateLimitError,      # 429
)

try:
    users = list_users(client=Client())
except NotFoundError as e:
    log.warning('not found: %s', e.detail)
except RateLimitError:
    backoff_and_retry()
except ServerError:
    page_oncall()
except BaseAPIError as e:
    log.error('unexpected %d: %s', e.status_code, e.detail)

Mapped status codes: 400, 401, 403, 404, 409, 422, 429, 500, 502, 503, 504. Other 4xx/5xx fall through to ClientError/ServerError. Subclass BaseAPIError in your client.py to customize error parsing.


♻️ Regenerating after spec changes

OtterAPI is idempotent for generated files:

  • models.py, endpoints.py, _client.py, _pagination.py, _export.py, _dataframe.py are rewritten on every run — never edit them by hand.
  • client.py is generated once and left alone — this is your customization seam.

🛠 Development

git clone https://github.com/danplischke/otterapi.git
cd otterapi
uv sync
uv run pytest
uv run pytest --cov=otterapi
uv run ruff format .
uv run ruff check .

📄 License

MIT License — see LICENSE for details.

Metadata

Release files for otterapi 0.1.11

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for otterapi 0.1.11
File Size Uploaded
otterapi-0.1.11.tar.gz 310.7 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for otterapi 0.1.11
File Interpreter ABI Platform
otterapi-0.1.11-py3-none-any.whl Python 3 none any Details

Total release size: 663.7 kB

Release files / otterapi-0.1.11.tar.gz

Download URL otterapi-0.1.11.tar.gz
Size 310.7 kB
Tags Source
SHA-256 checksum
How to use checksums
10fd3e57cbfbec955f32d7915c09315f1ed4fb773d395105a8bd60d5f0991ba5
BLAKE2b-256 checksum
How to use checksums
b5a3ffcae575c1e00e79d31950016902600b04a325843aee56a7bd8d62b10747
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Aug 13, 2026.

Transparency log

Release files / otterapi-0.1.11-py3-none-any.whl

Download URL otterapi-0.1.11-py3-none-any.whl
Size 353.0 kB
Tags Python 3
SHA-256 checksum
How to use checksums
b5f7b276154e7fda041e2c5f116c7effc774b6962d71dd6a9193c6b4b4cb8e1d
BLAKE2b-256 checksum
How to use checksums
92a357952119ca2d36c2f2fa8ba793d32dd5dde43bda862e48452a75e39d7724
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Aug 13, 2026.

Transparency log

Release history Release notifications | RSS feed

0.1.13

2 release files

0.1.12

2 release files

This release

0.1.11 This release

2 release files

0.1.10

2 release files

0.1.9

2 release files

0.1.8

2 release files

0.1.7

2 release files

0.1.6

2 release files

0.1.5

2 release files

0.1.4

2 release files

0.1.3

2 release files

0.1.2

2 release files

0.1.1

2 release files

0.1.0

2 release files

0.0.11

2 release files

0.0.10

2 release files

0.0.9

2 release files

0.0.8

2 release files

0.0.7

2 release files

0.0.6

2 release files

0.0.5

2 release files

0.0.4

2 release files

0.0.3

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page