Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,4 @@
/.venv
/dist
/.coverage
/.python-version
2 changes: 1 addition & 1 deletion .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
# See https://pre-commit.com/hooks.html for more hooks
repos:
- repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.15.2
rev: v0.15.12
hooks:
- id: ruff-format
name: ruff format
Expand Down
17 changes: 13 additions & 4 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,10 @@ make test-ci # pytest with coverage (no warnings)
make test-report # pytest with HTML coverage report
make lock # uv lock --upgrade

# Run a single test
uv run pytest tests/test_file.py::test_name # by name
uv run pytest -k 'pattern' # by keyword

# Run example server
uv run example/

Expand All @@ -37,10 +41,11 @@ uv run tools/update_swagger_ui/

### Core Flow

1. **`@docs(...)` decorator** (`_decorator.py`) — attaches an `ApiEndpoint` TypedDict as `_openapi_docs` attribute on handler functions
2. **`build_openapi_spec(app, info)`** (`_spec_builder.py`) — at startup, iterates `app.router.routes()`, finds handlers with `_openapi_docs`, and builds the full OpenAPI 3.1 spec. Supports both function handlers and class-based views (iterates HTTP methods on classes). Falls back to handler docstrings for descriptions, `__deprecated__` for deprecation, and `request_body` parameter annotations for body models.
3. **View factories** (`_views.py`) — `get_json_spec_view` and `get_swagger_view` return closures that capture the built spec, so it's computed once at startup
4. **`setup_docs(app, ...)`** (`_setup.py`) — registers three routes: static files (Swagger UI assets), JSON spec endpoint, and Swagger UI HTML page
1. **`@docs(...)` decorator** (`_decorator.py`) — attaches an `ApiEndpoint` TypedDict as `_openapi_docs` attribute on the original function; `@wraps` copies `__dict__` to the wrapper so the attribute is visible to the spec builder
2. **`build_openapi_spec(app, info)`** (`_spec_builder.py`) — at startup, iterates `app.router.routes()`, finds handlers with `_openapi_docs`, and builds the full OpenAPI 3.1 spec. Supports both function handlers and class-based views (iterates HTTP methods on classes). Falls back to handler docstrings for descriptions, `__deprecated__` for deprecation, and `request_body` parameter annotations for body models
3. **`SchemaCollector`** (`_spec_builder.py`) — accumulates Pydantic model schemas for `components/schemas`. Rewrites `$defs` refs to `#/components/schemas/` so nested models produce valid OpenAPI `$ref`s. Two entry points: `add_schema_model` (body/response models → full schema + ref) and `process_parameter` (parameter models → field-level schemas only, model itself not added to components)
4. **View factories** (`_views.py`) — `get_json_spec_view` and `get_swagger_view` return closures that capture the built spec, so it's computed once at startup
5. **`setup_docs(app, ...)`** (`_setup.py`) — registers three named routes (names defined in `_constants.py`): static files (`docs.swagger.static`), JSON spec endpoint (`docs.openapi.spec`), and Swagger UI HTML page (`docs.swagger.ui`). The view factories look up these route names to resolve URLs

### Key Design Decisions

Expand All @@ -49,6 +54,10 @@ uv run tools/update_swagger_ui/
- **Template substitution** — Swagger UI `index.html` uses `string.Template` with `$path`, `$static`, `$layout` placeholders
- **Bundled Swagger UI** — static assets live in `aiohttp_docs/swagger/`; `tools/update_swagger_ui/` automates downloading new versions from GitHub

### Testing

Tests use `pytest-aiohttp` for async handler testing.

## Code Style

