Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Forms #54

Merged
merged 2 commits into from
Dec 10, 2023
Merged
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
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@ The **third number** is for emergencies when we need to start branches for older
### Changed

- [`datetime.datetime`](https://docs.python.org/3/library/datetime.html#datetime-objects) and [`datetime.date`](https://docs.python.org/3/library/datetime.html#date-objects) are now supported in the OpenAPI schema, both in models and handler parameters.
- Simple forms are now supported using {class}`uapi.ReqForm`. [Learn more](handlers.md#forms).
- _uapi_ now sorts imports using Ruff.

## [v23.1.0](https://github.com/tinche/uapi/compare/v22.1.0...v23.1.0) - 2023-11-12

Expand Down
2 changes: 1 addition & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,4 @@ test:
pdm run pytest tests -x --ff

lint:
pdm run mypy src/ tests/ && pdm run ruff src/ tests/ && pdm run black --check -q src/ tests/ && pdm run isort -cq src/ tests/
pdm run mypy src/ tests/ && pdm run ruff src/ tests/ && pdm run black --check -q src/ tests/
1 change: 1 addition & 0 deletions docs/_static/custom.css
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,7 @@ span:target ~ h4:first-of-type,
span:target ~ h5:first-of-type,
span:target ~ h6:first-of-type {
text-decoration: underline dashed;
text-decoration-thickness: 1px;
}

div.article-container > article {
Expand Down
58 changes: 49 additions & 9 deletions docs/handlers.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,20 +17,22 @@ async def index() -> None:
app.route("/", index, methods=["GET"])
```

A _route_ is a combination of a _path_, an HTTP _method_, a _handler_, and a _route name_.

We **strongly recommend** not using async handlers with Flask or Django unless you know what you're doing, even though (technically) all supported frameworks support both sync and async handlers.

## Handler Names
## Route Names

Each handler is registered under a certain _name_.
The name is a simple string identifying the handler, and defaults to the name of the handler function or coroutine.
Names are propagated to the underlying frameworks, where they have framework-specific purposes.
Each route is registered under a certain _name_.
The name is a simple string identifying the route, and defaults to the name of the handler function or coroutine.
Names are propagated to the underlying frameworks where they have framework-specific purposes.

Names are also used in the generated OpenAPI schema:

- to generate the operation summary
- as the `operationId` Operation property property
- to generate the [Operation](https://swagger.io/specification/#operation-object) summary
- as the `operationId` Operation property

Names should be unique across handlers and methods, so if you want to register the same handler for two methods you will need to specify one of the names manually.
Names should be unique across routes, so if you want to register the same handler for two routes you will need to specify one of the names manually.

```python
@app.get("/")
Expand All @@ -44,7 +46,7 @@ async def multipurpose_handler() -> None:
### Query Parameters

To receive query parameters, annotate a handler parameter with any type that hasn't been overriden and is not a [path parameter](handlers.md#path-parameters).
The {py:class}`App <uapi.base.App>`'s dependency injection system is configured to fulfill handler parameters from query parameters by default; directly when annotated as strings or Any or through the App's converter if any other type.
The {class}`App <uapi.base.App>`'s dependency injection system is configured to fulfill handler parameters from query parameters by default; directly when annotated as strings or Any or through the App's converter if any other type.
Query parameters may have default values.

Query params will be present in the [OpenAPI schema](openapi.md); parameters with defaults will be rendered as `required=False`.
Expand Down Expand Up @@ -304,7 +306,45 @@ async def sets_cookies() -> Ok[str]
```

```{tip}
Since {meth}`uapi.cookies.set_cookie` returns a header mapping, multiple cookies can be set by using the `|` operator.
Since {meth}`uapi.cookies.set_cookie` returns a dictionary, multiple cookies can be set by using the `|` operator.
```

### Forms

Form data can be modeled as an _attrs_ class and declared as a `FormBody` parameter in the handler.

```python
from attrs import define

@define
class ArticleForm:
article_id: str

@app.post("/article")
async def create_article(article: FormBody[Article]) -> None:
# `article` is an instance of `Article`
...
```

```{note}
A parameter annotated as a `FormBody[T]` will be equivalent to `T` in the function body.

`FormBody[T]` is an easier way of saying `typing.Annotated[T, FormSpec()]`, and `typing.Annotated` is a way to add metadata to a type.
```

All underlying frameworks expect the `content-type` to be set to `application/x-www-form-urlencoded`, which browsers set by default.
If a different `content-type` header is set all frameworks silently supply an empty form payload; whether this succeeds or not depends on whether all form model fields have default values.

Only [`post`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/form#method) forms are currently supported; for `get` forms see [](handlers.md#query-parameters).
Consequently, receiving form data is only supported in `post` routes.

When a form payload cannot be successfully structured, a `400 Bad Request` response is returned.

Multipart forms are not yet supported.
Forms containing nested objects aren't supported due to the complexities of encoding; in these cases JSON endpoints should be preferred.

```{note}
Starlette requires an extra package, `python-multipart`, to be installed before forms can be handled.
```

### Framework-specific Request Objects
Expand Down
18 changes: 9 additions & 9 deletions docs/openapi.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ app = App()
# Serve the schema at /openapi.json by default
app.serve_openapi()

# Generate the schema, if you want to access it directly
# Generate the schema, if you want to access it directly or customize it
spec = app.make_openapi_spec()
```

Expand All @@ -34,7 +34,7 @@ The documentation viewer will be available at its default URL.
{meth}`App.serve_elements() <uapi.base.App.serve_elements>`
```

What is referred to as _handlers_ in _uapi_, OpenAPI refers to as _operations_.
What is referred to as _routes_ in _uapi_, OpenAPI refers to as _operations_.
This document uses the _uapi_ nomenclature by default.

_uapi_ comes with OpenAPI schema support for the following types:
Expand All @@ -47,12 +47,12 @@ _uapi_ comes with OpenAPI schema support for the following types:
- dates (`type: string, format: date`)
- datetimes (`type: string, format: date-time`)

## Handler Summaries and Descriptions
## Operation Summaries and Descriptions

OpenAPI allows handlers to have summaries and descriptions; summaries are usually used as operation labels in OpenAPI tooling.
OpenAPI allows operations to have summaries and descriptions; summaries are usually used as operation labels in OpenAPI tooling.

By default, uapi generates summaries from [handler names](handlers.md#handler-names).
This can be customized by using your own summary transformer, which is a function taking the actual handler function or coroutine and the handler name, and returning the summary string.
By default, uapi generates summaries from [route names](handlers.md#route-names).
This can be customized by using your own summary transformer, which is a function taking the actual handler function or coroutine and the route name, and returning the summary string.

```python
app = App()
Expand All @@ -64,7 +64,7 @@ def summary_transformer(handler: Callable, name: str) -> str:
app.serve_openapi(summary_transformer=summary_transformer)
```

Handler descriptions are generated from handler docstrings by default.
Operation descriptions are generated from handler docstrings by default.
This can again be customized by supplying your own description transformer, with the same signature as the summary transformer.

```python
Expand All @@ -86,12 +86,12 @@ OpenAPI allows Markdown to be used for descriptions.
## Endpoint Tags

OpenAPI supports grouping endpoints by tags.
You can specify tags for each handler when registering it:
You can specify tags for each route when registering it:

```python
@app.get("/{article_id}", tags=["articles"])
async def get_article(article_id: str) -> str:
return "Getting the article"
```

Depending on the OpenAPI visualization framework used, endpoints with tags are usually displayed grouped under the tag.
Depending on the OpenAPI visualization framework used, operations with tags are usually displayed grouped under the tag.
62 changes: 31 additions & 31 deletions pdm.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 3 additions & 2 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ dependencies = [
"incant >= 23.2.0",
"itsdangerous",
"attrs >= 23.1.0",
"orjson>=3.9.10",
"orjson >= 3.9.10",
]
requires-python = ">=3.10"
readme = "README.md"
Expand All @@ -31,7 +31,6 @@ dynamic = ["version"]
lint = [
"black",
"ruff",
"isort",
"mypy>=1.4.1",
]
test = [
Expand All @@ -42,6 +41,7 @@ test = [
"aioredis==1.3.1",
"uvicorn",
"uapi[frameworks]",
"python-multipart>=0.0.6",
]
frameworks = [
"aiohttp==3.9.0b0",
Expand Down Expand Up @@ -105,6 +105,7 @@ select = [
"PLC", # Pylint
"PIE", # flake8-pie
"RUF", # ruff
"I", # isort
]
ignore = [
"E501", # line length is handled by black
Expand Down
5 changes: 3 additions & 2 deletions src/uapi/__init__.py
Original file line number Diff line number Diff line change
@@ -1,20 +1,21 @@
from .cookies import Cookie
from .requests import Header, HeaderSpec, ReqBody, ReqBytes
from .requests import FormBody, Header, HeaderSpec, ReqBody, ReqBytes
from .responses import ResponseException
from .status import Found, Headers, SeeOther
from .types import Method, RouteName

__all__ = [
"Cookie",
"FormBody",
"Header",
"HeaderSpec",
"Method",
"redirect_to_get",
"redirect",
"ReqBody",
"ReqBytes",
"ResponseException",
"RouteName",
"Method",
]


Expand Down
Loading
Loading