Django и Pydantic

В этой статье мы рассмотрим, как интегрировать Pydantic с приложением Django, используя Пакеты Djantic и Django Ninja.

pydantic

Pydantic - это пакет на Python для проверки данных и управления настройками, основанный на подсказках типа Python. Он реализует подсказки по вводу данных во время выполнения, предоставляет удобные для пользователя ошибки, допускает пользовательские типы данных и хорошо работает со многими популярными IDE. Кроме того, он чрезвычайно быстр и прост в использовании!

Давайте рассмотрим пример:

from pydantic import BaseModel

class Song(BaseModel):
    id: int
    name: str

Здесь мы определили модель Song с двумя атрибутами, оба из которых обязательны:

  1. id является целым числом
  2. name является строкой

Затем при инициализации происходит проверка:

>>> song = Song(id=1, name='I can almost see you')
>>> song.name
'I can almost see you'

>> Song(id='1')
pydantic.error_wrappers.ValidationError: 1 validation error for Song
name
  field required (type=value_error.missing)

>>> Song(id='foo', name='I can almost see you')
pydantic.error_wrappers.ValidationError: 1 validation error for Song
id
  value is not a valid integer (type=type_error.integer)

Чтобы узнать больше о pydantic, обязательно прочитайте страницу Обзор из официальной документации.

pydantic и Джанго

В сочетании с Django мы можем использовать pydantic для обеспечения того, чтобы в нашем приложении использовались только те данные, которые соответствуют определенным схемам. Итак, мы определим схемы для проверки запросов и ответов, и при возникновении ошибки проверки мы просто вернем приятное для пользователя сообщение об ошибке.

Хотя вы можете интегрировать pydantic с Django без каких-либо сторонних пакетов, мы упростим этот процесс, используя следующие пакеты:

  1. Djantic - добавлена поддержка Pydantic для проверки данных модели
  2. Django Ninja - наряду с Pydantic, этот пакет предоставляет вам ряд дополнительных преимуществ, таких как автоматически генерируемая документация по API (через OpenAPI и Схема JSON), сериализация и управление версиями API

Django Ninja в значительной степени вдохновлен FastAPI. Проверьте это, если вам нравится быстрый API, но при этом вы хотите использовать многое из того, что может предложить Django.

Djantic

Теперь, когда у вас есть общее представление о том, что такое pydantic, давайте рассмотрим практический пример. Мы создадим простой RESTful API, с помощью Django и Django, который позволяет извлекать, список и создавать статьи.

Основные настройки

Начните с создания нового проекта Django:

$ mkdir django-with-pydantic && cd django-with-pydantic
$ python3.11 -m venv env
$ source env/bin/activate

(env)$ pip install django==4.2
(env)$ django-admin startproject core .

После этого создайте новое приложение с именем blog:

(env)$ python manage.py startapp blog

Зарегистрируйте приложение в core/settings.py в разделе INSTALLED_APPS:

# core/settings.py

INSTALLED_APPS = [
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
    'blog.apps.BlogConfig', # new
]

Создание моделей баз данных

Далее давайте создадим модель Article.

Добавьте к следующееblog/models.py:

# blog/models.py

from django.contrib.auth.models import User
from django.db import models


class Article(models.Model):
    author = models.ForeignKey(to=User, on_delete=models.CASCADE)
    created = models.DateTimeField(auto_now_add=True)
    title = models.CharField(max_length=512, unique=True)
    content = models.TextField()

    def __str__(self):
        return f'{self.author.username}: {self.title}'

Создайте, а затем примените изменения:

(env)$ python manage.py makemigrations
(env)$ python manage.py migrate

Зарегистрируйте модель в blog/admin.py , чтобы она была доступна из панели администратора Django:

# blog/admin.py

from django.contrib import admin

from blog.models import Article


admin.site.register(Article)

Установите Dj antic и создайте схемы

Установите pydantic и Djantic:

(env)$ pip install pydantic==1.10.7 djantic==0.7.0

