Как создать API с помощью Python и Django

Illustration of a stylish person in sunglasses sprinkling toppings onto a waffle on his plate. Illustration of a stylish person in sunglasses sprinkling toppings onto a waffle on his plate.

API, Application Programming Interface (программный интерфейс приложения), — очень широкое понятие в бэкенд-разработке. Тот API, который мы рассмотрим сегодня, представляет собой сайт без фронтенд-составляющей. Вместо рендеринга HTML-страниц, бэкенд возвращает данные в JSON формате для использования их в нативных или веб-приложениях. Самое пристальное внимание при написании API (как и при написании вебсайтов) нужно обратить на то, как он будет использоваться. Сегодня мы поговорим о том, как использовать Django для создания API для простого приложения со списком дел.

Нам понадобится несколько инструментов. Для выполнения всех шагов я бы рекомендовал вам, вместе с данной статьей, клонировать вот этот учебный проект из GitHub-репозитория. Также вы должны установить Python 3, Django 2.2, и djangorestframework 3.9 (из репозитория запустите pip install -r requirements.txt для установки библиотек). Если не все будет понятно с установкой Django, можно воспользоваться официальной документацией. Также вам нужно будет скачать бесплатную версию Postman. Postman – отличный инструмент для разработки и тестирования API, но в этой статье мы воспользуемся лишь его самыми базовыми функциями.

Для начала откройте папку taskmanager, содержащую manage.py, и выполните python manage.py migrate в командной строке, чтобы применить миграции баз данных к дефолтной sqlite базе данных Django. Создайте суперпользователя с помощью python manage.py createsuperuserи не забудьте записать имя пользователя и пароль. Они понадобятся нам позже. Затем выполните python manage.py runserver для взаимодействия с API.

Вы можете работать с API двумя способами: просматривая фронтенд Django REST фреймворка или выполняя http-запросы. Откройте браузер и перейдите к 127.0.0.1:8000 или к localhost через порт 8000, где Django-проекты запускаются по умолчанию. Вы увидите веб-страницу со списком доступных конечных точек API. Это важнейший принцип в RESTful подходе к API-разработке: сам API должен показывать пользователям, что доступно и как это использовать.

Сначала давайте посмотрим на функцию api_index в views.py. Она содержит список конечных точек, которые вы посещаете.

    @define_usage(returns={'url_usage': 'Dict'})
    @api_view(['GET'])
    @permission_classes((AllowAny,))
    def api_index(request):
        details = {}
        for item in list(globals().items()):
            if item[0][0:4] == 'api_':
                if hasattr(item[1], 'usage'):
                    details[reverse(item[1].__name__)] = item[1].usage
        return Response(details)

API функции для каждого представления (view в Django) обернуты тремя декораторами. Мы еще вернемся к @define_usage. @api_view нужен для Django REST фреймворка и отвечает за две вещи: шаблон для веб-страницы, которая в результате получится, и HTTP-метод, поддерживаемый конечной точкой. Чтобы разрешить доступ к этому url без проверки подлинности,@permission_classes, также из Django REST фреймворка, задан как AllowAny. Главная функция API обращается к глобальной области видимости приложения чтобы «собрать» все определенные нами функции. Так как мы добавили к каждой функции представления префикс api_, мы можем легко их отфильтровать и вернуть словарь, содержащий информацию об их вызовах. Детали вызовов предоставляются пользовательским декоратором, написанным в decorators.py.

    def define_usage(params=None, returns=None):
        def decorator(function):
            cls = function.view_class
            header = None
            # Нужна ли аутентификация для вызова этого представления?
            if IsAuthenticated in cls.permission_classes:
                header = {'Authorization': 'Token String'}
            # Создаем лист доступных методов, исключая 'OPTIONS'
            methods = [method.upper() for method in cls.http_method_names if method != 'options']
            # Создаем словарь для ответа
            usage = {'Request Types': methods, 'Headers': header, 'Body': params, 'Returns': returns}


            # Защита от побочных эффектов
            @wraps(function)
            def _wrapper(*args, **kwargs):
                return function(*args, **kwargs)
            _wrapper.usage = usage
            return _wrapper
        return decorator

