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

๐Ÿ›  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.11.tar.gz (173.1 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.11-py3-none-any.whl (192.8 kB view details)

Uploaded Python 3

File details

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

File metadata

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

File hashes

Hashes for otterapi-0.0.11.tar.gz
Algorithm Hash digest
SHA256 dabbb3319e4ee61df4ddc63961fbf50c3c14dce78d1f04654210540ea92b2736
MD5 7bbbfa731f8e6b72f8a792b02bdb503b
BLAKE2b-256 688ce6ef3304eef1d8e7cb1e13862aa4edae6717e5970ab40e6f904c3a43b396

See more details on using hashes here.

Provenance

The following attestation bundles were made for otterapi-0.0.11.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.11-py3-none-any.whl.

File metadata

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

File hashes

Hashes for otterapi-0.0.11-py3-none-any.whl
Algorithm Hash digest
SHA256 1a7e08932cdf0967414674f4cdc5eb28c0ca06df2d79bb5fd31f36e8e1c1b96a
MD5 25d15c7830aaacbba2181d4af8d45be9
BLAKE2b-256 b657fcb820497f4ccd17ee0fb8f261bf686488d3153a670344bc6c46fe89e6b8

See more details on using hashes here.

Provenance

The following attestation bundles were made for otterapi-0.0.11-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