cloudinary 1.44.2__tar.gz → 1.44.3__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {cloudinary-1.44.2/cloudinary.egg-info → cloudinary-1.44.3}/PKG-INFO +7 -6
- {cloudinary-1.44.2 → cloudinary-1.44.3}/README.md +3 -3
- {cloudinary-1.44.2 → cloudinary-1.44.3}/cloudinary/__init__.py +1 -1
- {cloudinary-1.44.2 → cloudinary-1.44.3}/cloudinary/uploader.py +30 -2
- {cloudinary-1.44.2 → cloudinary-1.44.3/cloudinary.egg-info}/PKG-INFO +7 -6
- {cloudinary-1.44.2 → cloudinary-1.44.3}/pyproject.toml +4 -3
- {cloudinary-1.44.2 → cloudinary-1.44.3}/setup.py +4 -3
- {cloudinary-1.44.2 → cloudinary-1.44.3}/test/test_metadata.py +8 -15
- {cloudinary-1.44.2 → cloudinary-1.44.3}/test/test_uploader.py +202 -13
- {cloudinary-1.44.2 → cloudinary-1.44.3}/LICENSE.txt +0 -0
- {cloudinary-1.44.2 → cloudinary-1.44.3}/MANIFEST.in +0 -0
- {cloudinary-1.44.2 → cloudinary-1.44.3}/cloudinary/api.py +0 -0
- {cloudinary-1.44.2 → cloudinary-1.44.3}/cloudinary/api_client/__init__.py +0 -0
- {cloudinary-1.44.2 → cloudinary-1.44.3}/cloudinary/api_client/call_account_api.py +0 -0
- {cloudinary-1.44.2 → cloudinary-1.44.3}/cloudinary/api_client/call_api.py +0 -0
- {cloudinary-1.44.2 → cloudinary-1.44.3}/cloudinary/api_client/execute_request.py +0 -0
- {cloudinary-1.44.2 → cloudinary-1.44.3}/cloudinary/api_client/tcp_keep_alive_manager.py +0 -0
- {cloudinary-1.44.2 → cloudinary-1.44.3}/cloudinary/auth_token.py +0 -0
- {cloudinary-1.44.2 → cloudinary-1.44.3}/cloudinary/cache/__init__.py +0 -0
- {cloudinary-1.44.2 → cloudinary-1.44.3}/cloudinary/cache/adapter/__init__.py +0 -0
- {cloudinary-1.44.2 → cloudinary-1.44.3}/cloudinary/cache/adapter/cache_adapter.py +0 -0
- {cloudinary-1.44.2 → cloudinary-1.44.3}/cloudinary/cache/adapter/key_value_cache_adapter.py +0 -0
- {cloudinary-1.44.2 → cloudinary-1.44.3}/cloudinary/cache/responsive_breakpoints_cache.py +0 -0
- {cloudinary-1.44.2 → cloudinary-1.44.3}/cloudinary/cache/storage/__init__.py +0 -0
- {cloudinary-1.44.2 → cloudinary-1.44.3}/cloudinary/cache/storage/file_system_key_value_storage.py +0 -0
- {cloudinary-1.44.2 → cloudinary-1.44.3}/cloudinary/cache/storage/key_value_storage.py +0 -0
- {cloudinary-1.44.2 → cloudinary-1.44.3}/cloudinary/compat.py +0 -0
- {cloudinary-1.44.2 → cloudinary-1.44.3}/cloudinary/exceptions.py +0 -0
- {cloudinary-1.44.2 → cloudinary-1.44.3}/cloudinary/forms.py +0 -0
- {cloudinary-1.44.2 → cloudinary-1.44.3}/cloudinary/http_client.py +0 -0
- {cloudinary-1.44.2 → cloudinary-1.44.3}/cloudinary/models.py +0 -0
- {cloudinary-1.44.2 → cloudinary-1.44.3}/cloudinary/poster/__init__.py +0 -0
- {cloudinary-1.44.2 → cloudinary-1.44.3}/cloudinary/poster/encode.py +0 -0
- {cloudinary-1.44.2 → cloudinary-1.44.3}/cloudinary/poster/streaminghttp.py +0 -0
- {cloudinary-1.44.2 → cloudinary-1.44.3}/cloudinary/provisioning/__init__.py +0 -0
- {cloudinary-1.44.2 → cloudinary-1.44.3}/cloudinary/provisioning/account.py +0 -0
- {cloudinary-1.44.2 → cloudinary-1.44.3}/cloudinary/provisioning/account_config.py +0 -0
- {cloudinary-1.44.2 → cloudinary-1.44.3}/cloudinary/search.py +0 -0
- {cloudinary-1.44.2 → cloudinary-1.44.3}/cloudinary/search_folders.py +0 -0
- {cloudinary-1.44.2 → cloudinary-1.44.3}/cloudinary/static/cloudinary/html/cloudinary_cors.html +0 -0
- {cloudinary-1.44.2 → cloudinary-1.44.3}/cloudinary/static/cloudinary/js/canvas-to-blob.min.js +0 -0
- {cloudinary-1.44.2 → cloudinary-1.44.3}/cloudinary/static/cloudinary/js/jquery.cloudinary.js +0 -0
- {cloudinary-1.44.2 → cloudinary-1.44.3}/cloudinary/static/cloudinary/js/jquery.fileupload-image.js +0 -0
- {cloudinary-1.44.2 → cloudinary-1.44.3}/cloudinary/static/cloudinary/js/jquery.fileupload-process.js +0 -0
- {cloudinary-1.44.2 → cloudinary-1.44.3}/cloudinary/static/cloudinary/js/jquery.fileupload-validate.js +0 -0
- {cloudinary-1.44.2 → cloudinary-1.44.3}/cloudinary/static/cloudinary/js/jquery.fileupload.js +0 -0
- {cloudinary-1.44.2 → cloudinary-1.44.3}/cloudinary/static/cloudinary/js/jquery.iframe-transport.js +0 -0
- {cloudinary-1.44.2 → cloudinary-1.44.3}/cloudinary/static/cloudinary/js/jquery.ui.widget.js +0 -0
- {cloudinary-1.44.2 → cloudinary-1.44.3}/cloudinary/static/cloudinary/js/load-image.all.min.js +0 -0
- {cloudinary-1.44.2 → cloudinary-1.44.3}/cloudinary/templates/cloudinary_direct_upload.html +0 -0
- {cloudinary-1.44.2 → cloudinary-1.44.3}/cloudinary/templates/cloudinary_includes.html +0 -0
- {cloudinary-1.44.2 → cloudinary-1.44.3}/cloudinary/templates/cloudinary_js_config.html +0 -0
- {cloudinary-1.44.2 → cloudinary-1.44.3}/cloudinary/templatetags/__init__.py +0 -0
- {cloudinary-1.44.2 → cloudinary-1.44.3}/cloudinary/templatetags/cloudinary.py +0 -0
- {cloudinary-1.44.2 → cloudinary-1.44.3}/cloudinary/utils.py +0 -0
- {cloudinary-1.44.2 → cloudinary-1.44.3}/cloudinary.egg-info/SOURCES.txt +0 -0
- {cloudinary-1.44.2 → cloudinary-1.44.3}/cloudinary.egg-info/dependency_links.txt +0 -0
- {cloudinary-1.44.2 → cloudinary-1.44.3}/cloudinary.egg-info/not-zip-safe +0 -0
- {cloudinary-1.44.2 → cloudinary-1.44.3}/cloudinary.egg-info/requires.txt +0 -0
- {cloudinary-1.44.2 → cloudinary-1.44.3}/cloudinary.egg-info/top_level.txt +0 -0
- {cloudinary-1.44.2 → cloudinary-1.44.3}/setup.cfg +0 -0
- {cloudinary-1.44.2 → cloudinary-1.44.3}/test/test_api.py +0 -0
- {cloudinary-1.44.2 → cloudinary-1.44.3}/test/test_api_authorization.py +0 -0
- {cloudinary-1.44.2 → cloudinary-1.44.3}/test/test_archive.py +0 -0
- {cloudinary-1.44.2 → cloudinary-1.44.3}/test/test_auth_token.py +0 -0
- {cloudinary-1.44.2 → cloudinary-1.44.3}/test/test_cloudinary_resource.py +0 -0
- {cloudinary-1.44.2 → cloudinary-1.44.3}/test/test_config.py +0 -0
- {cloudinary-1.44.2 → cloudinary-1.44.3}/test/test_expression_normalization.py +0 -0
- {cloudinary-1.44.2 → cloudinary-1.44.3}/test/test_http_client.py +0 -0
- {cloudinary-1.44.2 → cloudinary-1.44.3}/test/test_image.py +0 -0
- {cloudinary-1.44.2 → cloudinary-1.44.3}/test/test_metadata_rules.py +0 -0
- {cloudinary-1.44.2 → cloudinary-1.44.3}/test/test_provisioning_api.py +0 -0
- {cloudinary-1.44.2 → cloudinary-1.44.3}/test/test_search.py +0 -0
- {cloudinary-1.44.2 → cloudinary-1.44.3}/test/test_streaming_profiles.py +0 -0
- {cloudinary-1.44.2 → cloudinary-1.44.3}/test/test_utils.py +0 -0
- {cloudinary-1.44.2 → cloudinary-1.44.3}/test/test_video.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: cloudinary
|
|
3
|
-
Version: 1.44.
|
|
3
|
+
Version: 1.44.3
|
|
4
4
|
Summary: Python and Django SDK for Cloudinary
|
|
5
5
|
Author-email: Cloudinary <info@cloudinary.com>
|
|
6
6
|
License: Released under the MIT license.
|
|
@@ -18,21 +18,22 @@ Classifier: Environment :: Web Environment
|
|
|
18
18
|
Classifier: Framework :: Django
|
|
19
19
|
Classifier: Framework :: Django :: 1.11
|
|
20
20
|
Classifier: Framework :: Django :: 2.2
|
|
21
|
-
Classifier: Framework :: Django :: 3.2
|
|
22
21
|
Classifier: Framework :: Django :: 4.2
|
|
23
22
|
Classifier: Framework :: Django :: 5.0
|
|
24
23
|
Classifier: Framework :: Django :: 5.1
|
|
24
|
+
Classifier: Framework :: Django :: 5.2
|
|
25
|
+
Classifier: Framework :: Django :: 6.0
|
|
25
26
|
Classifier: Intended Audience :: Developers
|
|
26
27
|
Classifier: License :: OSI Approved :: MIT License
|
|
27
28
|
Classifier: Programming Language :: Python
|
|
28
29
|
Classifier: Programming Language :: Python :: 2
|
|
29
30
|
Classifier: Programming Language :: Python :: 2.7
|
|
30
31
|
Classifier: Programming Language :: Python :: 3
|
|
31
|
-
Classifier: Programming Language :: Python :: 3.9
|
|
32
32
|
Classifier: Programming Language :: Python :: 3.10
|
|
33
33
|
Classifier: Programming Language :: Python :: 3.11
|
|
34
34
|
Classifier: Programming Language :: Python :: 3.12
|
|
35
35
|
Classifier: Programming Language :: Python :: 3.13
|
|
36
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
36
37
|
Classifier: Topic :: Internet :: WWW/HTTP
|
|
37
38
|
Classifier: Topic :: Internet :: WWW/HTTP :: Dynamic Content
|
|
38
39
|
Classifier: Topic :: Multimedia :: Graphics
|
|
@@ -98,9 +99,9 @@ For the complete documentation, see the [Python SDK Guide](https://cloudinary.co
|
|
|
98
99
|
|-------------|------------|------------|
|
|
99
100
|
| 1.x | ✔ | ✔ |
|
|
100
101
|
|
|
101
|
-
| SDK Version | Django 1.11 | Django 2.x | Django 3.x | Django 4.x | Django 5.x |
|
|
102
|
-
|
|
103
|
-
| 1.x | ✔ | ✔ | ✔ | ✔ | ✔ |
|
|
102
|
+
| SDK Version | Django 1.11 | Django 2.x | Django 3.x | Django 4.x | Django 5.x | Django 6.x |
|
|
103
|
+
|-------------|-------------|------------|------------|------------|------------|------------|
|
|
104
|
+
| 1.x | ✔ | ✔ | ✔ | ✔ | ✔ | ✔ |
|
|
104
105
|
|
|
105
106
|
|
|
106
107
|
## Installation
|
|
@@ -44,9 +44,9 @@ For the complete documentation, see the [Python SDK Guide](https://cloudinary.co
|
|
|
44
44
|
|-------------|------------|------------|
|
|
45
45
|
| 1.x | ✔ | ✔ |
|
|
46
46
|
|
|
47
|
-
| SDK Version | Django 1.11 | Django 2.x | Django 3.x | Django 4.x | Django 5.x |
|
|
48
|
-
|
|
49
|
-
| 1.x | ✔ | ✔ | ✔ | ✔ | ✔ |
|
|
47
|
+
| SDK Version | Django 1.11 | Django 2.x | Django 3.x | Django 4.x | Django 5.x | Django 6.x |
|
|
48
|
+
|-------------|-------------|------------|------------|------------|------------|------------|
|
|
49
|
+
| 1.x | ✔ | ✔ | ✔ | ✔ | ✔ | ✔ |
|
|
50
50
|
|
|
51
51
|
|
|
52
52
|
## Installation
|
|
@@ -38,7 +38,7 @@ CL_BLANK = "data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAA
|
|
|
38
38
|
URI_SCHEME = "cloudinary"
|
|
39
39
|
API_VERSION = "v1_1"
|
|
40
40
|
|
|
41
|
-
VERSION = "1.44.
|
|
41
|
+
VERSION = "1.44.3"
|
|
42
42
|
|
|
43
43
|
_USER_PLATFORM_DETAILS = "; ".join((platform(), "Python {}".format(python_version())))
|
|
44
44
|
|
|
@@ -11,7 +11,7 @@ import cloudinary
|
|
|
11
11
|
from cloudinary import utils
|
|
12
12
|
from cloudinary.api_client.execute_request import EXCEPTION_CODES
|
|
13
13
|
from cloudinary.cache.responsive_breakpoints_cache import instance as responsive_breakpoints_cache_instance
|
|
14
|
-
from cloudinary.exceptions import Error
|
|
14
|
+
from cloudinary.exceptions import Error, AuthorizationRequired
|
|
15
15
|
from cloudinary.utils import build_eager
|
|
16
16
|
|
|
17
17
|
try:
|
|
@@ -262,6 +262,32 @@ def upload_resource(file, **options):
|
|
|
262
262
|
)
|
|
263
263
|
|
|
264
264
|
|
|
265
|
+
def _upload_large_part_with_auth_retry(file, http_headers, options):
|
|
266
|
+
"""
|
|
267
|
+
Uploads a single chunk, recovering once from an expired OAuth token via the
|
|
268
|
+
optional oauth_token_refresh_callback config hook. Retries the same chunk
|
|
269
|
+
(same http_headers, so same X-Unique-Upload-Id) to resume the upload.
|
|
270
|
+
|
|
271
|
+
:param file: The chunk to upload.
|
|
272
|
+
:param http_headers: Per-chunk headers, reused on retry.
|
|
273
|
+
:param options: Upload options (must not contain an oauth_token key).
|
|
274
|
+
:return: The result of the chunk upload API call.
|
|
275
|
+
:rtype: dict
|
|
276
|
+
"""
|
|
277
|
+
# Pin the token so the value handed to the callback is the one actually sent.
|
|
278
|
+
token = cloudinary.config().oauth_token
|
|
279
|
+
pinned = dict(options, oauth_token=token) if token else options
|
|
280
|
+
try:
|
|
281
|
+
return upload_large_part(file, http_headers=http_headers, **pinned)
|
|
282
|
+
except AuthorizationRequired:
|
|
283
|
+
callback = cloudinary.config().oauth_token_refresh_callback
|
|
284
|
+
if not callback:
|
|
285
|
+
raise
|
|
286
|
+
callback(token)
|
|
287
|
+
# Retry with the original options so call_api re-reads the refreshed token from config.
|
|
288
|
+
return upload_large_part(file, http_headers=http_headers, **options)
|
|
289
|
+
|
|
290
|
+
|
|
265
291
|
def upload_large(file, **options):
|
|
266
292
|
"""
|
|
267
293
|
Uploads a large file (in chunks) to Cloudinary.
|
|
@@ -308,7 +334,9 @@ def upload_large(file, **options):
|
|
|
308
334
|
"X-Unique-Upload-Id": upload_id
|
|
309
335
|
}
|
|
310
336
|
|
|
311
|
-
upload_result =
|
|
337
|
+
upload_result = _upload_large_part_with_auth_retry(
|
|
338
|
+
(file_name, chunk), http_headers, options
|
|
339
|
+
)
|
|
312
340
|
|
|
313
341
|
options["public_id"] = upload_result.get("public_id")
|
|
314
342
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: cloudinary
|
|
3
|
-
Version: 1.44.
|
|
3
|
+
Version: 1.44.3
|
|
4
4
|
Summary: Python and Django SDK for Cloudinary
|
|
5
5
|
Author-email: Cloudinary <info@cloudinary.com>
|
|
6
6
|
License: Released under the MIT license.
|
|
@@ -18,21 +18,22 @@ Classifier: Environment :: Web Environment
|
|
|
18
18
|
Classifier: Framework :: Django
|
|
19
19
|
Classifier: Framework :: Django :: 1.11
|
|
20
20
|
Classifier: Framework :: Django :: 2.2
|
|
21
|
-
Classifier: Framework :: Django :: 3.2
|
|
22
21
|
Classifier: Framework :: Django :: 4.2
|
|
23
22
|
Classifier: Framework :: Django :: 5.0
|
|
24
23
|
Classifier: Framework :: Django :: 5.1
|
|
24
|
+
Classifier: Framework :: Django :: 5.2
|
|
25
|
+
Classifier: Framework :: Django :: 6.0
|
|
25
26
|
Classifier: Intended Audience :: Developers
|
|
26
27
|
Classifier: License :: OSI Approved :: MIT License
|
|
27
28
|
Classifier: Programming Language :: Python
|
|
28
29
|
Classifier: Programming Language :: Python :: 2
|
|
29
30
|
Classifier: Programming Language :: Python :: 2.7
|
|
30
31
|
Classifier: Programming Language :: Python :: 3
|
|
31
|
-
Classifier: Programming Language :: Python :: 3.9
|
|
32
32
|
Classifier: Programming Language :: Python :: 3.10
|
|
33
33
|
Classifier: Programming Language :: Python :: 3.11
|
|
34
34
|
Classifier: Programming Language :: Python :: 3.12
|
|
35
35
|
Classifier: Programming Language :: Python :: 3.13
|
|
36
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
36
37
|
Classifier: Topic :: Internet :: WWW/HTTP
|
|
37
38
|
Classifier: Topic :: Internet :: WWW/HTTP :: Dynamic Content
|
|
38
39
|
Classifier: Topic :: Multimedia :: Graphics
|
|
@@ -98,9 +99,9 @@ For the complete documentation, see the [Python SDK Guide](https://cloudinary.co
|
|
|
98
99
|
|-------------|------------|------------|
|
|
99
100
|
| 1.x | ✔ | ✔ |
|
|
100
101
|
|
|
101
|
-
| SDK Version | Django 1.11 | Django 2.x | Django 3.x | Django 4.x | Django 5.x |
|
|
102
|
-
|
|
103
|
-
| 1.x | ✔ | ✔ | ✔ | ✔ | ✔ |
|
|
102
|
+
| SDK Version | Django 1.11 | Django 2.x | Django 3.x | Django 4.x | Django 5.x | Django 6.x |
|
|
103
|
+
|-------------|-------------|------------|------------|------------|------------|------------|
|
|
104
|
+
| 1.x | ✔ | ✔ | ✔ | ✔ | ✔ | ✔ |
|
|
104
105
|
|
|
105
106
|
|
|
106
107
|
## Installation
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "cloudinary"
|
|
3
3
|
description = "Python and Django SDK for Cloudinary"
|
|
4
|
-
version = "1.44.
|
|
4
|
+
version = "1.44.3"
|
|
5
5
|
|
|
6
6
|
authors = [{ name = "Cloudinary", email = "info@cloudinary.com" }]
|
|
7
7
|
license = { file = "LICENSE.txt" }
|
|
@@ -13,21 +13,22 @@ classifiers = [
|
|
|
13
13
|
"Framework :: Django",
|
|
14
14
|
"Framework :: Django :: 1.11",
|
|
15
15
|
"Framework :: Django :: 2.2",
|
|
16
|
-
"Framework :: Django :: 3.2",
|
|
17
16
|
"Framework :: Django :: 4.2",
|
|
18
17
|
"Framework :: Django :: 5.0",
|
|
19
18
|
"Framework :: Django :: 5.1",
|
|
19
|
+
"Framework :: Django :: 5.2",
|
|
20
|
+
"Framework :: Django :: 6.0",
|
|
20
21
|
"Intended Audience :: Developers",
|
|
21
22
|
"License :: OSI Approved :: MIT License",
|
|
22
23
|
"Programming Language :: Python",
|
|
23
24
|
"Programming Language :: Python :: 2",
|
|
24
25
|
"Programming Language :: Python :: 2.7",
|
|
25
26
|
"Programming Language :: Python :: 3",
|
|
26
|
-
"Programming Language :: Python :: 3.9",
|
|
27
27
|
"Programming Language :: Python :: 3.10",
|
|
28
28
|
"Programming Language :: Python :: 3.11",
|
|
29
29
|
"Programming Language :: Python :: 3.12",
|
|
30
30
|
"Programming Language :: Python :: 3.13",
|
|
31
|
+
"Programming Language :: Python :: 3.14",
|
|
31
32
|
"Topic :: Internet :: WWW/HTTP",
|
|
32
33
|
"Topic :: Internet :: WWW/HTTP :: Dynamic Content",
|
|
33
34
|
"Topic :: Multimedia :: Graphics",
|
|
@@ -7,7 +7,7 @@ if version_info[0] >= 3:
|
|
|
7
7
|
else:
|
|
8
8
|
# Following code is legacy (Python 2.7 compatibility) and will be removed in the future!
|
|
9
9
|
# TODO: Remove in next major update (when dropping Python 2.7 compatibility)
|
|
10
|
-
version = "1.44.
|
|
10
|
+
version = "1.44.3"
|
|
11
11
|
|
|
12
12
|
with open('README.md') as file:
|
|
13
13
|
long_description = file.read()
|
|
@@ -33,21 +33,22 @@ else:
|
|
|
33
33
|
"Framework :: Django",
|
|
34
34
|
"Framework :: Django :: 1.11",
|
|
35
35
|
"Framework :: Django :: 2.2",
|
|
36
|
-
"Framework :: Django :: 3.2",
|
|
37
36
|
"Framework :: Django :: 4.2",
|
|
38
37
|
"Framework :: Django :: 5.0",
|
|
39
38
|
"Framework :: Django :: 5.1",
|
|
39
|
+
"Framework :: Django :: 5.2",
|
|
40
|
+
"Framework :: Django :: 6.0",
|
|
40
41
|
"Intended Audience :: Developers",
|
|
41
42
|
"License :: OSI Approved :: MIT License",
|
|
42
43
|
"Programming Language :: Python",
|
|
43
44
|
"Programming Language :: Python :: 2",
|
|
44
45
|
"Programming Language :: Python :: 2.7",
|
|
45
46
|
"Programming Language :: Python :: 3",
|
|
46
|
-
"Programming Language :: Python :: 3.9",
|
|
47
47
|
"Programming Language :: Python :: 3.10",
|
|
48
48
|
"Programming Language :: Python :: 3.11",
|
|
49
49
|
"Programming Language :: Python :: 3.12",
|
|
50
50
|
"Programming Language :: Python :: 3.13",
|
|
51
|
+
"Programming Language :: Python :: 3.14",
|
|
51
52
|
"Topic :: Internet :: WWW/HTTP",
|
|
52
53
|
"Topic :: Internet :: WWW/HTTP :: Dynamic Content",
|
|
53
54
|
"Topic :: Multimedia :: Graphics",
|
|
@@ -10,7 +10,7 @@ from cloudinary import api
|
|
|
10
10
|
from cloudinary.exceptions import BadRequest, NotFound
|
|
11
11
|
from test.helper_test import (
|
|
12
12
|
UNIQUE_TEST_ID, get_uri, get_params, get_method, api_response_mock, ignore_exception, get_json_body,
|
|
13
|
-
URLLIB3_REQUEST, patch
|
|
13
|
+
URLLIB3_REQUEST, patch, CldTestCase
|
|
14
14
|
)
|
|
15
15
|
|
|
16
16
|
MOCK_RESPONSE = api_response_mock()
|
|
@@ -105,7 +105,7 @@ METADATA_FIELDS_TO_CREATE = [
|
|
|
105
105
|
disable_warnings()
|
|
106
106
|
|
|
107
107
|
|
|
108
|
-
class MetadataTest(
|
|
108
|
+
class MetadataTest(CldTestCase):
|
|
109
109
|
@classmethod
|
|
110
110
|
def setUpClass(cls):
|
|
111
111
|
cloudinary.reset_config()
|
|
@@ -151,9 +151,7 @@ class MetadataTest(unittest.TestCase):
|
|
|
151
151
|
if metadata_field["type"] in ["enum", "set"]:
|
|
152
152
|
self.assert_metadata_field_datasource(metadata_field["datasource"])
|
|
153
153
|
|
|
154
|
-
|
|
155
|
-
for key, value in values.items():
|
|
156
|
-
self.assertEqual(metadata_field[key], value)
|
|
154
|
+
self.assertObjectContainsSubset(metadata_field, values or {})
|
|
157
155
|
|
|
158
156
|
def assert_metadata_field_datasource(self, datasource):
|
|
159
157
|
"""Asserts that a given object fits the generic structure of a metadata field datasource
|
|
@@ -308,17 +306,15 @@ class MetadataTest(unittest.TestCase):
|
|
|
308
306
|
"external_id": EXTERNAL_ID_SET,
|
|
309
307
|
"label": new_label,
|
|
310
308
|
"type": "integer",
|
|
311
|
-
"mandatory": True,
|
|
312
309
|
"default_value": new_default_value,
|
|
313
|
-
"restrictions": {"readonly_ui": True}
|
|
310
|
+
"restrictions": {"readonly_ui": True},
|
|
314
311
|
})
|
|
315
312
|
|
|
316
313
|
self.assert_metadata_field(result, "string", {
|
|
317
314
|
"external_id": EXTERNAL_ID_GENERAL,
|
|
318
315
|
"label": new_label,
|
|
319
316
|
"default_value": new_default_value,
|
|
320
|
-
"
|
|
321
|
-
"restrictions": {"readonly_ui": True}
|
|
317
|
+
"restrictions": {"readonly_ui": True},
|
|
322
318
|
})
|
|
323
319
|
|
|
324
320
|
@unittest.skipUnless(cloudinary.config().api_secret, "requires api_key/api_secret")
|
|
@@ -501,8 +497,7 @@ class MetadataTest(unittest.TestCase):
|
|
|
501
497
|
|
|
502
498
|
self.assertTrue(get_uri(mocker).endswith("/metadata_fields/order"))
|
|
503
499
|
self.assertEqual(get_method(mocker), "PUT")
|
|
504
|
-
self.
|
|
505
|
-
self.assertEqual(get_json_body(mocker)['direction'], "asc")
|
|
500
|
+
self.assertObjectContainsSubset(get_json_body(mocker), {"order_by": "label", "direction": "asc"})
|
|
506
501
|
|
|
507
502
|
@patch(URLLIB3_REQUEST)
|
|
508
503
|
@unittest.skipUnless(cloudinary.config().api_secret, "requires api_key/api_secret")
|
|
@@ -513,8 +508,7 @@ class MetadataTest(unittest.TestCase):
|
|
|
513
508
|
|
|
514
509
|
self.assertTrue(get_uri(mocker).endswith("/metadata_fields/order"))
|
|
515
510
|
self.assertEqual(get_method(mocker), "PUT")
|
|
516
|
-
self.
|
|
517
|
-
self.assertEqual(get_json_body(mocker)['direction'], "desc")
|
|
511
|
+
self.assertObjectContainsSubset(get_json_body(mocker), {"order_by": "external_id", "direction": "desc"})
|
|
518
512
|
|
|
519
513
|
@patch(URLLIB3_REQUEST)
|
|
520
514
|
@unittest.skipUnless(cloudinary.config().api_secret, "requires api_key/api_secret")
|
|
@@ -525,8 +519,7 @@ class MetadataTest(unittest.TestCase):
|
|
|
525
519
|
|
|
526
520
|
self.assertTrue(get_uri(mocker).endswith("/metadata_fields/order"))
|
|
527
521
|
self.assertEqual(get_method(mocker), "PUT")
|
|
528
|
-
self.
|
|
529
|
-
self.assertEqual(get_json_body(mocker)['direction'], "asc")
|
|
522
|
+
self.assertObjectContainsSubset(get_json_body(mocker), {"order_by": "created_at", "direction": "asc"})
|
|
530
523
|
|
|
531
524
|
|
|
532
525
|
if __name__ == "__main__":
|
|
@@ -18,7 +18,7 @@ from test.cache.storage.dummy_cache_storage import DummyCacheStorage
|
|
|
18
18
|
from test.helper_test import uploader_response_mock, SUFFIX, TEST_IMAGE, get_params, get_headers, TEST_ICON, TEST_DOC, \
|
|
19
19
|
REMOTE_TEST_IMAGE, UTC, populate_large_file, TEST_UNICODE_IMAGE, get_uri, get_method, get_param, \
|
|
20
20
|
cleanup_test_resources_by_tag, cleanup_test_transformation, cleanup_test_resources, EVAL_STR, ON_SUCCESS_STR, \
|
|
21
|
-
URLLIB3_REQUEST, patch, retry_assertion, CldTestCase
|
|
21
|
+
URLLIB3_REQUEST, patch, retry_assertion, CldTestCase, http_response_mock
|
|
22
22
|
from test.test_utils import TEST_ID, TEST_FOLDER
|
|
23
23
|
|
|
24
24
|
MOCK_RESPONSE = uploader_response_mock()
|
|
@@ -196,11 +196,12 @@ class UploaderTest(CldTestCase):
|
|
|
196
196
|
|
|
197
197
|
result = uploader.upload(TEST_UNICODE_IMAGE, tags=[UNIQUE_TAG], use_filename=True, unique_filename=False)
|
|
198
198
|
|
|
199
|
-
self.
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
199
|
+
self.assertObjectContainsSubset(result, {
|
|
200
|
+
"width": TEST_IMAGE_WIDTH,
|
|
201
|
+
"height": TEST_IMAGE_HEIGHT,
|
|
202
|
+
"public_id": expected_name,
|
|
203
|
+
"original_filename": expected_name,
|
|
204
|
+
})
|
|
204
205
|
|
|
205
206
|
@unittest.skipUnless(cloudinary.config().api_secret, "requires api_key/api_secret")
|
|
206
207
|
def test_upload_file_io_without_filename(self):
|
|
@@ -211,9 +212,11 @@ class UploaderTest(CldTestCase):
|
|
|
211
212
|
|
|
212
213
|
result = uploader.upload(temp_file, tags=[UNIQUE_TAG])
|
|
213
214
|
|
|
214
|
-
self.
|
|
215
|
-
|
|
216
|
-
|
|
215
|
+
self.assertObjectContainsSubset(result, {
|
|
216
|
+
"width": TEST_IMAGE_WIDTH,
|
|
217
|
+
"height": TEST_IMAGE_HEIGHT,
|
|
218
|
+
"original_filename": "stream",
|
|
219
|
+
})
|
|
217
220
|
|
|
218
221
|
@unittest.skipUnless(cloudinary.config().api_secret, "requires api_key/api_secret")
|
|
219
222
|
def test_upload_custom_filename(self):
|
|
@@ -794,11 +797,13 @@ P9/AFGGFyjOXZtQAAAAAElFTkSuQmCC\
|
|
|
794
797
|
use_filename=True, unique_filename=False, filename=filename)
|
|
795
798
|
|
|
796
799
|
self.assertCountEqual(resource2["tags"], ["upload_large_tag", UNIQUE_TAG])
|
|
797
|
-
self.
|
|
798
|
-
|
|
800
|
+
self.assertObjectContainsSubset(resource2, {
|
|
801
|
+
"resource_type": "image",
|
|
802
|
+
"original_filename": filename,
|
|
803
|
+
"width": LARGE_FILE_WIDTH,
|
|
804
|
+
"height": LARGE_FILE_HEIGHT,
|
|
805
|
+
})
|
|
799
806
|
self.assertEqual(resource2["original_filename"], resource2["public_id"])
|
|
800
|
-
self.assertEqual(resource2["width"], LARGE_FILE_WIDTH)
|
|
801
|
-
self.assertEqual(resource2["height"], LARGE_FILE_HEIGHT)
|
|
802
807
|
|
|
803
808
|
resource3 = uploader.upload_large(temp_file_name, chunk_size=LARGE_FILE_SIZE,
|
|
804
809
|
tags=["upload_large_tag", UNIQUE_TAG])
|
|
@@ -832,6 +837,190 @@ P9/AFGGFyjOXZtQAAAAAElFTkSuQmCC\
|
|
|
832
837
|
self.assertEqual(resource["width"], LARGE_FILE_WIDTH)
|
|
833
838
|
self.assertEqual(resource["height"], LARGE_FILE_HEIGHT)
|
|
834
839
|
|
|
840
|
+
_OAUTH_CHUNK_SIZE = 4096
|
|
841
|
+
_OAUTH_FILE_SIZE = _OAUTH_CHUNK_SIZE * 3
|
|
842
|
+
|
|
843
|
+
@staticmethod
|
|
844
|
+
def _oauth_part_response(public_id="test_public_id"):
|
|
845
|
+
return {"public_id": public_id, "done": True}
|
|
846
|
+
|
|
847
|
+
def _run_upload_large_with_oauth(self, side_effect, chunk_size=None):
|
|
848
|
+
"""Runs upload_large over a 3-chunk in-memory file with upload_large_part mocked"""
|
|
849
|
+
chunk_size = chunk_size or self._OAUTH_CHUNK_SIZE
|
|
850
|
+
with io.BytesIO() as temp_file:
|
|
851
|
+
populate_large_file(temp_file, self._OAUTH_FILE_SIZE)
|
|
852
|
+
with patch("cloudinary.uploader.upload_large_part") as part_mock:
|
|
853
|
+
part_mock.side_effect = side_effect
|
|
854
|
+
result = uploader.upload_large(temp_file, chunk_size=chunk_size,
|
|
855
|
+
tags=[UNIQUE_TAG], resource_type="image")
|
|
856
|
+
return result, part_mock
|
|
857
|
+
|
|
858
|
+
def test_upload_large_oauth_resume_on_mid_stream_401(self):
|
|
859
|
+
"""Should refresh once and resume the same upload when a chunk gets a 401"""
|
|
860
|
+
cloudinary.config(oauth_token="expired-token")
|
|
861
|
+
|
|
862
|
+
rejected_tokens = []
|
|
863
|
+
|
|
864
|
+
def refresh(rejected):
|
|
865
|
+
rejected_tokens.append(rejected)
|
|
866
|
+
cloudinary.config(oauth_token="fresh-token")
|
|
867
|
+
|
|
868
|
+
cloudinary.config().oauth_token_refresh_callback = refresh
|
|
869
|
+
|
|
870
|
+
calls = {"n": 0}
|
|
871
|
+
|
|
872
|
+
def side_effect(file, http_headers=None, **options):
|
|
873
|
+
calls["n"] += 1
|
|
874
|
+
if calls["n"] == 2:
|
|
875
|
+
raise exceptions.AuthorizationRequired("Server returned unexpected status code - 401")
|
|
876
|
+
return self._oauth_part_response()
|
|
877
|
+
|
|
878
|
+
result, part_mock = self._run_upload_large_with_oauth(side_effect)
|
|
879
|
+
|
|
880
|
+
self.assertEqual(rejected_tokens, ["expired-token"])
|
|
881
|
+
self.assertEqual(part_mock.call_count, 4)
|
|
882
|
+
|
|
883
|
+
# The retry must reuse the failed chunk's X-Unique-Upload-Id and Content-Range to resume.
|
|
884
|
+
upload_ids = [c[1]["http_headers"]["X-Unique-Upload-Id"] for c in part_mock.call_args_list]
|
|
885
|
+
self.assertEqual(len(set(upload_ids)), 1)
|
|
886
|
+
ranges = [c[1]["http_headers"]["Content-Range"] for c in part_mock.call_args_list]
|
|
887
|
+
self.assertEqual(ranges[1], ranges[2])
|
|
888
|
+
|
|
889
|
+
self.assertEqual(result, self._oauth_part_response())
|
|
890
|
+
|
|
891
|
+
def test_upload_large_oauth_resume_on_first_chunk_401(self):
|
|
892
|
+
"""Should refresh and resume when the first chunk (no public_id yet) gets a 401"""
|
|
893
|
+
cloudinary.config(oauth_token="expired-token")
|
|
894
|
+
|
|
895
|
+
refresh_calls = {"n": 0}
|
|
896
|
+
|
|
897
|
+
def refresh(rejected):
|
|
898
|
+
refresh_calls["n"] += 1
|
|
899
|
+
cloudinary.config(oauth_token="fresh-token")
|
|
900
|
+
|
|
901
|
+
cloudinary.config().oauth_token_refresh_callback = refresh
|
|
902
|
+
|
|
903
|
+
calls = {"n": 0}
|
|
904
|
+
|
|
905
|
+
def side_effect(file, http_headers=None, **options):
|
|
906
|
+
calls["n"] += 1
|
|
907
|
+
if calls["n"] == 1:
|
|
908
|
+
self.assertIsNone(options.get("public_id"))
|
|
909
|
+
raise exceptions.AuthorizationRequired("Server returned unexpected status code - 401")
|
|
910
|
+
return self._oauth_part_response()
|
|
911
|
+
|
|
912
|
+
result, part_mock = self._run_upload_large_with_oauth(side_effect)
|
|
913
|
+
|
|
914
|
+
self.assertEqual(refresh_calls["n"], 1)
|
|
915
|
+
self.assertEqual(part_mock.call_count, 4)
|
|
916
|
+
upload_ids = [c[1]["http_headers"]["X-Unique-Upload-Id"] for c in part_mock.call_args_list]
|
|
917
|
+
self.assertEqual(len(set(upload_ids)), 1)
|
|
918
|
+
self.assertEqual(result, self._oauth_part_response())
|
|
919
|
+
|
|
920
|
+
def test_upload_large_oauth_single_retry_then_propagate(self):
|
|
921
|
+
"""Should retry once then propagate when the token stays rejected"""
|
|
922
|
+
cloudinary.config(oauth_token="expired-token")
|
|
923
|
+
|
|
924
|
+
refresh_calls = {"n": 0}
|
|
925
|
+
|
|
926
|
+
def refresh(rejected):
|
|
927
|
+
refresh_calls["n"] += 1
|
|
928
|
+
cloudinary.config(oauth_token="still-bad-token")
|
|
929
|
+
|
|
930
|
+
cloudinary.config().oauth_token_refresh_callback = refresh
|
|
931
|
+
|
|
932
|
+
def side_effect(file, http_headers=None, **options):
|
|
933
|
+
raise exceptions.AuthorizationRequired("Server returned unexpected status code - 401")
|
|
934
|
+
|
|
935
|
+
with self.assertRaises(exceptions.AuthorizationRequired):
|
|
936
|
+
self._run_upload_large_with_oauth(side_effect)
|
|
937
|
+
|
|
938
|
+
self.assertEqual(refresh_calls["n"], 1)
|
|
939
|
+
|
|
940
|
+
def test_upload_large_oauth_no_hook_propagates(self):
|
|
941
|
+
"""Should propagate the first 401 with no retry when no callback is registered"""
|
|
942
|
+
cloudinary.config(oauth_token="expired-token")
|
|
943
|
+
|
|
944
|
+
calls = {"n": 0}
|
|
945
|
+
|
|
946
|
+
def side_effect(file, http_headers=None, **options):
|
|
947
|
+
calls["n"] += 1
|
|
948
|
+
raise exceptions.AuthorizationRequired("Server returned unexpected status code - 401")
|
|
949
|
+
|
|
950
|
+
with self.assertRaises(exceptions.AuthorizationRequired):
|
|
951
|
+
self._run_upload_large_with_oauth(side_effect)
|
|
952
|
+
|
|
953
|
+
self.assertEqual(calls["n"], 1)
|
|
954
|
+
|
|
955
|
+
def test_upload_large_oauth_non_auth_error_not_retried(self):
|
|
956
|
+
"""Should not retry non-auth errors even when a callback is registered"""
|
|
957
|
+
cloudinary.config(oauth_token="some-token")
|
|
958
|
+
|
|
959
|
+
refresh_calls = {"n": 0}
|
|
960
|
+
|
|
961
|
+
def refresh(rejected):
|
|
962
|
+
refresh_calls["n"] += 1
|
|
963
|
+
|
|
964
|
+
cloudinary.config().oauth_token_refresh_callback = refresh
|
|
965
|
+
|
|
966
|
+
for error in (exceptions.BadRequest("bad request"),
|
|
967
|
+
exceptions.Error("Socket error: some transient failure")):
|
|
968
|
+
refresh_calls["n"] = 0
|
|
969
|
+
calls = {"n": 0}
|
|
970
|
+
|
|
971
|
+
def side_effect(file, http_headers=None, _err=error, **options):
|
|
972
|
+
calls["n"] += 1
|
|
973
|
+
raise _err
|
|
974
|
+
|
|
975
|
+
with self.assertRaises(type(error)):
|
|
976
|
+
self._run_upload_large_with_oauth(side_effect)
|
|
977
|
+
|
|
978
|
+
self.assertEqual(calls["n"], 1)
|
|
979
|
+
self.assertEqual(refresh_calls["n"], 0)
|
|
980
|
+
|
|
981
|
+
@patch(URLLIB3_REQUEST)
|
|
982
|
+
def test_upload_large_oauth_rejected_token_is_the_one_sent(self, request_mock):
|
|
983
|
+
"""Should pass the token actually sent (pinned) to the callback when it rotates per read"""
|
|
984
|
+
token_counter = {"n": 0}
|
|
985
|
+
|
|
986
|
+
class RotatingConfig(cloudinary.Config):
|
|
987
|
+
@property
|
|
988
|
+
def oauth_token(self):
|
|
989
|
+
token_counter["n"] += 1
|
|
990
|
+
return "token-{}".format(token_counter["n"])
|
|
991
|
+
|
|
992
|
+
@oauth_token.setter
|
|
993
|
+
def oauth_token(self, value):
|
|
994
|
+
pass
|
|
995
|
+
|
|
996
|
+
rotating = RotatingConfig()
|
|
997
|
+
rejected_seen = {}
|
|
998
|
+
|
|
999
|
+
def refresh(rejected):
|
|
1000
|
+
rejected_seen["token"] = rejected
|
|
1001
|
+
request_mock.return_value = uploader_response_mock()
|
|
1002
|
+
|
|
1003
|
+
rotating.oauth_token_refresh_callback = refresh
|
|
1004
|
+
|
|
1005
|
+
responses = {"first": True}
|
|
1006
|
+
|
|
1007
|
+
def request_side_effect(*args, **kwargs):
|
|
1008
|
+
if responses["first"]:
|
|
1009
|
+
responses["first"] = False
|
|
1010
|
+
rejected_seen["sent"] = kwargs["headers"].get("authorization", "").replace("Bearer ", "")
|
|
1011
|
+
return http_response_mock('{"error":{"message":"401"}}', status=401)
|
|
1012
|
+
return uploader_response_mock()
|
|
1013
|
+
|
|
1014
|
+
request_mock.side_effect = request_side_effect
|
|
1015
|
+
|
|
1016
|
+
with patch("cloudinary.config", return_value=rotating):
|
|
1017
|
+
with io.BytesIO() as temp_file:
|
|
1018
|
+
populate_large_file(temp_file, self._OAUTH_CHUNK_SIZE)
|
|
1019
|
+
uploader.upload_large(temp_file, chunk_size=self._OAUTH_CHUNK_SIZE,
|
|
1020
|
+
tags=[UNIQUE_TAG], resource_type="image")
|
|
1021
|
+
|
|
1022
|
+
self.assertEqual(rejected_seen["token"], rejected_seen["sent"])
|
|
1023
|
+
|
|
835
1024
|
@patch(URLLIB3_REQUEST)
|
|
836
1025
|
@unittest.skipUnless(cloudinary.config().api_secret, "requires api_key/api_secret")
|
|
837
1026
|
def test_upload_preset(self, mocker):
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{cloudinary-1.44.2 → cloudinary-1.44.3}/cloudinary/cache/storage/file_system_key_value_storage.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{cloudinary-1.44.2 → cloudinary-1.44.3}/cloudinary/static/cloudinary/html/cloudinary_cors.html
RENAMED
|
File without changes
|
{cloudinary-1.44.2 → cloudinary-1.44.3}/cloudinary/static/cloudinary/js/canvas-to-blob.min.js
RENAMED
|
File without changes
|
{cloudinary-1.44.2 → cloudinary-1.44.3}/cloudinary/static/cloudinary/js/jquery.cloudinary.js
RENAMED
|
File without changes
|
{cloudinary-1.44.2 → cloudinary-1.44.3}/cloudinary/static/cloudinary/js/jquery.fileupload-image.js
RENAMED
|
File without changes
|
{cloudinary-1.44.2 → cloudinary-1.44.3}/cloudinary/static/cloudinary/js/jquery.fileupload-process.js
RENAMED
|
File without changes
|
|
File without changes
|
{cloudinary-1.44.2 → cloudinary-1.44.3}/cloudinary/static/cloudinary/js/jquery.fileupload.js
RENAMED
|
File without changes
|
{cloudinary-1.44.2 → cloudinary-1.44.3}/cloudinary/static/cloudinary/js/jquery.iframe-transport.js
RENAMED
|
File without changes
|
|
File without changes
|
{cloudinary-1.44.2 → cloudinary-1.44.3}/cloudinary/static/cloudinary/js/load-image.all.min.js
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|