Skip to main content

A cute little companion that generates type-safe clients from OpenAPI documents.

Project description

๐Ÿฆฆ OtterAPI

A cute and intelligent OpenAPI client generator that dives deep into your OpenAPIs

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
  • Module Splitting - Organize large APIs into multiple organized files
  • 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, aget_pet_by_id

# Synchronous usage
pet = get_pet_by_id(pet_id=123)

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

๐Ÿ“ Configuration

Basic Configuration

documents:
  - source: https://petstore3.swagger.io/api/v3/openapi.json
    output: petstore_client

  - source: ./local-api.json
    output: local_client
    base_url: https://api.example.com

Full Configuration Options

documents:
  - source: https://api.example.com/openapi.json  # URL or file path (required)
    output: ./client                               # Output directory (required)
    base_url: https://api.example.com              # Override base URL from spec
    models_file: models.py                         # Models filename (default: models.py)
    endpoints_file: endpoints.py                   # Endpoints filename (default: endpoints.py)
    generate_async: true                           # Generate async functions (default: true)
    generate_sync: true                            # Generate sync functions (default: true)
    client_class_name: MyAPIClient                 # Client class name (default: from API title)
    module_split:                                  # Module splitting configuration
      enabled: false                               # Enable splitting (default: false)
      # ... see Module Splitting section below

๐Ÿ“ฆ Module Splitting

For large APIs with many endpoints, OtterAPI can split the generated code into multiple organized modules instead of a single endpoints.py file.

Why Use Module Splitting?

  • Better Organization - Group related endpoints together
  • Easier Navigation - Find endpoints quickly in smaller files
  • Improved IDE Performance - Smaller files load faster
  • Cleaner Imports - Import only what you need from specific 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

Uses the first tag from each operation to determine the module:

module_split:
  enabled: true
  strategy: tag
  min_endpoints: 1

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

path - Split by URL Path

Uses the first segment(s) of the URL path:

module_split:
  enabled: true
  strategy: path
  path_depth: 1                    # Number of path segments to use
  global_strip_prefixes:           # Remove these prefixes first
    - /api/v1
    - /api/v2

Result: /api/v1/users/123 โ†’ users.py, /api/v1/orders/456 โ†’ orders.py

custom - Explicit Module Mapping

Define exactly which paths go to which modules using glob patterns:

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

hybrid - Combined Strategy (Default)

Tries custom module_map first, then falls back to tags, then path:

module_split:
  enabled: true
  strategy: hybrid
  module_map:
    health:                        # Custom mapping takes priority
      - /health
      - /ready
  # Remaining endpoints use tags if available, otherwise path

none - All to Fallback

All endpoints go to a single fallback module:

module_split:
  enabled: true
  strategy: none
  fallback_module: api             # All endpoints go here

Pattern Syntax

The module map supports glob patterns:

