Skip to main content

RestDoctor

BestDoctor's batteries for REST services.

Для чего нужен RestDoctor

У нас в BestDoctor есть свой API Guide, в котором написано, как API должно быть построено. А еще у нас есть Django и довольно логично использовать Django Rest Framework. Он достаточно гибкий, однако в некоторых местах мы хотим получить больше контроля и соблюдения своих правил.

Поэтому мы написали свою надстройку над DRF, которая имеет

  1. Полную изоляцию между версиями API
  2. Версионирование через заголовок Accept
  3. Декларативную настройку сериализаторов и классов разрешений для View и ViewSet
  4. Прокачанную генерацию схемы

Быстрый старт

Добавляем пакет restdoctor в зависимости или ставим через pip, добавляем restdoctor в INSTALLED_APPS.

После этого можно использовать ViewSet'ы из restdoctor, заменив импорты rest_framework на restdoctor.rest_framework.

Пример на основе tutorial DRF. Было:

from django.contrib.auth.models import User
from rest_framework import viewsets
from rest_framework import permissions
from tutorial.quickstart.serializers import UserSerializer, UserListSerializer


class UserViewSet(viewsets.ModelViewSet):
    """
    API endpoint that allows users to be viewed or edited.
    """
    queryset = User.objects.all().order_by('-date_joined')
    serializer_class = UserSerializer
    permission_classes = [permissions.IsAuthenticated]

    def get_serializer_class(self):
        if self.action == 'list':
            return UserListSerializer
        return self.serializer_class

Стало:

from django.contrib.auth.models import User
from restdoctor.rest_framework import viewsets
from rest_framework import permissions
from tutorial.quickstart.serializers import UserSerializer, UserListSerializer


class UserViewSet(viewsets.ModelViewSet):
    """
    API endpoint that allows users to be viewed or edited.
    """
    queryset = User.objects.all().order_by('-date_joined')
    serializer_class_map = {
        'default': UserSerializer,
        'list': {
            'response': UserListSerializer,
        },
    }
    permission_classes_map = {
        'default': [permissions.IsAuthenticated]
    }

Дальнейшая настройка

Для разбора формата из заголовка Accept необходимо добавить middleware в конфигурацию приложения:

ROOT_URLCONF = ...

MIDDLEWARE = [
    ...,
    'restdoctor.django.middleware.api_selector.ApiSelectorMiddleware',
]

API_PREFIXES = ('/api',)
API_FORMATS = ('full', 'compact')

После этого для префиксов, указанных в API_PREFIXES? будет производиться разбор заголовка Accept. Во время обработки запроса во View или ViewSet в request добавится атрибут api_params.

Установка и конфигурирование

Добавляем настройки в Settings:

ROOT_URLCONF = 'app.urls'

INSTALLED_APPS = [
    ...,
    'rest_framework',
    'restdoctor',
]

MIDDLEWARE = [
    ...,
    'restdoctor.django.middleware.api_selector.ApiSelectorMiddleware',
]

API_FALLBACK_VERSION = 'fallback'
API_FALLBACK_FOR_APPLICATION_JSON_ONLY = False
API_DEFAULT_VERSION = 'v1'
API_DEFAULT_FORMAT = 'full'
API_PREFIXES = ('/api',)
API_FORMATS = ('full', 'compact')
API_RESOURCE_DISCRIMINATIVE_PARAM = 'view_type'
API_RESOURCE_DEFAULT = 'common'
API_RESOURCE_SET_PARAM = False
API_RESOURCE_SET_PARAM_FOR_DEFAULT = False
API_V1_URLCONF = 'api.v1_urls'
API_VERSIONS = {
    'fallback': ROOT_URLCONF,
    'v1': API_V1_URLCONF,
}

Использование в проекте

Максимально наследуемся от restdoctor там, где есть выбор между rest_framework и restdoctor.rest_framework.

from restdoctor.rest_framework.serializers import ModelSerializer
from restdoctor.rest_framework.views import GenericAPIView, RetrieveAPIView, ListAPIView
from restdoctor.rest_framework.viewsets import ModelViewSet, ReadOnlyModelViewSet

Версионирование

RestDoctor маршрутизирует вызовы по заголовку Accept на изолированный UrlConf.

  1. Во-первых, это означает, что без корректного заголовка Accept ручки API могут быть недоступны и отдавать 404.
  2. А во-вторых, в приложении может быть несколько различных версий API, которые не будут "видеть" друг друга.

Общий формат заголовка следующий:

application/vnd.{vendor}.{version}[-{resource}][.{format}][+json]

