senzo 0.1.0
Generate typed Python API clients from OpenAPI specs
Requires Python: >=3.10

# Senzo

Senzo is a Python library for generating typed API clients from OpenAPI 3.x specifications. Unlike other client generators, Senzo is designed to support a number of HTTP clients (aiohttp, httpx, requests) for synchronous/async clients as well as several dataclass-like libraries such as Pydantic and msgspec.

**Note:** Senzo is still in development and is not yet ready for production use.

## Using Senzo

Senzo is designed to be used programmatically through the Python package as opposed to a CLI tool. One can write a simple Python script in order to generate your client package. For example, if you wanted to build an API client that utilized Pydantic models for serialization and httpx for HTTP requests, you could do the following:

```python
import json
import senzo

from senzo.backends.dataclass.pydantic import PydanticBackend
from senzo.backends.http.httpx import HttpxBackend

with open("openapi.json") as f:
    spec = json.load(f)

tree = senzo.generate_tree(
    spec,
    package_name="my_api",
    dataclass_backend=PydanticBackend(),
    http_backend=HttpxBackend(async_mode=True),
)
senzo.write_package(tree, output_dir="./my_api")
```

Senzo is designed to make it easy to generate synchronous and asynchronous clients for your API. For example, if you wanted to generate a synchronous client in addition to the asynchronous client, you could do the following:

```python
import json
import senzo

from senzo.backends.dataclass.pydantic import PydanticBackend
from senzo.backends.http.httpx import HttpxBackend

with open("openapi.json") as f:
    spec = json.load(f)

tree = senzo.generate_tree(
    spec,
    package_name="my_api",
    dataclass_backend=PydanticBackend(),
    http_backend=HttpxBackend(async_mode=True),
)
senzo.write_package(tree, output_dir="./my_api")

tree = senzo.generate_tree(
    spec,
    package_name="my_sync_api",
    dataclass_backend=PydanticBackend(),
    http_backend=HttpxBackend(async_mode=False),
)
senzo.write_package(tree, output_dir="./my_sync_api")
```

The generated output is a small importable package (e.g. `my_api/`) containing:

- `client.py` (HTTP client methods)
- `types/models.py` (schema models)
- `_base.py` (runtime helpers: errors, response wrapper, optional pagination/websocket support)
- `py.typed` (PEP 561 marker)

## Use the generated client

Exact method names depend on `operationId` and `tag_style`.

```python
from my_api import Client

async with Client(base_url="https://api.example.com") as client:
    result = await client.some_operation(...)
```

## Configuration

### TagStyle

Controls how operations are organized:

- `TagStyle.FLAT` (default): one `Client` class with all methods
- `TagStyle.FLAT_PREFIXED`: one `Client`, methods prefixed with tag
- `TagStyle.GROUPED`: `Client` exposes sub-clients per tag (uses the first tag only)

```python
import senzo
tree = senzo.generate_tree(spec, tag_style=senzo.TagStyle.GROUPED, ...)
```

### EnumStyle (inline enums)

Given an inline enum in your OpenAPI spec:

```yaml
status:
  type: string
  enum: ["pending", "approved", "rejected"]
```

| Style | Generated Type |
|-------|----------------|
| `EnumStyle.LITERAL` (default) | `Literal["pending", "approved", "rejected"]` |
| `EnumStyle.ENUM` | `enum.Enum` class |
| `EnumStyle.STR_ENUM` | `StrEnum` class (Python 3.11+, recommended) |
| `EnumStyle.STR` | `str` (no type safety) |

Controls how _inline_ OpenAPI enums (schemas with `"enum": [...]` but no named component) are represented in type annotations.

```python
from senzo import generate_tree, EnumStyle

tree = generate_tree(spec, enum_style=EnumStyle.LITERAL, ...)
```

Named enum schemas under `components/schemas` are generated by the selected dataclass backend.

## Backends

### Dataclass (model) backends

- `senzo.backends.dataclass.pydantic.PydanticBackend`
- `senzo.backends.dataclass.msgspec.MsgspecBackend`

These backends control:

- how schema objects are rendered (Pydantic models vs msgspec structs)
- how JSON encoding/decoding code is emitted into the client

### HTTP backends

- `senzo.backends.http.httpx.HttpxBackend(async_mode=...)` (sync or async)
- `senzo.backends.http.aiohttp.AiohttpBackend()` (async only)
- `senzo.backends.http.requests.RequestsBackend()` (sync only)

## Pagination (optional)

Senzo can generate an iterator helper `<operation>_iter` for operations that match a token-based pagination pattern.

Enable it via:

- generator config: `pagination={operation_id: {...}}`, or
- per-operation OpenAPI extension: `x-senzo-pagination: {...}`

Fields:

- `items` (default `"items"`): response property containing the item list
- `next_token` (default `"next_page"`): response property containing the next token
- `token_param` (default `"page_token"`): query parameter name for the page token
- `limit_param` (default `"limit"`): query parameter name for the page size

Limitations:

- requires `token_param` and `limit_param` query parameters to exist
- requires a success response type with `items` and `next_token` properties
- does not paginate operations with a request body

## WebSocket (optional; aiohttp backend only)

WebSocket endpoints are generated only when:

- the OpenAPI operation has `x-websocket: true`, and
- the selected HTTP backend supports websockets (currently `AiohttpBackend`)

Message types can be declared via `x-websocket-messages`:

```yaml
paths:
  /ws/chat:
    get:
      operationId: connect_chat
      x-websocket: true
      x-websocket-messages:
        send:
          $ref: "#/components/schemas/ChatMessage"
        receive:
          $ref: "#/components/schemas/ServerEvent"
```

## Hooks (generation-time; optional)

Senzo can bake calls to user-provided hook functions into the generated client. Hooks are configured at generation time using `HooksConfig` and are referenced by import path.

Supported hook slots:

- `pre_hook(inner, request, info) -> request`
- `post_hook(inner, response, info) -> response`
- `on_error(inner, error, info) -> None`
- `on_result(inner, result, info) -> result`

## Acknowledgements

Senzo is heavily influenced by [Progenitor](https://github.com/oxidecomputer/progenitor), an OpenAPI client generator for Rust developed by Oxide.