Skip to content

Commit

Permalink
Shorthands WIP (#57)
Browse files Browse the repository at this point in the history
* Shorthands WIP

* Fix test

* Refactor for circular imports

* More OpenAPI tests

* Refactor

* Shorthand unions

* Fix tests

* Remove dead code

* More shorthand tests

* Improve coverage

* Shorthand docs

* Revert test change

* Shorthand typing

* Fix type

* Fixes

* More tests

* Typing tests

* Deps

* attrs response shorthand

* Tweak Mypy test

* Update docs

* Small refactor

* Update HISTORY

* Test attrs unions

* Coverage

* Refactor

* Tweak docs

* Changelog
  • Loading branch information
Tinche committed Dec 19, 2023
1 parent dda8f86 commit b4df868
Show file tree
Hide file tree
Showing 41 changed files with 2,530 additions and 1,254 deletions.
8 changes: 8 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,11 +14,19 @@ The **third number** is for emergencies when we need to start branches for older

### Changed

- Return types of handlers are now type-checked.
([#57](https://github.com/Tinche/uapi/pull/57))
- Introduce [Response Shorthands](https://uapi.threeofwands.com/en/latest/response_shorthands.html), port the `str`, `bytes`, `None` and _attrs_ response types to them.
([#57](https://github.com/Tinche/uapi/pull/57))
- Unions containing shorthands and _uapi_ response classes (and any combination of these) are now better supported.
([#57](https://github.com/Tinche/uapi/pull/57))
- [`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.
([#53](https://github.com/Tinche/uapi/pull/53))
- Simple forms are now supported using `uapi.ReqForm[T]`. [Learn more](handlers.md#forms).
([#54](https://github.com/Tinche/uapi/pull/54))
- _uapi_ now sorts imports using Ruff.
- _uapi_ is now tested against Mypy.
([#57](https://github.com/Tinche/uapi/pull/57))

## [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
@@ -1,7 +1,7 @@
.PHONY: test lint

test:
pdm run pytest tests -x --ff
pdm run pytest tests -x --ff --mypy-only-local-stub

lint:
pdm run mypy src/ tests/ && pdm run ruff src/ tests/ && pdm run black --check -q src/ tests/
6 changes: 3 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,11 +14,11 @@ An _uapi_ app can be easily integrated into an existing project based on one of
Using _uapi_ enables you to:

- write **either async or sync** styles of handlers, depending on the underlying framework used.
- use and customize a **function composition** (dependency injection) system, based on [incant](https://incant.threeofwands.com).
- use and customize a [**function composition** (dependency injection) system](https://uapi.threeofwands.com/en/stable/composition.html), based on [incant](https://incant.threeofwands.com).
- automatically **serialize and deserialize** data through [attrs](https://www.attrs.org) and [cattrs](https://catt.rs).
- generate and use **OpenAPI** descriptions of your endpoints.
- generate and use [**OpenAPI**](https://uapi.threeofwands.com/en/stable/openapi.html) descriptions of your endpoints.
- optionally **type-check** your handlers with [Mypy](https://mypy.readthedocs.io/en/stable/).
- write and use reusable and **powerful middleware**, which integrates with the OpenAPI schema.
- write and use reusable and [**powerful middleware**](https://uapi.threeofwands.com/en/stable/addons.html), which integrates into the OpenAPI schema.
- **integrate** with existing apps based on [Django](https://docs.djangoproject.com/en/stable/), [Starlette](https://www.starlette.io/), [Flask](https://flask.palletsprojects.com), [Quart](https://pgjones.gitlab.io/quart/) or [aiohttp](https://docs.aiohttp.org).

Here's a simple taste (install Flask and gunicorn first):
Expand Down
1 change: 1 addition & 0 deletions docs/conf.py
Original file line number Diff line number Diff line change
Expand Up @@ -67,5 +67,6 @@
}

myst_heading_anchors = 3
myst_enable_extensions = ["attrs_block"]
autodoc_typehints = "description"
autoclass_content = "both"
22 changes: 16 additions & 6 deletions docs/handlers.md
Original file line number Diff line number Diff line change
Expand Up @@ -451,7 +451,7 @@ Whether the response contains the `content-type` header is up to the underlying
Flask, Quart and Django add a `text/html` content type by default.
```

A longer equivalent, with the added benefit of being able to specify response headers, is returning the {py:class}`NoContent <uapi.status.NoContent>` response explicitly.
A longer equivalent, with the added benefit of being able to specify response headers, is returning the {class}`NoContent <uapi.status.NoContent>` response explicitly.

```python
from uapi.status import NoContent
Expand All @@ -462,6 +462,9 @@ async def delete_article() -> NoContent:
return NoContent(headers={"key": "value"})
```

_This functionality is handled by {class}`NoneShorthand <uapi.shorthands.NoneShorthand>`._


### Strings and Bytes `(200 OK)`

If your handler returns a string or bytes, the response will be returned directly alongside the `200 OK` status code.
Expand All @@ -474,12 +477,14 @@ async def get_article_image() -> bytes:

For strings, the `content-type` header is set to `text/plain`, and for bytes to `application/octet-stream`.

_This functionality is handled by {class}`StrShorthand <uapi.shorthands.StrShorthand>` and {class}`BytesShorthand <uapi.shorthands.BytesShorthand>`._

### _attrs_ Classes

Handlers can return an instance of an _attrs_ class.
The return value with be deserialized into JSON using the App _cattrs_ converter, which can be customized as per the usual _cattrs_ ways.

The status code will be set to `200 OK`, and the content type to `application/json`. The class will be added to the OpenAPI schema.
The status code will be set to `200 OK` and the content type to `application/json`. The class will be added to the OpenAPI schema.

```python
from attrs import define
Expand All @@ -493,6 +498,11 @@ async def get_article() -> Article:
...
```

### Custom Response Shorthands

The `str`, `bytes`, `None` and _attrs_ return types are examples of _response shorthands_.
Custom response shorthands can be defined and added to apps; [see the Response Shorthands section for the details](response_shorthands.md).

### _uapi_ Status Code Classes

_uapi_ {py:obj}`contains a variety of classes <uapi.status>`, mapping to status codes, for returning from handlers.
Expand All @@ -509,7 +519,7 @@ async def get_article() -> Ok[Article]:

### Returning Multiple Status Codes

If your handler can return multiple status codes, use a union of _uapi_ response types.
Use a union of _uapi_ response types and shorthands if your handler can return multiple status codes.

All responses defined this way will be rendered in the OpenAPI schema.

Expand All @@ -522,8 +532,8 @@ async def user_profile() -> Ok[Profile] | NoContent:
### _uapi_ ResponseExceptions

Any raised instances of {class}`uapi.ResponseException` will be caught and transformed into a proper response.
Like any exception, ResponseExceptions short-circuit handlers so they can be useful for validation and middleware.
In other cases, simply returning a response instead is cheaper and usually more type-safe.
Like any exception, `ResponseExceptions` short-circuit handlers so they can be useful for validation and middleware.
In other cases, simply returning a response is faster and usually more type-safe.

ResponseExceptions contain instances of _uapi_ status code classes and so can return rich response data, just like any normal response.

Expand All @@ -539,7 +549,7 @@ async def get_article() -> Ok[Article]:
...
```

Since exceptions don't show up in the handler signature, they won't be present in the generated OpenAPI schema.
Since exceptions don't show up in the handler signature they won't be present in the generated OpenAPI schema.
If you need them to, you can add the actual response type into the handler response signature as part of a union:

```python
Expand Down
5 changes: 3 additions & 2 deletions docs/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ handlers.md
composition.md
openapi.md
addons.md
response_shorthands.md
changelog.md
indices.md
modules.rst
Expand All @@ -22,9 +23,9 @@ Using _uapi_ enables you to:
- write **either async or sync** styles of handlers, depending on the underlying framework used.
- use and customize a [**function composition** (dependency injection) system](composition.md), based on [incant](https://incant.threeofwands.com).
- automatically **serialize and deserialize** data through [attrs](https://www.attrs.org/en/stable/) and [cattrs](https://catt.rs).
- generate and use **OpenAPI** descriptions of your endpoints.
- generate and use [**OpenAPI**](openapi.md) descriptions of your endpoints.
- optionally **type-check** your handlers with [Mypy](https://mypy.readthedocs.io/en/stable/).
- write and use **powerful middleware**.
- write and use [**powerful middleware**](addons.md), which integrates into the OpenAPI schema.
- **integrate** with existing apps based on [Django](https://docs.djangoproject.com/en/stable/), [Starlette](https://www.starlette.io/), [Flask](https://flask.palletsprojects.com/en/latest/), [Quart](https://pgjones.gitlab.io/quart/) or [Aiohttp](https://docs.aiohttp.org/en/stable/).

# Installation
Expand Down
106 changes: 106 additions & 0 deletions docs/response_shorthands.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
```{currentmodule} uapi.shorthands
```
# Response Shorthands

Custom response shorthands are created by defining a custom instance of the {class}`ResponseShorthand` protocol.
This involves implementing two to four functions, depending on the amount of functionality required.

## A `datetime.datetime` Shorthand

Here are the steps needed to implement a new shorthand, enabling handlers to return [`datetime.datetime`](https://docs.python.org/3/library/datetime.html#datetime-objects) instances directly.

First, we need to create the shorthand class by subclassing the {class}`ResponseShorthand` generic protocol.

```python
from datetime import datetime

from uapi.shorthands import ResponseShorthand

class DatetimeShorthand(ResponseShorthand[datetime]):
pass
```

Note that the shorthand is generic over the type we want to enable.
This protocol contains four static methods (functions); two mandatory ones and two optional ones.

The first function we need to override is {meth}`ResponseShorthand.response_adapter_factory`.
This function needs to produce an adapter which converts an instance of our type (`datetime`) into a _uapi_ [status code class](handlers.md#uapi-status-code-classes), so _uapi_ can adapt the value for the underlying framework.

{emphasize-lines="6-8"}
```python
from uapi.shorthands import ResponseAdapter
from uapi.status import BaseResponse, Ok

class DatetimeShorthand(ResponseShorthand[datetime]):

@staticmethod
def response_adapter_factory(type: Any) -> ResponseAdapter:
return lambda value: Ok(value.isoformat(), headers={"content-type": "date"})
```

The second function is {meth}`ResponseShorthand.is_union_member`.
This function is used to recognize if a return value is an instance of the shorthand type when the return type is a union.
For example, if the return type is `datetime | str`, uapi needs to be able to detect and handle both cases.

{emphasize-lines="3-5"}
```python
class DatetimeShorthand(ResponseShorthand[datetime]):

@staticmethod
def is_union_member(value: Any) -> bool:
return isinstance(value, datetime)
```

With these two functions we have a minimal shorthand implementation.
We can add it to an app to be able to use it:

{emphasize-lines="5"}
```
from uapi.starlette import App # Or any other app
app = App()
app = app.add_response_shorthand(DatetimeShorthand)
```

And we're done.

### OpenAPI Integration

If we stop here our shorthand won't show up in the [generated OpenAPI schema](openapi.md).
To enable OpenAPI integration we need to implement one more function, {meth}`ResponseShorthand.make_openapi_response`.

This function returns the [OpenAPI response definition](https://swagger.io/specification/#responses-object) for the shorthand.

{emphasize-lines="5-10"}
```python
from uapi.openapi import MediaType, Response, Schema

class DatetimeShorthand(ResponseShorthand[datetime]):

@staticmethod
def make_openapi_response() -> Response:
return Response(
"OK",
{"date": MediaType(Schema(Schema.Type.STRING, format="datetime"))},
)
```

### Custom Type Matching

Registered shorthands are matched to handler return types using simple identity and [`issubclass`](https://docs.python.org/3/library/functions.html#issubclass) checks.
Sometimes, more sophisticated matching is required.

For example, the default {class}`NoneShorthand <NoneShorthand>` shorthand wouldn't work for some handlers without custom matching since it needs to match both `None` and `NoneType`. This matching can be customized by overriding the {meth}`ResponseShorthand.can_handle` function.

Here's what a dummy implementation would look like for our `DatetimeShorthand`.

{emphasize-lines="3-5"}
```python
class DatetimeShorthand(ResponseShorthand[datetime]):

@staticmethod
def can_handle(type: Any) -> bool:
return issubclass(type, datetime)
```
8 changes: 8 additions & 0 deletions docs/uapi.rst
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,14 @@ uapi.responses module
:undoc-members:
:show-inheritance:

uapi.shorthands module
----------------------

.. automodule:: uapi.shorthands
:members:
:undoc-members:
:show-inheritance:

uapi.starlette module
---------------------

Expand Down

0 comments on commit b4df868

Please sign in to comment.