Декоратор — часть синтаксиса, которая позволяет легко определять функции высокого порядка, чтобы добавить функциям представления атрибуты (как классам). Представления на основе функций с декораторами — отличный компромисс между простыми функциями и представлениями на основе классов в Django. Этот декоратор предоставляет четыре информационных элемента: типы запросов, заголовки, параметры и возвращаемое значение каждой функции. Декоратор генерирует заголовок и информацию о методе на основе информации, полученной от других декораторов, прикрепленных к функции, и принимает в качестве входных данных параметры и возвращаемые значения на момент вызова данного декоратора.

Помимо вывода результатов index-запроса, мы также будем использовать наш API для взаимодействия с данными пользователя, так что нам понадобится какой-то способ аутентификации. Если бы это был не просто учебный проект, а что-то посерьезнее, можно было бы реализовать регистрацию пользователей (к тому же, это отличная практика, если вы хотите проверить, насколько вы разобрались с понятиями из этой статьи). Но вместо этого мы просто войдем под учетной записью суперпользователя, которую создали заранее.

 @define_usage(params={'username': 'String', 'password': 'String'},
               returns={'authenticated': 'Bool', 'token': 'Token String'})
 @api_view(['POST'])
 @permission_classes((AllowAny,))
 def api_signin(request):
     try:
         username = request.data['username']
         password = request.data['password']
     except:
         return Response({'error': 'Please provide correct username and password'},
                         status=HTTP_400_BAD_REQUEST)
     user = authenticate(username=username, password=password)
     if user is not None:
         token, _ = Token.objects.get_or_create(user=user)
         return Response({'authenticated': True, 'token': "Token " + token.key})
     else:
         return Response({'authenticated': False, 'token': None})

Важно отметить, что для того, чтобы идентификация на основе токенов заработала, нужно настроить несколько параметров. Гид по настройке можно найти в файле settings.py учебного проекта.

Чтобы верифицировать пользователя, метод api_signin запрашивает имя пользователя и пароль и использует встроенный в Django метод authenticate. Если предоставленные учетные данные верны, он возвращает токен, позволяющий клиенту получить доступ к защищенным конечным точкам API. Помните о том, что данный токен предоставляет те же права доступа, что и пароль, и поэтому должен надежно храниться в клиентском приложении. Теперь мы наконец можем поработать с Postman. Откройте приложение и используйте его для отправки post-запроса к /signin/, как показано на скриншоте.

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

    class Task(models.Model):
        user = models.ForeignKey(User, on_delete=models.CASCADE) #Каждая задача принадлежит только одному пользователю
        description = models.CharField(max_length=150) #У каждой задачи есть описание
        due = models.DateField() #У каждой задачи есть дата выполнения, тип datetime.date

Модель Task представляет собой довольно простой подход к менеджменту задач нашего приложения. Каждый элемент имеет описание, например, "Написать API с помощью Django" и дату выполнения. Задачи также связаны внешним ключом с объектом Django User, что означает, что каждая задача принадлежит только одному конкретному пользователю, но каждый пользователь может иметь неограниченное количество задач или не иметь вовсе. Каждый объект Django также имеет идентификатор, уникальное целое число, которое можно использовать для ссылки на индивидуальные задачи.

Приложения типа этого часто называют CRUD-приложениями, от "Create, Read, Update, Destroy" (Создание, Чтение, Модификация, Удаление), четырех операций, поддерживаемых нашим приложением на объектах Task.

Для начала создадим пустой список задач, связанных с конкретным пользователем. Используйте Postman для создания GET-запроса к /all/, как на скриншоте ниже. Не забудьте добавить токен к заголовкам этого и всех последующих запросов.

    @define_usage(returns={'tasks': 'Dict'})
    @api_view(['GET'])
    @authentication_classes((SessionAuthentication, BasicAuthentication, TokenAuthentication))
    @permission_classes((IsAuthenticated,))
    def api_all_tasks(request):
        tasks = taskSerializer(request.user.task_set.all(), many=True)
        return Response({'tasks': tasks.data})

