Package: restdoctor
RestDoctorBestDoctor's batteries for REST services.Для чего нужен RestDoctorУ нас в BestDoctor естьсвой API Guide, в котором написано, как API должно быть
построено. А еще у нас есть Django и довольно логично использовать Django Rest Framework. Он достаточно гибкий,
однако в некоторых местах мы хотим получить больше контроля и соблюдения своих правил.Поэтому мы написали свою надстройку над DRF, которая имеетПолную изоляцию между версиями APIВерсионирование через заголовокAcceptДекларативную настройку сериализаторов и классов разрешений дляViewиViewSetПрокачанную генерацию схемыБыстрый стартДобавляем пакетrestdoctorв зависимости или ставим через pip, добавляемrestdoctorвINSTALLED_APPS.После этого можно использовать ViewSet'ы из restdoctor, заменив импортыrest_frameworkнаrestdoctor.rest_framework.Пример на основе tutorial DRF. Было:fromdjango.contrib.auth.modelsimportUserfromrest_frameworkimportviewsetsfromrest_frameworkimportpermissionsfromtutorial.quickstart.serializersimportUserSerializer,UserListSerializerclassUserViewSet(viewsets.ModelViewSet):"""API endpoint that allows users to be viewed or edited."""queryset=User.objects.all().order_by('-date_joined')serializer_class=UserSerializerpermission_classes=[permissions.IsAuthenticated]defget_serializer_class(self):ifself.action=='list':returnUserListSerializerreturnself.serializer_classСтало:fromdjango.contrib.auth.modelsimportUserfromrestdoctor.rest_frameworkimportviewsetsfromrest_frameworkimportpermissionsfromtutorial.quickstart.serializersimportUserSerializer,UserListSerializerclassUserViewSet(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=FalseAPI_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=FalseAPI_RESOURCE_SET_PARAM_FOR_DEFAULT=FalseAPI_V1_URLCONF='api.v1_urls'API_VERSIONS={'fallback':ROOT_URLCONF,'v1':API_V1_URLCONF,}Использование в проектеМаксимально наследуемся от restdoctor там, где есть выбор междуrest_frameworkиrestdoctor.rest_framework.fromrestdoctor.rest_framework.serializersimportModelSerializerfromrestdoctor.rest_framework.viewsimportGenericAPIView,RetrieveAPIView,ListAPIViewfromrestdoctor.rest_framework.viewsetsimportModelViewSet,ReadOnlyModelViewSetВерсионированиеRestDoctor маршрутизирует вызовы по заголовкуAcceptна изолированныйUrlConf.Во-первых, это означает, что без корректного заголовкаAcceptручки API могут быть недоступны и отдавать 404.А во-вторых, в приложении может быть несколько различных версий API, которые не будут "видеть" друг друга.Общий формат заголовка следующий:application/vnd.{vendor}.{version}[-{resource}][.{format}][+json]Где vendor задается на уровне приложения параметромAPI_VENDOR_STRING, список версий и сопоставление их UrlConf'ам
определяется параметромAPI_VERSIONS.Саму маршрутизацию для входящего запроса проводит middlewareApiSelectorMiddleware, которую надо включить в
настройках.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).SerializerClassMapApiViewDRF позволяет достаточно компактно определять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_mapSerializerClassMapApiViewпозволяет задавать сериализаторы для разных action'ов и форматов ответа отдельно для
request и response фазы обработки запроса.fromrestdoctor.rest_framework.viewsetsimportModelViewSetfromapp.api.serializersimport(MyDefaultSerializer,MyCompactSerializer,MyAntoherSerializer,MyCreateSerializer,MyUpdateSerializer,)classMyApiView(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иupdateaction
переопределяем сериализаторы для обработки request'а.Кроме того, мы определили сериализатор дляcompactформата и отдельно для actionlistиupdateформатыanother_format,version:2,version:3.
Формат с версиями работает по принципу поиска точной или меньшей версии сериализатора.
Отдельно добавлена дополнительное формирование meta информации.permission_classes_mapПо аналогии сserializer_class_mapдля декларативного задания разных наборовpermission_classesна разных action'ах
можно определитьpermission_classes_map:fromrestdoctor.rest_framework.viewsetsimportModelViewSetfromapp.api.permissionsimportPermissionA,PermissionBclassMyViewSet(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Определяет обработчик дляretrieveaction. Определяет методget_item:classRetrieveModelMixin(BaseListModelMixin):defretrieve(self,request:Request,*args:typing.Any,**kwargs:typing.Any)->Response:item=self.get_item(request_serializer)...defget_item(self,request_serializer:BaseSerializer)->typing.Union[typing.Dict,QuerySet]:returnself.get_object()Т.е. можно использоватьRetrieveModelMixinдля работы с любыми словарями, а не только моделями, надо только
переопределитьViewSet.get_item.ListModelMixinОпределяет обработчик дляlistaction. Определяет методget_collection:classListModelMixin(BaseListModelMixin):deflist(self,request:Request,*args:typing.Any,**kwargs:typing.Any)->Response:queryset=self.get_collection()...defget_collection(self,request_serializer:BaseSerializer)->typing.Union[typing.List,QuerySet]:returnself.filter_queryset(self.get_queryset())Т.е. можно использоватьListModelMixinдля работы с любыми коллекциями, а не только моделями, надо только
переопределитьViewSet.get_collection. При этом, если задан сериализатор дляlist, то он будет использован
для query-параметров, что позволит получить эти параметры и использовать дополнительно к filterset'у.Определяет формирование дополнительнойmetaинформации. Определяет методget_meta_data:classListModelMixin(BaseListModelMixin):defget_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.classListModelMixin(BaseListModelMixin):defperform_list(self,data:typing.Union[typing.List,QuerySet])->None:Sender(data)ListModelViewSetЗадан только обработчик дляlistaction.ReadOnlyModelViewSetЗаданы обработчики дляlistиretrieveaction'ов.CreateUpdateReadModelViewSetЗаданы обработчики дляlist,retrieve,create,updateaction'ов.ModelViewSetПолный набор action'ов:list,retrieve,create,update,destroy.PydanticSerializerДля использования сериализатор на основеpydanticнеобходимо наследовать
сериализатор отPydanticSerializer, указать вMetapydantic_modelиpydantic_use_aliases(при необходимости).Параметрpydantic_use_aliasesпозволяет использоватьалиасы pydantic моделейдля сериализации.classPydanticSerializer(PydanticSerializer):classMeta:pydantic_model=PydanticModelpydantic_use_aliases=TrueГенерация схемыПоддерживается генерация схемы openapi версий 3.0.2 и 3.1.0.
Схема по умолчанию задается параметромAPI_DEFAULT_OPENAPI_VERSIONи равна3.0.2.Пример генерации схемы (версия из settings):python3./manage.pygenerateschema--urlconfapi.v1.urls--generator_classrestdoctor.rest_framework.schema.RefsSchemaGenerator>your_app/static/openapi.schemaПример генерации схемы версии openapi 3.0.2:python3./manage.pygenerateschema--urlconfapi.v1.urls--generator_classrestdoctor.rest_framework.schema.RefsSchemaGenerator30>your_app/static/openapi-30.schemaПример генерации смхемы версии openapi 3.1.0:python3./manage.pygenerateschema--urlconfapi.v1.urls--generator_classrestdoctor.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 с помощью следующей команды:makeinstall-hooks