Реализация загрузки файла в конечную точку Cloudinary с помощью Python + DRF

Вдохновение для этого поста

Недавно я реализовал конечную точку загрузки изображений в Cloudinary, используя python и Django Rest Framework. Весь процесс был довольно простым, но все стало немного интереснее, когда я попытался протестировать конечную точку.

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

Надеюсь, вам будет весело читать.

Настройте виртуальную среду и установите зависимости

Начнем с создания рабочего каталога, который мы назовем django-app. В вашем терминале запустите:

mkdir django-app
cd django-app

Приведенные выше команды создадут каталог с именем django-app и войдут в этот каталог с вашего терминала.

Далее нужно настроить виртуальную среду с помощью pipenv следующим образом:

sudo pip install pipenv #installs pipenv
pipenv shell # creates virtual environment using pipenv 

Затем мы устанавливаем все наши зависимости.

pipenv install django djangorestframework cloudinary

Настройка нашего проекта django

Примечание: Если вы уже работали с django и django-rest-framework, вы можете пропустить этот раздел.

После того, как наши зависимости были установлены, мы создаем наш проект Django, запустив:

django-admin startproject upload_project .

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

.
|-- Pipfile
|-- Pipfile.lock
|-- manage.py
`-- upload_project
    |-- __init__.py
    |-- settings.py
    |-- urls.py
    `-- wsgi.py

Теперь мы создаем новое приложение django, upload_app запустив:

python manage.py startapp upload_app

Структура папок должна выглядеть так:

.
|-- Pipfile
|-- Pipfile.lock
|-- manage.py
|-- upload_app
|   |-- __init__.py
|   |-- admin.py
|   |-- apps.py
|   |-- migrations
|   |   `-- __init__.py
|   |-- models.py
|   |-- tests.py
|   `-- views.py
`-- upload_project
    |-- __init__.py
    |-- settings.py
    |-- urls.py
    `-- wsgi.py

Следующее, что нужно добавить Django Rest Framework а также upload_app в списке установленных приложений, изменив массив INSTALLED_APPS в upload_project/settings.py.

INSTALLED_APPS=[
  ...,
 	'rest_framework',
    'upload_app',
    ...,
];

Если все работает хорошо, вы сможете запустить сервер, выполнив:

python manage.py runserver

Настройка Cloudinary

Прежде чем мы сможем начать загрузку изображений в облачное хранилище, мы должны сначала создать облачную учетную запись на Облачный веб-сайт.

Как только это будет сделано, вы сможете получить API_KEY, API_SECRET а также CLOUD_NAME.

Чтобы настроить Cloudinary, перейдите на settings.py и добавьте следующий блок кода вверху

...
import cloudinary

cloudinary.config(cloud_name="<cloud-name-here>",
                  api_key='<api_key_here>',
                  api_secret="<api_secret>")

...


Обратите внимание, что в идеале вы хотели бы поместить данные конфигурации, подобные этим, в переменную среды, поскольку вы не хотите, чтобы эта конфиденциальная информация была доступна для общественности.

Реализовать конечную точку

Вы можете реализовать конечную точку, добавив представление в upload_app.views.py к

from rest_framework.views import APIView
from rest_framework.response import Response
from rest_framework.parsers import MultiPartParser, JSONParser

import cloudinary.uploader

class UploadView(APIView):
    parser_classes = (
        MultiPartParser,
        JSONParser,
    )

    @staticmethod
    def post(request):
        file = request.data.get('picture')

        upload_data = cloudinary.uploader.upload(file)
        return Response({
            'status': 'success',
            'data': upload_data,
        }, status=201)


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

UploadView класс определяет набор parsers. MultiPartParser позволяет представлению распознавать изображения при их отправке в приложение.

Затем post метод сообщает, что пытается получить файл в picture атрибут тела запроса, загружает изображение в облако с помощью cloudinary.uploader.upload и, наконец, отправляет ответ от cloudinary обратно пользователю.

Теперь пришло время добавить URL-адрес загрузки в наше приложение. Мы делаем это, обновляя urls.py следующим образом

from upload_app.views import UploadView
urlpatterns = [
    path('api/upload-image', UploadView.as_view()),
]

Протестируйте приложение с помощью почтальона

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

На приведенных ниже снимках экрана показан пример работы конечной точки:

Скриншот 2019-06-04 в 21:05:34.png

Скриншот 2019-06-04 в 21.05.13.png

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

NB: выполнение запроса может занять несколько секунд в зависимости от вашего интернет-соединения.

Написание автоматических тестов для конечной точки

Настройка теста и установка pytest-django

Теперь мы собираемся сделать последнюю часть этого, написание автоматических тестов для конечной точки. мы собираемся использовать django-pytest чтобы проверить конечную точку, поэтому мы хотели бы установить ее следующим образом:

pipenv install pytest-django

Во время установки вы можете создать файл pytest.ini, который устанавливает pytest-django. Этот файл должен выглядеть так:


[pytest]
DJANGO_SETTINGS_MODULE = upload_project.settings
python_files = tests.py test_*.py 

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