Где vendor задается на уровне приложения параметром API_VENDOR_STRING, список версий и сопоставление их UrlConf'ам определяется параметром API_VERSIONS.

Саму маршрутизацию для входящего запроса проводит middleware ApiSelectorMiddleware, которую надо включить в настройках.

ROOT_URLCONF = 'app.urls'

MIDDLEWARE = [
    ...,
    'restdoctor.django.middleware.api_selector.ApiSelectorMiddleware',
]

API_V1_URLCONF = 'api.v1.urls'

API_VENDOR_STRING = 'RestDoctor'

API_FALLBACK_VERSION = 'fallback'
API_DEFAULT_VERSION = 'v1'
API_VERSIONS = {
    API_FALLBACK_VERSION: ROOT_URLCONF,
    API_DEFAULT_VERSION: API_V1_URLCONF,
}

Маршрутизация по API_VERSIONS срабатывает, если Accept начинается с application/vnd.{vendor}, если не указана версия, то берется API_DEFAULT_VERSION. Если Accept не содержит корректной vendor-строки, то выбирается API_FALLBACK_VERSION.

Версия может быть указана в формате {version}-{resource}, тогда ResourceViewSet будет использовать эту информацию для выбора ViewSet.

Кроме того, может быть дополнительно указан {format} для выбора формата ответа, по факту выбор сериализатора в SerializerClassMapApiView.

Также у формата тоже могут быть версии. Если {format} в API_FORMATS задан version:{2,3,5} в запросе Accept фигурирует только номер версии version:5. Выбор сериализатора происходит от большого к меньшему.

В случае успешного определения версии и параметров API из заголовка Accept, middleware выбирает для дальнейшей обработки запроса конкретный UrlConf и добавляет к объекту request атрибут api_params.

Формат ответа API

Нашим API Guide задан формат ответа, за который отвечает RestDoctorRenderer (restdoctor.rest_framework.renderers.RestDoctorRenderer). Включается он только для запросов, содержащих атрибут api_params, и работает этот механизм через content_negotiation_class заданный в базовом для View и ViewSet миксине NegotiatedMixin (restdoctor.rest_framework.mixins.NegotiatedMixin).

SerializerClassMapApiView

DRF позволяет достаточно компактно определять ModelSeraizlier + ModelViewSet, однако оставляет достаточно много свободы в одних местах, не предоставляя ее в других.

Например, можно переопределить serializer_class в классе ViewSet'а, либо определять его динамически через ViewSet.get_serializer_class, однако нельзя переопределять сериализаторы отдельно для запроса, отдельно для ответа. Т.е. нельзя задать отдельный сериализатор для update, используя сериализатор для retrieve для возврата измененной сущности.

SerializerClassMapApiView дает возможность декларативно задавать сериализаторы для различных action, отдельно для request и response.

Поддержка на уровне базовых миксинов для ViewSet'ов позволяет прозрачно заменить, например, ReadOnlyModelViewSet в импортах с rest_framework.viewsets на restdoctor.rest_framework.viewsets.

serializer_class_map

SerializerClassMapApiView позволяет задавать сериализаторы для разных action'ов и форматов ответа отдельно для request и response фазы обработки запроса.

from restdoctor.rest_framework.viewsets import ModelViewSet

from app.api.serializers import (
    MyDefaultSerializer, MyCompactSerializer, MyAntoherSerializer,
    MyCreateSerializer, MyUpdateSerializer,
)


class MyApiView(SerializerClassMapApiView):
    """Пример работы с serializer_class_map."""

    serializer_class_map = {
        'default': MyDefaultSerializer,
        'default.compact': MyCompactSerializer,
         'create': {
            'request': MyCreateSerializer,
         },
         'update': {
            'request': MyUpdateSerializer,
            'request.version:3': MyVersion3UpdateSerializer,
            'request.version:2': MyVersionUpdateSerializer,
         },
         'list': {
            'response.another_format': MyAnotherSerializer,
            'meta': MyMetaSerializer,
        }
    }

В этом примере мы задаем MyDefaultSerializer как базовый для ViewSet. Но для create и update action переопределяем сериализаторы для обработки request'а.

Кроме того, мы определили сериализатор для compact формата и отдельно для action list и update форматы another_format, version:2, version:3. Формат с версиями работает по принципу поиска точной или меньшей версии сериализатора. Отдельно добавлена дополнительное формирование meta информации.

permission_classes_map

По аналогии с serializer_class_map для декларативного задания разных наборов permission_classes на разных action'ах можно определить permission_classes_map:

from restdoctor.rest_framework.viewsets import ModelViewSet