Теперь мы можем определить схему, которая будет использоваться для-

  1. Проверьте поля из полезной нагрузки запроса, а затем используйте эти данные для создания новых объектов модели
  2. Извлекать и проверять объекты модели для объектов-ответов

Создайте новый файл с именем blog/schemas.py:

# blog/schemas.py

from djantic import ModelSchema

from blog.models import Article


class ArticleSchema(ModelSchema):
    class Config:
        model = Article

Это самая простая из возможных схем, которая вытекает из нашей модели.

Модели Django должны быть загружены перед схемами, поэтому схемы должны храниться в отдельном файле, чтобы избежать ошибок при загрузке модели.

С помощью схем вы также можете определить, какие поля следует и не следует включать из конкретной модели, передав exclude или include в Config. Например, чтобы исключить author:

class ArticleSchema(ModelSchema):
    class Config:
        model = Article
        exclude = ['author']

# or

class ArticleSchema(ModelSchema):
    class Config:
        model = Article
        include = ['created', 'title', 'content']

Вы также можете использовать схемы для переопределения свойств модели Django, изменив поля внутри схемы. Например:

class ArticleSchema(ModelSchema):
    title: Optional[str]

    class Config:
        model = Article

Просмотры и URL-адреса

Далее давайте настроим следующие конечные точки:

  1. /blog/articles/create/ создает новую статью
  2. /blog/articles/<ARTICLE_ID>/ извлекает одну статью
  3. /blog/articles/ содержит список всех статей

Добавьте следующие виды в blog/views.py:

# blog/views.py

import json

from django.contrib.auth.models import User
from django.http import JsonResponse
from django.views.decorators.csrf import csrf_exempt
from django.views.decorators.http import require_http_methods

from blog.models import Article
from blog.schemas import ArticleSchema


@csrf_exempt  # testing purposes; you should always pass your CSRF token with your POST requests (+ authentication)
@require_http_methods('POST')
def create_article(request):
    try:
        json_data = json.loads(request.body)

        # fetch the user and pass it to schema
        author = User.objects.get(id=json_data['author'])
        article = Article(
            author=author,
            title=json_data['title'],
            content=json_data['content']
        )
        article.save()
        schema = ArticleSchema.from_django(article)

        return JsonResponse({
            'article': schema.dict()
        })
    except User.DoesNotExist:
        return JsonResponse({'detail': 'Cannot find a user with this id.'}, status=404)


def get_article(request, article_id):
    try:
        article = Article.objects.get(id=article_id)
        schema = ArticleSchema.from_django(article)
        return JsonResponse({
            'article': schema.dict()
        })
    except Article.DoesNotExist:
        return JsonResponse({'detail': 'Cannot find an article with this id.'}, status=404)


def get_all_articles(request):
    articles = Article.objects.all()
    data = []

    for article in articles:
        schema = ArticleSchema.from_django(article)
        data.append(schema.dict())

    return JsonResponse({
        'articles': data
    })

Обратите внимание на области, в которых мы используем схему, ArticleSchema:

  1. ArticleSchema.create() создает новый Article объект
  2. schema.dict() возвращает словарь полей и значений, которые мы передали в JsonResponse
  3. ArticleSchema.from_django() генерирует схему из Article объекта

Помните: и create(), и from_django() также будут проверять соответствие данных схеме.

Добавьте urls.py файл в "блог" и укажите следующие URL-адреса:

# blog/urls.py

from django.urls import path

from blog import views

urlpatterns = [
    path('articles/create/', views.create_article),
    path('articles/<str:article_id>/', views.get_article),
    path('articles/', views.get_all_articles),
]

Теперь давайте зарегистрируем URL-адреса наших приложений в базовом проекте:

# core/urls.py

from django.contrib import admin
from django.shortcuts import render
from django.urls import path, include  # new import


urlpatterns = [
    path('admin/', admin.site.urls),
    path('blog/', include('blog.urls')),  # new
]