- **Ruff** is the sole linter/formatter: line length 120, single quotes, spaces, Google docstring convention
Expand Down
26 changes: 21 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
Auto-generate [OpenAPI 3.1](https://spec.openapis.org/oas/v3.1.2) specification and
[Swagger UI](https://swagger.io/tools/swagger-ui/) documentation for
[aiohttp](https://docs.aiohttp.org/) web servers.
Swagger version: <!-- SWAGGER_UI_VERSION_START -->[v5.31.2](https://github.com/swagger-api/swagger-ui/releases/tag/v5.31.2)<!-- SWAGGER_UI_VERSION_END -->
Swagger version: <!-- SWAGGER_UI_VERSION_START -->[v5.32.5](https://github.com/swagger-api/swagger-ui/releases/tag/v5.32.5)<!-- SWAGGER_UI_VERSION_END -->

Annotate your route handlers with the `@docs()` decorator and call `setup_docs()` once at startup — the library
builds the full spec and serves both the JSON endpoint and the interactive Swagger UI.
Expand Down Expand Up @@ -55,7 +55,7 @@ def main() -> None:
version='0.1.0',
),
spec_url_path='/api/openapi.json', # URL to for OpenAPI Specification
swagger_url_path='/api/doc', # URL for Swagger
swagger_url_path='/api/docs', # URL for Swagger
)

web.run_app(app)
Expand All @@ -65,7 +65,7 @@ if __name__ == '__main__':
main()
```

After starting the server, open `http://localhost:8080/api/doc` to see the Swagger UI.
After starting the server, open `http://localhost:8080/api/docs` to see the Swagger UI.


## Examples
Expand Down Expand Up @@ -355,7 +355,7 @@ def main():
app = web.Application()

# disable docs for production
# `/api/openapi.json`, `/api/doc` and `/static/swagger` will return 404 Not Found
# `/api/openapi.json`, `/api/docs` and `/static/swagger` will return 404 Not Found
setup_docs(
app,
info=Info(title='My API', version='0.1.0'),
Expand All @@ -375,5 +375,21 @@ if __name__ == '__main__':
- Nested models, root models
- Links in responses
- Move from TypedDict to pydantic models (because of "termsOfService", and "Parameter.in" for instance)
- Authorization

Version 1:

- authorization: done, but need to be checked and tested
- servers: done, but need to be checked and tested
- security: done, but need to be checked and tested
- components: done (probably partially), need to be checked and tested
- tags
- webhooks
- externalDocs

Version 2:

- Automatic validation of the body, response, query, path etc (based on annotations)

## Documentation

OpenAPI Specification: https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.1.2.md
24 changes: 22 additions & 2 deletions aiohttp_docs/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,26 @@

from ._decorator import docs
from ._doc_models import ApiEndpoint, Response, Responses
from ._enums import SwaggerLayout
from ._enums import SecuritySchemeIn, SecuritySchemeType, SwaggerLayout
from ._setup import setup_docs
from ._spec_models import Contact, Example, Info, Licence, Operation, Parameter, PathItem
from ._spec_models import (
Components,
Contact,
Example,
Info,
Licence,
Operation,
Parameter,
PathItem,
SecurityRequirement,
SecurityScheme,
Server,
ServerVariable,
)

__all__ = (
'ApiEndpoint',
'Components',
'Contact',
'Example',
'Info',
Expand All @@ -17,6 +31,12 @@
'PathItem',
'Response',
'Responses',
'SecurityRequirement',
'SecurityScheme',
'SecuritySchemeIn',
'SecuritySchemeType',
'Server',
'ServerVariable',
'SwaggerLayout',
'docs',
'setup_docs',
Expand Down
6 changes: 3 additions & 3 deletions aiohttp_docs/_doc_models.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,11 @@

from pydantic import BaseModel

type Responses = dict[HTTPStatus | int, Response | type[BaseModel] | None]
type Responses = dict[HTTPStatus | int, Response | type[BaseModel]]


class Response(TypedDict, total=False):
model: Required[type[BaseModel] | None]
model: type[BaseModel]
description: str


Expand All @@ -21,4 +21,4 @@ class ApiEndpoint(TypedDict, total=False):
header_model: type[BaseModel]
cookie_model: type[BaseModel]
body_model: type[BaseModel]
response_models: Responses
response_models: Required[Responses]
14 changes: 14 additions & 0 deletions aiohttp_docs/_enums.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,3 +11,17 @@ class ParameterType(StrEnum):
QUERY = 'query'
HEADER = 'header'
COOKIE = 'cookie'


class SecuritySchemeIn(StrEnum):
QUERY = 'query'
HEADER = 'header'
COOKIE = 'cookie'


class SecuritySchemeType(StrEnum):
API_KEY = 'apiKey'
HTTP = 'http'
MUTUAL_TLS = 'mutualTLS'
OAUTH2 = 'oauth2'
OPEN_ID_CONNECT = 'openIdConnect'
10 changes: 8 additions & 2 deletions aiohttp_docs/_setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,16 +3,19 @@
from ._constants import ROUTE_NAME_SPEC_VIEW, ROUTE_NAME_SWAGGER_STATIC, ROUTE_NAME_SWAGGER_VIEW, SWAGGER_UI_DIR_PATH
from ._enums import SwaggerLayout
from ._spec_builder import build_openapi_spec
from ._spec_models import Info
from ._spec_models import Info, SecurityRequirement, SecurityScheme, Server
from ._views import get_json_spec_view, get_swagger_view


def setup_docs( # noqa: PLR0913
app: web.Application,
*,
info: Info,
servers: list[Server] | None = None,
security_schemes: dict[str, SecurityScheme] | None = None,
security: list[SecurityRequirement] | None = None,
spec_url_path: str = '/api/openapi.json',
swagger_url_path: str = '/api/doc',
swagger_url_path: str = '/api/docs',
static_url_path: str = '/static/swagger',
layout: SwaggerLayout = SwaggerLayout.BASE,
enabled: bool = True,
Expand All @@ -24,6 +27,9 @@ def setup_docs( # noqa: PLR0913
spec = build_openapi_spec(
app=app,
info=info,
servers=servers,
security_schemes=security_schemes,
security=security,
)

app.router.add_static(
Expand Down
Loading