Metadata-Version: 2.3
Name: pyoas
Version: 0.5.2
Summary: Generate Pydantic v2 models and FastAPI routers from OpenAPI 3.0/3.1 specs
Requires-Dist: jinja2>=3.1
Requires-Dist: jsonref>=1.1
Requires-Dist: openapi-spec-validator>=0.7
Requires-Dist: pydantic>=2.9
Requires-Dist: pyyaml>=6.0
Requires-Dist: typer>=0.12
Requires-Dist: watchdog>=3.0
Requires-Dist: pyoas[fastapi,claude,testing] ; extra == 'all'
Requires-Dist: fastapi>=0.110 ; extra == 'fastapi'
Requires-Dist: polyfactory>=2.0 ; extra == 'testing'
Requires-Python: >=3.12
Provides-Extra: all
Provides-Extra: claude
Provides-Extra: fastapi
Provides-Extra: testing
Description-Content-Type: text/markdown

# pyoas

Generate Pydantic v2 models and FastAPI routers from an OpenAPI spec. Organized as a uv workspace with independent, installable packages.

## Packages

| Package | Purpose |
|---|---|
| `pyoas` | Spec loading, ref resolution, tag extraction, Jinja2 rendering, Pydantic v2 model generation, CLI |
| `pyoas[fastapi]` | FastAPI router generation + service stubs + test scaffolding (adds FastAPI dependency) |
| `pyoas[claude]` | Claude Code skill generation (optional, no extra runtime dependencies) |

`pyoas[fastapi]` and `pyoas[claude]` both extend the base `pyoas` package.

## Quick start

```shell
# Install
uv add pyoas[fastapi]

# Create a minimal config
cat > pyoas.yaml << 'EOF'
spec: openapi.yaml
output:
  models: src/generated/models
  routers: src/generated/routers
EOF

# Generate Pydantic models and FastAPI routers
uv run pyoas generate

# Optionally scaffold service stubs and test files
uv run pyoas scaffold services
```

A more complete config with all optional features:

```yaml
spec: openapi.yaml
output:
  models: src/generated/models
  routers: src/generated/routers
services:
  generate: true
  output: src/services
  import_path: myapp.services  # Python import path for service module
tests:
  generate: true
  output: tests/generated
  not_found_exception: "HTTPException(status_code=404, detail='Not found')"
skills:
  generate: true  # requires pyoas[claude]
```

Then:

```shell
uv run pyoas generate   # models + routers + service stubs + tests + skills
```

## Configuration reference (`pyoas.yaml`)

### `spec`

Path to the OpenAPI 3.0/3.1 spec file (YAML or JSON). Resolved relative to the config file. **Required.**

### `output`

| Key | Default | Description |
|---|---|---|
| `models` | `src/generated/models` | Output directory for generated model files |
| `routers` | `src/generated/routers` | Output directory for generated router files |
| `models_import` | _(derived)_ | Python import path for models; derived from `models` path if omitted |
| `routers_import` | _(derived)_ | Python import path for routers; derived from `routers` path if omitted |
| `source_root` | `src` | Filesystem prefix stripped when deriving Python import paths |

### `default_tag`

Default: `"default"`. Operations with no tag are grouped under this name.

### `model_config`

| Key | Default | Description |
|---|---|---|
| `extra` | `"ignore"` | Pydantic `extra` setting for response/shared models |
| `request_extra` | `"forbid"` | Pydantic `extra` setting for request-only models |
| `frozen` | `false` | Makes generated models immutable |
| `populate_by_name` | `true` | Allow populating fields by Python name as well as alias |
| `include_unreferenced` | `false` | Also generate models for schemas not referenced by any operation |

### `fields`

| Key | Default | Description |
|---|---|---|
| `snake_case` | `true` | Convert camelCase field names to snake_case with an alias |
| `enums_as_literals` | `true` | Render small enums as `Literal[...]` instead of `Enum` subclasses |
| `unique_items_as_set` | `true` | Render arrays with `uniqueItems: true` as `set[T]`; set to `false` to keep `list[T]` |

