Реализация загрузки файла в конечную точку 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()),
]
Протестируйте приложение с помощью почтальона
Мы можем протестировать наш код, используя почтальон чтобы увидеть, что это работает.
На приведенных ниже снимках экрана показан пример работы конечной точки:
На приведенных выше снимках экрана мы видим, что мы используем данные формы для выбора изображений с нашего локального компьютера. Затем мы отправляем запрос в наш 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
Единственный тест должен провалиться
Заключительные примечания
Я хотел бы закончить рассказом о некоторых хороших практиках, которые были пропущены в этом руководстве.
Обычно вы хотите ограничить размер изображений, которые вы загружаете в облако, чтобы гарантировать, что вы не израсходуете свое пространство очень быстро.
Вы хотели бы проверить, что файл, который пользователь пытается загрузить, является изображением. Этого можно добиться, изменив метод post в
views.py
.И последнее, потому что загрузка изображений в облачные сервисы занимает много времени. Обычно вы не хотите, чтобы пользователь ждал его завершения, прежде чем вы передадите ему управление. Поэтому вы, вероятно, захотите выполнить такую тяжелую задачу в отдельном работнике, используя
Celery
Наконец, когда пользователь хочет загрузить изображение в celery. Вероятно, вы захотите удалить предыдущее изображение пользователя в своей базе данных. Вы делаете это, позвонив
cloudinary.uploader.destroy
.
Я надеюсь, что эта статья была полезной. Вы можете получить доступ к коду в моем Репозиторий Github