Pattern Matches Example
/users Exact path /users only
/users/* Single segment /users/123, /users/abc
/users/** Any depth /users/123, /users/123/profile/settings
/v?/users Single character /v1/users, /v2/users

Nested Module Maps

Create hierarchical module structures:

module_split:
  enabled: true
  strategy: custom
  module_map:
    identity:                      # Parent module
      users:                       # Child: identity/users.py
        - /users/*
        - /users/**
      auth:                        # Child: identity/auth.py
        - /auth/*
        - /login
        - /logout
      roles:                       # Child: identity/roles.py
        - /roles/*
    billing:
      invoices:
        - /invoices/*
      payments:
        - /payments/*

Advanced Module Definition

Full control over each module:

module_split:
  enabled: true
  strategy: custom
  module_map:
    v2_api:
      paths:                       # Explicit paths key
        - /v2/**
      strip_prefix: /v2            # Strip this prefix from paths in this module
      description: "API v2 endpoints (deprecated)"  # Module docstring
      modules:                     # Nested submodules
        users:
          paths:
            - /users/*
        billing:
          paths:
            - /billing/*

Module Split Options Reference

Option Type Default Description
enabled bool false Enable module splitting
strategy string hybrid Strategy: none, path, tag, hybrid, custom
fallback_module string common Module name for unmatched endpoints
min_endpoints int 2 Minimum endpoints per module (smaller modules get consolidated)
flat_structure bool false true: flat files, false: nested directories
path_depth int 1 Path segments to use for path strategy (1-5)
global_strip_prefixes list common prefixes Prefixes to strip from all paths before matching
module_map object {} Custom module mappings

Output Structure Examples

Flat Structure (default):

client/
โ”œโ”€โ”€ __init__.py          # Re-exports all endpoints
โ”œโ”€โ”€ models.py            # Pydantic models
โ”œโ”€โ”€ _client.py           # Base client class
โ”œโ”€โ”€ client.py            # User-customizable client
โ”œโ”€โ”€ users.py             # User endpoints
โ”œโ”€โ”€ orders.py            # Order endpoints
โ””โ”€โ”€ health.py            # Health check endpoints

Nested Structure (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

Complete Example

documents:
  - source: https://api.mycompany.com/openapi.json
    output: ./mycompany_client
    module_split:
      enabled: true
      strategy: custom

      # Strip API version prefixes
      global_strip_prefixes:
        - /api/v1
        - /api/v2
        - /api/v3

      # Consolidate small modules (< 3 endpoints) into fallback
      min_endpoints: 3
      fallback_module: misc

      # Custom module organization
      module_map:
        # Simple health checks
        health:
          - /health
          - /ready
          - /metrics

        # User management
        users:
          - /users
          - /users/*
          - /users/**

        # Authentication
        auth:
          - /auth/*
          - /login
          - /logout
          - /refresh

        # Nested billing module
        billing:
          paths:
            - /billing/**
          description: "Billing and payment endpoints"
          modules:
            invoices:
              - /invoices/*
            subscriptions:
              - /subscriptions/*
            payments:
              - /payments/*

๐Ÿ“Š DataFrame Conversion

OtterAPI can generate additional methods that return pandas or polars DataFrames directly, making it easy to analyze API responses.

Enabling DataFrame Methods

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

Generated Methods

When enabled, endpoints that return lists get additional DataFrame methods:

Original Method Pandas Method Polars Method
get_users() get_users_df() get_users_pl()
aget_users() aget_users_df() aget_users_pl()

Basic Usage

from client import find_pets_by_status, find_pets_by_status_df, find_pets_by_status_pl

# Get as Pydantic models (existing behavior)
pets = find_pets_by_status("available")
for pet in pets:
    print(f"{pet.id}: {pet.name}")

# Get as pandas DataFrame
pdf = find_pets_by_status_df("available")
print(pdf.head())
print(pdf.describe())

# Get as polars DataFrame
plf = find_pets_by_status_pl("available")
print(plf.schema)
print(plf.head())

Handling Nested Responses

For APIs that return data nested under a key (e.g., {"data": {"users": [...]}}):

dataframe:
  enabled: true
  pandas: true
  polars: true
  default_path: "data.items"      # Default path for all endpoints
  endpoints:
    get_users:
      path: "data.users"          # Override for specific endpoint
    get_analytics:
      path: "response.events"

You can also override the path at runtime:

# Use configured path
df = get_users_df()

# Override path at call time
df = get_users_df(path="response.data.users")

Selective Generation

Control which endpoints get DataFrame methods:

dataframe:
  enabled: true
  pandas: true
  polars: true
  include_all: false              # Don't generate for all endpoints
  endpoints:
    list_users:
      enabled: true               # Only generate for this endpoint
    get_analytics:
      enabled: true
      path: "events"
      polars: true
      pandas: false               # Only polars for this endpoint

DataFrame Configuration Options

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

Per-Endpoint Options

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

๐Ÿ“– Using Generated Code

Direct Function Imports

# Import specific endpoints
from client import get_user, create_user, list_orders

# Async versions are prefixed with 'a'
from client import aget_user, acreate_user, alist_orders

# Sync usage
user = get_user(user_id=123)
orders = list_orders(status="pending", limit=10)

# Async usage
import asyncio

async def main():
    user = await aget_user(user_id=123)
    orders = await alist_orders(status="pending")

asyncio.run(main())

Module-Specific Imports (with splitting)

# Import from specific modules
from client.users import get_user, create_user
from client.orders import list_orders, get_order
from client.auth import login, logout

Using the Client Class

from client import Client

# Create client with default settings
client = Client()

# Or customize the client
client = Client(
    base_url="https://api.example.com",
    timeout=30.0,
    headers={
        "Authorization": "Bearer your-token",
        "X-Custom-Header": "value"
    }
)

# Use client methods (sync)
user = client.get_user(user_id=123)
orders = client.list_orders(status="pending")

# Use async methods
import asyncio

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

asyncio.run(main())

Working with Models

from client.models import User, Order, CreateUserRequest

# Models are Pydantic BaseModels
new_user = CreateUserRequest(
    name="John Doe",
    email="john@example.com"
)

# Create user
user = create_user(body=new_user)

# Access typed response
print(user.id)
print(user.name)
print(user.email)

๐Ÿ”ง 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

# Initialize a new config file
otter init

# Validate configuration
otter validate

๐Ÿ Programmatic API

For scripts and CI pipelines you can drive code generation from Python instead of the CLI:

from otterapi.codegen import Codegen
from otterapi.config import DocumentConfig

config = DocumentConfig(
    source='https://petstore3.swagger.io/api/v3/openapi.json',
    output='./client',
    base_url='https://petstore3.swagger.io/api/v3',
    # Any of the optional sections from otter.yml work here too:
    # pagination=PaginationConfig(enabled=True),
    # export=ExportConfig(enabled=True, formats=['parquet']),
)
Codegen(config).generate()

Codegen.generate() writes the same files the CLI would and raises pydantic.ValidationError (wrapped as ConfigValidationError when loading from YAML/JSON/TOML) on bad config -- friendly multi-line messages include the source path and the offending field path.


โ— Error Handling

Generated clients raise a typed exception hierarchy rooted at BaseAPIError -- you no longer need to inspect .status_code to discriminate by error class:

from my_client import (
    Client, list_users,
    BaseAPIError,        # catches every API error
    ClientError,         # all 4xx
    ServerError,         # all 5xx
    NotFoundError,       # specific: HTTP 404
    RateLimitError,      # specific: HTTP 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 API error %d: %s", e.status_code, e.detail)

Mapped status codes get specific subclasses (400 / 401 / 403 / 404 / 409 / 422 / 429 / 500 / 502 / 503 / 504); other 4xx / 5xx codes fall through to ClientError / ServerError. Subclass BaseAPIError in your generated client.py to customize how from_response parses detail payloads.


โ™ป๏ธ Regenerating after spec changes

OtterAPI is idempotent for the regenerated files:

  • models.py, endpoints.py, _client.py, _pagination.py, _export.py, _dataframe.py are rewritten on every otter generate -- never edit them by hand.
  • client.py is generated once and then left alone. This is your customization seam (auth, retries, logging, custom error parsing).

If a spec change renames a model or endpoint, the corresponding symbol in your generated package's __init__.py will move with it. Pin a version of OtterAPI in your dev/test deps if you want a guaranteed stable surface across regen runs.


๐Ÿ›  Development

# Clone the repository
git clone https://github.com/yourusername/otterapi.git
cd otterapi

# Install dependencies with uv
uv sync

# Run tests
uv run pytest

# Run tests with coverage
uv run pytest --cov=otterapi

# Run the generator
uv run python -m otterapi generate

# Format code
uv run ruff format .

# Lint code
uv run ruff check .

๐Ÿ“„ License

MIT License - see LICENSE for details.

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

otterapi-0.0.13.tar.gz (224.2 kB view details)

Uploaded Source

Built Distribution

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

otterapi-0.0.13-py3-none-any.whl (274.6 kB view details)

Uploaded Python 3

File details

Details for the file otterapi-0.0.13.tar.gz.

File metadata

  • Download URL: otterapi-0.0.13.tar.gz
  • Upload date:
  • Size: 224.2 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.12

File hashes

Hashes for otterapi-0.0.13.tar.gz
Algorithm Hash digest
SHA256 0107087660877f3bdcb9ca4f44a26327e1a5ef430c336ebf39539b4915676209
MD5 2fc643ded082be4f08f3d497c68c6d44
BLAKE2b-256 16c7bfc88a6b09180d3a552eed656d090ad0d737f1347855ea3f9fd4a24834c5

See more details on using hashes here.

Provenance

The following attestation bundles were made for otterapi-0.0.13.tar.gz:

Publisher: release.yml on danplischke/otterapi

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file otterapi-0.0.13-py3-none-any.whl.

File metadata

  • Download URL: otterapi-0.0.13-py3-none-any.whl
  • Upload date:
  • Size: 274.6 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.12

File hashes

Hashes for otterapi-0.0.13-py3-none-any.whl
Algorithm Hash digest
SHA256 6be7f2dd7cf2eb66a89d2a49d2da9d8cbbac39d030a729d957cb8c89b5ebff78
MD5 75981b64b67a4d62dc07e31d8b2fdc88
BLAKE2b-256 ba7fa8b51c7e6b954305a5325cd4b2c55499442530e78754c4f40d376c82bded

See more details on using hashes here.

Provenance

The following attestation bundles were made for otterapi-0.0.13-py3-none-any.whl:

Publisher: release.yml on danplischke/otterapi

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

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