annotated_fastapi_router


Nameannotated_fastapi_router JSON
Version 0.1.0 PyPI version JSON
download
home_pagehttps://github.com/feodor-ra/annotated-fastapi-router
SummaryFastAPI annotated api routed
upload_time2024-07-06 08:00:28
maintainerNone
docs_urlNone
authorFedor Ratschew
requires_python<4.0,>=3.11
licenseMIT
keywords annotated fastapi
VCS
bugtrack_url
requirements No requirements were recorded.
Travis-CI No Travis.
coveralls test coverage No coveralls.
            # Annotated FastAPI router

Этот пакет содержит улучшенный `fastapi.APIRouter`, который содержит шорт-каты для удобной аннотации
эндпоинтов через `typing.Annotated`, что бы задавать значения `status_code` или модели ответов в `responses`.

- [примеры](#примеры)
  - [статус-код](#статус-код)
  - [модели ошибок](#модель-ответов-для-ошибок)

Warning *the English part of README.md is translated via ChatGPT and may not be accurate*

This package contains an enhanced `fastapi.APIRouter` that includes shortcuts for annotating endpoints conveniently using `typing.Annotated`, allowing you to set values like `status_code` or response models in `responses`.

- [examples](#examples)
  - [status code](#status-code)
  - [responses](#responses)

## Примеры

### Статус-код

Указывать `status_code` у эндпоинта можно следующим образом:

```python
from http import HTTPStatus
from typing import Annotated

from fastapi import FastAPI
from pydantic import BaseModel

from annotated_fastapi_router import AnnotatedAPIRouter, Status


class ResponseModel(BaseModel):
    message: str


router = AnnotatedAPIRouter()


@router.get("/my_endpoint")
async def my_endpoint() -> Annotated[ResponseModel, Status[HTTPStatus.ACCEPTED]]:
    return ResponseModel(message="hello")


router.add_api_route("/my_endpoint", my_endpoint, methods=["POST"])
```

В результате swagger будет содержать следующую OpenAPI-схему:

![GET request swagger](https://github.com/feodor-ra/annotated-fastapi-router/blob/f4a0b2028ba5ceedca76de6af491f9ef6e312c3a/docs/use_status_get.png)
![POST request swagger](https://github.com/feodor-ra/annotated-fastapi-router/blob/f4a0b2028ba5ceedca76de6af491f9ef6e312c3a/docs/use_status_post.png)

При запросе статус код так же будет проставлен в ответ:

![POST response swagger](https://github.com/feodor-ra/annotated-fastapi-router/blob/f4a0b2028ba5ceedca76de6af491f9ef6e312c3a/docs/use_status_get_response.png)

Однако если статус код будет указан явно в декораторе или при добавлении в роутер, `Status` в аннотации будет проигнорирован.

```python
@router.get("/my_endpoint")
@router.get("/my_endpoint_too", status_code=HTTPStatus.CREATED)
async def my_endpoint() -> Annotated[ResponseModel, Status[HTTPStatus.ACCEPTED]]:
    return ResponseModel(message="hello")


router.add_api_route("/my_endpoint", my_endpoint, methods=["POST"])
router.add_api_route("/my_endpoint_too", my_endpoint, status_code=HTTPStatus.CREATED, methods=["POST"])
```

![GET request swagger keep](https://github.com/feodor-ra/annotated-fastapi-router/blob/f4a0b2028ba5ceedca76de6af491f9ef6e312c3a/docs/keep_status_get_swagger.png)
![POST request swagger keep](https://github.com/feodor-ra/annotated-fastapi-router/blob/f4a0b2028ba5ceedca76de6af491f9ef6e312c3a/docs/keep_status_post_swagger.png)

**Важно:** `Status` принимает в себя только перечисления из `HTTPStatus`,
на любые другие объекты будет подыматься ошибка `TypeError`.

### Модель ответов для ошибок

Для автоматического построения моделей для `responses` используется тип ошибок `ResponseError`.
В его наследниках обязательно нужно указывать переменную типа `status` и `model`.

`status` указывает для каких кодов ошибок нужно будет построить модель, если будет передано несколько
типов ошибок для одного и того же `HTTPStatus`, то полученная модель будет объединять в себе все модели `model`.

```python
from http import HTTPStatus
from typing import Annotated, Self

from fastapi import FastAPI
from pydantic import BaseModel

from annotated_fastapi_router import AnnotatedAPIRouter, Errors, ResponseError


class ResponseModel(BaseModel):
    message: str


class ErrorMessageModel(BaseModel):
    message: str


class OtherErrorModel(BaseModel):
    code: int


class MyError(ResponseError[ErrorMessageModel]):
    status = HTTPStatus.BAD_REQUEST
    model = ErrorMessageModel

    def __init__(self: "MyError", msg: str) -> None:
        self.message = msg


class OtherError(ResponseError[OtherErrorModel]):
    status = HTTPStatus.BAD_REQUEST
    model = OtherErrorModel

    async def entity(self: Self) -> OtherErrorModel:
        return self.model(code=self.status)


router = AnnotatedAPIRouter()


@router.get("/my_endpoint")
@router.post("/my_endpoint")
async def endpoint(how_iam: str) -> Annotated[ResponseModel, Errors[MyError, OtherError]]:
    if how_iam == "me":
        return ResponseModel(message="hello")
    if how_iam.isdigit():
        raise OtherError
    raise MyError("I don't know you")
```

Таким образом будет построены две модели ошибок – `MyEndpointPOSTBadRequest` и `MyEndpointGETBadRequest`.

![Build error model GET](https://github.com/feodor-ra/annotated-fastapi-router/blob/f4a0b2028ba5ceedca76de6af491f9ef6e312c3a/docs/build_error_model_get.png)
![Build error model POST](https://github.com/feodor-ra/annotated-fastapi-router/blob/f4a0b2028ba5ceedca76de6af491f9ef6e312c3a/docs/build_error_model_post.png)

Имя модели генерируется из пути, методов (если указаны) роута и phrase статус-кода ошибки.
Делается это потому что для `fastapi<=0.95.0` требуется что бы все названия моделей (если они отличаются) имели уникальные имена.
Если модель ошибки состоит только из одной модели – тогда будет использовано имя оригинальной модели (что бы избегать баг в OpenAPI-схеме с некорректным определением компонента).

Логику построения имени модели можно изменить наследованием от `AnnotatedAPIRouter` и переопределением метода `response_model_name`.

Так же для построение экземпляра модели ошибки по умолчанию использует атрибуты экземпляра ошибки (через `__dict__`),
но если это поведение необходимо изменить можно реализовать метод `entity` (как в примере в класса `OtherError`).

Если `responses` уже определяется в декораторе роута или при явном добавлении в него и содержит описание для ошибки,
которая указана в `Annotated`, то она не будет перезаписывать данные переданные в `responses`.

```python
@router.get("/my_endpoint_too", responses={HTTPStatus.BAD_REQUEST: {"model": dict[str, str]}})
async def endpoint(how_iam: str) -> Annotated[ResponseModel, Errors[MyError, OtherError]]:
    if how_iam == "me":
        return ResponseModel(message="hello")
    if how_iam.isdigit():
        raise OtherError
    raise MyError("I don't know you")
```

![Keep responses data model](https://github.com/feodor-ra/annotated-fastapi-router/blob/f4a0b2028ba5ceedca76de6af491f9ef6e312c3a/docs/keep_build_error_model.png)

Для того что бы автоматически отлавливать ошибки из эндпоинтов и указывать в ответе тот статус-код, который в указан в них,
необходимо добавить в `FastAPI` приложение обработчик ошибок `ResponseError.handler`.

```python
app.add_exception_handler(ResponseError, ResponseError.handler)
```

![Handle MyError](https://github.com/feodor-ra/annotated-fastapi-router/blob/f4a0b2028ba5ceedca76de6af491f9ef6e312c3a/docs/handle_my_error.png)
![Handle OtherError](https://github.com/feodor-ra/annotated-fastapi-router/blob/f4a0b2028ba5ceedca76de6af491f9ef6e312c3a/docs/handle_other_error.png)

## Examples

### Status-code

To specify the status_code of the endpoint, you can do it as follows:

```python
from http import HTTPStatus
from typing import Annotated

from fastapi import FastAPI
from pydantic import BaseModel

from annotated_fastapi_router import AnnotatedAPIRouter, Status


class ResponseModel(BaseModel):
    message: str


router = AnnotatedAPIRouter()


@router.get("/my_endpoint")
async def my_endpoint() -> Annotated[ResponseModel, Status[HTTPStatus.ACCEPTED]]:
    return ResponseModel(message="hello")


router.add_api_route("/my_endpoint", my_endpoint, methods=["POST"])
```

As a result, the swagger will contain the following OpenAPI schema:

![GET request swagger](https://github.com/feodor-ra/annotated-fastapi-router/blob/f4a0b2028ba5ceedca76de6af491f9ef6e312c3a/docs/use_status_get.png)
![POST request swagger](https://github.com/feodor-ra/annotated-fastapi-router/blob/f4a0b2028ba5ceedca76de6af491f9ef6e312c3a/docs/use_status_post.png)

When the endpoint is requested, the status code will also be included in the response:

![POST response swagger](https://github.com/feodor-ra/annotated-fastapi-router/blob/f4a0b2028ba5ceedca76de6af491f9ef6e312c3a/docs/use_status_get_response.png)

However, if the status code is explicitly provided in the decorator or when adding it to the router, the `Status` in the annotation will be ignored.

```python
@router.get("/my_endpoint")
@router.get("/my_endpoint_too", status_code=HTTPStatus.CREATED)
async def my_endpoint() -> Annotated[ResponseModel, Status[HTTPStatus.ACCEPTED]]:
    return ResponseModel(message="hello")


router.add_api_route("/my_endpoint", my_endpoint, methods=["POST"])
router.add_api_route("/my_endpoint_too", my_endpoint, status_code=HTTPStatus.CREATED, methods=["POST"])
```

![GET request swagger keep](https://github.com/feodor-ra/annotated-fastapi-router/blob/f4a0b2028ba5ceedca76de6af491f9ef6e312c3a/docs/keep_status_get_swagger.png)
![POST request swagger keep](https://github.com/feodor-ra/annotated-fastapi-router/blob/f4a0b2028ba5ceedca76de6af491f9ef6e312c3a/docs/keep_status_post_swagger.png)

**Important**: `Status` only accepts enumerations from `HTTPStatus`; any other objects will raise a `TypeError`

### Responses

To automatically build models for `responses`, the error type `ResponseError` is used.
In its subclasses, it is necessary to specify variables of type `status` and `model`.

`status` indicates for which error codes the model will need to be constructed.
If multiple error types are provided for the same HTTPStatus, the resulting model will combine all model models.

```python
from http import HTTPStatus
from typing import Annotated, Self

from fastapi import FastAPI
from pydantic import BaseModel

from annotated_fastapi_router import AnnotatedAPIRouter, Errors, ResponseError


class ResponseModel(BaseModel):
    message: str


class ErrorMessageModel(BaseModel):
    message: str


class OtherErrorModel(BaseModel):
    code: int


class MyError(ResponseError[ErrorMessageModel]):
    status = HTTPStatus.BAD_REQUEST
    model = ErrorMessageModel

    def __init__(self: "MyError", msg: str) -> None:
        self.message = msg


class OtherError(ResponseError[OtherErrorModel]):
    status = HTTPStatus.BAD_REQUEST
    model = OtherErrorModel

    async def entity(self: Self) -> OtherErrorModel:
        return self.model(code=self.status)


router = AnnotatedAPIRouter()


@router.get("/my_endpoint")
@router.post("/my_endpoint")
async def endpoint(how_iam: str) -> Annotated[ResponseModel, Errors[MyError, OtherError]]:
    if how_iam == "me":
        return ResponseModel(message="hello")
    if how_iam.isdigit():
        raise OtherError
    raise MyError("I don't know you")
```

In this way, two error models will be built - `MyEndpointPOSTBadRequest` and `MyEndpointGETBadRequest`.

![Build error model GET](https://github.com/feodor-ra/annotated-fastapi-router/blob/f4a0b2028ba5ceedca76de6af491f9ef6e312c3a/docs/build_error_model_get.png)
![Build error model POST](https://github.com/feodor-ra/annotated-fastapi-router/blob/f4a0b2028ba5ceedca76de6af491f9ef6e312c3a/docs/build_error_model_post.png)

The model name is generated from the path, methods (if specified) of the route, and the status code error phrase.
This is done because for `fastapi<=0.95.0`, all model names (if they differ) must have unique names.
If the error model consists of only one model, the name of the original model will be used (to avoid a bug in the OpenAPI schema with an incorrect component definition).

The logic for naming the model can be changed by inheriting from `AnnotatedAPIRouter`
and overriding the `response_model_name` method.

Also, to build an instance of the error model by default, it uses the error instance attributes (via `__dict__`),
but if this behavior needs to be changed, you can implement the `entity` method
(as shown in the example in the `OtherError` class).

If `responses` are already defined in the route decorator or explicitly added to it and contain
a description for an error specified in `Annotated`, it will not overwrite the data passed to `responses`.

```python
@router.get("/my_endpoint_too", responses={HTTPStatus.BAD_REQUEST: {"model": dict[str, str]}})
async def endpoint(how_iam: str) -> Annotated[ResponseModel, Errors[MyError, OtherError]]:
    if how_iam == "me":
        return ResponseModel(message="hello")
    if how_iam.isdigit():
        raise OtherError
    raise MyError("I don't know you")
```

![Keep responses data model](https://github.com/feodor-ra/annotated-fastapi-router/blob/f4a0b2028ba5ceedca76de6af491f9ef6e312c3a/docs/keep_build_error_model.png)

To automatically catch errors from endpoints and include the status code specified in them in the response, you need to add the error handler `ResponseError.handler` to the `FastAPI` application.

```python
app.add_exception_handler(ResponseError, ResponseError.handler)
```

![Handle MyError](https://github.com/feodor-ra/annotated-fastapi-router/blob/f4a0b2028ba5ceedca76de6af491f9ef6e312c3a/docs/handle_my_error.png)
![Handle OtherError](https://github.com/feodor-ra/annotated-fastapi-router/blob/f4a0b2028ba5ceedca76de6af491f9ef6e312c3a/docs/handle_other_error.png)

            

Raw data

            {
    "_id": null,
    "home_page": "https://github.com/feodor-ra/annotated-fastapi-router",
    "name": "annotated_fastapi_router",
    "maintainer": null,
    "docs_url": null,
    "requires_python": "<4.0,>=3.11",
    "maintainer_email": null,
    "keywords": "annotated, fastapi",
    "author": "Fedor Ratschew",
    "author_email": "feodor.ra@me.com",
    "download_url": "https://files.pythonhosted.org/packages/3d/b1/eb50222fb44cf202dbf67dea2ecf8376ee513c9230b11d505c56153b032a/annotated_fastapi_router-0.1.0.tar.gz",
    "platform": null,
    "description": "# Annotated FastAPI router\n\n\u042d\u0442\u043e\u0442 \u043f\u0430\u043a\u0435\u0442 \u0441\u043e\u0434\u0435\u0440\u0436\u0438\u0442 \u0443\u043b\u0443\u0447\u0448\u0435\u043d\u043d\u044b\u0439 `fastapi.APIRouter`, \u043a\u043e\u0442\u043e\u0440\u044b\u0439 \u0441\u043e\u0434\u0435\u0440\u0436\u0438\u0442 \u0448\u043e\u0440\u0442-\u043a\u0430\u0442\u044b \u0434\u043b\u044f \u0443\u0434\u043e\u0431\u043d\u043e\u0439 \u0430\u043d\u043d\u043e\u0442\u0430\u0446\u0438\u0438\n\u044d\u043d\u0434\u043f\u043e\u0438\u043d\u0442\u043e\u0432 \u0447\u0435\u0440\u0435\u0437 `typing.Annotated`, \u0447\u0442\u043e \u0431\u044b \u0437\u0430\u0434\u0430\u0432\u0430\u0442\u044c \u0437\u043d\u0430\u0447\u0435\u043d\u0438\u044f `status_code` \u0438\u043b\u0438 \u043c\u043e\u0434\u0435\u043b\u0438 \u043e\u0442\u0432\u0435\u0442\u043e\u0432 \u0432 `responses`.\n\n- [\u043f\u0440\u0438\u043c\u0435\u0440\u044b](#\u043f\u0440\u0438\u043c\u0435\u0440\u044b)\n  - [\u0441\u0442\u0430\u0442\u0443\u0441-\u043a\u043e\u0434](#\u0441\u0442\u0430\u0442\u0443\u0441-\u043a\u043e\u0434)\n  - [\u043c\u043e\u0434\u0435\u043b\u0438 \u043e\u0448\u0438\u0431\u043e\u043a](#\u043c\u043e\u0434\u0435\u043b\u044c-\u043e\u0442\u0432\u0435\u0442\u043e\u0432-\u0434\u043b\u044f-\u043e\u0448\u0438\u0431\u043e\u043a)\n\nWarning *the English part of README.md is translated via ChatGPT and may not be accurate*\n\nThis package contains an enhanced `fastapi.APIRouter` that includes shortcuts for annotating endpoints conveniently using `typing.Annotated`, allowing you to set values like `status_code` or response models in `responses`.\n\n- [examples](#examples)\n  - [status code](#status-code)\n  - [responses](#responses)\n\n## \u041f\u0440\u0438\u043c\u0435\u0440\u044b\n\n### \u0421\u0442\u0430\u0442\u0443\u0441-\u043a\u043e\u0434\n\n\u0423\u043a\u0430\u0437\u044b\u0432\u0430\u0442\u044c `status_code` \u0443 \u044d\u043d\u0434\u043f\u043e\u0438\u043d\u0442\u0430 \u043c\u043e\u0436\u043d\u043e \u0441\u043b\u0435\u0434\u0443\u044e\u0449\u0438\u043c \u043e\u0431\u0440\u0430\u0437\u043e\u043c:\n\n```python\nfrom http import HTTPStatus\nfrom typing import Annotated\n\nfrom fastapi import FastAPI\nfrom pydantic import BaseModel\n\nfrom annotated_fastapi_router import AnnotatedAPIRouter, Status\n\n\nclass ResponseModel(BaseModel):\n    message: str\n\n\nrouter = AnnotatedAPIRouter()\n\n\n@router.get(\"/my_endpoint\")\nasync def my_endpoint() -> Annotated[ResponseModel, Status[HTTPStatus.ACCEPTED]]:\n    return ResponseModel(message=\"hello\")\n\n\nrouter.add_api_route(\"/my_endpoint\", my_endpoint, methods=[\"POST\"])\n```\n\n\u0412 \u0440\u0435\u0437\u0443\u043b\u044c\u0442\u0430\u0442\u0435 swagger \u0431\u0443\u0434\u0435\u0442 \u0441\u043e\u0434\u0435\u0440\u0436\u0430\u0442\u044c \u0441\u043b\u0435\u0434\u0443\u044e\u0449\u0443\u044e OpenAPI-\u0441\u0445\u0435\u043c\u0443:\n\n![GET request swagger](https://github.com/feodor-ra/annotated-fastapi-router/blob/f4a0b2028ba5ceedca76de6af491f9ef6e312c3a/docs/use_status_get.png)\n![POST request swagger](https://github.com/feodor-ra/annotated-fastapi-router/blob/f4a0b2028ba5ceedca76de6af491f9ef6e312c3a/docs/use_status_post.png)\n\n\u041f\u0440\u0438 \u0437\u0430\u043f\u0440\u043e\u0441\u0435 \u0441\u0442\u0430\u0442\u0443\u0441 \u043a\u043e\u0434 \u0442\u0430\u043a \u0436\u0435 \u0431\u0443\u0434\u0435\u0442 \u043f\u0440\u043e\u0441\u0442\u0430\u0432\u043b\u0435\u043d \u0432 \u043e\u0442\u0432\u0435\u0442:\n\n![POST response swagger](https://github.com/feodor-ra/annotated-fastapi-router/blob/f4a0b2028ba5ceedca76de6af491f9ef6e312c3a/docs/use_status_get_response.png)\n\n\u041e\u0434\u043d\u0430\u043a\u043e \u0435\u0441\u043b\u0438 \u0441\u0442\u0430\u0442\u0443\u0441 \u043a\u043e\u0434 \u0431\u0443\u0434\u0435\u0442 \u0443\u043a\u0430\u0437\u0430\u043d \u044f\u0432\u043d\u043e \u0432 \u0434\u0435\u043a\u043e\u0440\u0430\u0442\u043e\u0440\u0435 \u0438\u043b\u0438 \u043f\u0440\u0438 \u0434\u043e\u0431\u0430\u0432\u043b\u0435\u043d\u0438\u0438 \u0432 \u0440\u043e\u0443\u0442\u0435\u0440, `Status` \u0432 \u0430\u043d\u043d\u043e\u0442\u0430\u0446\u0438\u0438 \u0431\u0443\u0434\u0435\u0442 \u043f\u0440\u043e\u0438\u0433\u043d\u043e\u0440\u0438\u0440\u043e\u0432\u0430\u043d.\n\n```python\n@router.get(\"/my_endpoint\")\n@router.get(\"/my_endpoint_too\", status_code=HTTPStatus.CREATED)\nasync def my_endpoint() -> Annotated[ResponseModel, Status[HTTPStatus.ACCEPTED]]:\n    return ResponseModel(message=\"hello\")\n\n\nrouter.add_api_route(\"/my_endpoint\", my_endpoint, methods=[\"POST\"])\nrouter.add_api_route(\"/my_endpoint_too\", my_endpoint, status_code=HTTPStatus.CREATED, methods=[\"POST\"])\n```\n\n![GET request swagger keep](https://github.com/feodor-ra/annotated-fastapi-router/blob/f4a0b2028ba5ceedca76de6af491f9ef6e312c3a/docs/keep_status_get_swagger.png)\n![POST request swagger keep](https://github.com/feodor-ra/annotated-fastapi-router/blob/f4a0b2028ba5ceedca76de6af491f9ef6e312c3a/docs/keep_status_post_swagger.png)\n\n**\u0412\u0430\u0436\u043d\u043e:** `Status` \u043f\u0440\u0438\u043d\u0438\u043c\u0430\u0435\u0442 \u0432 \u0441\u0435\u0431\u044f \u0442\u043e\u043b\u044c\u043a\u043e \u043f\u0435\u0440\u0435\u0447\u0438\u0441\u043b\u0435\u043d\u0438\u044f \u0438\u0437 `HTTPStatus`,\n\u043d\u0430 \u043b\u044e\u0431\u044b\u0435 \u0434\u0440\u0443\u0433\u0438\u0435 \u043e\u0431\u044a\u0435\u043a\u0442\u044b \u0431\u0443\u0434\u0435\u0442 \u043f\u043e\u0434\u044b\u043c\u0430\u0442\u044c\u0441\u044f \u043e\u0448\u0438\u0431\u043a\u0430 `TypeError`.\n\n### \u041c\u043e\u0434\u0435\u043b\u044c \u043e\u0442\u0432\u0435\u0442\u043e\u0432 \u0434\u043b\u044f \u043e\u0448\u0438\u0431\u043e\u043a\n\n\u0414\u043b\u044f \u0430\u0432\u0442\u043e\u043c\u0430\u0442\u0438\u0447\u0435\u0441\u043a\u043e\u0433\u043e \u043f\u043e\u0441\u0442\u0440\u043e\u0435\u043d\u0438\u044f \u043c\u043e\u0434\u0435\u043b\u0435\u0439 \u0434\u043b\u044f `responses` \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0435\u0442\u0441\u044f \u0442\u0438\u043f \u043e\u0448\u0438\u0431\u043e\u043a `ResponseError`.\n\u0412 \u0435\u0433\u043e \u043d\u0430\u0441\u043b\u0435\u0434\u043d\u0438\u043a\u0430\u0445 \u043e\u0431\u044f\u0437\u0430\u0442\u0435\u043b\u044c\u043d\u043e \u043d\u0443\u0436\u043d\u043e \u0443\u043a\u0430\u0437\u044b\u0432\u0430\u0442\u044c \u043f\u0435\u0440\u0435\u043c\u0435\u043d\u043d\u0443\u044e \u0442\u0438\u043f\u0430 `status` \u0438 `model`.\n\n`status` \u0443\u043a\u0430\u0437\u044b\u0432\u0430\u0435\u0442 \u0434\u043b\u044f \u043a\u0430\u043a\u0438\u0445 \u043a\u043e\u0434\u043e\u0432 \u043e\u0448\u0438\u0431\u043e\u043a \u043d\u0443\u0436\u043d\u043e \u0431\u0443\u0434\u0435\u0442 \u043f\u043e\u0441\u0442\u0440\u043e\u0438\u0442\u044c \u043c\u043e\u0434\u0435\u043b\u044c, \u0435\u0441\u043b\u0438 \u0431\u0443\u0434\u0435\u0442 \u043f\u0435\u0440\u0435\u0434\u0430\u043d\u043e \u043d\u0435\u0441\u043a\u043e\u043b\u044c\u043a\u043e\n\u0442\u0438\u043f\u043e\u0432 \u043e\u0448\u0438\u0431\u043e\u043a \u0434\u043b\u044f \u043e\u0434\u043d\u043e\u0433\u043e \u0438 \u0442\u043e\u0433\u043e \u0436\u0435 `HTTPStatus`, \u0442\u043e \u043f\u043e\u043b\u0443\u0447\u0435\u043d\u043d\u0430\u044f \u043c\u043e\u0434\u0435\u043b\u044c \u0431\u0443\u0434\u0435\u0442 \u043e\u0431\u044a\u0435\u0434\u0438\u043d\u044f\u0442\u044c \u0432 \u0441\u0435\u0431\u0435 \u0432\u0441\u0435 \u043c\u043e\u0434\u0435\u043b\u0438 `model`.\n\n```python\nfrom http import HTTPStatus\nfrom typing import Annotated, Self\n\nfrom fastapi import FastAPI\nfrom pydantic import BaseModel\n\nfrom annotated_fastapi_router import AnnotatedAPIRouter, Errors, ResponseError\n\n\nclass ResponseModel(BaseModel):\n    message: str\n\n\nclass ErrorMessageModel(BaseModel):\n    message: str\n\n\nclass OtherErrorModel(BaseModel):\n    code: int\n\n\nclass MyError(ResponseError[ErrorMessageModel]):\n    status = HTTPStatus.BAD_REQUEST\n    model = ErrorMessageModel\n\n    def __init__(self: \"MyError\", msg: str) -> None:\n        self.message = msg\n\n\nclass OtherError(ResponseError[OtherErrorModel]):\n    status = HTTPStatus.BAD_REQUEST\n    model = OtherErrorModel\n\n    async def entity(self: Self) -> OtherErrorModel:\n        return self.model(code=self.status)\n\n\nrouter = AnnotatedAPIRouter()\n\n\n@router.get(\"/my_endpoint\")\n@router.post(\"/my_endpoint\")\nasync def endpoint(how_iam: str) -> Annotated[ResponseModel, Errors[MyError, OtherError]]:\n    if how_iam == \"me\":\n        return ResponseModel(message=\"hello\")\n    if how_iam.isdigit():\n        raise OtherError\n    raise MyError(\"I don't know you\")\n```\n\n\u0422\u0430\u043a\u0438\u043c \u043e\u0431\u0440\u0430\u0437\u043e\u043c \u0431\u0443\u0434\u0435\u0442 \u043f\u043e\u0441\u0442\u0440\u043e\u0435\u043d\u044b \u0434\u0432\u0435 \u043c\u043e\u0434\u0435\u043b\u0438 \u043e\u0448\u0438\u0431\u043e\u043a \u2013 `MyEndpointPOSTBadRequest` \u0438 `MyEndpointGETBadRequest`.\n\n![Build error model GET](https://github.com/feodor-ra/annotated-fastapi-router/blob/f4a0b2028ba5ceedca76de6af491f9ef6e312c3a/docs/build_error_model_get.png)\n![Build error model POST](https://github.com/feodor-ra/annotated-fastapi-router/blob/f4a0b2028ba5ceedca76de6af491f9ef6e312c3a/docs/build_error_model_post.png)\n\n\u0418\u043c\u044f \u043c\u043e\u0434\u0435\u043b\u0438 \u0433\u0435\u043d\u0435\u0440\u0438\u0440\u0443\u0435\u0442\u0441\u044f \u0438\u0437 \u043f\u0443\u0442\u0438, \u043c\u0435\u0442\u043e\u0434\u043e\u0432 (\u0435\u0441\u043b\u0438 \u0443\u043a\u0430\u0437\u0430\u043d\u044b) \u0440\u043e\u0443\u0442\u0430 \u0438 phrase \u0441\u0442\u0430\u0442\u0443\u0441-\u043a\u043e\u0434\u0430 \u043e\u0448\u0438\u0431\u043a\u0438.\n\u0414\u0435\u043b\u0430\u0435\u0442\u0441\u044f \u044d\u0442\u043e \u043f\u043e\u0442\u043e\u043c\u0443 \u0447\u0442\u043e \u0434\u043b\u044f `fastapi<=0.95.0` \u0442\u0440\u0435\u0431\u0443\u0435\u0442\u0441\u044f \u0447\u0442\u043e \u0431\u044b \u0432\u0441\u0435 \u043d\u0430\u0437\u0432\u0430\u043d\u0438\u044f \u043c\u043e\u0434\u0435\u043b\u0435\u0439 (\u0435\u0441\u043b\u0438 \u043e\u043d\u0438 \u043e\u0442\u043b\u0438\u0447\u0430\u044e\u0442\u0441\u044f) \u0438\u043c\u0435\u043b\u0438 \u0443\u043d\u0438\u043a\u0430\u043b\u044c\u043d\u044b\u0435 \u0438\u043c\u0435\u043d\u0430.\n\u0415\u0441\u043b\u0438 \u043c\u043e\u0434\u0435\u043b\u044c \u043e\u0448\u0438\u0431\u043a\u0438 \u0441\u043e\u0441\u0442\u043e\u0438\u0442 \u0442\u043e\u043b\u044c\u043a\u043e \u0438\u0437 \u043e\u0434\u043d\u043e\u0439 \u043c\u043e\u0434\u0435\u043b\u0438 \u2013 \u0442\u043e\u0433\u0434\u0430 \u0431\u0443\u0434\u0435\u0442 \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u043d\u043e \u0438\u043c\u044f \u043e\u0440\u0438\u0433\u0438\u043d\u0430\u043b\u044c\u043d\u043e\u0439 \u043c\u043e\u0434\u0435\u043b\u0438 (\u0447\u0442\u043e \u0431\u044b \u0438\u0437\u0431\u0435\u0433\u0430\u0442\u044c \u0431\u0430\u0433 \u0432 OpenAPI-\u0441\u0445\u0435\u043c\u0435 \u0441 \u043d\u0435\u043a\u043e\u0440\u0440\u0435\u043a\u0442\u043d\u044b\u043c \u043e\u043f\u0440\u0435\u0434\u0435\u043b\u0435\u043d\u0438\u0435\u043c \u043a\u043e\u043c\u043f\u043e\u043d\u0435\u043d\u0442\u0430).\n\n\u041b\u043e\u0433\u0438\u043a\u0443 \u043f\u043e\u0441\u0442\u0440\u043e\u0435\u043d\u0438\u044f \u0438\u043c\u0435\u043d\u0438 \u043c\u043e\u0434\u0435\u043b\u0438 \u043c\u043e\u0436\u043d\u043e \u0438\u0437\u043c\u0435\u043d\u0438\u0442\u044c \u043d\u0430\u0441\u043b\u0435\u0434\u043e\u0432\u0430\u043d\u0438\u0435\u043c \u043e\u0442 `AnnotatedAPIRouter` \u0438 \u043f\u0435\u0440\u0435\u043e\u043f\u0440\u0435\u0434\u0435\u043b\u0435\u043d\u0438\u0435\u043c \u043c\u0435\u0442\u043e\u0434\u0430 `response_model_name`.\n\n\u0422\u0430\u043a \u0436\u0435 \u0434\u043b\u044f \u043f\u043e\u0441\u0442\u0440\u043e\u0435\u043d\u0438\u0435 \u044d\u043a\u0437\u0435\u043c\u043f\u043b\u044f\u0440\u0430 \u043c\u043e\u0434\u0435\u043b\u0438 \u043e\u0448\u0438\u0431\u043a\u0438 \u043f\u043e \u0443\u043c\u043e\u043b\u0447\u0430\u043d\u0438\u044e \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0435\u0442 \u0430\u0442\u0440\u0438\u0431\u0443\u0442\u044b \u044d\u043a\u0437\u0435\u043c\u043f\u043b\u044f\u0440\u0430 \u043e\u0448\u0438\u0431\u043a\u0438 (\u0447\u0435\u0440\u0435\u0437 `__dict__`),\n\u043d\u043e \u0435\u0441\u043b\u0438 \u044d\u0442\u043e \u043f\u043e\u0432\u0435\u0434\u0435\u043d\u0438\u0435 \u043d\u0435\u043e\u0431\u0445\u043e\u0434\u0438\u043c\u043e \u0438\u0437\u043c\u0435\u043d\u0438\u0442\u044c \u043c\u043e\u0436\u043d\u043e \u0440\u0435\u0430\u043b\u0438\u0437\u043e\u0432\u0430\u0442\u044c \u043c\u0435\u0442\u043e\u0434 `entity` (\u043a\u0430\u043a \u0432 \u043f\u0440\u0438\u043c\u0435\u0440\u0435 \u0432 \u043a\u043b\u0430\u0441\u0441\u0430 `OtherError`).\n\n\u0415\u0441\u043b\u0438 `responses` \u0443\u0436\u0435 \u043e\u043f\u0440\u0435\u0434\u0435\u043b\u044f\u0435\u0442\u0441\u044f \u0432 \u0434\u0435\u043a\u043e\u0440\u0430\u0442\u043e\u0440\u0435 \u0440\u043e\u0443\u0442\u0430 \u0438\u043b\u0438 \u043f\u0440\u0438 \u044f\u0432\u043d\u043e\u043c \u0434\u043e\u0431\u0430\u0432\u043b\u0435\u043d\u0438\u0438 \u0432 \u043d\u0435\u0433\u043e \u0438 \u0441\u043e\u0434\u0435\u0440\u0436\u0438\u0442 \u043e\u043f\u0438\u0441\u0430\u043d\u0438\u0435 \u0434\u043b\u044f \u043e\u0448\u0438\u0431\u043a\u0438,\n\u043a\u043e\u0442\u043e\u0440\u0430\u044f \u0443\u043a\u0430\u0437\u0430\u043d\u0430 \u0432 `Annotated`, \u0442\u043e \u043e\u043d\u0430 \u043d\u0435 \u0431\u0443\u0434\u0435\u0442 \u043f\u0435\u0440\u0435\u0437\u0430\u043f\u0438\u0441\u044b\u0432\u0430\u0442\u044c \u0434\u0430\u043d\u043d\u044b\u0435 \u043f\u0435\u0440\u0435\u0434\u0430\u043d\u043d\u044b\u0435 \u0432 `responses`.\n\n```python\n@router.get(\"/my_endpoint_too\", responses={HTTPStatus.BAD_REQUEST: {\"model\": dict[str, str]}})\nasync def endpoint(how_iam: str) -> Annotated[ResponseModel, Errors[MyError, OtherError]]:\n    if how_iam == \"me\":\n        return ResponseModel(message=\"hello\")\n    if how_iam.isdigit():\n        raise OtherError\n    raise MyError(\"I don't know you\")\n```\n\n![Keep responses data model](https://github.com/feodor-ra/annotated-fastapi-router/blob/f4a0b2028ba5ceedca76de6af491f9ef6e312c3a/docs/keep_build_error_model.png)\n\n\u0414\u043b\u044f \u0442\u043e\u0433\u043e \u0447\u0442\u043e \u0431\u044b \u0430\u0432\u0442\u043e\u043c\u0430\u0442\u0438\u0447\u0435\u0441\u043a\u0438 \u043e\u0442\u043b\u0430\u0432\u043b\u0438\u0432\u0430\u0442\u044c \u043e\u0448\u0438\u0431\u043a\u0438 \u0438\u0437 \u044d\u043d\u0434\u043f\u043e\u0438\u043d\u0442\u043e\u0432 \u0438 \u0443\u043a\u0430\u0437\u044b\u0432\u0430\u0442\u044c \u0432 \u043e\u0442\u0432\u0435\u0442\u0435 \u0442\u043e\u0442 \u0441\u0442\u0430\u0442\u0443\u0441-\u043a\u043e\u0434, \u043a\u043e\u0442\u043e\u0440\u044b\u0439 \u0432 \u0443\u043a\u0430\u0437\u0430\u043d \u0432 \u043d\u0438\u0445,\n\u043d\u0435\u043e\u0431\u0445\u043e\u0434\u0438\u043c\u043e \u0434\u043e\u0431\u0430\u0432\u0438\u0442\u044c \u0432 `FastAPI` \u043f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u0435 \u043e\u0431\u0440\u0430\u0431\u043e\u0442\u0447\u0438\u043a \u043e\u0448\u0438\u0431\u043e\u043a `ResponseError.handler`.\n\n```python\napp.add_exception_handler(ResponseError, ResponseError.handler)\n```\n\n![Handle MyError](https://github.com/feodor-ra/annotated-fastapi-router/blob/f4a0b2028ba5ceedca76de6af491f9ef6e312c3a/docs/handle_my_error.png)\n![Handle OtherError](https://github.com/feodor-ra/annotated-fastapi-router/blob/f4a0b2028ba5ceedca76de6af491f9ef6e312c3a/docs/handle_other_error.png)\n\n## Examples\n\n### Status-code\n\nTo specify the status_code of the endpoint, you can do it as follows:\n\n```python\nfrom http import HTTPStatus\nfrom typing import Annotated\n\nfrom fastapi import FastAPI\nfrom pydantic import BaseModel\n\nfrom annotated_fastapi_router import AnnotatedAPIRouter, Status\n\n\nclass ResponseModel(BaseModel):\n    message: str\n\n\nrouter = AnnotatedAPIRouter()\n\n\n@router.get(\"/my_endpoint\")\nasync def my_endpoint() -> Annotated[ResponseModel, Status[HTTPStatus.ACCEPTED]]:\n    return ResponseModel(message=\"hello\")\n\n\nrouter.add_api_route(\"/my_endpoint\", my_endpoint, methods=[\"POST\"])\n```\n\nAs a result, the swagger will contain the following OpenAPI schema:\n\n![GET request swagger](https://github.com/feodor-ra/annotated-fastapi-router/blob/f4a0b2028ba5ceedca76de6af491f9ef6e312c3a/docs/use_status_get.png)\n![POST request swagger](https://github.com/feodor-ra/annotated-fastapi-router/blob/f4a0b2028ba5ceedca76de6af491f9ef6e312c3a/docs/use_status_post.png)\n\nWhen the endpoint is requested, the status code will also be included in the response:\n\n![POST response swagger](https://github.com/feodor-ra/annotated-fastapi-router/blob/f4a0b2028ba5ceedca76de6af491f9ef6e312c3a/docs/use_status_get_response.png)\n\nHowever, if the status code is explicitly provided in the decorator or when adding it to the router, the `Status` in the annotation will be ignored.\n\n```python\n@router.get(\"/my_endpoint\")\n@router.get(\"/my_endpoint_too\", status_code=HTTPStatus.CREATED)\nasync def my_endpoint() -> Annotated[ResponseModel, Status[HTTPStatus.ACCEPTED]]:\n    return ResponseModel(message=\"hello\")\n\n\nrouter.add_api_route(\"/my_endpoint\", my_endpoint, methods=[\"POST\"])\nrouter.add_api_route(\"/my_endpoint_too\", my_endpoint, status_code=HTTPStatus.CREATED, methods=[\"POST\"])\n```\n\n![GET request swagger keep](https://github.com/feodor-ra/annotated-fastapi-router/blob/f4a0b2028ba5ceedca76de6af491f9ef6e312c3a/docs/keep_status_get_swagger.png)\n![POST request swagger keep](https://github.com/feodor-ra/annotated-fastapi-router/blob/f4a0b2028ba5ceedca76de6af491f9ef6e312c3a/docs/keep_status_post_swagger.png)\n\n**Important**: `Status` only accepts enumerations from `HTTPStatus`; any other objects will raise a `TypeError`\n\n### Responses\n\nTo automatically build models for `responses`, the error type `ResponseError` is used.\nIn its subclasses, it is necessary to specify variables of type `status` and `model`.\n\n`status` indicates for which error codes the model will need to be constructed.\nIf multiple error types are provided for the same HTTPStatus, the resulting model will combine all model models.\n\n```python\nfrom http import HTTPStatus\nfrom typing import Annotated, Self\n\nfrom fastapi import FastAPI\nfrom pydantic import BaseModel\n\nfrom annotated_fastapi_router import AnnotatedAPIRouter, Errors, ResponseError\n\n\nclass ResponseModel(BaseModel):\n    message: str\n\n\nclass ErrorMessageModel(BaseModel):\n    message: str\n\n\nclass OtherErrorModel(BaseModel):\n    code: int\n\n\nclass MyError(ResponseError[ErrorMessageModel]):\n    status = HTTPStatus.BAD_REQUEST\n    model = ErrorMessageModel\n\n    def __init__(self: \"MyError\", msg: str) -> None:\n        self.message = msg\n\n\nclass OtherError(ResponseError[OtherErrorModel]):\n    status = HTTPStatus.BAD_REQUEST\n    model = OtherErrorModel\n\n    async def entity(self: Self) -> OtherErrorModel:\n        return self.model(code=self.status)\n\n\nrouter = AnnotatedAPIRouter()\n\n\n@router.get(\"/my_endpoint\")\n@router.post(\"/my_endpoint\")\nasync def endpoint(how_iam: str) -> Annotated[ResponseModel, Errors[MyError, OtherError]]:\n    if how_iam == \"me\":\n        return ResponseModel(message=\"hello\")\n    if how_iam.isdigit():\n        raise OtherError\n    raise MyError(\"I don't know you\")\n```\n\nIn this way, two error models will be built - `MyEndpointPOSTBadRequest` and `MyEndpointGETBadRequest`.\n\n![Build error model GET](https://github.com/feodor-ra/annotated-fastapi-router/blob/f4a0b2028ba5ceedca76de6af491f9ef6e312c3a/docs/build_error_model_get.png)\n![Build error model POST](https://github.com/feodor-ra/annotated-fastapi-router/blob/f4a0b2028ba5ceedca76de6af491f9ef6e312c3a/docs/build_error_model_post.png)\n\nThe model name is generated from the path, methods (if specified) of the route, and the status code error phrase.\nThis is done because for `fastapi<=0.95.0`, all model names (if they differ) must have unique names.\nIf the error model consists of only one model, the name of the original model will be used (to avoid a bug in the OpenAPI schema with an incorrect component definition).\n\nThe logic for naming the model can be changed by inheriting from `AnnotatedAPIRouter`\nand overriding the `response_model_name` method.\n\nAlso, to build an instance of the error model by default, it uses the error instance attributes (via `__dict__`),\nbut if this behavior needs to be changed, you can implement the `entity` method\n(as shown in the example in the `OtherError` class).\n\nIf `responses` are already defined in the route decorator or explicitly added to it and contain\na description for an error specified in `Annotated`, it will not overwrite the data passed to `responses`.\n\n```python\n@router.get(\"/my_endpoint_too\", responses={HTTPStatus.BAD_REQUEST: {\"model\": dict[str, str]}})\nasync def endpoint(how_iam: str) -> Annotated[ResponseModel, Errors[MyError, OtherError]]:\n    if how_iam == \"me\":\n        return ResponseModel(message=\"hello\")\n    if how_iam.isdigit():\n        raise OtherError\n    raise MyError(\"I don't know you\")\n```\n\n![Keep responses data model](https://github.com/feodor-ra/annotated-fastapi-router/blob/f4a0b2028ba5ceedca76de6af491f9ef6e312c3a/docs/keep_build_error_model.png)\n\nTo automatically catch errors from endpoints and include the status code specified in them in the response, you need to add the error handler `ResponseError.handler` to the `FastAPI` application.\n\n```python\napp.add_exception_handler(ResponseError, ResponseError.handler)\n```\n\n![Handle MyError](https://github.com/feodor-ra/annotated-fastapi-router/blob/f4a0b2028ba5ceedca76de6af491f9ef6e312c3a/docs/handle_my_error.png)\n![Handle OtherError](https://github.com/feodor-ra/annotated-fastapi-router/blob/f4a0b2028ba5ceedca76de6af491f9ef6e312c3a/docs/handle_other_error.png)\n",
    "bugtrack_url": null,
    "license": "MIT",
    "summary": "FastAPI annotated api routed",
    "version": "0.1.0",
    "project_urls": {
        "Homepage": "https://github.com/feodor-ra/annotated-fastapi-router",
        "Repository": "https://github.com/feodor-ra/annotated-fastapi-router"
    },
    "split_keywords": [
        "annotated",
        " fastapi"
    ],
    "urls": [
        {
            "comment_text": "",
            "digests": {
                "blake2b_256": "18fefa366676b1e9b530aeb796797542258ec2579361a32806aaeb35f61dee1e",
                "md5": "022b5dd62dd0635bbaa72d86de47d663",
                "sha256": "8cca229a4ae1c5a64f3f06c2e1e24618916f74c045d0ab6f7c095be7cff6e2a1"
            },
            "downloads": -1,
            "filename": "annotated_fastapi_router-0.1.0-py3-none-any.whl",
            "has_sig": false,
            "md5_digest": "022b5dd62dd0635bbaa72d86de47d663",
            "packagetype": "bdist_wheel",
            "python_version": "py3",
            "requires_python": "<4.0,>=3.11",
            "size": 9672,
            "upload_time": "2024-07-06T08:00:26",
            "upload_time_iso_8601": "2024-07-06T08:00:26.701287Z",
            "url": "https://files.pythonhosted.org/packages/18/fe/fa366676b1e9b530aeb796797542258ec2579361a32806aaeb35f61dee1e/annotated_fastapi_router-0.1.0-py3-none-any.whl",
            "yanked": false,
            "yanked_reason": null
        },
        {
            "comment_text": "",
            "digests": {
                "blake2b_256": "3db1eb50222fb44cf202dbf67dea2ecf8376ee513c9230b11d505c56153b032a",
                "md5": "4aeff5a360910add66fab75bef2b717f",
                "sha256": "cb2c37cbe7e9467ff1c2fb2984f387573e8b5a99efa356584187943db4e2b130"
            },
            "downloads": -1,
            "filename": "annotated_fastapi_router-0.1.0.tar.gz",
            "has_sig": false,
            "md5_digest": "4aeff5a360910add66fab75bef2b717f",
            "packagetype": "sdist",
            "python_version": "source",
            "requires_python": "<4.0,>=3.11",
            "size": 10919,
            "upload_time": "2024-07-06T08:00:28",
            "upload_time_iso_8601": "2024-07-06T08:00:28.237110Z",
            "url": "https://files.pythonhosted.org/packages/3d/b1/eb50222fb44cf202dbf67dea2ecf8376ee513c9230b11d505c56153b032a/annotated_fastapi_router-0.1.0.tar.gz",
            "yanked": false,
            "yanked_reason": null
        }
    ],
    "upload_time": "2024-07-06 08:00:28",
    "github": true,
    "gitlab": false,
    "bitbucket": false,
    "codeberg": false,
    "github_user": "feodor-ra",
    "github_project": "annotated-fastapi-router",
    "travis_ci": false,
    "coveralls": false,
    "github_actions": true,
    "lcname": "annotated_fastapi_router"
}
        
Elapsed time: 0.36198s