Проверка на вменяемость

Для тестирования сначала создайте суперпользователя:

(env)$ python manage.py createsuperuser

Затем запустите сервер разработки:

(env)$ python manage.py runserver

В новом окне терминала добавьте новую статью с помощью cURL:

$ curl --header "Content-Type: application/json" --request POST \
  --data '{"author":"1","title":"Something Interesting", "content":"Really interesting."}' \
  http://localhost:8000/blog/articles/create/

Вы должны увидеть что-то вроде:

{
    "article": {
        "id": 1,
        "author": 1,
        "created": "2021-02-01T20:01:35.904Z",
        "title": "Something Interesting",
        "content": "Really interesting."
    }
}

После этого вы сможете просмотреть статью по ссылкам http://127.0.0.1:8000/blog/articles/1/ и http://127.0.0.1:8000/blog/articles/.

Схема ответа

Хотите удалить поле created из ответов по всем статьям?

Добавьте новую схему в blog/schemas.py:

class ArticleResponseSchema(ModelSchema):
    class Config:
        model = Article
        exclude = ['created']

Затем обновите представление:

def get_all_articles(request):
    articles = Article.objects.all()
    data = []

    for article in articles:
        schema = ArticleResponseSchema.from_django(article)
        data.append(schema.dict())

    return JsonResponse({
        'articles': data
    })

Не забудьте импортировать ArticleResponseSchema:

from blog.schemas import ArticleSchema, ArticleResponseSchema

Проверьте это на http://127.0.0.1:8000/blog/articles/.

Джанго Ниндзя

Django Ninja - это инструмент для создания API-интерфейсов с подсказками типов на основе Django и Python. Как уже упоминалось, он поставляется с рядом мощных функций. Он "быстр в освоении, быстром программировании, быстром запуске".

Основные настройки

Создаем новый проект на Django:

$ mkdir django-with-ninja && cd django-with-ninja
$ python3.11 -m venv env
$ source env/bin/activate

(env)$ pip install django==4.2
(env)$ django-admin startproject core .

Создайте новое приложение под названием blog:

(env)$ python manage.py startapp blog

Зарегистрируйте приложение в core/settings.py в разделе INSTALLED_APPS:

# core/settings.py

INSTALLED_APPS = [
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
    'blog.apps.BlogConfig', # new
]

Создание моделей баз данных

Затем добавьте Article модель в blog/models.py:

# blog/models.py

from django.contrib.auth.models import User
from django.db import models


class Article(models.Model):
    author = models.ForeignKey(to=User, on_delete=models.CASCADE)
    created = models.DateTimeField(auto_now_add=True)
    title = models.CharField(max_length=512, unique=True)
    content = models.TextField()

    def __str__(self):
        return f'{self.author.username}: {self.title}'

Создайте и примените миграции:

(env)$ python manage.py makemigrations
(env)$ python manage.py migrate

Зарегистрируйте модель в blog/admin.py:

# blog/admin.py

from django.contrib import admin
from blog.models import Article


admin.site.register(Article)

Установите Django Ninja и создайте схемы

Установка:

(env)$ pip install django-ninja==0.21.0

Как и в случае с Dj antic, вам необходимо создавать схемы для проверки ваших запросов и ответов. Django Ninja обеспечивает гибкость при создании схем, используя как автоматическую генерацию схем, так и ручную генерацию схем. Автоматическая генерация схемы позволяет вам автоматически создавать схему на основе ваших моделей Django, в то время как ручная генерация схемы дает вам больше контроля над схемой.

Чтобы создать схему для модели User с помощью автоматической генерации схемы, добавьте следующее в blog/schemas.py:

from django.contrib.auth.models import User
from ninja import ModelSchema


class UserSchema(ModelSchema):
    class Config:
        model = User
        fields = ['id', 'username']

Для получения дополнительной информации о поддержке автоматической генерации схем ознакомьтесь с Схемами из Django models.