### `format`

| Key | Default | Description |
|---|---|---|
| `enabled` | `true` | Run `ruff format` on generated files after writing |

### `templates`

| Key | Default | Description |
|---|---|---|
| `models` | `null` | Path to a directory of custom Jinja2 templates overriding model templates |
| `routers` | `null` | Path to a directory of custom Jinja2 templates overriding router templates |

### `services`

| Key | Default | Description |
|---|---|---|
| `generate` | `false` | Scaffold service stub files |
| `output` | `src/services` | Output directory for service files |
| `overwrite` | `false` | Overwrite existing service files on re-run |
| `import_path` | `""` | Python import path used by routers to import the service (e.g. `myapp.services`) |

### `tests`

| Key | Default | Description |
|---|---|---|
| `generate` | `false` | Scaffold pytest test stub files |
| `output` | `tests/generated` | Output directory for test files |
| `overwrite` | `false` | Overwrite existing test files on re-run (default: append new test classes only) |
| `not_found_exception` | `null` | Exception expression used in `test_not_found` stubs (e.g. `HTTPException(status_code=404)`) |

### `skills`

Requires `pyoas[claude]` to be installed.

| Key | Default | Description |
|---|---|---|
| `generate` | `false` | Generate Claude Code skill files |
| `output` | `.claude/commands` | Output directory for skill files |
| `overwrite` | `false` | Overwrite existing skill files on re-run |

### `webhooks`

OAS 3.1 webhooks are extracted but not generated by default.

| Key | Default | Description |
|---|---|---|
| `generate` | `false` | Generate FastAPI routers for webhook operations (OAS 3.1 only) |

### `extensions`

Register custom Jinja2 filters and globals loaded at render time via `importlib`.

| Key | Default | Description |
|---|---|---|
| `filters` | `null` | `"module:attr"` pointing to a callable returning `dict[str, Callable]` |
| `globals` | `null` | `"module:attr"` pointing to a callable returning `dict[str, Any]` |

Example:

```yaml
extensions:
  filters: myapp.pyoas_extensions:custom_filters
  globals: myapp.pyoas_extensions:custom_globals
```

### `plugins`

List of plugin class specifiers loaded at generation time. Each entry is a
`"module:ClassName"` string. Plugins can also be discovered automatically via
`pyproject.toml` entry-points (group `"pyoas.plugins"`).

```yaml
plugins:
  - myapp_plugin:HeaderPlugin
```

See `examples/plugin_example/` for a working example.

## Generated output

### Models (`pyoas`)

One file per tag: `{models_output}/{tag}/models.py`. Schemas referenced by multiple tags go to `{models_output}/shared/models.py`.

```
src/generated/models/
  __init__.py
  pets/
    models.py      # Pet, PetCreate, PetList, ...
  shared/
    models.py      # schemas used by more than one tag
```

### Routers (`pyoas[fastapi]`)

One file per tag: `{routers_output}/{tag}/router.py`. An `__init__.py` at the root re-exports all routers.

```
src/generated/routers/
  __init__.py      # from .pets import router as pets_router; ...
  pets/
    router.py      # APIRouter with typed endpoint stubs
```

### Service stubs

One file per tag: `{services_output}/{tag}.py`. Scaffolded once; never overwritten by default.

```
src/services/
  pets.py          # PetsService class with async method stubs
```

### Test scaffolding

One test file per tag plus a shared `conftest.py` with model factories.

```
tests/generated/
  conftest.py      # make_pet(), make_pet_list(), ...
  test_pets.py     # TestListPets, TestCreatePet, TestGetPet, ...
```

Each test class covers one endpoint and includes:
- `test_endpoint_exists` — verifies the route returns something other than 404/405
- Validation tests for required fields, numeric bounds, string constraints, enum violations
- `test_not_found` — verifies 404 when the service raises the configured exception
- `test_success` — happy-path stub (auto-implemented for GET/DELETE, stubbed for others)

## CLI reference

### Generation

```shell
pyoas models        # generate Pydantic models only
pyoas fastapi       # generate FastAPI routers only
pyoas generate      # generate models + routers (+ services/tests/skills if configured)
```

