first commit
This commit is contained in:
374
src/apps/core/openapi.py
Normal file
374
src/apps/core/openapi.py
Normal file
@@ -0,0 +1,374 @@
|
||||
"""
|
||||
Утилиты для документирования API (OpenAPI/Swagger).
|
||||
|
||||
Предоставляет декораторы и утилиты для улучшения
|
||||
автоматически генерируемой документации.
|
||||
"""
|
||||
|
||||
from typing import Any
|
||||
|
||||
from drf_yasg import openapi
|
||||
from drf_yasg.utils import swagger_auto_schema
|
||||
|
||||
|
||||
def api_docs(
|
||||
*,
|
||||
summary: str,
|
||||
description: str | None = None,
|
||||
request_body: Any = None,
|
||||
responses: dict[int, Any] | None = None,
|
||||
tags: list[str] | None = None,
|
||||
operation_id: str | None = None,
|
||||
deprecated: bool = False,
|
||||
security: list[dict[str, list[str]]] | None = None,
|
||||
manual_parameters: list[openapi.Parameter] | None = None,
|
||||
):
|
||||
"""
|
||||
Декоратор для документирования API эндпоинтов.
|
||||
|
||||
Упрощённая обёртка над swagger_auto_schema с поддержкой
|
||||
типовых паттернов документирования.
|
||||
|
||||
Args:
|
||||
summary: Краткое описание эндпоинта (отображается в списке)
|
||||
description: Подробное описание (отображается при раскрытии)
|
||||
request_body: Схема тела запроса (serializer или openapi.Schema)
|
||||
responses: Словарь возможных ответов {status_code: schema}
|
||||
tags: Теги для группировки в документации
|
||||
operation_id: Уникальный идентификатор операции
|
||||
deprecated: Пометить как устаревший
|
||||
security: Требования безопасности
|
||||
manual_parameters: Дополнительные параметры запроса
|
||||
|
||||
Пример использования:
|
||||
class UserView(APIView):
|
||||
@api_docs(
|
||||
summary="Получить текущего пользователя",
|
||||
description="Возвращает данные аутентифицированного пользователя",
|
||||
responses={
|
||||
200: UserSerializer,
|
||||
401: "Не авторизован",
|
||||
},
|
||||
tags=["Пользователи"],
|
||||
)
|
||||
def get(self, request):
|
||||
...
|
||||
"""
|
||||
# Преобразуем упрощённые responses в формат openapi
|
||||
formatted_responses = {}
|
||||
if responses:
|
||||
for code, schema in responses.items():
|
||||
if isinstance(schema, str):
|
||||
# Простое текстовое описание
|
||||
formatted_responses[code] = openapi.Response(description=schema)
|
||||
elif isinstance(schema, type):
|
||||
# Serializer class
|
||||
formatted_responses[code] = openapi.Response(
|
||||
description=_get_status_description(code),
|
||||
schema=schema,
|
||||
)
|
||||
elif isinstance(schema, openapi.Response):
|
||||
formatted_responses[code] = schema
|
||||
else:
|
||||
formatted_responses[code] = schema
|
||||
|
||||
return swagger_auto_schema(
|
||||
operation_summary=summary,
|
||||
operation_description=description,
|
||||
request_body=request_body,
|
||||
responses=formatted_responses or None,
|
||||
tags=tags,
|
||||
operation_id=operation_id,
|
||||
deprecated=deprecated,
|
||||
security=security,
|
||||
manual_parameters=manual_parameters,
|
||||
)
|
||||
|
||||
|
||||
def _get_status_description(status_code: int) -> str:
|
||||
"""Возвращает описание HTTP статуса на русском."""
|
||||
descriptions = {
|
||||
200: "Успешный запрос",
|
||||
201: "Ресурс создан",
|
||||
204: "Успешно, без содержимого",
|
||||
400: "Некорректный запрос",
|
||||
401: "Не авторизован",
|
||||
403: "Доступ запрещён",
|
||||
404: "Ресурс не найден",
|
||||
409: "Конфликт",
|
||||
422: "Ошибка валидации",
|
||||
429: "Слишком много запросов",
|
||||
500: "Внутренняя ошибка сервера",
|
||||
}
|
||||
return descriptions.get(status_code, f"HTTP {status_code}")
|
||||
|
||||
|
||||
# Предопределённые схемы ответов
|
||||
class CommonResponses:
|
||||
"""
|
||||
Общие схемы ответов для документации.
|
||||
|
||||
Пример использования:
|
||||
@api_docs(
|
||||
summary="Удалить ресурс",
|
||||
responses={
|
||||
204: CommonResponses.NO_CONTENT,
|
||||
404: CommonResponses.NOT_FOUND,
|
||||
},
|
||||
)
|
||||
def delete(self, request, pk):
|
||||
...
|
||||
"""
|
||||
|
||||
SUCCESS = openapi.Response(
|
||||
description="Успешный запрос",
|
||||
schema=openapi.Schema(
|
||||
type=openapi.TYPE_OBJECT,
|
||||
properties={
|
||||
"success": openapi.Schema(type=openapi.TYPE_BOOLEAN, default=True),
|
||||
"data": openapi.Schema(type=openapi.TYPE_OBJECT),
|
||||
},
|
||||
),
|
||||
)
|
||||
|
||||
CREATED = openapi.Response(
|
||||
description="Ресурс успешно создан",
|
||||
schema=openapi.Schema(
|
||||
type=openapi.TYPE_OBJECT,
|
||||
properties={
|
||||
"success": openapi.Schema(type=openapi.TYPE_BOOLEAN, default=True),
|
||||
"data": openapi.Schema(type=openapi.TYPE_OBJECT),
|
||||
},
|
||||
),
|
||||
)
|
||||
|
||||
NO_CONTENT = openapi.Response(description="Успешно, без содержимого")
|
||||
|
||||
BAD_REQUEST = openapi.Response(
|
||||
description="Некорректный запрос",
|
||||
schema=openapi.Schema(
|
||||
type=openapi.TYPE_OBJECT,
|
||||
properties={
|
||||
"success": openapi.Schema(type=openapi.TYPE_BOOLEAN, default=False),
|
||||
"errors": openapi.Schema(
|
||||
type=openapi.TYPE_ARRAY,
|
||||
items=openapi.Schema(
|
||||
type=openapi.TYPE_OBJECT,
|
||||
properties={
|
||||
"code": openapi.Schema(type=openapi.TYPE_STRING),
|
||||
"message": openapi.Schema(type=openapi.TYPE_STRING),
|
||||
},
|
||||
),
|
||||
),
|
||||
},
|
||||
),
|
||||
)
|
||||
|
||||
UNAUTHORIZED = openapi.Response(
|
||||
description="Требуется аутентификация",
|
||||
schema=openapi.Schema(
|
||||
type=openapi.TYPE_OBJECT,
|
||||
properties={
|
||||
"detail": openapi.Schema(
|
||||
type=openapi.TYPE_STRING,
|
||||
default="Учётные данные не предоставлены.",
|
||||
),
|
||||
},
|
||||
),
|
||||
)
|
||||
|
||||
FORBIDDEN = openapi.Response(
|
||||
description="Доступ запрещён",
|
||||
schema=openapi.Schema(
|
||||
type=openapi.TYPE_OBJECT,
|
||||
properties={
|
||||
"detail": openapi.Schema(
|
||||
type=openapi.TYPE_STRING,
|
||||
default="У вас нет прав для выполнения этого действия.",
|
||||
),
|
||||
},
|
||||
),
|
||||
)
|
||||
|
||||
NOT_FOUND = openapi.Response(
|
||||
description="Ресурс не найден",
|
||||
schema=openapi.Schema(
|
||||
type=openapi.TYPE_OBJECT,
|
||||
properties={
|
||||
"success": openapi.Schema(type=openapi.TYPE_BOOLEAN, default=False),
|
||||
"errors": openapi.Schema(
|
||||
type=openapi.TYPE_ARRAY,
|
||||
items=openapi.Schema(
|
||||
type=openapi.TYPE_OBJECT,
|
||||
properties={
|
||||
"code": openapi.Schema(
|
||||
type=openapi.TYPE_STRING, default="not_found"
|
||||
),
|
||||
"message": openapi.Schema(
|
||||
type=openapi.TYPE_STRING, default="Ресурс не найден"
|
||||
),
|
||||
},
|
||||
),
|
||||
),
|
||||
},
|
||||
),
|
||||
)
|
||||
|
||||
VALIDATION_ERROR = openapi.Response(
|
||||
description="Ошибка валидации",
|
||||
schema=openapi.Schema(
|
||||
type=openapi.TYPE_OBJECT,
|
||||
properties={
|
||||
"success": openapi.Schema(type=openapi.TYPE_BOOLEAN, default=False),
|
||||
"errors": openapi.Schema(
|
||||
type=openapi.TYPE_ARRAY,
|
||||
items=openapi.Schema(
|
||||
type=openapi.TYPE_OBJECT,
|
||||
properties={
|
||||
"code": openapi.Schema(type=openapi.TYPE_STRING),
|
||||
"message": openapi.Schema(type=openapi.TYPE_STRING),
|
||||
"details": openapi.Schema(type=openapi.TYPE_OBJECT),
|
||||
},
|
||||
),
|
||||
),
|
||||
},
|
||||
),
|
||||
)
|
||||
|
||||
RATE_LIMITED = openapi.Response(
|
||||
description="Превышен лимит запросов",
|
||||
schema=openapi.Schema(
|
||||
type=openapi.TYPE_OBJECT,
|
||||
properties={
|
||||
"detail": openapi.Schema(
|
||||
type=openapi.TYPE_STRING,
|
||||
default="Превышен лимит запросов. Повторите позже.",
|
||||
),
|
||||
},
|
||||
),
|
||||
)
|
||||
|
||||
SERVER_ERROR = openapi.Response(
|
||||
description="Внутренняя ошибка сервера",
|
||||
schema=openapi.Schema(
|
||||
type=openapi.TYPE_OBJECT,
|
||||
properties={
|
||||
"success": openapi.Schema(type=openapi.TYPE_BOOLEAN, default=False),
|
||||
"errors": openapi.Schema(
|
||||
type=openapi.TYPE_ARRAY,
|
||||
items=openapi.Schema(
|
||||
type=openapi.TYPE_OBJECT,
|
||||
properties={
|
||||
"code": openapi.Schema(
|
||||
type=openapi.TYPE_STRING, default="internal_error"
|
||||
),
|
||||
"message": openapi.Schema(
|
||||
type=openapi.TYPE_STRING,
|
||||
default="Внутренняя ошибка сервера",
|
||||
),
|
||||
},
|
||||
),
|
||||
),
|
||||
},
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
# Параметры запроса
|
||||
class CommonParameters:
|
||||
"""
|
||||
Общие параметры для документации API.
|
||||
|
||||
Пример использования:
|
||||
@api_docs(
|
||||
summary="Список ресурсов",
|
||||
manual_parameters=[
|
||||
CommonParameters.PAGE,
|
||||
CommonParameters.PAGE_SIZE,
|
||||
CommonParameters.SEARCH,
|
||||
],
|
||||
)
|
||||
def get(self, request):
|
||||
...
|
||||
"""
|
||||
|
||||
PAGE = openapi.Parameter(
|
||||
name="page",
|
||||
in_=openapi.IN_QUERY,
|
||||
type=openapi.TYPE_INTEGER,
|
||||
description="Номер страницы",
|
||||
default=1,
|
||||
)
|
||||
|
||||
PAGE_SIZE = openapi.Parameter(
|
||||
name="page_size",
|
||||
in_=openapi.IN_QUERY,
|
||||
type=openapi.TYPE_INTEGER,
|
||||
description="Количество элементов на странице",
|
||||
default=20,
|
||||
)
|
||||
|
||||
SEARCH = openapi.Parameter(
|
||||
name="search",
|
||||
in_=openapi.IN_QUERY,
|
||||
type=openapi.TYPE_STRING,
|
||||
description="Поисковый запрос",
|
||||
)
|
||||
|
||||
ORDERING = openapi.Parameter(
|
||||
name="ordering",
|
||||
in_=openapi.IN_QUERY,
|
||||
type=openapi.TYPE_STRING,
|
||||
description="Поле сортировки (префикс '-' для убывания)",
|
||||
)
|
||||
|
||||
ID = openapi.Parameter(
|
||||
name="id",
|
||||
in_=openapi.IN_PATH,
|
||||
type=openapi.TYPE_INTEGER,
|
||||
description="ID ресурса",
|
||||
required=True,
|
||||
)
|
||||
|
||||
|
||||
def paginated_response(serializer_class: type) -> openapi.Response:
|
||||
"""
|
||||
Создаёт схему пагинированного ответа.
|
||||
|
||||
Пример использования:
|
||||
@api_docs(
|
||||
summary="Список пользователей",
|
||||
responses={200: paginated_response(UserSerializer)},
|
||||
)
|
||||
def get(self, request):
|
||||
...
|
||||
"""
|
||||
return openapi.Response(
|
||||
description="Пагинированный список",
|
||||
schema=openapi.Schema(
|
||||
type=openapi.TYPE_OBJECT,
|
||||
properties={
|
||||
"success": openapi.Schema(type=openapi.TYPE_BOOLEAN, default=True),
|
||||
"data": openapi.Schema(
|
||||
type=openapi.TYPE_ARRAY,
|
||||
items=openapi.Schema(type=openapi.TYPE_OBJECT),
|
||||
),
|
||||
"meta": openapi.Schema(
|
||||
type=openapi.TYPE_OBJECT,
|
||||
properties={
|
||||
"pagination": openapi.Schema(
|
||||
type=openapi.TYPE_OBJECT,
|
||||
properties={
|
||||
"count": openapi.Schema(type=openapi.TYPE_INTEGER),
|
||||
"page": openapi.Schema(type=openapi.TYPE_INTEGER),
|
||||
"page_size": openapi.Schema(type=openapi.TYPE_INTEGER),
|
||||
"total_pages": openapi.Schema(
|
||||
type=openapi.TYPE_INTEGER
|
||||
),
|
||||
},
|
||||
),
|
||||
},
|
||||
),
|
||||
},
|
||||
),
|
||||
)
|
||||
Reference in New Issue
Block a user