Чтобы создать остальные схемы вручную, добавьте следующее в blog/schemas.py:

from datetime import datetime  # new line

from django.contrib.auth.models import User
from ninja import ModelSchema, Schema  # updated line


# Auto-schema generation for UserSchema
class UserSchema(ModelSchema):
    class Config:
        model = User
        model_fields = ['id', 'username']


# Manual schema generation for ArticleIn and ArticleOut
class ArticleIn(Schema):  # new schema
    author: int
    title: str
    content: str


class ArticleOut(Schema):  # new schema
    id: int
    author: UserSchema
    created: datetime
    title: str
    content: str

Теперь у нас есть три разные схемы:

  1. UserSchema проверяет и преобразует данные в пользовательскую модель Django и из нее
  2. ArticleIn проверяет и десериализует данные, передаваемые в API для создания статей
  3. ArticleOut проверяет и сериализует данные из модели Article

В Django Ninja есть концепция маршрутизатора, который используется для разделения API на несколько модулей.

Создать blog/api.py файл:

# blog/api.py

from typing import List

from django.contrib.auth.models import User
from django.shortcuts import get_object_or_404
from ninja import Router

from blog.models import Article
from blog.schemas import ArticleOut, ArticleIn

router = Router()


@router.post('/articles/create')
def create_article(request, payload: ArticleIn):
    data = payload.dict()
    try:
        author = User.objects.get(id=data['author'])
        del data['author']
        article = Article.objects.create(author=author, **data)
        return {
            'detail': 'Article has been successfully created.',
            'id': article.id,
        }
    except User.DoesNotExist:
        return {'detail': 'The specific user cannot be found.'}


@router.get('/articles/{article_id}', response=ArticleOut)
def get_article(request, article_id: int):
    article = get_object_or_404(Article, id=article_id)
    return article


@router.get('/articles', response=List[ArticleOut])
def get_articles(request):
    articles = Article.objects.all()
    return articles

Здесь мы создали три функции, которые служат нашими представлениями. Django Ninja использует декораторы HTTP-операций, в которых вы определяете структуру URL-адреса, параметры пути и необязательный запрос и ответ схемы.

Примечания:

  1. get_article использует ArticleOut для своей схемы ответа. ArticleOut, таким образом, будет использоваться для автоматической проверки и сериализации данных из модели.
  2. В get_articles набор запросов Django, например, articles = Article.objects.all(), будет правильно проверен с помощью List[ArticleOut].

Регистрация конечных точек API

Последнее, что нам нужно сделать, это создать новый экземпляр NinjaAPI и зарегистрировать наш API-маршрутизатор в core/urls.py:

# core/urls.py

from django.contrib import admin
from django.urls import path
from ninja import NinjaAPI
from blog.api import router as blog_router

api = NinjaAPI()
api.add_router('/blog/', blog_router)

urlpatterns = [
    path('admin/', admin.site.urls),
    path('api/', api.urls),
]

После этого Django Ninja автоматически создаст следующие конечные точки:

  1. /blog/articles/create/ создает новую статью
  2. /blog/articles/<ARTICLE_ID>/ извлекает одну статью
  3. /blog/articles/ содержит список всех статей

Проверка на вменяемость

Создайте суперпользователя, а затем запустите сервер разработки:

(env)$ python manage.py createsuperuser
(env)$ python manage.py runserver

Перейдите к http://localhost:8000/api/docs , чтобы просмотреть автоматически созданную интерактивную документацию по API:

api docs

Здесь вы можете просматривать зарегистрированные конечные точки и взаимодействовать с ними.

Попробуйте добавить новую статью:

api docs

Попробуйте остальные конечные точки самостоятельно.

Заключение

В этой статье мы сначала рассмотрели, что такое pydantic, а затем рассмотрели, как интегрировать его с приложением Django с помощью Djantic и Django Ninja.

Полный код вы можете найти на GitHub:

  1. django-with-pydantic
  2. django-with-ninja
Вернуться на верх