Skip to content

Commit

Permalink
Add more technical response schemas and examples to docs
Browse files Browse the repository at this point in the history
  • Loading branch information
augusto-herrmann committed Oct 4, 2024
1 parent dad78ce commit 4429720
Show file tree
Hide file tree
Showing 2 changed files with 190 additions and 77 deletions.
143 changes: 83 additions & 60 deletions src/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -95,46 +95,8 @@ async def docs_redirect(
tags=["Auth"],
response_model=schemas.Token,
responses={
status.HTTP_422_UNPROCESSABLE_ENTITY: {
"model": response_schemas.ValidationErrorResponse,
"description": response_schemas.ValidationErrorResponse.get_title(),
"content": {
"application/json": {
"examples": {
"Invalid email format": {
"value": response_schemas.ValidationErrorResponse(
detail=[
response_schemas.ValidationError(
type="value_error",
loc=["email"],
msg="value is not a valid email address: "
"An email address must have an @-sign.",
input="my_username",
ctx={
"reason": "An email address must have an @-sign."
},
url="https://errors.pydantic.dev/2.8/v/value_error",
)
]
).json()
}
}
}
},
},
status.HTTP_401_UNAUTHORIZED: {
"model": response_schemas.UnauthorizedErrorResponse,
"description": response_schemas.UnauthorizedErrorResponse.get_title(),
"content": {
"application/json": {
"examples": {
"Invalid credentials": {
"value": {"detail": "Username ou password incorretos"}
}
}
}
},
},
**response_schemas.email_validation_error,
401: response_schemas.UnauthorizedErrorResponse.docs(),
},
)
async def login_for_access_token(
Expand Down Expand Up @@ -173,20 +135,7 @@ async def login_for_access_token(
summary="Lista usuários da API.",
tags=["Auth"],
response_model=list[schemas.UsersGetSchema],
responses={
status.HTTP_401_UNAUTHORIZED: {
"description": "Unauthorized access",
"content": {
"application/json": {
"examples": {
"Invalid credentials": {
"value": {"detail": "Not authenticated"}
}
}
}
},
},
},
responses=response_schemas.not_admin_error,
)
async def get_users(
user_logged: Annotated[ # pylint: disable=unused-argument
Expand All @@ -203,6 +152,15 @@ async def get_users(
"/user/{email}",
summary="Cria ou altera usuário na API.",
tags=["Auth"],
response_model=schemas.UsersGetSchema,
responses={
**response_schemas.not_admin_error,
422: response_schemas.ValidationErrorResponse.docs(
examples=response_schemas.value_response_example(
"email deve ser igual na url e no json"
)
),
},
)
async def create_or_update_user(
user_logged: Annotated[ # pylint: disable=unused-argument
Expand Down Expand Up @@ -259,6 +217,15 @@ async def create_or_update_user(
"/user/{email}",
summary="Consulta um usuário da API.",
tags=["Auth"],
response_model=schemas.UsersGetSchema,
responses={
**response_schemas.not_admin_error,
404: response_schemas.NotFoundErrorResponse.docs(
examples=response_schemas.value_response_example(
"Usuário `[email protected]` não existe"
)
),
},
)
async def get_user(
user_logged: Annotated[ # pylint: disable=unused-argument
Expand All @@ -285,6 +252,23 @@ async def get_user(
"/user/forgot_password/{email}",
summary="Solicita recuperação de acesso à API.",
tags=["Auth"],
responses={
**response_schemas.email_validation_error,
200: response_schemas.OKMessageResponse.docs(
examples={
"OK": {
"value": response_schemas.OKMessageResponse(
message="Email enviado!"
).json(),
},
}
),
404: response_schemas.NotFoundErrorResponse.docs(
examples=response_schemas.value_response_example(
"Usuário `[email protected]` não existe"
)
),
},
)
async def forgot_password(
email: str,
Expand Down Expand Up @@ -312,6 +296,18 @@ async def forgot_password(
"/user/reset_password/",
summary="Criar nova senha a partir do token de acesso.",
tags=["Auth"],
responses={
200: response_schemas.OKMessageResponse.docs(
examples={
"OK": {
"value": response_schemas.OKMessageResponse(
message="Senha do Usuário `[email protected]` atualizada"
).json(),
},
}
),
400: response_schemas.BadRequestErrorResponse.docs(),
},
)
async def reset_password(
access_token: str,
Expand All @@ -336,8 +332,16 @@ async def reset_password(
"/organizacao/{origem_unidade}/{cod_unidade_autorizadora}"
"/plano_entregas/{id_plano_entregas}",
summary="Consulta plano de entregas",
response_model=schemas.PlanoEntregasSchema,
tags=["plano de entregas"],
response_model=schemas.PlanoEntregasSchema,
responses={
**response_schemas.outra_unidade_error,
404: response_schemas.NotFoundErrorResponse.docs(
examples=response_schemas.value_response_example(
"Plano de entregas não encontrado"
)
),
},
)
async def get_plano_entrega(
user: Annotated[schemas.UsersSchema, Depends(crud_auth.get_current_active_user)],
Expand Down Expand Up @@ -369,8 +373,9 @@ async def get_plano_entrega(
"/organizacao/{origem_unidade}/{cod_unidade_autorizadora}"
"/plano_entregas/{id_plano_entregas}",
summary="Cria ou substitui plano de entregas",
response_model=schemas.PlanoEntregasSchema,
tags=["plano de entregas"],
response_model=schemas.PlanoEntregasSchema,
responses=response_schemas.outra_unidade_error,
)
async def create_or_update_plano_entregas(
user: Annotated[schemas.UsersSchema, Depends(crud_auth.get_current_active_user)],
Expand Down Expand Up @@ -458,8 +463,16 @@ async def create_or_update_plano_entregas(
"/organizacao/{origem_unidade}/{cod_unidade_autorizadora}"
"/plano_trabalho/{id_plano_trabalho}",
summary="Consulta plano de trabalho",
response_model=schemas.PlanoTrabalhoSchema,
tags=["plano de trabalho"],
response_model=schemas.PlanoTrabalhoSchema,
responses={
**response_schemas.outra_unidade_error,
404: response_schemas.NotFoundErrorResponse.docs(
examples=response_schemas.value_response_example(
"Plano de trabalho não encontrado"
)
),
},
)
async def get_plano_trabalho(
user: Annotated[schemas.UsersSchema, Depends(crud_auth.get_current_active_user)],
Expand Down Expand Up @@ -491,8 +504,9 @@ async def get_plano_trabalho(
"/organizacao/{origem_unidade}/{cod_unidade_autorizadora}"
"/plano_trabalho/{id_plano_trabalho}",
summary="Cria ou substitui plano de trabalho",
response_model=schemas.PlanoTrabalhoSchema,
tags=["plano de trabalho"],
response_model=schemas.PlanoTrabalhoSchema,
responses=response_schemas.outra_unidade_error,
)
async def create_or_update_plano_trabalho(
user: Annotated[schemas.UsersSchema, Depends(crud_auth.get_current_active_user)],
Expand Down Expand Up @@ -589,8 +603,16 @@ async def create_or_update_plano_trabalho(
"/organizacao/{origem_unidade}/{cod_unidade_autorizadora}"
"/{cod_unidade_lotacao}/participante/{matricula_siape}",
summary="Consulta um Participante",
response_model=schemas.ParticipanteSchema,
tags=["participante"],
response_model=schemas.ParticipanteSchema,
responses={
**response_schemas.outra_unidade_error,
404: response_schemas.NotFoundErrorResponse.docs(
examples=response_schemas.value_response_example(
"Participante não encontrado"
)
),
},
)
async def get_participante(
user: Annotated[schemas.UsersSchema, Depends(crud_auth.get_current_active_user)],
Expand Down Expand Up @@ -624,8 +646,9 @@ async def get_participante(
"/organizacao/{origem_unidade}/{cod_unidade_autorizadora}"
"/{cod_unidade_lotacao}/participante/{matricula_siape}",
summary="Envia um participante",
response_model=schemas.ParticipanteSchema,
tags=["participante"],
response_model=schemas.ParticipanteSchema,
responses=response_schemas.outra_unidade_error,
)
async def create_or_update_participante(
user: Annotated[schemas.UsersSchema, Depends(crud_auth.get_current_active_user)],
Expand Down
124 changes: 107 additions & 17 deletions src/response_schemas.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,14 @@
"""Esquemas Pydantic para as respostas da API.
"""
"""Esquemas Pydantic para as respostas da API com mensagens técnicas não
relacionadas aos requisitos de negócio."""

from abc import ABC
from typing import Optional

from fastapi import status
from pydantic import BaseModel

# Classes de respostas de dados da API para mensagens técnicas


class ResponseData(ABC, BaseModel):
"""Classe abstrata para conter os métodos comuns a todos os modelos
Expand All @@ -15,24 +18,37 @@ class ResponseData(ABC, BaseModel):

@classmethod
def get_title(cls):
"""Retorna o título da resposta."""
return cls._title.default

@classmethod
def docs(cls, examples: Optional[dict] = None) -> dict:
"""Retorna a documentação da resposta para o método, exibida pelo
FastAPI na interface OpenAPI."""
docs = {
"model": cls,
"description": cls.get_title(),
"content": {"application/json": {}},
}
if examples is not None:
docs["content"]["application/json"]["examples"] = examples
return docs

class ValidationError(BaseModel):
"""Estrutura retornada pelo Pydantic para cada erro de validação."""

type: str
loc: list[str]
msg: str
ctx: dict
class OKMessageResponse(ResponseData):
"""Resposta da API para mensagens bem sucedidas."""

_status_code = status.HTTP_200_OK
_title = "OK"
message: str

class ValidationErrorResponse(ResponseData):
"""Resposta da API para erros de validação."""

_status_code = status.HTTP_422_UNPROCESSABLE_ENTITY
_title = "Unprocessable Entity"
detail: list[ValidationError]
class BadRequestErrorResponse(ResponseData):
"""Resposta da API para erros de autorização."""

_status_code = status.HTTP_400_BAD_REQUEST
_title = "Bad request"
detail: str


class UnauthorizedErrorResponse(ResponseData):
Expand All @@ -50,8 +66,82 @@ class ForbiddenErrorResponse(ResponseData):
_title = "Forbidden"
detail: str

RESPONSE_MODEL_FOR_STATUS_CODE = {
status.HTTP_422_UNPROCESSABLE_ENTITY: ValidationErrorResponse,
status.HTTP_401_UNAUTHORIZED: UnauthorizedErrorResponse,
status.HTTP_403_FORBIDDEN: ForbiddenErrorResponse,

class NotFoundErrorResponse(ResponseData):
"""Resposta da API para erros de permissão."""

_status_code = status.HTTP_404_NOT_FOUND
_title = "Not found"
detail: str


class ValidationError(BaseModel):
"""Estrutura retornada pelo Pydantic para cada erro de validação."""

type: str
loc: list[str]
msg: str
ctx: dict


class ValidationErrorResponse(ResponseData):
"""Resposta da API para erros de validação."""

_status_code = status.HTTP_422_UNPROCESSABLE_ENTITY
_title = "Unprocessable Entity"
detail: list[ValidationError]


# Funções auxiliares para documentação de possíveis respostas técnicas
# na API.

# Documentação de respostas comuns a diversos endpoints
not_logged_error = {
401: UnauthorizedErrorResponse.docs(
examples={"Invalid credentials": {"value": {"detail": "Not authenticated"}}}
),
}
not_admin_error = {
**not_logged_error,
403: ForbiddenErrorResponse.docs(
examples={
"Forbidden": {
"value": {"detail": "Usuário não tem permissões de administrador"}
}
}
),
}
outra_unidade_error = {
**not_logged_error,
403: ForbiddenErrorResponse.docs(
examples={
"Forbidden": {
"value": {
"detail": "Usuário não tem permissão na cod_unidade_autorizadora informada"
}
}
}
),
}
email_validation_error = {
422: ValidationErrorResponse.docs(
examples={
"Invalid email format": {
"value": ValidationErrorResponse(
detail=[
ValidationError(
type="value_error",
loc=["email"],
msg="value is not a valid email address: "
"An email address must have an @-sign.",
input="my_username",
ctx={"reason": "An email address must have an @-sign."},
url="https://errors.pydantic.dev/2.8/v/value_error",
)
]
).json()
}
}
)
}
value_response_example = lambda message: {"example": {"value": {"detail": message}}}

0 comments on commit 4429720

Please sign in to comment.