Generate modern Python clients from OpenAPI 3.0 and 3.1 documents
—
Extensible Jinja2-based template system for customizing generated code structure, formatting, and content. The template system provides complete control over how OpenAPI specifications are transformed into Python client code.
The core template system built on Jinja2 with custom filters and globals.
class Project:
env: Environment # Jinja2 environment with custom configuration
def __init__(
self,
*,
openapi: GeneratorData,
config: Config,
custom_template_path: Optional[Path] = None,
) -> None:
"""
Initialize project with template environment.
The environment includes:
- Custom template filters (snakecase, kebabcase, pascalcase, any)
- Global variables (config, utils, openapi data, etc.)
- Template loader with optional custom template override
Parameters:
- openapi: Parsed OpenAPI specification data
- config: Generation configuration
- custom_template_path: Optional directory containing custom templates
"""Built-in filters for string transformation and formatting.
# Internal constant used by the template system
TEMPLATE_FILTERS = {
"snakecase": utils.snake_case, # Convert to snake_case
"kebabcase": utils.kebab_case, # Convert to kebab-case
"pascalcase": utils.pascal_case, # Convert to PascalCase
"any": any, # Python any() function
}Global variables and functions available in all templates.
# Template globals include:
config: Config # Generation configuration
utils: module # Utility functions module
python_identifier: Callable # Create valid Python identifiers
class_name: Callable # Create valid class names
package_name: str # Generated package name
package_dir: Path # Package directory path
package_description: str # Package description
package_version: str # Package version
project_name: str # Project name
project_dir: Path # Project directory path
openapi: GeneratorData # Parsed OpenAPI data
endpoint_collections_by_tag: dict # Endpoints grouped by tagThe template system generates code through a structured pipeline.
class Project:
def build(self) -> Sequence[GeneratorError]:
"""
Create the project from templates using this pipeline:
1. _create_package() - Generate package structure and __init__.py
2. _build_metadata() - Generate project metadata files
3. _build_models() - Generate data model classes
4. _build_api() - Generate API endpoint functions and client classes
5. _run_post_hooks() - Execute post-generation commands
Returns:
List of errors encountered during generation
"""The built-in templates generate a complete Python client structure:
templates/
├── package_init.py.jinja # Package __init__.py
├── types.py.jinja # Type definitions
├── client.py.jinja # HTTP client classes
├── errors.py.jinja # Error definitions
├── model.py.jinja # Individual model classes
├── str_enum.py.jinja # String enum classes
├── int_enum.py.jinja # Integer enum classes
├── literal_enum.py.jinja # Literal enum types
├── models_init.py.jinja # Models package __init__.py
├── api_init.py.jinja # API package __init__.py
├── endpoint_module.py.jinja # API endpoint functions
├── endpoint_init.py.jinja # Tag-specific API __init__.py
├── pyproject.toml.jinja # Project metadata
├── setup.py.jinja # Setup.py metadata
├── README.md.jinja # Generated documentation
└── .gitignore.jinja # Git ignore rulesOverride any default template by creating a custom template directory:
custom-templates/
├── client.py.jinja # Custom client implementation
├── model.py.jinja # Custom model generation
└── README.md.jinja # Custom documentationTemplates have access to the complete parsed OpenAPI specification:
{# Access OpenAPI metadata #}
{{ openapi.title }}
{{ openapi.version }}
{{ openapi.description }}
{# Access models and enums #}
{% for model in openapi.models %}
{{ model.class_info.name }}
{{ model.class_info.module_name }}
{% endfor %}
{% for enum in openapi.enums %}
{{ enum.class_info.name }}
{{ enum.value_type }}
{% endfor %}
{# Access API endpoints #}
{% for tag, collection in endpoint_collections_by_tag.items() %}
{% for endpoint in collection.endpoints %}
{{ endpoint.name }}
{{ endpoint.method }}
{{ endpoint.path }}
{% endfor %}
{% endfor %}All configuration options are available in templates:
{# Project configuration #}
{{ config.project_name_override }}
{{ config.package_name_override }}
{{ config.field_prefix }}
{# Generation options #}
{% if config.docstrings_on_attributes %}
# Generate attribute docstrings
{% endif %}
{% if config.literal_enums %}
# Use literal types
{% endif %}Access utility functions for string manipulation:
{# String transformations #}
{{ "MyClassName" | snakecase }} {# -> my_class_name #}
{{ "my_function" | pascalcase }} {# -> MyFunction #}
{{ "some-value" | kebabcase }} {# -> some-value #}
{# Python identifier creation #}
{{ python_identifier("field name", config.field_prefix) }}
{{ class_name("model-name", config.field_prefix) }}# Copy default templates
cp -r /path/to/openapi_python_client/templates ./my-templates
# Modify templates as needed
vim ./my-templates/client.py.jinja
# Use custom templates
openapi-python-client generate \
--url https://api.example.com/openapi.json \
--custom-template-path ./my-templates{# custom-templates/client.py.jinja #}
"""Custom HTTP client implementation."""
from typing import Dict, Optional, Union
import httpx
class CustomClient:
"""Custom client with enhanced features."""
def __init__(
self,
base_url: str,
*,
cookies: Optional[Dict[str, str]] = None,
headers: Optional[Dict[str, str]] = None,
timeout: float = 30.0,
verify_ssl: bool = True,
follow_redirects: bool = False,
custom_auth: Optional[str] = None, # Custom authentication
) -> None:
self._base_url = base_url
self._cookies = cookies
self._headers = headers or {}
# Add custom authentication header
if custom_auth:
self._headers["X-Custom-Auth"] = custom_auth
self._client = httpx.Client(
base_url=base_url,
cookies=cookies,
headers=self._headers,
timeout=timeout,
verify=verify_ssl,
follow_redirects=follow_redirects,
)
def get_headers(self) -> Dict[str, str]:
"""Get current headers."""
return self._client.headers.copy()
def close(self) -> None:
"""Close the underlying client."""
self._client.close()
def __enter__(self) -> "CustomClient":
return self
def __exit__(self, *args) -> None:
self.close(){# base_model.jinja #}
{% macro model_docstring(model) %}
"""{{ model.description | default("Generated model class.") }}"""
{% endmacro %}
{# model.py.jinja #}
{% from "base_model.jinja" import model_docstring %}
class {{ model.class_info.name }}:
{{ model_docstring(model) }}
# ... rest of modelfrom openapi_python_client import Project
def custom_filter(value):
return value.upper().replace(" ", "_")
project = Project(openapi=data, config=config)
project.env.filters["custom"] = custom_filterfrom jinja2 import Environment, FileSystemLoader
env = Environment(loader=FileSystemLoader("./my-templates"))
template = env.get_template("model.py.jinja")
result = template.render(model=sample_model, config=sample_config)Install with Tessl CLI
npx tessl i tessl/pypi-openapi-python-client