Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 15 additions & 9 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,6 @@ jobs:
strategy:
matrix:
include:
- python-version: "3.9"
toxenv: py39-core
- python-version: "3.10"
toxenv: py310-core
- python-version: "3.11"
Expand All @@ -19,16 +17,24 @@ jobs:
toxenv: py312-core
- python-version: "3.13"
toxenv: py313-core
- python-version: "3.9"
toxenv: py39-django32
- python-version: "3.14"
toxenv: py314-core
- python-version: "3.10"
toxenv: py310-django42
- python-version: "3.11"
toxenv: py311-django42
- python-version: "3.12"
toxenv: py312-django50
- python-version: "3.10"
toxenv: py310-django50
- python-version: "3.10"
toxenv: py310-django51
- python-version: "3.10"
toxenv: py310-django52
- python-version: "3.13"
toxenv: py313-django42
- python-version: "3.13"
toxenv: py313-django51
toxenv: py313-django52
- python-version: "3.12"
toxenv: py312-django60
- python-version: "3.14"
toxenv: py314-django60

steps:
- uses: actions/checkout@v4
Expand Down
6 changes: 3 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -44,9 +44,9 @@ For the complete documentation, see the [Python SDK Guide](https://cloudinary.co
|-------------|------------|------------|
| 1.x | ✔ | ✔ |

| SDK Version | Django 1.11 | Django 2.x | Django 3.x | Django 4.x | Django 5.x |
|-------------|-------------|------------|------------|------------|------------|
| 1.x | ✔ | ✔ | ✔ | ✔ | ✔ |
| SDK Version | Django 1.11 | Django 2.x | Django 3.x | Django 4.x | Django 5.x | Django 6.x |
|-------------|-------------|------------|------------|------------|------------|------------|
| 1.x | ✔ | ✔ | ✔ | ✔ | ✔ | ✔ |


## Installation
Expand Down
5 changes: 3 additions & 2 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -13,21 +13,22 @@ classifiers = [
"Framework :: Django",
"Framework :: Django :: 1.11",
"Framework :: Django :: 2.2",
"Framework :: Django :: 3.2",
"Framework :: Django :: 4.2",
"Framework :: Django :: 5.0",
"Framework :: Django :: 5.1",
"Framework :: Django :: 5.2",
"Framework :: Django :: 6.0",
"Intended Audience :: Developers",
"License :: OSI Approved :: MIT License",
"Programming Language :: Python",
"Programming Language :: Python :: 2",
"Programming Language :: Python :: 2.7",
"Programming Language :: Python :: 3",
"Programming Language :: Python :: 3.9",
"Programming Language :: Python :: 3.10",
"Programming Language :: Python :: 3.11",
"Programming Language :: Python :: 3.12",
"Programming Language :: Python :: 3.13",
"Programming Language :: Python :: 3.14",
"Topic :: Internet :: WWW/HTTP",
"Topic :: Internet :: WWW/HTTP :: Dynamic Content",
"Topic :: Multimedia :: Graphics",
Expand Down
5 changes: 3 additions & 2 deletions setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,21 +33,22 @@
"Framework :: Django",
"Framework :: Django :: 1.11",
"Framework :: Django :: 2.2",
"Framework :: Django :: 3.2",
"Framework :: Django :: 4.2",
"Framework :: Django :: 5.0",
"Framework :: Django :: 5.1",
"Framework :: Django :: 5.2",
"Framework :: Django :: 6.0",
"Intended Audience :: Developers",
"License :: OSI Approved :: MIT License",
"Programming Language :: Python",
"Programming Language :: Python :: 2",
"Programming Language :: Python :: 2.7",
"Programming Language :: Python :: 3",
"Programming Language :: Python :: 3.9",
"Programming Language :: Python :: 3.10",
"Programming Language :: Python :: 3.11",
"Programming Language :: Python :: 3.12",
"Programming Language :: Python :: 3.13",
"Programming Language :: Python :: 3.14",
"Topic :: Internet :: WWW/HTTP",
"Topic :: Internet :: WWW/HTTP :: Dynamic Content",
"Topic :: Multimedia :: Graphics",
Expand Down
12 changes: 12 additions & 0 deletions test/helper_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -351,3 +351,15 @@ def count_elements(lst):
if count1 != count2:
standard_msg = '%s != %s' % (count1, count2)
self.fail(self._formatMessage(msg, standard_msg))

def assertObjectContainsSubset(self, actual, expected, msg=None):
"""
Fail unless every key/value pair in expected is present in actual.
Nested dicts are compared recursively, so actual may contain extra keys.
"""
for key, value in expected.items():
self.assertIn(key, actual, msg)
if isinstance(value, dict):
self.assertObjectContainsSubset(actual[key], value, msg)
else:
self.assertEqual(actual[key], value, msg)
23 changes: 8 additions & 15 deletions test/test_metadata.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
from cloudinary.exceptions import BadRequest, NotFound
from test.helper_test import (
UNIQUE_TEST_ID, get_uri, get_params, get_method, api_response_mock, ignore_exception, get_json_body,
URLLIB3_REQUEST, patch
URLLIB3_REQUEST, patch, CldTestCase
)

MOCK_RESPONSE = api_response_mock()
Expand Down Expand Up @@ -105,7 +105,7 @@
disable_warnings()


class MetadataTest(unittest.TestCase):
class MetadataTest(CldTestCase):
@classmethod
def setUpClass(cls):
cloudinary.reset_config()
Expand Down Expand Up @@ -151,9 +151,7 @@ def assert_metadata_field(self, metadata_field, field_type=None, values=None):
if metadata_field["type"] in ["enum", "set"]:
self.assert_metadata_field_datasource(metadata_field["datasource"])

values = values or {}
for key, value in values.items():
self.assertEqual(metadata_field[key], value)
self.assertObjectContainsSubset(metadata_field, values or {})

def assert_metadata_field_datasource(self, datasource):
"""Asserts that a given object fits the generic structure of a metadata field datasource
Expand Down Expand Up @@ -308,17 +306,15 @@ def test08_update_metadata_field(self):
"external_id": EXTERNAL_ID_SET,
"label": new_label,
"type": "integer",
"mandatory": True,
"default_value": new_default_value,
"restrictions": {"readonly_ui": True}
"restrictions": {"readonly_ui": True},
})

self.assert_metadata_field(result, "string", {
"external_id": EXTERNAL_ID_GENERAL,
"label": new_label,
"default_value": new_default_value,
"mandatory": True,
"restrictions": {"readonly_ui": True}
"restrictions": {"readonly_ui": True},
})

@unittest.skipUnless(cloudinary.config().api_secret, "requires api_key/api_secret")
Expand Down Expand Up @@ -501,8 +497,7 @@ def test_reorder_metadata_fields_by_label(self, mocker):

self.assertTrue(get_uri(mocker).endswith("/metadata_fields/order"))
self.assertEqual(get_method(mocker), "PUT")
self.assertEqual(get_json_body(mocker)['order_by'], "label")
self.assertEqual(get_json_body(mocker)['direction'], "asc")
self.assertObjectContainsSubset(get_json_body(mocker), {"order_by": "label", "direction": "asc"})

@patch(URLLIB3_REQUEST)
@unittest.skipUnless(cloudinary.config().api_secret, "requires api_key/api_secret")
Expand All @@ -513,8 +508,7 @@ def test_reorder_metadata_fields_by_external_id(self, mocker):

self.assertTrue(get_uri(mocker).endswith("/metadata_fields/order"))
self.assertEqual(get_method(mocker), "PUT")
self.assertEqual(get_json_body(mocker)['order_by'], "external_id")
self.assertEqual(get_json_body(mocker)['direction'], "desc")
self.assertObjectContainsSubset(get_json_body(mocker), {"order_by": "external_id", "direction": "desc"})

@patch(URLLIB3_REQUEST)
@unittest.skipUnless(cloudinary.config().api_secret, "requires api_key/api_secret")
Expand All @@ -525,8 +519,7 @@ def test_reorder_metadata_fields_by_created_at(self, mocker):

self.assertTrue(get_uri(mocker).endswith("/metadata_fields/order"))
self.assertEqual(get_method(mocker), "PUT")
self.assertEqual(get_json_body(mocker)['order_by'], "created_at")
self.assertEqual(get_json_body(mocker)['direction'], "asc")
self.assertObjectContainsSubset(get_json_body(mocker), {"order_by": "created_at", "direction": "asc"})


if __name__ == "__main__":
Expand Down
29 changes: 17 additions & 12 deletions test/test_uploader.py
Original file line number Diff line number Diff line change
Expand Up @@ -196,11 +196,12 @@ def test_upload_unicode_filename(self):

result = uploader.upload(TEST_UNICODE_IMAGE, tags=[UNIQUE_TAG], use_filename=True, unique_filename=False)

self.assertEqual(result["width"], TEST_IMAGE_WIDTH)
self.assertEqual(result["height"], TEST_IMAGE_HEIGHT)

self.assertEqual(expected_name, result["public_id"])
self.assertEqual(expected_name, result["original_filename"])
self.assertObjectContainsSubset(result, {
"width": TEST_IMAGE_WIDTH,
"height": TEST_IMAGE_HEIGHT,
"public_id": expected_name,
"original_filename": expected_name,
})

@unittest.skipUnless(cloudinary.config().api_secret, "requires api_key/api_secret")
def test_upload_file_io_without_filename(self):
Expand All @@ -211,9 +212,11 @@ def test_upload_file_io_without_filename(self):

result = uploader.upload(temp_file, tags=[UNIQUE_TAG])

self.assertEqual(result["width"], TEST_IMAGE_WIDTH)
self.assertEqual(result["height"], TEST_IMAGE_HEIGHT)
self.assertEqual('stream', result["original_filename"])
self.assertObjectContainsSubset(result, {
"width": TEST_IMAGE_WIDTH,
"height": TEST_IMAGE_HEIGHT,
"original_filename": "stream",
})

@unittest.skipUnless(cloudinary.config().api_secret, "requires api_key/api_secret")
def test_upload_custom_filename(self):
Expand Down Expand Up @@ -794,11 +797,13 @@ def test_upload_large(self):
use_filename=True, unique_filename=False, filename=filename)

self.assertCountEqual(resource2["tags"], ["upload_large_tag", UNIQUE_TAG])
self.assertEqual(resource2["resource_type"], "image")
self.assertEqual(resource2["original_filename"], filename)
self.assertObjectContainsSubset(resource2, {
"resource_type": "image",
"original_filename": filename,
"width": LARGE_FILE_WIDTH,
"height": LARGE_FILE_HEIGHT,
})
self.assertEqual(resource2["original_filename"], resource2["public_id"])
self.assertEqual(resource2["width"], LARGE_FILE_WIDTH)
self.assertEqual(resource2["height"], LARGE_FILE_HEIGHT)

resource3 = uploader.upload_large(temp_file_name, chunk_size=LARGE_FILE_SIZE,
tags=["upload_large_tag", UNIQUE_TAG])
Expand Down
12 changes: 7 additions & 5 deletions tox.ini
Original file line number Diff line number Diff line change
@@ -1,23 +1,25 @@
[tox]
envlist =
py{27,39,310,311,312,313}-core
py{27,310,311,312,313,314}-core
py{27}-django{111}
py{39,310,311,312,313}-django{32,42,50,51}
py{310,311,312,313,314}-django{42,50,51,52}
py{312,313,314}-django{60}

[testenv]
usedevelop = True
commands =
core: python -m pytest test
django{111,32}: django-admin.py test -v2 django_tests {env:D_ARGS:}
django{42,50,51}: django-admin test -v2 django_tests {env:D_ARGS:}
django{111}: django-admin.py test -v2 django_tests {env:D_ARGS:}
django{42,50,51,52,60}: django-admin test -v2 django_tests {env:D_ARGS:}
passenv = *
deps =
pytest
py27: mock
django111: Django>=1.11,<1.12
django32: Django>=3.2,<3.3
django42: Django>=4.2,<4.3
django50: Django>=5.0,<5.1
django51: Django>=5.1,<5.2
django52: Django>=5.2,<5.3
django60: Django>=6.0,<6.1
setenv =
DJANGO_SETTINGS_MODULE=django_tests.settings
Loading