Написание фактического теста

Эта конечная точка имеет 3 основные особенности, которые делают написание юнит-тестов для нее особым случаем:

  • Он взаимодействует с внешним сервисом и пытается сделать Ajax-запрос к Cloudinary.
  • Он включает в себя загрузку изображения. В идеале это изображение должно быть таким же. Он также должен иметь одинаковый размер каждый раз, когда мы достигаем конечной точки.

Давайте немного обсудим значение этих функций

Примечание по тестированию внешних сервисов

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

В нашем случае мы будем имитировать ответ, возвращенный Cloudinary, и использовать его в качестве основы для нашего теста. Мы собираемся сделать это издевательство, используя unnitest.mock.Mock. С помощью этого модуля мы можем имитировать ответ, возвращаемый cloudinary, вместо фактического выполнения запроса.

Примечание по отправке изображений


Первое, что нам нужно сделать, это добавить изображение в проект. Я поместил изображение с именем mock-image.png в upload_app, чтобы новая файловая система теперь выглядела так

.
|-- Pipfile
|-- Pipfile.lock
|-- db.sqlite3
|-- manage.py
|-- upload_app
|   |-- __init__.py
|   |-- admin.py
|   |-- apps.py
|   |-- migrations
|   |   `-- __init__.py
|   |-- mock-image.png
|   |-- models.py
|   |-- tests.py
|   `-- views.py
`-- upload_project
    |-- __init__.py
    |-- settings.py
    |-- urls.py
    `-- wsgi.py


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

Для этого мы создадим tempfile.TemporaryFile объект, который содержит содержимое нашего файла изображения. Затем мы передаем этот объект TemporaryFile в наш тест.

Написание тестов

Мы будем писать тест на upload_app.tests.py. Вот как это выглядит

import pytest
from unittest.mock import Mock
from rest_framework.test import APIClient
import cloudinary.uploader
import os
from tempfile import TemporaryFile

django_client = APIClient()
class TestUploadImage():
    def test_upload_image_to_cloudinary_succeeds(
            self):

        cloudinary_mock_response = {
            'public_id': 'public-id',
            'secure_url': '
        }
        cloudinary.uploader.upload = Mock(
            side_effect=lambda *args: cloudinary_mock_response)


        with TemporaryFile() as temp_image_obj:
            for line in open(os.path.dirname(__file__) + '/mock-image.png', 'rb'):
                temp_image_obj.write(line)

            response = django_client.post(
                '/api/upload-image',
                {'picture': temp_image_obj},
                format="multipart",
            )

            response_data = response.data

            assert response.status_code == 201
            assert response_data['status'] == 'success'
            assert response_data['data'] ==cloudinary_mock_response
            assert cloudinary.uploader.upload.called


Первые несколько строк импортируют в проект необходимые модули. Затем мы определяем тестовый класс с именем TestUploadImage который содержит только один тестовый пример test_upload_image_to_cloudinary_succeeds.

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

Затем, используя unittest.mock.Mock, мы имитируем cloudinary.uploader.upload функцию, чтобы она возвращала наш фиктивный словарь, а не выполняла вызов API к Cloudinary.

В следующей строке диспетчер контекста используется для создания TemporaryFile. TemporaryFiles не являются настоящими файлами, но позволяют нам создавать подобные файлам объекты, которые мы можем использовать в нашем коде. В нашем примере мы используем его для временного хранения информации о нашем файле изображения непосредственно перед отправкой почтового запроса в наше приложение. Вы можете прочитать документы для получения дополнительной информации о TemporaryFiles

Цикл for в следующей строке считывает наше изображение и сохраняет его в файле TemporaryFile. После этого к API обращается ранее созданный django_client, за которым следует набор утверждений, гарантирующих, что он вернет то, что ожидалось.

Обратите внимание, что последнее утверждение, cloudinary.uploader.upload.calledпроверяет, был ли вызван метод cloudinary.uploader.upload.

Запустить тест

Вы можете запустить тест через

pytest

Единственный тест должен провалиться

Заключительные примечания

Я хотел бы закончить рассказом о некоторых хороших практиках, которые были пропущены в этом руководстве.

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

  2. Вы хотели бы проверить, что файл, который пользователь пытается загрузить, является изображением. Этого можно добиться, изменив метод post в views.py.

  3. И последнее, потому что загрузка изображений в облачные сервисы занимает много времени. Обычно вы не хотите, чтобы пользователь ждал его завершения, прежде чем вы передадите ему управление. Поэтому вы, вероятно, захотите выполнить такую ​​​​тяжелую задачу в отдельном работнике, используя Celery

  4. Наконец, когда пользователь хочет загрузить изображение в celery. Вероятно, вы захотите удалить предыдущее изображение пользователя в своей базе данных. Вы делаете это, позвонив cloudinary.uploader.destroy.

Я надеюсь, что эта статья была полезной. Вы можете получить доступ к коду в моем Репозиторий Github

Похожие записи

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *