Разработка RESTful-интерфейсов при помощи Python, Django и Django REST Framework

Эта статья является исчерпывающим руководством для начинающих программистов, желающих разрабатывать проекты с RESTful интерфейсами, используя при этом Python, Django и Django Rest Framework.

Введение

  • Django — это веб-фреймворк, написанный на Python.
  • Python — это высокоуровневый интерпретируемый язык программирования общего назначения.
  • API (Application Programming Interface, интерфейс приложения) — это набор правил и механизмов, посредством которых приложение или его части взаимодействуют с другими приложениями.
  • REST (Representational State Transfer) — это программная архитектура интерфейсов REST.

Как сказано в диссертации Роя Филдинга: «REST — это архитектурный стиль, который в основном использует существующие технологии и протоколы интернета. Попросту говоря, это представление данных для клиента в подходящем для него формате.»

Следовательно, RESTful + API (интерфейс) является широко используемой терминологией для реализации такой архитектуры и связанных с ней ограничений (например, в веб-сервисах).

Вот пример запроса GET из интерфейса Github:

$ curl https://api.github.com/users/joshuadeguzman

Вы получите примерно следующее:

{
  "login": "joshuadeguzman",
  "id": 20706361,
  "node_id": "MDQ6VXNlcjIwNzA2MzYx",
  "avatar_url": "https://avatars1.githubusercontent.com/u/20706361?v=4",
  "gravatar_id": "",
  "url": "https://api.github.com/users/joshuadeguzman",
  "html_url": "https://github.com/joshuadeguzman",
  "followers_url": "https://api.github.com/users/joshuadeguzman/followers",
  "following_url": "https://api.github.com/users/joshuadeguzman/following{/other_user}",
  "gists_url": "https://api.github.com/users/joshuadeguzman/gists{/gist_id}",
  "starred_url": "https://api.github.com/users/joshuadeguzman/starred{/owner}{/repo}",
  "subscriptions_url": "https://api.github.com/users/joshuadeguzman/subscriptions",
  "organizations_url": "https://api.github.com/users/joshuadeguzman/orgs",
  "repos_url": "https://api.github.com/users/joshuadeguzman/repos",
  "events_url": "https://api.github.com/users/joshuadeguzman/events{/privacy}",
  "received_events_url": "https://api.github.com/users/joshuadeguzman/received_events",
  "type": "User",
  "site_admin": false,
  "name": "Joshua de Guzman",
  "company": "@freelancer",
  "blog": "https://joshuadeguzman.me",
  "location": "Manila, PH",
  "email": null,
  "hireable": true,
  "bio": "Android Engineer at @freelancer. Building tools for humans.",
  "public_repos": 75,
  "public_gists": 2,
  "followers": 38,
  "following": 10,
  "created_at": "2016-07-28T15:19:54Z",
  "updated_at": "2019-06-16T10:26:39Z"
}

Получен набор данных в формате JSON.

JSON ( JavaScript Object Notation) — это открытый файловый формат, в котором обычный текст используется для передачи объектов данных, состоящих из пар атрибут-значение и типов данных массива. Конечно, существуют и другие форматы, такие как XML, INI, CSV и так далее. Но на сегодняшний день, JSON, благодаря своей интуитивной и понятной структуре, применяется крайне широко вне зависимости от того, какой язык программирования используется.

Python и Django

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

В языке Python используется английское представление слов (например, для методов, резервных ключевых слов и управляющих команд), что позволяет любому новичку легко начать программировать. Система типов в Python динамическая: он проверяет типы данных во время выполнения программы. Python также осуществляет автоматическое управление памятью.

print(5 + 5) # This will result to 10

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

Его основными чертами являются дизайн для быстрой разработки, безопасность и масштабируемость.

Краткий обзор Django

В Django стандартным способом для внесения изменений в базу данных является использование модулей миграции.

Возьмем пользовательскую модель User:

from django.db import models

