diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 2049999..214cbef 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -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" @@ -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 diff --git a/README.md b/README.md index 63b1705..13443b7 100644 --- a/README.md +++ b/README.md @@ -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 diff --git a/pyproject.toml b/pyproject.toml index a717773..4f69300 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -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", diff --git a/setup.py b/setup.py index 7f79405..a28c9a7 100644 --- a/setup.py +++ b/setup.py @@ -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", diff --git a/test/helper_test.py b/test/helper_test.py index c4579cd..616a771 100644 --- a/test/helper_test.py +++ b/test/helper_test.py @@ -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) diff --git a/test/test_metadata.py b/test/test_metadata.py index 9870f78..8f21216 100644 --- a/test/test_metadata.py +++ b/test/test_metadata.py @@ -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() @@ -105,7 +105,7 @@ disable_warnings() -class MetadataTest(unittest.TestCase): +class MetadataTest(CldTestCase): @classmethod def setUpClass(cls): cloudinary.reset_config() @@ -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 @@ -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") @@ -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") @@ -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") @@ -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__": diff --git a/test/test_uploader.py b/test/test_uploader.py index b9e1d39..d05d9ee 100644 --- a/test/test_uploader.py +++ b/test/test_uploader.py @@ -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): @@ -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): @@ -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]) diff --git a/tox.ini b/tox.ini index dee98c6..42441ce 100644 --- a/tox.ini +++ b/tox.ini @@ -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