Функция api_all_tasks довольно проста. Стоит обратить внимание лишь на смену требований к проверке подлинности и классов разрешения на аутентификацию токеном. У нас есть новый декоратор @authentication_classes, позволяющий выполнять как дефолтные методы аутентификации Django REST framework, так иTokenAuthentication. Это позволяет нам ссылаться на все экземпляры User как на request.user, как если бы пользователи залогинились через стандартную Django-сессию. Декоратор @define_usage показывает нам, что api_all_tasks не принимает параметров (в отличие от GET-запроса) и возвращает лишь одну вещь — список задач. Поскольку данная функция возвращает данные в формате JSON (JavaScript Object Notation), мы используем сериализатор, чтобы сообщить Django, как парсить данные для отправки.

    class taskSerializer(serializers.ModelSerializer):
        class Meta:
            model = Task
            fields = ('id', 'description', 'due')

Эта простая модель определяет данные для класса Task: идентификатор, описание и дату выполнения. Сериализаторы могут добавлять и исключать поля и данные из модели. Например, вот этот сериализатор не возвращает идентификатор пользователя, т.к. он бесполезен для конечного клиента.

    @define_usage(params={'description': 'String', 'due_in': 'Int'},
                  returns={'done': 'Bool'})
    @api_view(['PUT'])
    @authentication_classes((SessionAuthentication, BasicAuthentication, TokenAuthentication))
    @permission_classes((IsAuthenticated,))
    def api_new_task(request):
        task = Task(user=request.user,
                    description=request.data['description'],
                    due=date.today() + timedelta(days=int(request.data['due_in'])))
        task.save()
        return Response({'done': True})

Теперь нам нужно создать задачу. Для этого используем api_new_task. Обычно для создания объекта в базе данных используется PUT-запрос. Обратите внимание, что этот метод, как и два других, не требует предварительной сериализации данных. Вместо этого мы передаем параметры в конструктор объекта класса Task, их же мы затем сохраним в базу данных. Мы отправляем количество дней для выполнения задачи, так как это гораздо проще, чем пытаться отправить объект Python-класса Date. Затем в API мы сохраняем какую-нибудь дату в далеком будущем. Чтобы увидеть созданный объект, нужно создать запрос к /new/ для создания задачи и повторить запрос к /all/.

    @define_usage(params={'task_id': 'Int', 'description': 'String', 'due_in': 'Int'},
                  returns={'done': 'Bool'})
    @api_view(['POST'])
    @authentication_classes((SessionAuthentication, BasicAuthentication, TokenAuthentication))
    @permission_classes((IsAuthenticated,))
    def api_update_task(request):
        task = request.user.task_set.get(id=int(request.data['task_id']))
        try:
            task.description = request.data['description']
        except: #Обновление описания необязательно
            pass
        try:
            task.due = date.today() + timedelta(days=int(request.data['due_in']))
        except: #Обновление даты выполнения необязательно
            pass
        task.save()
        return Response({'done': True})

Для редактирования только что созданной задачи нужно создать POST-запрос к api_update_task через /update/. Мы включаем task_id для ссылки на правильную задачу из пользовательского task_set. Код здесь будет немного сложнее, т.к. мы хотим иметь возможность обновлять описания и/ или дату выполнения задачи.

    @define_usage(params={'task_id': 'Int'},
                  returns={'done': 'Bool'})
    @api_view(['DELETE'])
    @authentication_classes((SessionAuthentication, BasicAuthentication, TokenAuthentication))
    @permission_classes((IsAuthenticated,))
    def api_delete_task(request):
        task = request.user.task_set.get(id=int(request.data['task_id']))
        task.delete()
        return Response({'done': True})

Используйте DELETE-запрос кapi_delete_task через /delete/ для удаления задачи. Этот метод работает аналогично функции api_update_task, за исключением того, что вместо изменения задачи он удаляет ее.

Сегодня мы с вами разобрались, как реализовать index-запрос, аутентификацию на основе токенов и четыре основных HTTP-метода для Django API. Вы можете использовать эти знания для поддержки любых веб- и нативных мобильных приложений или для разработки публичного API для обмена данными. Не стесняйтесь клонировать учебный проект, содержащий весь код, представленный в этой статье, и попробуйте реализовать такие расширения как пагинация, ограничение числа запросов и создание пользователя.