class User(models.Model):
    first_name = models.CharField(max_length=50)
    middle_name = models.CharField(max_length=50)
    last_name = models.CharField(max_length=50)

    def __str__(self):
        return self.name

Если в вашу модель были внесены какие-либо изменения, запустите модуль makemigrations.

$ python manage.py makemigrations

Затем вы можете синхронизировать базу данных с набором моделей и миграций.

$ python manage.py migrate

Создание REST интерфейсов при помощи Django Rest Framework

Django REST Framework (DRF) — это мощный и гибкий инструмент для создания веб-интерфейсов (web APIs). Он дает возможность разработчикам не изобретать колесо, самостоятельно создавая сложный и громоздкий REST-интерфейс с нуля. Чем сложнее будут становиться ваши проекты, тем лучше вы будете осознавать необходимость использования DRF или других полезных REST-фреймворков.

1. Инсталляция и настройка проекта

Создаем директорию проекта

$ mkdir djangoapi

Устанавливаем пакет virtualenv при помощи pip.

Виртуальное окружение позволяет проекту иметь свои дополнительные библиотеки, а также обновлять пакеты внутри окружения, не затрагивая глобальное окружение или другие проекты.

pip это менеджер пакетов. С его помощью можно устанавливать пакеты, написанные на языке Python, и далее управлять ими.

$ pip install virtualenv

Создаем папку виртуального окружения в директории нашего проекта.

$ cd djangoapi
$ virtualenv venv

Активируем виртуальное окружение.

$ source venv/bin/activate

Чтобы отменить изменения, просто запустите команду deactivate. Подробнее об этом — в документации virtualenv.

Устанавливаем библиотеки djangodjangorestframework.

$ pip install django
$ pip install djangorestframework

Создаем наш проект в django.

$ django-admin startproject blog

Запускаем наш проект.

$ python manage.py runserver

System check identified no issues (0 silenced).

You have 15 unapplied migration(s). Your project may not work properly until you apply the migrations for app(s): admin, auth, contenttypes, sessions.
Run 'python manage.py migrate' to apply them.

August 16, 2018 - 09:58:36
Django version 2.1, using settings 'blog.settings'
Starting development server at http://127.0.0.1:8000/
Quit the server with CONTROL-C.

Непримененными миграциями (unapplied migration(s)) являются файлы, включенные при создании проекта django.

Чтобы их синхронизировать, просто запустите команду migrate.

$ python manage.py migrate

Running migrations:
  Applying contenttypes.0001_initial... OK
  Applying auth.0001_initial... OK
  Applying admin.0001_initial... OK
  Applying admin.0002_logentry_remove_auto_add... OK
  Applying admin.0003_logentry_add_action_flag_choices... OK
  Applying contenttypes.0002_remove_content_type_name... OK
  Applying auth.0002_alter_permission_name_max_length... OK
  Applying auth.0003_alter_user_email_max_length... OK
  Applying auth.0004_alter_user_username_opts... OK
  Applying auth.0005_alter_user_last_login_null... OK
  Applying auth.0006_require_contenttypes_0002... OK
  Applying auth.0007_alter_validators_add_error_messages... OK
  Applying auth.0008_alter_user_username_max_length... OK
  Applying auth.0009_alter_user_last_name_max_length... OK
  Applying sessions.0001_initial... OK

В качестве базы данных нашего проекта установлена SQLite база данных под названием db.sqlite3.

Создаем приложение в нашем проекте.

$ cd blog
$ python manage.py startapp posts

Структура проекта на данный момент должна иметь следующий вид:

$ find .
./posts
./posts/migrations
./posts/migrations/__init__.py
./posts/models.py
./posts/__init__.py
./posts/apps.py
./posts/admin.py
./posts/tests.py
./posts/views.py
./db.sqlite3
./blog
./blog/__init__.py
./blog/__pycache__
./blog/__pycache__/settings.cpython-36.pyc
./blog/__pycache__/wsgi.cpython-36.pyc
./blog/__pycache__/__init__.cpython-36.pyc
./blog/__pycache__/urls.cpython-36.pyc
./blog/settings.py
./blog/urls.py
./blog/wsgi.py
./manage.py