Generation commands accept:
- `--config PATH` — path to config file (default: `pyoas.yaml`)
- `--tags TAG1,TAG2` — limit generation to specific tags
- `--clean` — purge output directory before generating
- `--quiet` — suppress progress output (errors still shown)
- `--verbose` — show per-tag timing alongside progress

### Scaffolding

```shell
pyoas scaffold services       # scaffold service stubs (skips existing files)
pyoas scaffold tests          # scaffold pytest test files (skips existing files)
pyoas scaffold dependencies   # scaffold auth dependency stubs
pyoas scaffold skills         # scaffold Claude Code skill files
pyoas scaffold webhooks       # print webhook router mount instructions
```

### Diagnostics

```shell
pyoas doctor         # pre-flight checks: missing operationIds, broken refs, tag collisions, …
pyoas validate       # parse and validate the spec file; exits non-zero on error
pyoas diff           # dry-run generation and report added/removed/changed files
pyoas drift          # detect service methods that are missing or out of sync with the spec
```

`doctor` and `validate` accept `--json` to emit structured JSON instead of coloured text.

### Maintenance

```shell
pyoas fix            # auto-fix common spec issues (assign operationIds, deduplicate, normalise tags)
pyoas fix --dry-run  # show what would be changed without writing the file
pyoas fix --tag-casing lower  # normalise tags to lowercase (default: title-case)

pyoas migrate OLD_SPEC NEW_SPEC          # diff two specs and classify breaking changes
pyoas migrate OLD NEW --json             # structured JSON output
pyoas migrate OLD NEW --breaking-only   # suppress non-breaking changes (useful in CI)
```

`migrate` exits non-zero when breaking changes are found.

### Initialisation

```shell
pyoas init openapi.yaml            # generate a starter pyoas.yaml
pyoas init openapi.yaml --force    # overwrite existing config
```

## Plugin architecture

Plugins let you post-process generated files without forking pyoas. A plugin is a plain Python class with four lifecycle hooks:

```python
from typing import Any

class MyPlugin:
    name = "my_plugin"
    version = "1.0.0"

    def on_spec_loaded(self, spec: dict, resolved: dict) -> tuple[dict, dict]:
        return spec, resolved  # return unchanged, or modify and return

    def on_model_file_written(self, tag: str, path: str, content: str) -> str:
        return content  # return modified content (must be non-empty)

    def on_router_file_written(self, tag: str, path: str, content: str) -> str:
        return content

    def on_generate_complete(self, stats: dict[str, Any]) -> None:
        print(f"Done: {stats['files_written']} file(s) written")
```

Activate via `pyoas.yaml`:

```yaml
plugins:
  - myapp.plugin:MyPlugin
```

Or via `pyproject.toml` entry-points for distributable plugins:

```toml
[project.entry-points."pyoas.plugins"]
my_plugin = "myapp.plugin:MyPlugin"
```

`pyoas doctor` validates plugin imports before generation starts. See
`examples/plugin_example/` for a complete working example.

## Claude Code integration (`pyoas[claude]`)

Install `pyoas[claude]` and set `skills.generate: true` in your config. Running `pyoas generate` will write Claude Code skill files to `.claude/commands/`:

| Skill | Invocation | Purpose |
|---|---|---|
| `implement-tests.md` | `/implement-tests tests/generated/test_pets.py` | Implement all `pytest.skip("implement me")` stubs in a test file |
| `add-test-case.md` | `/add-test-case tests/generated/test_pets.py "scenario"` | Add a new test method for the described scenario |
| `review-generated.md` | `/review-generated` | Cross-reference generated code against the OpenAPI spec and flag issues |

## Development

```shell
# Install in editable mode with all extras
uv sync --extra fastapi --extra claude

# Run all tests
uv run pytest

# Run tests for a single area
uv run pytest tests/fastapi/

# Update snapshots
uv run pytest tests/fastapi/ --snapshot-update

# Lint and type-check
uv run ruff check src/
uv run mypy src/
```