from app.api.permissions import PermissionA, PermissionB


class MyViewSet(ModelViewSet):
    permission_classes_map = {
        'default': [PermissionA],
        'retrieve': [PermissionB],
    }

Замечание про action

В DRF action появляется во время регистрации ViewSet с помощью Router. При этом для разделения list/detail ресурсов используются разные наборы action_maps:

list_action_map = {'get': 'list', 'post': 'create'}
detail_action_map = {'get': 'retrieve', 'put': 'update'}

Django-механизмы роутинга создают функцию-обработчик, которая инстанцирует View/ViewSet с нужными параметрами. При этом один и тот же класс ViewSet будет присутствовать в UrlConf в двух экземплярах с разными action_map. Во время обработки запроса по HTTP методу будет определен action и вызван соответствующий метод экземпляра ViewSet. И во время обработки запроса у ViewSet всегда задан self.action.

Однако это не так для View, поэтому в SerializerClassMapApiView добавлен атрибут action, на который завязывается поиск сериализатора в serializer_class_map.

Миксины и ModelViewSet

Миксины задают базовые операции ModelViewSet для 'list', 'retrieve', 'create', 'update', 'destroy' action'ов.

От DRF-версий они отличаются в основном тем, что используют SerializerClassMapApiView.get_request_serializer и SerializerClassMapApiView.get_response_serializer вместо View.get_serializer.

RetrieveModelMixin

Определяет обработчик для retrieve action. Определяет метод get_item:

class RetrieveModelMixin(BaseListModelMixin):
    def retrieve(self, request: Request, *args: typing.Any, **kwargs: typing.Any) -> Response:
        item = self.get_item(request_serializer)
        ...


    def get_item(self, request_serializer: BaseSerializer) -> typing.Union[typing.Dict, QuerySet]:
        return self.get_object()

Т.е. можно использовать RetrieveModelMixin для работы с любыми словарями, а не только моделями, надо только переопределить ViewSet.get_item.

ListModelMixin

Определяет обработчик для list action. Определяет метод get_collection:

class ListModelMixin(BaseListModelMixin):
    def list(self, request: Request, *args: typing.Any, **kwargs: typing.Any) -> Response:
        queryset = self.get_collection()
        ...

    def get_collection(self, request_serializer: BaseSerializer) -> typing.Union[typing.List, QuerySet]:
        return self.filter_queryset(self.get_queryset())

Т.е. можно использовать ListModelMixin для работы с любыми коллекциями, а не только моделями, надо только переопределить ViewSet.get_collection. При этом, если задан сериализатор для list, то он будет использован для query-параметров, что позволит получить эти параметры и использовать дополнительно к filterset'у.

Определяет формирование дополнительной meta информации. Определяет метод get_meta_data:

class ListModelMixin(BaseListModelMixin):
    def get_meta_data(self) -> typing.Dict[str, typing.Any]:
        return {'test': typing.Any}

Т.е. можно использовать ListModelMixin для формирования дополнительной информации в поле meta. Для корректной работы нужно определить сериализатор для meta.

    serializer_class_map = {
         'default': MyDefaultSerializer,
         'list': {
            'meta': MyMetaSerializer,
        }
    }

Задан обработчик perform_list для выбранных данных в пагинации. Для работы нужно переопределить метод perform_list.

class ListModelMixin(BaseListModelMixin):
    def perform_list(self, data: typing.Union[typing.List, QuerySet]) -> None:
        Sender(data)

ListModelViewSet

Задан только обработчик для list action.

ReadOnlyModelViewSet

Заданы обработчики для list и retrieve action'ов.

CreateUpdateReadModelViewSet

Заданы обработчики для list, retrieve, create, update action'ов.

ModelViewSet

Полный набор action'ов: list, retrieve, create, update, destroy.

PydanticSerializer

Для использования сериализатор на основе pydantic необходимо наследовать сериализатор от PydanticSerializer, указать в Meta pydantic_model и pydantic_use_aliases (при необходимости).

Параметр pydantic_use_aliases позволяет использовать алиасы pydantic моделей для сериализации.

class PydanticSerializer(PydanticSerializer):
    class Meta:
        pydantic_model = PydanticModel
        pydantic_use_aliases = True

Генерация схемы

Поддерживается генерация схемы openapi версий 3.0.2 и 3.1.0. Схема по умолчанию задается параметром API_DEFAULT_OPENAPI_VERSION и равна 3.0.2.

Пример генерации схемы (версия из settings):

python3 ./manage.py generateschema --urlconf api.v1.urls --generator_class restdoctor.rest_framework.schema.RefsSchemaGenerator > your_app/static/openapi.schema