2. Модель

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

# djangoapi/blog/posts/models.py
from django.db import models

# Создаем здесь нашу модель.

class Post(models.Model):
    title = models.CharField(max_length=255)
    content = models.TextField()
    is_featured = models.BooleanField(default=False)

    def __str__(self):
        return self.name

Метод __str__ вызывается при помощи встроенной функции str(), а также при выполнении инструкции print() для вычисления неформального строкового представления объекта.

Если вы сейчас запустите команду makemigrations, Django пока никаких изменений не заметит.

$ No changes detected

Чтобы решить эту проблему, нужно в файле setting.py добавить наше приложение post в список установленных приложений.

# djangoapi/blog/blog/settings.py

INSTALLED_APPS = [
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
    'posts' # Add it here
]

Теперь можно применить миграцию.

$ python manage.py makemigrations

Migrations for 'posts':
  posts/migrations/0001_initial.py
    - Create model Post

$ python manage.py migrate

Operations to perform:
  Apply all migrations: admin, auth, contenttypes, posts, sessions
Running migrations:
  Applying posts.0001_initial... OK

3. Сериализация

Сериализаторы позволяют переводить структуру данных или объекта в формат, который можно сохранить или передать, а впоследствии восстановить обратно.

Создадим файлы serializers.py и views.py и разместим их следующим образом:

# posts/api
posts/api/serializers.py
posts/api/views.py

# posts/migrations
posts/migrations/

# posts
posts/admin.py
posts/apps.py
posts/models.py
posts/tests.py
posts/views.py
# posts/api/serializers.py

from ..models import Post
from rest_framework import serializers

class PostSerializer(serializers.ModelSerializer):
    class Meta:
        model = Post
        fields = ('title', 'content', 'is_featured') # if not declared, all fields of the model will be shown

В нашем руководстве мы используем класс ModelSerializer, более подробно о нем можно прочитать в документации.

4. Представления (views)

Функция представления, или для краткости просто представление, является функцией Python. Она принимает на вход веб-запрос и возвращает ответ.

# posts/api/views.py

from ..models import Post
from . import serializers
from rest_framework import generics, status
from rest_framework.response import Response

class PostListView(generics.ListAPIView):
    queryset = Post.objects.all()
    serializer_class = serializers.PostSerializer

Как видно из данного кода, ListAPIView используется только для чтения, чтобы представлять коллекцию экземпляров модели.

В данном отрывке кода мы использовали методы представлений из класса generics, который находится в модуле rest_framework. Более подробно о них можно прочитать в документации.

5. URLs

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

# posts/urls.py

from django.urls import path
from . import views
from .api import views

urlpatterns = [
    path('', views.PostListView.as_view(), name=None)
]

6. Завершаем настройку

Давайте убедимся, что модуль rest_framework добавлен в список приложений нашего проекта.

# djangoapi/blog/blog/settings.py

INSTALLED_APPS = [
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
    'rest_framework', # Add it here
    'posts'
]

7. Админка Django

Так как мы пока не настроили наши POST-запросы, будем пополнять нашу базу данных через панель администратора Django.

Для этого создадим аккаунт admin с паролем 1234password и правами супеюзера.

$ python manage.py createsuperuser --email admin@example.com --username admin

Password:
Password (again):
This password is too common.
Bypass password validation and create user anyway? [y/N]: y
Superuser created successfully.

Теперь зарегистрируем модель Post в панели администратора.

# posts/admin.py

from django.contrib import admin
from .models import Post

# Регистрируйте модели здесь.
admin.site.register(Post)

Вот и все. Теперь зайдите в панель администратора и обновите записи в модели Post. Более подробно об этом можно прочитать в документации.

8. Тестируем наш интерфейс (API)

$ python manage.py runserver
GET /api/v1/posts/
HTTP 200 OK
Allow: GET, HEAD, OPTIONS
Content-Type: application/json
Vary: Accept

