Authentication
Intro
Django Ninja provides several tools to help you deal with authentication and authorization easily, rapidly, in a standard way, and without having to study and learn all the security specifications.
The core concept is that when you describe an API operation, you can define an authentication object.
In this example, the client will only be able to call the pets method if it uses Django session authentication (the default is cookie based), otherwise an HTTP-401 error will be returned.
Automatic OpenAPI schema
Here’s an example where the client, in order to authenticate, needs to pass a header:
Authorization: Bearer supersecret
Now, when you click the Authorize button, you will get a prompt to input your authentication token.
When you do test calls, the Authorization header will be passed for every request.
Global authentication
In case you need to secure all methods of your API, you can pass the auth argument to the NinjaAPI constructor:
And, if you need to overrule some of those methods, you can do that on the operation level again by passing the auth argument. In this example, authentication will be disabled for the /token operation:
Available auth options
Custom function
The » auth= » argument accepts any Callable object. NinjaAPI passes authentication only if the callable object returns a value that can be converted to boolean True. This return value will be assigned to the request.auth attribute.
API Key
Some API’s use API keys for authorization. An API key is a token that a client provides when making API calls to identify itself. The key can be sent in the query string:
or as a request header:
Django Ninja comes with built-in classes to help you handle these cases.
in Query
In this example we take a token from GET[‘api_key’] and find a Client in the database that corresponds to this key. The Client instance will be set to the request.auth attribute.
Note: param_name is the name of the GET parameter that will be checked for. If not set, the default of » key » will be used.
in Header
in Cookie
HTTP Bearer
HTTP Basic Auth
Multiple authenticators
The auth argument also allows you to pass multiple authenticators:
Router authentication
Use auth argument on Router to apply authenticator to all operations declared in it:
or using router constructor
Custom exceptions
Raising an exception that has an exception handler will return the response from that handler in the same way an operation would:
Operations parameters
You can group your API operations using the tags argument ( list[str] ).
Tagged operations may be handled differently by various tools and libraries. For example, the Swagger UI uses tags to group the displayed operations.
Router tags
You can use tags argument to apply tags to all operations declared by router:
Operation: Summary
Summary is a human-readable name for your operation.
By default, it’s generated by capitalizing your operation function name:
If you want to override it or translate it to other language, use the summary argument in the api decorator.
Operation: Description
If you need to provide more information about your operation, use either the description argument or normal Python docstrings:
When you need to provide a long multi line description, you can use Python docstrings for the function definition:
OpenAPI operationId
The OpenAPI operationId is an optional unique string used to identify an operation. If provided, these IDs must be unique among all operations described in your API.
If you want to set it individually for each operation, use the operation_id argument:
If you want to override global behavior, you can inherit the NinjaAPI instance and override the get_openapi_operation_id method.
It will be called for each operation that you defined, so you can set your custom naming logic like this:
Operation: Deprecated
If you need to mark an operation as deprecated without removing it, use the deprecated argument:
It will be marked as deprecated in the JSON Schema and also in the interactive OpenAPI docs:
Response output options
There are a few arguments that lets you tune response’s output:
by_alias
Whether field aliases should be used as keys in the response (defaults to False ).
exclude_unset
Whether fields that were not set when creating the schema, and have their default values, should be excluded from the response (defaults to False ).
exclude_defaults
Whether fields which are equal to their default values (whether set or otherwise) should be excluded from the response (defaults to False ).
exclude_none
Whether fields which are equal to None should be excluded from the response (defaults to False ).
Include/Exclude operation from schema(docs)
If you need to exclude some operation from OpenAPI schema use include_in_schema argument:
url_name
Allows you to set api endpoint url name (using django path’s naming)
Routers
Real world applications can almost never fit all logic into a single file.
Django Ninja comes with an easy way to split your API into multiple modules using Routers.
Let’s say you have a Django project with a structure like this:
To add API’s to each of the Django applications, create an api.py module in each app:
Then do the same for the news app with news/api.py :
Finally, let’s group them together. In your top level project folder (next to urls.py ), create another api.py file with the main NinjaAPI instance:
It should look like this:
Now we import all the routers from the various apps, and include them into the main API instance:
Router authentication
Use auth argument to apply authenticator to all operations declared by router:
or using router constructor
Router tags
You can use tags argument to apply tags to all operations declared by router:
or using router constructor
Nested routers
There are also times when you need to split your logic up even more. Django Ninja makes it possible to include a router into another router as many times as you like, and finally include the top level router into the main api instance.
Basically, what that means is that you have add_router both on the api instance and on the router instance:
Now you have the following endpoints:
Great! Now go have a look at the automatically generated docs:
Архитектура в Django проектах — как выжить
Думаю, ни для кого не секрет, что в разговорах опытных разработчиков Python, и не только, часто проскальзывают фразы о том, что Django это зло, что в Django плохая архитектура и на ней невозможно написать большой проект без боли. Часто даже средний Django проект сложно поддерживать и расширять. Предлагаю разобраться, почему так происходит и что с Django проектами не так.
Немного теории
Когда мы начинаем изучать Django без опыта из других языков и фреймворков, помимо документации мы читаем туториалы, статьи, книги, и почти во всех видим что-то подобное:
Django — это фреймворк, использующий шаблон проектирования Model-View-Controller (MVC).
И дальше куча неточных схем и объяснений о том, что такое MVC. Почему они неточные и что с ними не так, можно посмотреть здесь или здесь.
Обычно в таких схемах MVC описывают подобным образом:
Model — доступ к хранилищу данных
View — это интерфейс, с которым взаимодействует пользователь
Controller — некий связывающий объект между model и view.
Данные распространенные схемы только запутывают и мешают, когда вы хотите написать приложение, в котором есть бизнес-логика.
Стоит обратить внимание на две вещи.
Первое, часто под M в MVC подразумевают — модель данных, и говорят, что это некий класс, который отвечает за предоставление доступа к базе данных. Что неверно, и не соответствует классическому MVC и его потомкам MV*. В классическом MVC под M подразумевается domain model — объектная модель домена, объединяющая данные и поведение. Если говорить точнее, то M в MVC это интерфейс к доменной модели, так как domain model это некий слой объектов, описывающий различные стороны определенной области бизнеса. Где одни объекты призваны имитировать элементы данных, которыми оперируют в этой области, а другие должны формализовать те или иные бизнес-правила.
Второе, в Django нет выделенного слоя controller, и когда вам говорят, что в Django слой views — это контроллер, не верьте этим людям. Обратитесь к официальной документации, а точнее к FAQ, тогда можно увидеть, что этот слой вписывается в принципы слоя View в MVC, особенно, если рассматривать DRF, а как такового слоя Controller в Django нет. Как говорится в FAQ, если вам очень хочется аббревиатур, то можно использовать в контексте Django аббревиатуру MTV (Model, Template, and View). Если очень хочется рассматривать Web MVC и сравнивать Django с другими фреймворками, то для простоты можно считать view контроллером.
Несмотря на то, что Django не соответствует MVC аббревиатуре, в ней реализуется главный смысл MVC — отделение бизнес-логики от логики представления данных. Но на практике это не всегда так по нескольким причинам, которые мы рассмотрим ниже.
Перейдем к практике
Выделим в Django приложениях несколько слоев, которые есть в каждом туториале и почти в каждом проекте:
Не будем рассматривать каждый слой подробно, это все можно найти в документации. В основном будем рассматривать Django c использованием DRF. Попробуем разобрать на двух простых кейсах, что стоит помещать в каждом из слоев и какая ответственность у каждого слоя.
Первый кейс — создание заказа. При создании заказа нам нужно:
проверить валидность заказа и доступность товаров
зарезервировать товар на складе
передать заявку менеджеру
оповестить пользователя о том, что его заказ принят в работу
Второй кейс — просмотр списка моих заказов. Здесь все просто, мы должны показать пользователю список его заказов:
получить список заказов пользователя
Слой serializers/forms
У слоя serializers три основные функции (все выводы для serializers справедливы и для forms):
преобразовывать данные запроса в типы данных Python
преобразовывать сложные Python объекты в простые типы данных Python (например, Django модели в dict)
Дополнительно сериалайзеры имеют два метода, create и update, которые вызываются в методе save() и почти всегда используются во view.
Пример использования из документации:
Где-то в нашей view:
В данном подходе за сохранение и обновление сущностей отвечает сериалайзер, точнее он оперирует методами модели.
Можно использовать ModelSerializer и ModelViewSet, что позволяет писать CRUD методы в пару-тройку строк.
Если углубиться в реализацию ModelViewSet и ModelSerializer, то можно заметить, что за сохранение и обновление сущностей также отвечает сериалайзер.
В таком случае, кажется, что переопределение create отличное место для того, чтобы описать там все бизнес-процессы и правила создания заказа.
Если с методом создания мы что-то придумали, то логику получения заказов пользователя придется помещать в view.
Получается такая схема:
Плюсы данного подхода:
Легко делать CRUD
Django и DRF предоставляют очень удобные инструменты с помощью которых можно легко создавать CRUD.
Минусы данного подхода:
Нарушение идей MVC
Мы смешиваем бизнес-логику с задачей сериализации (получения/отображения) данных в одном слое. Ни о каком выделенном слое бизнес-логики у нас нет и речи.
Сериалайзеры стоит относить к слою View в MVC в контексте Django. Когда мы располагаем в сериалайзерах свою бизнес логику, мы нарушаем главный принцип MVC — отделение логики представления данных от бизнес-логики.
Нельзя переиспользовать
Не получится переиспользовать логику одного сериалайзера в другом сериалайзере, или в каком-то другом компоненте.
Сложно тестировать
Сложно протестировать бизнес-логику независимо от логики сериализации и валидации данных.
Сложно поддерживать
Остается открытым вопрос — куда помещать методы на чтение данных, можно перенести их в слой views.Не все правила бизнес-логики можно поместить в сериалайзеры, иногда это излишне, потому что мы не используем сами сериалайзеры.
В итоге мы получаем разбросанные бизнес-правила по всему проекту, которые невозможно переиспользовать и очень сложно поддерживать.
Высокая зависимость от фреймворка
Высокая зависимость от DRF Serializers или Django Forms. Если мы захотим поменять способ сериализации и отказаться от serializers, то придется переносить или переписывать нашу логику. Также будет сложно переехать с Django Forms на DRF Serializers или наоборот.
Правильные обязанности слоя
Сериализация/десериализация данных
Сериалайзер хорошо умеет сериализовывать данные, для этого его и нужно использовать.
Валидация данных
Без написания кастомных валидаторов. Если вам требуется написать кастомный валидатор, то, скорее всего, это бизнес-правило, и данную проверку лучше вынести в слой с бизнес-логикой (где бы он ни был).
Заключение
Сериалайзеры точно не подходят для написания в них бизнес-логики. Если вам нужно что-то большее, чем CRUD, то стоит отказаться от использования метода save у сериалайзеров, так как сохранение данных не должно входить в их обязанности.
Стоит отказаться от ModelSerializer с его магическими методами create и update и заменить их на обычные сериалайзеры (можно использовать ModelSerializer как read only — для удобства). Если вы пишете какое-то простое приложение, где кроме CRUD ничего не нужно, то можно не отказываться от удобства DRF и использовать сериалайзеры как предлагается в документации.
Слой Views
Слой View в контексте Django отвечает за представление и обработку пользовательских данных, в нем мы описываем, какие данные нам нужны и как мы хотим их представить. Если вспомнить то, о чем мы говорили в начале, то можно сразу сделать вывод, что во views не нужно писать бизнес-логику, иначе мы смешиваем логику представления данных с бизнес-логикой. Даже если считать, что views в Django это контроллеры, то размещать в них бизнес-логику тоже не стоит, иначе у вас получатся ТТУКи («Толстые, тупые, уродливые контроллеры»; Fat Stupid Ugly Controllers).
Но часто можно увидеть что-то подобное:
По дефолту, в ModelViewSet для сохранения и обновления данных используется сериалайзер, что не входит в его обязанности. Это можно исправить, полностью переопределить метод perform_create (не вызывать super, но тогда встает вопрос об объективности наследования от ModelViewSet). Можно написать кастомные методы в ModelViewSet или написать кастомные APIView:
В отличии от слоя serializers, мы теперь легко можем поместить нашу логику получения заказов в слой views.
Тем самым, ограничив не только получение списка, но и другие методы CRUD, что, иногда, очень удобно и быстро. Также, можно переопределить каждый метод по отдельности.
Получается такая схема:
Стоит помнить, что мы отказались от использования save у serializers. В таком случае слой serializers остается “чистым” и выполняет только “правильные” обязанности.
Плюсы данного подхода
Легко делать CRUD
Django и DRF предоставляют очень удобные инструменты, с помощью которых можно легко создавать CRUD и views не исключение.
Минусы данного подхода
Нарушение идей MVC
Мы смешиваем в одном слое логику представления данных и бизнес-логику приложения.
Нельзя переиспользовать
Не получится переиспользовать логику одной view в другой view, или в каком-то другом компоненте. Так же будут проблемы, если мы захотим вызывать нашу бизнес-логику из других интерфейсов, например, из Celery задач.
Высокая зависимость от фреймворка
Высокая зависимость от DRF View или Django View. Если мы захотим поменять способ обработки запроса и отказаться от views, то придется переносить или переписывать нашу логику. Будет сложно переехать с Django View на DRF View или наоборот.
Сложно тестировать
Достаточно сложно протестировать код во views независимо от serializers и остальной инфраструктуры Django + придется использовать http client для тестирования.
Сложно поддерживать
Со временем views разрастаются, часть логики переносится в Celery задачи, часть в модели, код во views дублируется, так как их нельзя переиспользовать — все это приводит к тому, что проект сложно поддерживать.
Правильные обязанности слоя
Обработка запроса
Во view мы принимаем и обрабатываем запрос клиента, подготавливаем данные для передачи в бизнес-логику.
Делегирование сериализации данных сериалайзерам
Всю логику сериализации данных должны выполнять сериалайзеры.
Вызов методов бизнес-логики
После подготовки и сериализации данных вызывается интерфейс бизнес-логики.
Логика представления данных
Мы должны обработать ответ от методов бизнес-логики и предоставить нужные данные клиенту.
Заключение
Вывод примерно такой же как и с serializers — в views не стоит размещать бизнес-логику.
Стоит отказаться от ModelViewSet и миксинов, так как они используют сериалайзеры для сохранения данных, вместо этого использовать обычные APIView или GenericAPIView.
Если вам нужен только CRUD, то можно использовать подход который предоставляет ModelViewSet и не усложнять себе жизнь.
Слой models
Если опираться на более продвинутые туториалы или вспомнить слой Model в MVC, то кажется, что models отличное место для размещения бизнес-логики.
Это может выглядеть примерно так:
В view мы сериализуем данные с помощью serializer и вызываем метод создания заказа у класса модели. В данном случае мы реализовали classmethod, что бы не было необходимости создавать экземпляр модели. Иначе нам придется понимать какие данные относятся к полям модели, а какие мы должны передать в метод создания, а это уже некие бизнес- правила.
Стоит заметить, что не нужно писать бизнес-логику в методе save(), так как это базовый метод модели и он может неоднократно использоваться в различных частях кода.
Для методов получения данных в таком случае стоит использовать Managers.
Получается такая схема:
В данном случае слои serializers и views становятся “чистыми” и правила бизнес-логики концентрируются в одном слое.
Плюсы данного подхода
Следование идеям MVC
Мы отделили логику представления данных от логики предметной области. View только подготавливает данные и вызывает методы модели, все бизнес правила и процессы описаны в методах модели. Данный подход соответствует главной идее MVC.
Легко тестировать
Вся бизнес-логика собрана в одном слое, который не зависит от других слоев, например, от views или serializers. Каждый метод модели можно протестировать по отдельности как обычный python код. Остается только замокать метод save и базовые методы managers или использовать базу данных, если требуется.
Можно переиспользовать
Методы модели можно вызывать из любого компонента, DRF Views, Django Views, Celery задачи и т.д.
Минусы данного подхода
Зависимость от фреймворка
У нас все еще есть зависимость от фреймворка, но это не так критично. Так как отказ от Django models и ORM или их замена — очень редкий кейс.
Сложно поддерживать большие проекты
В больших проектах много бизнес-правил и если все их описывать в одном классе модели, то модель разрастается и превращается в божественный объект, который сложно читать и поддерживать. Сложно масштабировать и разделять код по файлам, так как мы ограничены требованиями фреймворка. Непонятно, куда помещать методы, которые оперируют несколькими моделями, возможно, из разных модулей.
Усложнение CRUD проектов
Если вам нужен только CRUD, то данный подход увеличивает время разработки и не приносит плюсов.
Заключение
Мы не нарушили главных идей MVC и наш код соответствует им. Такой подход можно использовать в малых проектах, когда бизнес-логики не много и она умещается в классах моделей.
Слой Services
Мы перебрали все дефолтные слои в Django приложении, теперь можем вспомнить о том, что под слоем Model в MVC подразумевается не один объект, а набор объектов.
Выделим отдельный сервисный слой services внутри слоя Model, который будет отвечать за бизнес-правила предметной области и приложения. В models оставить только простые property, в которых нет сложных бизнес-правил, и методы для работы с собственными данными модели, например обновление полей. Тогда наши кейсы можно реализовать так:
Стоит придерживаться следующему подходу:
views — подготовка данных запроса, вызов бизнес логики, подготовка ответа
serializers — сериализация данных, простая валидация
services — простые функции с бизнес правилами или классы (Service Objects)
managers — содержит в себе правила работы с данными (доступ к данным)
models — единственный окончательный источник правды о данных
Получение заказов пользователя:
Получается такая схема:
Плюсы данного подхода
Следование идеям MVC
Как и в предыдущем подходе, мы полностью отделили бизнес-логику от логики представления.
Легко тестировать
Сервисы представляют собой простые Python функции, которые легко тестировать.
Можно переиспользовать
Мы можем вызывать наши сервисы из любого компонента + можем повторно использовать какие-то сервисы в других проектах.
Легко поддерживать и расширять
В данном подходе выделен отдельный слой под бизнес-логику и логику приложения, при росте проекта сервисы можно декомпозировать и расширять.
Гибкость
Существует множество подходов написания и расширения сервисного слоя.
Минусы данного подхода
Зависимость от фреймворка
Доменный слой не отделен от слоя приложения и инфраструктуры. Мы все еще используем Django модели в качестве сущностей. У нас могут возникнуть проблемы, когда мы захотим отказаться от Django ORM, но это очень редкий кейс и для многих проектов неактуален.
Усложнение CRUD проектов
Если вам нужен только CRUD, то данный подход увеличивает время разработки и не приносит плюсов.
Заключение
Данный подход удобно использовать в проектах различной сложности и размера. В проекте понятна структура, каждый слой и компонент имеет свою ответственность и не нарушает ее границы. Сервисы легко декомпозировать и отделять друг от друга, в более сложных случаях их можно объединять за фасадами.
На самом деле, каким может быть сервисный слой и как его лучше выделять и разделять это тема отдельной статьи и даже книги, об этом много пишут Мартин Фаулер, Роберт Мартин и другие.
Что касается Django, советую обратить внимание на стайл гайд от HackSoftware у них схожие взгляды, но они разделяют сервисный слой на два компонента (services и selectors) и не используют кастомные методы в managers. Подход написания serializers и включения их во views я взял у них. Также стоит посмотреть на идеи ребят из dry-python.
Общий итог
Получается, что поддерживаемость и “чистота” Django проектов страдает от удобства и плюшек фреймворка. Django и DRF очень классные инструменты, но не все их возможности стоит использовать. Можно сделать вывод, что, чем больше ваш проект и чем сложнее в нем бизнес-правила и сущности, тем более абстрактным и независимым от фреймворка должен быть ваш код. И выделение сервисного слоя — это далеко не предел и не идеал архитектуры приложения.