Пример генерации схемы версии openapi 3.0.2:

python3 ./manage.py generateschema --urlconf api.v1.urls --generator_class restdoctor.rest_framework.schema.RefsSchemaGenerator30 > your_app/static/openapi-30.schema

Пример генерации смхемы версии openapi 3.1.0:

python3 ./manage.py generateschema --urlconf api.v1.urls --generator_class restdoctor.rest_framework.schema.RefsSchemaGenerator31 > your_app/static/openapi-31.schema

Опции генерации

API_STRICT_SCHEMA_VALIDATION
  • делает обязательным использование описаний у полей (help_text, verbose_name у модели)
  • проверяет на совпадение аннотацию поля и атрибут allow_null
  • проверяет на совпадение аннотацию поля и атрибут many

Если какая-то проверка не проходит, генерация схемы завершается ошибкой.

API_SCHEMA_PRIORITIZE_SERIALIZER_PARAMETERS

При включении этой опции для схемы будут выбираться поля сериализатора, даже если они дублируют существующие.

API_SCHEMA_FILTER_MAP_PATH

Путь до кастомной схемы обработки фильтров для DjangoFilterBackend, по умолчанию - restdoctor.rest_framework.schema.filters.FILTER_MAP.

pre-commit

Этот репозиторий использует git-хуки настроенные с помощью pre-commit поэтому если планируется дальнейшее внесение изменений в репозиторий необходимо инициализировать pre-commit с помощью следующей команды:

make install-hooks

Release files for restdoctor 0.1.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for restdoctor 0.1.0
File Size Uploaded
restdoctor-0.1.0.tar.gz 83.7 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for restdoctor 0.1.0
File Interpreter ABI Platform
restdoctor-0.1.0-py3-none-any.whl Python 3 none any Details

Total release size: 201.7 kB

Release files / restdoctor-0.1.0.tar.gz

Download URL restdoctor-0.1.0.tar.gz
Size 83.7 kB
Tags Source
SHA-256 checksum
How to use checksums
41b71084c1b0c28cc2b628097ed0c1383f728899fa684c07cce0cb4591a8abe4
BLAKE2b-256 checksum
How to use checksums
ba408c4c00358abb85b6d0a82a0ae5538bf78beaca0f5cfff3418f67b0067e09
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/6.1.0 CPython/3.13.7

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Feb 27, 2026.

Transparency log

Release files / restdoctor-0.1.0-py3-none-any.whl

Download URL restdoctor-0.1.0-py3-none-any.whl
Size 118.0 kB
Tags Python 3
SHA-256 checksum
How to use checksums
7359a4d827d2266279189c9c8ab915fa413f367a0d4b670620c7708d961bda68
BLAKE2b-256 checksum
How to use checksums
152f44a3f0edc91ad871bf1525f798c11fc46ca0eb9e529268d171776ac2afe7
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/6.1.0 CPython/3.13.7

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Feb 27, 2026.

Transparency log

Release history Release notifications | RSS feed

This release

0.1.0 This release

2 release files

0.0.63

2 release files

0.0.61

2 release files

0.0.60

2 release files

0.0.59

2 release files

0.0.58

2 release files

0.0.56

2 release files

0.0.55

2 release files

0.0.54

2 release files

0.0.53

2 release files

0.0.52

2 release files

0.0.51

2 release files

0.0.50

2 release files

0.0.49

2 release files

0.0.48

2 release files

0.0.47

2 release files

0.0.46

2 release files

0.0.45

2 release files

0.0.43

2 release files

0.0.42

2 release files

0.0.41

2 release files

0.0.40

2 release files

0.0.39

2 release files

0.0.38

2 release files

0.0.37

2 release files

0.0.32

2 release files

0.0.31

2 release files

0.0.30

2 release files

0.0.29

2 release files

0.0.28

2 release files

0.0.27

2 release files

0.0.26

2 release files

0.0.25

2 release files

0.0.24

2 release files

0.0.23

1 release file

0.0.22

1 release file

0.0.21

1 release file

0.0.20

1 release file

0.0.19

1 release file

0.0.18

1 release file

0.0.17

1 release file

0.0.16

1 release file

0.0.15

1 release file

0.0.14

1 release file

0.0.13

1 release file

0.0.12

1 release file

0.0.11

1 release file

0.0.10

1 release file

0.0.9

1 release file

0.0.8

1 release file

0.0.7

1 release file

0.0.6

1 release file

0.0.5

1 release file

0.0.4

1 release file

0.0.3

1 release file

0.0.2

1 release file

0.0.1

1 release file

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page