[
    {
        "title": "Example Post #1",
        "content": "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.",
        "is_featured": false
    },
    {
        "title": "Example Post #2",
        "content": "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.",
        "is_featured": true
    }
]

Великолепно! Теперь настало время обновить наши представления и на этом закончить со стандартными CRUD-операциями.

9. Добавляем новые представления (views)

POST — это метод, используемый для создания новых ресурсов в нашей базе данных либо для обновления старых.

# posts/api/views.py

from ..models import Post
from . import serializers
from rest_framework import generics, status
from rest_framework.response import Response

class PostCreateView(generics.CreateAPIView):
    queryset = Post.objects.all()
    serializer_class = serializers.PostSerializer

    def create(self, request, *args, **kwargs):
        super(PostCreateView, self).create(request, args, kwargs)
        response = {"status_code": status.HTTP_200_OK,
                    "message": "Successfully created",
                    "result": request.data}
        return Response(response)

Когда мы хотим передать список данных, тем самым предотвращая соответствующий POST-запрос, или создаем запись в базе данных для конкретного представления списка, то, как правило, мы разделяем классы представлений List и Create.

Варианты использования для разных приложений всегда разнятся, для объединения логики набора связанных представлений вы вольны использовать  ListCreateAPIView или даже ViewSets.

Примечание. Поскольку мы хотим отображать данные более систематически, мы можем переопределить метод create и отобразить наш собственный обработчик пользовательских ответов.

Добавим дополнительные представления с методами GETPATCH, и DELETE.

class PostDetailView(generics.RetrieveUpdateDestroyAPIView):
    queryset = Post.objects.all()
    serializer_class = serializers.PostSerializer

    def retrieve(self, request, *args, **kwargs):
        super(PostDetailView, self).retrieve(request, args, kwargs)
        instance = self.get_object()
        serializer = self.get_serializer(instance)
        data = serializer.data
        response = {"status_code": status.HTTP_200_OK,
                    "message": "Successfully retrieved",
                    "result": data}
        return Response(response)

    def patch(self, request, *args, **kwargs):
        super(PostDetailView, self).patch(request, args, kwargs)
        instance = self.get_object()
        serializer = self.get_serializer(instance)
        data = serializer.data
        response = {"status_code": status.HTTP_200_OK,
                    "message": "Successfully updated",
                    "result": data}
        return Response(response)

    def delete(self, request, *args, **kwargs):
        super(PostDetailView, self).delete(request, args, kwargs)
        response = {"status_code": status.HTTP_200_OK,
                    "message": "Successfully deleted"}
        return Response(response)

10. Обновление URL

# posts/urls.py

from django.urls import path
from . import views
from .api import views

urlpatterns = [
    path('', views.PostListView.as_view(), name=None),
    path('create/', views.PostCreateView.as_view(), name=None),
    path('<int:pk>/', views.PostDetailView.as_view(), name=None)
]

Теперь мы можем отправлять запросы к нашему API через Postman, наше приложение, или делать GET-запросы из нашего браузера. Например:

POST /api/v1/posts/create/
HTTP 200 OK
Allow: POST, OPTIONS
Content-Type: application/json
Vary: Accept

{
    "status_code": 200,
    "message": "Successfully created",
    "result": {
        "csrfmiddlewaretoken": "rnSUN3XOIghnXA0yKghnQgxg0do39xhorYene5ALw3gWGThK5MjG6YjL8VUb7v2h",
        "title": "Creating a resource",
        "content": "Howdy mate!"
    }
}
GET /api/v1/posts/1/
HTTP 200 OK
Allow: GET, PUT, PATCH, DELETE, HEAD, OPTIONS
Content-Type: application/json
Vary: Accept

{
    "status_code": 200,
    "message": "Successfully retrieved",
    "result": {
        "title": "Sample Post",
        "content": "Sample Post Content",
        "is_featured": false
    }
}

Вот и все. Мы ухитрились создать RESTful-интерфейс при помощи DRF. Ура!

Исходный код

Исходный код доступен на Github.