cloudinary 1.46.3__tar.gz → 1.47.0__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.46.3/cloudinary.egg-info → cloudinary-1.47.0}/PKG-INFO +2 -1
- {cloudinary-1.46.3 → cloudinary-1.47.0}/README.md +1 -0
- {cloudinary-1.46.3 → cloudinary-1.47.0}/cloudinary/__init__.py +1 -1
- {cloudinary-1.46.3 → cloudinary-1.47.0}/cloudinary/api_client/execute_request.py +9 -1
- cloudinary-1.47.0/cloudinary/collections.py +289 -0
- {cloudinary-1.46.3 → cloudinary-1.47.0}/cloudinary/docs/README.md +1 -0
- {cloudinary-1.46.3 → cloudinary-1.47.0}/cloudinary/docs/import-and-call.md +2 -1
- cloudinary-1.47.0/cloudinary/docs/organize-and-share-collections.md +222 -0
- {cloudinary-1.46.3 → cloudinary-1.47.0}/cloudinary/docs/platform-capabilities.md +1 -0
- {cloudinary-1.46.3 → cloudinary-1.47.0}/cloudinary/docs/troubleshoot-errors.md +4 -3
- {cloudinary-1.46.3 → cloudinary-1.47.0}/cloudinary/exceptions.py +8 -0
- {cloudinary-1.46.3 → cloudinary-1.47.0}/cloudinary/uploader.py +1 -0
- {cloudinary-1.46.3 → cloudinary-1.47.0}/cloudinary/utils.py +2 -0
- {cloudinary-1.46.3 → cloudinary-1.47.0/cloudinary.egg-info}/PKG-INFO +2 -1
- {cloudinary-1.46.3 → cloudinary-1.47.0}/cloudinary.egg-info/SOURCES.txt +3 -0
- {cloudinary-1.46.3 → cloudinary-1.47.0}/pyproject.toml +1 -1
- {cloudinary-1.46.3 → cloudinary-1.47.0}/setup.py +1 -1
- cloudinary-1.47.0/test/test_collections.py +351 -0
- {cloudinary-1.46.3 → cloudinary-1.47.0}/LICENSE +0 -0
- {cloudinary-1.46.3 → cloudinary-1.47.0}/MANIFEST.in +0 -0
- {cloudinary-1.46.3 → cloudinary-1.47.0}/cloudinary/api.py +0 -0
- {cloudinary-1.46.3 → cloudinary-1.47.0}/cloudinary/api_client/__init__.py +0 -0
- {cloudinary-1.46.3 → cloudinary-1.47.0}/cloudinary/api_client/call_account_api.py +0 -0
- {cloudinary-1.46.3 → cloudinary-1.47.0}/cloudinary/api_client/call_api.py +0 -0
- {cloudinary-1.46.3 → cloudinary-1.47.0}/cloudinary/api_client/tcp_keep_alive_manager.py +0 -0
- {cloudinary-1.46.3 → cloudinary-1.47.0}/cloudinary/auth_token.py +0 -0
- {cloudinary-1.46.3 → cloudinary-1.47.0}/cloudinary/cache/__init__.py +0 -0
- {cloudinary-1.46.3 → cloudinary-1.47.0}/cloudinary/cache/adapter/__init__.py +0 -0
- {cloudinary-1.46.3 → cloudinary-1.47.0}/cloudinary/cache/adapter/cache_adapter.py +0 -0
- {cloudinary-1.46.3 → cloudinary-1.47.0}/cloudinary/cache/adapter/key_value_cache_adapter.py +0 -0
- {cloudinary-1.46.3 → cloudinary-1.47.0}/cloudinary/cache/responsive_breakpoints_cache.py +0 -0
- {cloudinary-1.46.3 → cloudinary-1.47.0}/cloudinary/cache/storage/__init__.py +0 -0
- {cloudinary-1.46.3 → cloudinary-1.47.0}/cloudinary/cache/storage/file_system_key_value_storage.py +0 -0
- {cloudinary-1.46.3 → cloudinary-1.47.0}/cloudinary/cache/storage/key_value_storage.py +0 -0
- {cloudinary-1.46.3 → cloudinary-1.47.0}/cloudinary/compat.py +0 -0
- {cloudinary-1.46.3 → cloudinary-1.47.0}/cloudinary/docs/configure-cloudinary.md +0 -0
- {cloudinary-1.46.3 → cloudinary-1.47.0}/cloudinary/docs/get-credentials.md +0 -0
- {cloudinary-1.46.3 → cloudinary-1.47.0}/cloudinary/docs/moderate-upload.md +0 -0
- {cloudinary-1.46.3 → cloudinary-1.47.0}/cloudinary/docs/search-and-manage-assets.md +0 -0
- {cloudinary-1.46.3 → cloudinary-1.47.0}/cloudinary/docs/sign-browser-upload.md +0 -0
- {cloudinary-1.46.3 → cloudinary-1.47.0}/cloudinary/docs/transform-and-deliver-image.md +0 -0
- {cloudinary-1.46.3 → cloudinary-1.47.0}/cloudinary/docs/transform-and-deliver-video.md +0 -0
- {cloudinary-1.46.3 → cloudinary-1.47.0}/cloudinary/docs/upload-image.md +0 -0
- {cloudinary-1.46.3 → cloudinary-1.47.0}/cloudinary/docs/upload-large-video.md +0 -0
- {cloudinary-1.46.3 → cloudinary-1.47.0}/cloudinary/docs/use-structured-metadata.md +0 -0
- {cloudinary-1.46.3 → cloudinary-1.47.0}/cloudinary/docs/use-with-django.md +0 -0
- {cloudinary-1.46.3 → cloudinary-1.47.0}/cloudinary/forms.py +0 -0
- {cloudinary-1.46.3 → cloudinary-1.47.0}/cloudinary/http_client.py +0 -0
- {cloudinary-1.46.3 → cloudinary-1.47.0}/cloudinary/models.py +0 -0
- {cloudinary-1.46.3 → cloudinary-1.47.0}/cloudinary/poster/__init__.py +0 -0
- {cloudinary-1.46.3 → cloudinary-1.47.0}/cloudinary/poster/encode.py +0 -0
- {cloudinary-1.46.3 → cloudinary-1.47.0}/cloudinary/poster/streaminghttp.py +0 -0
- {cloudinary-1.46.3 → cloudinary-1.47.0}/cloudinary/provisioning/__init__.py +0 -0
- {cloudinary-1.46.3 → cloudinary-1.47.0}/cloudinary/provisioning/account.py +0 -0
- {cloudinary-1.46.3 → cloudinary-1.47.0}/cloudinary/provisioning/account_config.py +0 -0
- {cloudinary-1.46.3 → cloudinary-1.47.0}/cloudinary/search.py +0 -0
- {cloudinary-1.46.3 → cloudinary-1.47.0}/cloudinary/search_folders.py +0 -0
- {cloudinary-1.46.3 → cloudinary-1.47.0}/cloudinary/static/cloudinary/html/cloudinary_cors.html +0 -0
- {cloudinary-1.46.3 → cloudinary-1.47.0}/cloudinary/static/cloudinary/js/canvas-to-blob.min.js +0 -0
- {cloudinary-1.46.3 → cloudinary-1.47.0}/cloudinary/static/cloudinary/js/jquery.cloudinary.js +0 -0
- {cloudinary-1.46.3 → cloudinary-1.47.0}/cloudinary/static/cloudinary/js/jquery.fileupload-image.js +0 -0
- {cloudinary-1.46.3 → cloudinary-1.47.0}/cloudinary/static/cloudinary/js/jquery.fileupload-process.js +0 -0
- {cloudinary-1.46.3 → cloudinary-1.47.0}/cloudinary/static/cloudinary/js/jquery.fileupload-validate.js +0 -0
- {cloudinary-1.46.3 → cloudinary-1.47.0}/cloudinary/static/cloudinary/js/jquery.fileupload.js +0 -0
- {cloudinary-1.46.3 → cloudinary-1.47.0}/cloudinary/static/cloudinary/js/jquery.iframe-transport.js +0 -0
- {cloudinary-1.46.3 → cloudinary-1.47.0}/cloudinary/static/cloudinary/js/jquery.ui.widget.js +0 -0
- {cloudinary-1.46.3 → cloudinary-1.47.0}/cloudinary/static/cloudinary/js/load-image.all.min.js +0 -0
- {cloudinary-1.46.3 → cloudinary-1.47.0}/cloudinary/templates/cloudinary_direct_upload.html +0 -0
- {cloudinary-1.46.3 → cloudinary-1.47.0}/cloudinary/templates/cloudinary_includes.html +0 -0
- {cloudinary-1.46.3 → cloudinary-1.47.0}/cloudinary/templates/cloudinary_js_config.html +0 -0
- {cloudinary-1.46.3 → cloudinary-1.47.0}/cloudinary/templatetags/__init__.py +0 -0
- {cloudinary-1.46.3 → cloudinary-1.47.0}/cloudinary/templatetags/cloudinary.py +0 -0
- {cloudinary-1.46.3 → cloudinary-1.47.0}/cloudinary.egg-info/dependency_links.txt +0 -0
- {cloudinary-1.46.3 → cloudinary-1.47.0}/cloudinary.egg-info/not-zip-safe +0 -0
- {cloudinary-1.46.3 → cloudinary-1.47.0}/cloudinary.egg-info/requires.txt +0 -0
- {cloudinary-1.46.3 → cloudinary-1.47.0}/cloudinary.egg-info/top_level.txt +0 -0
- {cloudinary-1.46.3 → cloudinary-1.47.0}/setup.cfg +0 -0
- {cloudinary-1.46.3 → cloudinary-1.47.0}/test/test_api.py +0 -0
- {cloudinary-1.46.3 → cloudinary-1.47.0}/test/test_api_authorization.py +0 -0
- {cloudinary-1.46.3 → cloudinary-1.47.0}/test/test_archive.py +0 -0
- {cloudinary-1.46.3 → cloudinary-1.47.0}/test/test_auth_token.py +0 -0
- {cloudinary-1.46.3 → cloudinary-1.47.0}/test/test_cloudinary_resource.py +0 -0
- {cloudinary-1.46.3 → cloudinary-1.47.0}/test/test_config.py +0 -0
- {cloudinary-1.46.3 → cloudinary-1.47.0}/test/test_expression_normalization.py +0 -0
- {cloudinary-1.46.3 → cloudinary-1.47.0}/test/test_http_client.py +0 -0
- {cloudinary-1.46.3 → cloudinary-1.47.0}/test/test_image.py +0 -0
- {cloudinary-1.46.3 → cloudinary-1.47.0}/test/test_metadata.py +0 -0
- {cloudinary-1.46.3 → cloudinary-1.47.0}/test/test_metadata_rules.py +0 -0
- {cloudinary-1.46.3 → cloudinary-1.47.0}/test/test_notifications.py +0 -0
- {cloudinary-1.46.3 → cloudinary-1.47.0}/test/test_provisioning_api.py +0 -0
- {cloudinary-1.46.3 → cloudinary-1.47.0}/test/test_request_id.py +0 -0
- {cloudinary-1.46.3 → cloudinary-1.47.0}/test/test_search.py +0 -0
- {cloudinary-1.46.3 → cloudinary-1.47.0}/test/test_streaming_profiles.py +0 -0
- {cloudinary-1.46.3 → cloudinary-1.47.0}/test/test_triggers.py +0 -0
- {cloudinary-1.46.3 → cloudinary-1.47.0}/test/test_uploader.py +0 -0
- {cloudinary-1.46.3 → cloudinary-1.47.0}/test/test_utils.py +0 -0
- {cloudinary-1.46.3 → cloudinary-1.47.0}/test/test_video.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: cloudinary
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.47.0
|
|
4
4
|
Summary: Upload, transform, optimize, and manage images and videos with Cloudinary from Python or Django.
|
|
5
5
|
Author-email: Cloudinary <info@cloudinary.com>
|
|
6
6
|
License: MIT License
|
|
@@ -142,6 +142,7 @@ Save as `quickstart.py` and run `python quickstart.py`. [Create a free account](
|
|
|
142
142
|
- [Transform and deliver an image](cloudinary/docs/transform-and-deliver-image.md)
|
|
143
143
|
- [Transform and deliver a video](cloudinary/docs/transform-and-deliver-video.md)
|
|
144
144
|
- [Search and manage assets](cloudinary/docs/search-and-manage-assets.md)
|
|
145
|
+
- [Organize and share collections](cloudinary/docs/organize-and-share-collections.md)
|
|
145
146
|
- [Moderate an upload](cloudinary/docs/moderate-upload.md)
|
|
146
147
|
- [Use structured metadata](cloudinary/docs/use-structured-metadata.md)
|
|
147
148
|
- [Use with Django](cloudinary/docs/use-with-django.md)
|
|
@@ -71,6 +71,7 @@ Save as `quickstart.py` and run `python quickstart.py`. [Create a free account](
|
|
|
71
71
|
- [Transform and deliver an image](cloudinary/docs/transform-and-deliver-image.md)
|
|
72
72
|
- [Transform and deliver a video](cloudinary/docs/transform-and-deliver-video.md)
|
|
73
73
|
- [Search and manage assets](cloudinary/docs/search-and-manage-assets.md)
|
|
74
|
+
- [Organize and share collections](cloudinary/docs/organize-and-share-collections.md)
|
|
74
75
|
- [Moderate an upload](cloudinary/docs/moderate-upload.md)
|
|
75
76
|
- [Use structured metadata](cloudinary/docs/use-structured-metadata.md)
|
|
76
77
|
- [Use with Django](cloudinary/docs/use-with-django.md)
|
|
@@ -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.
|
|
41
|
+
VERSION = "1.47.0"
|
|
42
42
|
|
|
43
43
|
_USER_PLATFORM_DETAILS = "; ".join((platform(), "Python {}".format(python_version())))
|
|
44
44
|
|
|
@@ -7,11 +7,14 @@ from urllib3.exceptions import HTTPError
|
|
|
7
7
|
|
|
8
8
|
import cloudinary
|
|
9
9
|
from cloudinary.exceptions import (
|
|
10
|
+
Error,
|
|
10
11
|
BadRequest,
|
|
11
12
|
AuthorizationRequired,
|
|
12
13
|
NotAllowed,
|
|
13
14
|
NotFound,
|
|
15
|
+
NotAcceptable,
|
|
14
16
|
AlreadyExists,
|
|
17
|
+
Locked,
|
|
15
18
|
RateLimited,
|
|
16
19
|
GeneralError
|
|
17
20
|
)
|
|
@@ -23,8 +26,10 @@ EXCEPTION_CODES = {
|
|
|
23
26
|
401: AuthorizationRequired,
|
|
24
27
|
403: NotAllowed,
|
|
25
28
|
404: NotFound,
|
|
29
|
+
406: NotAcceptable,
|
|
26
30
|
409: AlreadyExists,
|
|
27
31
|
420: RateLimited,
|
|
32
|
+
423: Locked,
|
|
28
33
|
429: RateLimited,
|
|
29
34
|
500: GeneralError,
|
|
30
35
|
503: GeneralError
|
|
@@ -88,8 +93,11 @@ def execute_request(http_connector, method, params, headers, auth, api_url, **op
|
|
|
88
93
|
# Error is parsing json
|
|
89
94
|
raise GeneralError("Error parsing server response (%d) - %s. Got - %s" % (response.status, body, e))
|
|
90
95
|
|
|
96
|
+
if response.status >= 400 and "error" not in result and "message" in result:
|
|
97
|
+
result = {"error": {"message": result["message"]}}
|
|
98
|
+
|
|
91
99
|
if "error" in result:
|
|
92
|
-
exception_class = EXCEPTION_CODES.get(response.status) or
|
|
100
|
+
exception_class = EXCEPTION_CODES.get(response.status) or Error
|
|
93
101
|
raise exception_class("Error {0} - {1}".format(response.status, result["error"]["message"]))
|
|
94
102
|
|
|
95
103
|
return Response(result, response)
|
|
@@ -0,0 +1,289 @@
|
|
|
1
|
+
# Copyright Cloudinary
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
|
|
5
|
+
from six import string_types
|
|
6
|
+
|
|
7
|
+
import cloudinary
|
|
8
|
+
from cloudinary.api import only
|
|
9
|
+
from cloudinary.api_client.call_api import _call_v2_api
|
|
10
|
+
from cloudinary.utils import base64url_decode
|
|
11
|
+
|
|
12
|
+
CUSTOMER_ID_HEADER = "X-Cld-Customer-External-Id"
|
|
13
|
+
|
|
14
|
+
__COLLECTION_PARAMS = ["name", "description", "configuration"]
|
|
15
|
+
|
|
16
|
+
__PUBLIC_PAGE_PARAMS = ["name", "available_from", "expires_at", "show_download_original_option",
|
|
17
|
+
"download_transformations", "create_download_transformations"]
|
|
18
|
+
|
|
19
|
+
__PUBLIC_PAGE_UPDATE_PARAMS = __PUBLIC_PAGE_PARAMS + ["protection_mode", "allowed_emails", "allowed_domains",
|
|
20
|
+
"allow_any", "custom_layout_url"]
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def collections(**options):
|
|
24
|
+
"""
|
|
25
|
+
Lists the collections the caller may read, most-recently-updated first by default.
|
|
26
|
+
|
|
27
|
+
:param options: Additional options.
|
|
28
|
+
:keyword str query: Case-insensitive filter on the collection name. Matches collections with a
|
|
29
|
+
word in their name that starts with this text.
|
|
30
|
+
:keyword str sort_by: Sort expression, "<field> <direction>", optionally comma-separated.
|
|
31
|
+
Field is one of "name", "created_at" or "updated_at"; direction is "asc"
|
|
32
|
+
or "desc". Defaults to "updated_at desc".
|
|
33
|
+
:keyword int max_results: Maximum number of collections to return. 1-100, default 20.
|
|
34
|
+
:keyword str next_cursor: The cursor from a previous response, to fetch the next page.
|
|
35
|
+
:return: The collections, as {"data": [...], "next_cursor": ...}.
|
|
36
|
+
:rtype: Response
|
|
37
|
+
"""
|
|
38
|
+
params = only(options, "query", "sort_by", "max_results", "next_cursor")
|
|
39
|
+
return __call_collections_api("get", [], params, **options)
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def create_collection(name, **options):
|
|
43
|
+
"""
|
|
44
|
+
Creates a collection and makes the caller its owner.
|
|
45
|
+
|
|
46
|
+
:param name: The name of the collection.
|
|
47
|
+
:type name: str
|
|
48
|
+
:param options: Additional options.
|
|
49
|
+
:keyword str description: The description of the collection.
|
|
50
|
+
:keyword str type: "manual" (the default) holds an explicit asset list, populated with
|
|
51
|
+
`add_collection_assets`; "dynamic" is populated by a saved search expression.
|
|
52
|
+
:keyword dict configuration: The configuration of a dynamic collection, as
|
|
53
|
+
{"expression": "<search expression>"}. Required for "dynamic",
|
|
54
|
+
not allowed for "manual".
|
|
55
|
+
:return: The created collection.
|
|
56
|
+
:rtype: Response
|
|
57
|
+
"""
|
|
58
|
+
params = only(options, "description", "type", "configuration")
|
|
59
|
+
params["name"] = name
|
|
60
|
+
return __call_collections_api("post", [], params, **options)
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def collection(collection_id, **options):
|
|
64
|
+
"""
|
|
65
|
+
Returns a single collection.
|
|
66
|
+
|
|
67
|
+
:param collection_id: The external ID of the collection.
|
|
68
|
+
:type collection_id: str
|
|
69
|
+
:param options: Additional options.
|
|
70
|
+
:return: The collection.
|
|
71
|
+
:rtype: Response
|
|
72
|
+
"""
|
|
73
|
+
return __call_collections_api("get", [collection_id], {}, **options)
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def update_collection(collection_id, **options):
|
|
77
|
+
"""
|
|
78
|
+
Updates a collection. Only the fields passed are changed. The type of a collection can't be changed.
|
|
79
|
+
|
|
80
|
+
:param collection_id: The external ID of the collection.
|
|
81
|
+
:type collection_id: str
|
|
82
|
+
:param options: Additional options.
|
|
83
|
+
:keyword str name: The new name of the collection.
|
|
84
|
+
:keyword str description: The new description of the collection.
|
|
85
|
+
:keyword dict configuration: The new configuration of a dynamic collection, as
|
|
86
|
+
{"expression": "<search expression>"}.
|
|
87
|
+
:return: The updated collection.
|
|
88
|
+
:rtype: Response
|
|
89
|
+
"""
|
|
90
|
+
params = only(options, *__COLLECTION_PARAMS)
|
|
91
|
+
return __call_collections_api("put", [collection_id], params, **options)
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def delete_collection(collection_id, **options):
|
|
95
|
+
"""
|
|
96
|
+
Deletes a collection. The assets in it are not modified or deleted.
|
|
97
|
+
|
|
98
|
+
:param collection_id: The external ID of the collection.
|
|
99
|
+
:type collection_id: str
|
|
100
|
+
:param options: Additional options.
|
|
101
|
+
:return: A dictionary with a "message" key.
|
|
102
|
+
:rtype: Response
|
|
103
|
+
"""
|
|
104
|
+
return __call_collections_api("delete", [collection_id], {}, **options)
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def collection_assets(collection_id, **options):
|
|
108
|
+
"""
|
|
109
|
+
Lists the assets in a collection that the caller may view, newest first by default.
|
|
110
|
+
|
|
111
|
+
:param collection_id: The external ID of the collection.
|
|
112
|
+
:type collection_id: str
|
|
113
|
+
:param options: Additional options.
|
|
114
|
+
:keyword str sort_by: Sort expression, "<field> <direction>", optionally comma-separated.
|
|
115
|
+
Field is one of "created_at", "uploaded_at", "public_id", "display_name"
|
|
116
|
+
or "bytes"; direction is "asc" or "desc". Defaults to "created_at desc".
|
|
117
|
+
Send the same sort_by on every page.
|
|
118
|
+
:keyword int max_results: Maximum number of assets to return. 1-100, default 20.
|
|
119
|
+
:keyword str next_cursor: The cursor from a previous response, to fetch the next page.
|
|
120
|
+
:return: The assets, as {"data": [...], "next_cursor": ...}.
|
|
121
|
+
:rtype: Response
|
|
122
|
+
"""
|
|
123
|
+
params = only(options, "sort_by", "max_results", "next_cursor")
|
|
124
|
+
return __call_collections_api("get", [collection_id, "assets"], params, **options)
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
def add_collection_assets(collection_id, asset_ids, **options):
|
|
128
|
+
"""
|
|
129
|
+
Adds assets to a collection.
|
|
130
|
+
|
|
131
|
+
:param collection_id: The external ID of the collection.
|
|
132
|
+
:type collection_id: str
|
|
133
|
+
:param asset_ids: The asset IDs of the assets to add. A single ID may be passed as a string.
|
|
134
|
+
:type asset_ids: list[str] or str
|
|
135
|
+
:param options: Additional options.
|
|
136
|
+
:return: The collection.
|
|
137
|
+
:rtype: Response
|
|
138
|
+
"""
|
|
139
|
+
return __call_collections_api("post", [collection_id, "assets"], __asset_ids_list(asset_ids), **options)
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
def remove_collection_assets(collection_id, asset_ids, **options):
|
|
143
|
+
"""
|
|
144
|
+
Removes assets from a collection. Assets not in the collection are ignored, and the assets
|
|
145
|
+
themselves are not modified or deleted.
|
|
146
|
+
|
|
147
|
+
:param collection_id: The external ID of the collection.
|
|
148
|
+
:type collection_id: str
|
|
149
|
+
:param asset_ids: The asset IDs of the assets to remove. A single ID may be passed as a string.
|
|
150
|
+
:type asset_ids: list[str] or str
|
|
151
|
+
:param options: Additional options.
|
|
152
|
+
:return: The collection.
|
|
153
|
+
:rtype: Response
|
|
154
|
+
"""
|
|
155
|
+
return __call_collections_api("delete", [collection_id, "assets"], __asset_ids_list(asset_ids), **options)
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
def create_collection_public_page(collection_id, **options):
|
|
159
|
+
"""
|
|
160
|
+
Publishes a collection as a shareable public page.
|
|
161
|
+
|
|
162
|
+
:param collection_id: The external ID of the collection.
|
|
163
|
+
:type collection_id: str
|
|
164
|
+
:param options: Additional options.
|
|
165
|
+
:keyword str name: The name of the public page.
|
|
166
|
+
:keyword str available_from: The date-time (ISO 8601) the page becomes available.
|
|
167
|
+
:keyword str expires_at: The date-time (ISO 8601) the page expires.
|
|
168
|
+
:keyword bool show_download_original_option: Whether viewers can download the original assets.
|
|
169
|
+
:keyword list download_transformations: Named transformations viewers can apply when downloading,
|
|
170
|
+
as [{"name": "<name>", "types": ["image", ...]}, ...].
|
|
171
|
+
At most 200.
|
|
172
|
+
:keyword bool create_download_transformations: Whether to create any listed transformation name
|
|
173
|
+
that isn't a transformation template yet.
|
|
174
|
+
:return: The created public page, including its URL.
|
|
175
|
+
:rtype: Response
|
|
176
|
+
"""
|
|
177
|
+
params = only(options, *__PUBLIC_PAGE_PARAMS)
|
|
178
|
+
return __call_collections_api("post", [collection_id, "public_pages"], params, **options)
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
def collection_public_pages(collection_id, **options):
|
|
182
|
+
"""
|
|
183
|
+
Lists the public pages of a collection.
|
|
184
|
+
|
|
185
|
+
:param collection_id: The external ID of the collection.
|
|
186
|
+
:type collection_id: str
|
|
187
|
+
:param options: Additional options.
|
|
188
|
+
:return: The public pages, as {"data": [...]}.
|
|
189
|
+
:rtype: Response
|
|
190
|
+
"""
|
|
191
|
+
return __call_collections_api("get", [collection_id, "public_pages"], {}, **options)
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
def collection_public_page(collection_id, public_page_id, **options):
|
|
195
|
+
"""
|
|
196
|
+
Returns a single public page of a collection.
|
|
197
|
+
|
|
198
|
+
:param collection_id: The external ID of the collection.
|
|
199
|
+
:type collection_id: str
|
|
200
|
+
:param public_page_id: The external ID of the public page.
|
|
201
|
+
:type public_page_id: str
|
|
202
|
+
:param options: Additional options.
|
|
203
|
+
:return: The public page.
|
|
204
|
+
:rtype: Response
|
|
205
|
+
"""
|
|
206
|
+
return __call_collections_api("get", [collection_id, "public_pages", public_page_id], {}, **options)
|
|
207
|
+
|
|
208
|
+
|
|
209
|
+
def update_collection_public_page(collection_id, public_page_id, **options):
|
|
210
|
+
"""
|
|
211
|
+
Updates the settings of a public page of a collection.
|
|
212
|
+
|
|
213
|
+
:param collection_id: The external ID of the collection.
|
|
214
|
+
:type collection_id: str
|
|
215
|
+
:param public_page_id: The external ID of the public page.
|
|
216
|
+
:type public_page_id: str
|
|
217
|
+
:param options: Additional options. Accepts the keywords of `create_collection_public_page`, and:
|
|
218
|
+
:keyword str protection_mode: "none" or "email_otp". "email_otp" requires the secured-sharing
|
|
219
|
+
feature on the plan.
|
|
220
|
+
:keyword list allowed_emails: The emails allowed to open a protected page.
|
|
221
|
+
:keyword list allowed_domains: The email domains allowed to open a protected page.
|
|
222
|
+
:keyword bool allow_any: Whether any email may open a protected page.
|
|
223
|
+
:keyword str custom_layout_url: The URL of a custom layout for the page.
|
|
224
|
+
:return: The updated public page.
|
|
225
|
+
:rtype: Response
|
|
226
|
+
"""
|
|
227
|
+
params = only(options, *__PUBLIC_PAGE_UPDATE_PARAMS)
|
|
228
|
+
return __call_collections_api("put", [collection_id, "public_pages", public_page_id], params, **options)
|
|
229
|
+
|
|
230
|
+
|
|
231
|
+
def delete_collection_public_page(collection_id, public_page_id, **options):
|
|
232
|
+
"""
|
|
233
|
+
Unpublishes a public page of a collection. The collection and its assets are not changed.
|
|
234
|
+
|
|
235
|
+
:param collection_id: The external ID of the collection.
|
|
236
|
+
:type collection_id: str
|
|
237
|
+
:param public_page_id: The external ID of the public page.
|
|
238
|
+
:type public_page_id: str
|
|
239
|
+
:param options: Additional options.
|
|
240
|
+
:return: A dictionary with a "message" key.
|
|
241
|
+
:rtype: Response
|
|
242
|
+
"""
|
|
243
|
+
return __call_collections_api("delete", [collection_id, "public_pages", public_page_id], {}, **options)
|
|
244
|
+
|
|
245
|
+
|
|
246
|
+
def __asset_ids_list(asset_ids):
|
|
247
|
+
"""
|
|
248
|
+
Wraps a single asset ID in a list.
|
|
249
|
+
|
|
250
|
+
:internal
|
|
251
|
+
"""
|
|
252
|
+
return [asset_ids] if isinstance(asset_ids, string_types) else list(asset_ids)
|
|
253
|
+
|
|
254
|
+
|
|
255
|
+
def __call_collections_api(method, uri, params, **options):
|
|
256
|
+
"""
|
|
257
|
+
Private function that assists with performing an API call to the collections endpoints.
|
|
258
|
+
|
|
259
|
+
:param method: The HTTP method. Valid methods: get, post, put, delete
|
|
260
|
+
:param uri: REST endpoint of the API (without 'collections', the module or the cloud name)
|
|
261
|
+
:param params: Query/body parameters passed to the method
|
|
262
|
+
:param options: Additional options
|
|
263
|
+
:rtype: Response
|
|
264
|
+
:internal
|
|
265
|
+
"""
|
|
266
|
+
oauth_token = options.get("oauth_token", cloudinary.config().oauth_token)
|
|
267
|
+
customer_id = __customer_external_id(oauth_token) if oauth_token else None
|
|
268
|
+
if customer_id:
|
|
269
|
+
extra_headers = {CUSTOMER_ID_HEADER: customer_id}
|
|
270
|
+
extra_headers.update(options.get("extra_headers") or {})
|
|
271
|
+
options["extra_headers"] = extra_headers
|
|
272
|
+
|
|
273
|
+
return _call_v2_api(method, ["collections"] + uri, params, module="asset_management", **options)
|
|
274
|
+
|
|
275
|
+
|
|
276
|
+
def __customer_external_id(oauth_token):
|
|
277
|
+
"""
|
|
278
|
+
Reads ext.customer_external_id from an unverified JWT payload.
|
|
279
|
+
|
|
280
|
+
:param oauth_token: The OAuth access token.
|
|
281
|
+
:return: The customer external ID, or None if absent or unreadable.
|
|
282
|
+
:internal
|
|
283
|
+
"""
|
|
284
|
+
try:
|
|
285
|
+
value = json.loads(base64url_decode(oauth_token.split(".")[1]))["ext"]["customer_external_id"]
|
|
286
|
+
except (IndexError, KeyError, TypeError, ValueError):
|
|
287
|
+
return None
|
|
288
|
+
|
|
289
|
+
return value if isinstance(value, string_types) and value else None
|
|
@@ -40,6 +40,7 @@ directory of the repository.
|
|
|
40
40
|
- [Transform and deliver an image](transform-and-deliver-image.md)
|
|
41
41
|
- [Transform and deliver a video](transform-and-deliver-video.md)
|
|
42
42
|
- [Search and manage assets](search-and-manage-assets.md)
|
|
43
|
+
- [Organize and share collections](organize-and-share-collections.md)
|
|
43
44
|
- [Moderate an upload](moderate-upload.md)
|
|
44
45
|
- [Use structured metadata](use-structured-metadata.md)
|
|
45
46
|
- [Use with Django](use-with-django.md)
|
|
@@ -14,7 +14,8 @@ Import the submodules you use explicitly. `import cloudinary` on its own gives y
|
|
|
14
14
|
`config()`, `CloudinaryImage`, `CloudinaryVideo`, `Search`, and `cloudinary.utils`,
|
|
15
15
|
but **not** `cloudinary.uploader` or `cloudinary.api` — reaching for those raises
|
|
16
16
|
`AttributeError: module 'cloudinary' has no attribute 'uploader'` until you import
|
|
17
|
-
them.
|
|
17
|
+
them. The same applies to `cloudinary.collections` — use
|
|
18
|
+
`from cloudinary import collections`.
|
|
18
19
|
|
|
19
20
|
Calls are synchronous and return plain dictionaries. Positional arguments come first,
|
|
20
21
|
then keyword options that pass through to the API:
|
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
# Organize and share collections
|
|
2
|
+
|
|
3
|
+
## When to use
|
|
4
|
+
|
|
5
|
+
Grouping existing assets into a named set, and sharing that set through a public link.
|
|
6
|
+
|
|
7
|
+
A collection references assets; it does not move, copy, or change them. Pick the right
|
|
8
|
+
tool:
|
|
9
|
+
|
|
10
|
+
- **Collection** (`cloudinary.collections`) — a curated set you name, list, and share as a
|
|
11
|
+
public page. An asset can be in many collections.
|
|
12
|
+
- **Tag** (`uploader.add_tag`) — a label for filtering and bulk operations. No sharing.
|
|
13
|
+
- **Folder** (`api.create_folder`) — where an asset lives. An asset has exactly one.
|
|
14
|
+
- **Search** (`cloudinary.Search`) — a one-off query. Nothing is saved.
|
|
15
|
+
|
|
16
|
+
There are two types of collection, fixed at creation:
|
|
17
|
+
|
|
18
|
+
- **manual** (the default) — holds exactly the assets you add and remove.
|
|
19
|
+
- **dynamic** — holds whatever matches a saved search expression, and updates itself as
|
|
20
|
+
assets start or stop matching. You cannot add or remove assets by hand.
|
|
21
|
+
|
|
22
|
+
## Rules
|
|
23
|
+
|
|
24
|
+
- `cloudinary.collections` must be imported explicitly. `import cloudinary` alone does not
|
|
25
|
+
load it.
|
|
26
|
+
- Every response wraps its payload in `"data"`: `result["data"]["id"]`, not `result["id"]`.
|
|
27
|
+
- Add and remove assets by `asset_id`. A `public_id` fails with `NotFound: Error 404 -
|
|
28
|
+
Unknown resource`.
|
|
29
|
+
- Store the collection `id` and the public page `id`. Every other call takes them.
|
|
30
|
+
- Never pass `None` as an ID. The SDK drops it from the URL and calls a different endpoint:
|
|
31
|
+
`collections.collection(None)` lists all collections instead of failing.
|
|
32
|
+
|
|
33
|
+
## Complete flow
|
|
34
|
+
|
|
35
|
+
```python
|
|
36
|
+
import cloudinary
|
|
37
|
+
import cloudinary.exceptions
|
|
38
|
+
import cloudinary.uploader
|
|
39
|
+
from cloudinary import collections # must be imported explicitly
|
|
40
|
+
|
|
41
|
+
# Configuration is read from CLOUDINARY_URL automatically.
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def main():
|
|
45
|
+
asset = cloudinary.uploader.upload("https://res.cloudinary.com/demo/image/upload/sample.jpg")
|
|
46
|
+
|
|
47
|
+
collection = collections.create_collection(
|
|
48
|
+
"Summer campaign",
|
|
49
|
+
description="Approved images for the launch",
|
|
50
|
+
)["data"]
|
|
51
|
+
|
|
52
|
+
collections.add_collection_assets(collection["id"], [asset["asset_id"]])
|
|
53
|
+
|
|
54
|
+
page = collections.create_collection_public_page(collection["id"], name="Launch preview")["data"]
|
|
55
|
+
print(page["url"]) # https://collection.cloudinary.com/<cloud_name>/<page_id>
|
|
56
|
+
|
|
57
|
+
return collection["id"], page["id"]
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
if __name__ == "__main__":
|
|
61
|
+
try:
|
|
62
|
+
main()
|
|
63
|
+
except cloudinary.exceptions.Error as error:
|
|
64
|
+
print("Collections call failed: {0}".format(error))
|
|
65
|
+
raise SystemExit(1)
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
## Result fields to keep
|
|
69
|
+
|
|
70
|
+
| Call | Returns | Keep |
|
|
71
|
+
|---|---|---|
|
|
72
|
+
| `create_collection`, `update_collection`, `add_collection_assets`, `remove_collection_assets` | `{"data": {collection}}` | `data.id` |
|
|
73
|
+
| `collection` | `{"data": {collection}}`, plus `asset_count` and `cover_asset_url` | |
|
|
74
|
+
| `collections`, `collection_assets` | `{"data": [...], "total_count": N, "next_cursor": "..."}` | `next_cursor` |
|
|
75
|
+
| `create_collection_public_page`, `update_collection_public_page`, `collection_public_page` | `{"data": {page}}` | `data.id`, `data.url` |
|
|
76
|
+
| `collection_public_pages` | `{"data": [...]}` | |
|
|
77
|
+
| `delete_collection`, `delete_collection_public_page` | `{"message": "..."}` | |
|
|
78
|
+
|
|
79
|
+
A collection has `id`, `name`, `description`, `type`, `configuration`, `created_at`, and
|
|
80
|
+
`updated_at`. An asset in `collection_assets` has `asset_id`, `public_id`, `display_name`,
|
|
81
|
+
`resource_type`, `type`, `format`, `bytes`, `width`, `height`, `secure_url`, and
|
|
82
|
+
`created_at`.
|
|
83
|
+
|
|
84
|
+
## Managing assets in a manual collection
|
|
85
|
+
|
|
86
|
+
```python
|
|
87
|
+
collections.add_collection_assets(collection_id, ["asset_id_1", "asset_id_2"])
|
|
88
|
+
collections.add_collection_assets(collection_id, "asset_id_3") # a single ID works too
|
|
89
|
+
collections.remove_collection_assets(collection_id, ["asset_id_1"])
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
Adding an asset that is already in the collection succeeds and changes nothing. Removing an
|
|
93
|
+
asset does not delete it.
|
|
94
|
+
|
|
95
|
+
Changes can take a few seconds to show up in `collection_assets`. For read-after-write, poll
|
|
96
|
+
until the asset appears instead of reading once.
|
|
97
|
+
|
|
98
|
+
## Dynamic collections
|
|
99
|
+
|
|
100
|
+
```python
|
|
101
|
+
collection = collections.create_collection(
|
|
102
|
+
"Summer images",
|
|
103
|
+
type="dynamic",
|
|
104
|
+
configuration={"expression": "tags=summer AND resource_type:image"},
|
|
105
|
+
)["data"]
|
|
106
|
+
|
|
107
|
+
collections.update_collection(collection["id"], configuration={"expression": "tags=summer"})
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
The expression uses the same syntax as `cloudinary.Search`. A new dynamic collection takes a few seconds to populate.
|
|
111
|
+
|
|
112
|
+
## Listing and paging
|
|
113
|
+
|
|
114
|
+
```python
|
|
115
|
+
cursor = None
|
|
116
|
+
while True:
|
|
117
|
+
page = collections.collection_assets(collection_id, sort_by="created_at desc",
|
|
118
|
+
max_results=100, next_cursor=cursor)
|
|
119
|
+
for asset in page["data"]:
|
|
120
|
+
print(asset["asset_id"], asset["secure_url"])
|
|
121
|
+
cursor = page.get("next_cursor")
|
|
122
|
+
if not cursor:
|
|
123
|
+
break
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
`next_cursor` is absent on the last page; read it with `.get()`. Send the same `sort_by` on
|
|
127
|
+
every page — a cursor is only valid for the sort it was issued with, and the server does not
|
|
128
|
+
reject a mismatch.
|
|
129
|
+
|
|
130
|
+
| Option | `collections` | `collection_assets` |
|
|
131
|
+
|---|---|---|
|
|
132
|
+
| `max_results` | 1-100, default 20 | 1-100, default 20 |
|
|
133
|
+
| `sort_by` | `name`, `created_at`, `updated_at` + `asc`/`desc`; default `updated_at desc` | `created_at`, `uploaded_at`, `public_id`, `display_name`, `bytes` + `asc`/`desc`; default `created_at desc` |
|
|
134
|
+
| `query` | matches a word in the name that starts with the text: `"sum"` matches "Summer campaign", `"umm"` does not | — |
|
|
135
|
+
|
|
136
|
+
Combine sort keys with commas: `sort_by="name asc, created_at desc"`.
|
|
137
|
+
|
|
138
|
+
## Sharing with public pages
|
|
139
|
+
|
|
140
|
+
```python
|
|
141
|
+
page = collections.create_collection_public_page(
|
|
142
|
+
collection_id,
|
|
143
|
+
name="Press kit",
|
|
144
|
+
expires_at="2026-12-31T00:00:00Z",
|
|
145
|
+
show_download_original_option=False,
|
|
146
|
+
download_transformations=[{"name": "thumbnail_400", "types": ["image"]}],
|
|
147
|
+
create_download_transformations=True,
|
|
148
|
+
)["data"]
|
|
149
|
+
|
|
150
|
+
collections.update_collection_public_page(collection_id, page["id"], name="Press kit (final)")
|
|
151
|
+
collections.collection_public_pages(collection_id)
|
|
152
|
+
collections.delete_collection_public_page(collection_id, page["id"]) # unpublish
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
Anyone with `page["url"]` can open the page until it expires or is unpublished. Unpublishing
|
|
156
|
+
does not change the collection or its assets.
|
|
157
|
+
|
|
158
|
+
- `download_transformations` must name existing named transformations, or the call fails with
|
|
159
|
+
`BadRequest: Error 400 - Unknown named transformation '<name>'`. Create one with
|
|
160
|
+
`cloudinary.api.create_transformation("thumbnail_400", "c_thumb,w_400")`.
|
|
161
|
+
- The first time a named transformation is offered for download, pass
|
|
162
|
+
`create_download_transformations=True`. Without it the call fails with
|
|
163
|
+
`BadRequest: Error 400 - Named transformation '<name>' is not available for download yet`.
|
|
164
|
+
- `types` (`["image"]`, `["video"]`, or omitted for both) is fixed the first time a name is
|
|
165
|
+
offered for download, on any public page. Later requests must omit it or send the same
|
|
166
|
+
list.
|
|
167
|
+
- `show_download_original_option` defaults to `True`.
|
|
168
|
+
- Restricting a page to specific viewers is update-only:
|
|
169
|
+
`update_collection_public_page(..., protection_mode="email_otp", allowed_emails=[...],
|
|
170
|
+
allowed_domains=[...])`. It needs the secured-sharing feature on your plan, or it fails
|
|
171
|
+
with `NotAllowed: Error 403 - Feature is not allowed`.
|
|
172
|
+
|
|
173
|
+
## Updating and deleting
|
|
174
|
+
|
|
175
|
+
```python
|
|
176
|
+
collections.update_collection(collection_id, name="Summer campaign (final)")
|
|
177
|
+
collections.delete_collection(collection_id)
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
Updates change only the fields you pass: `name`, `description`, and, for dynamic collections,
|
|
181
|
+
`configuration`. The type cannot change; `type` is ignored on update. Deleting a collection
|
|
182
|
+
does not delete its assets.
|
|
183
|
+
|
|
184
|
+
## Authentication and plans
|
|
185
|
+
|
|
186
|
+
The methods work with an API key and secret, or with an OAuth token
|
|
187
|
+
(`cloudinary.config(oauth_token=...)`, scope `asset_management`).
|
|
188
|
+
|
|
189
|
+
Collections are a premium Assets Enterprise feature. On the Assets Free plan you can have at
|
|
190
|
+
most 3 collections, with up to 50 assets each. Dynamic collections and protected public pages
|
|
191
|
+
must be enabled for your product environment.
|
|
192
|
+
|
|
193
|
+
## Troubleshooting
|
|
194
|
+
|
|
195
|
+
- `NotAllowed: Error 403 - Cannot have more than 3 classic collections` — the plan's
|
|
196
|
+
collection limit. Delete a collection or upgrade.
|
|
197
|
+
- `NotAllowed: Error 403 - Feature is not allowed` — dynamic collections or `email_otp`
|
|
198
|
+
protection are not enabled for your product environment.
|
|
199
|
+
- ``NotFound: Error 404 - We can`t find a collection with this id ...`` — wrong or deleted
|
|
200
|
+
collection ID.
|
|
201
|
+
- `NotFound: Error 404 - Unknown resource` from add or remove — you passed a `public_id`, or
|
|
202
|
+
an `asset_id` that does not exist.
|
|
203
|
+
- `NotAcceptable: Error 406 - unsupported operation for dynamic collection` — you tried to add
|
|
204
|
+
or remove assets in a dynamic collection. Change its expression instead.
|
|
205
|
+
- `BadRequest: Error 400 - dynamic collections require a configuration with a non-empty
|
|
206
|
+
expression` — `type="dynamic"` without `configuration`.
|
|
207
|
+
- `BadRequest: Error 400 - configuration is only allowed for dynamic collections` —
|
|
208
|
+
`configuration` on a manual collection.
|
|
209
|
+
- `BadRequest: Error 400 - Unknown named transformation ...` or `... is not available for
|
|
210
|
+
download yet` — see [Sharing with public pages](#sharing-with-public-pages).
|
|
211
|
+
- `BadRequest: Error 400 - invalid sort_by field ...` or `... max_results ...` — see the
|
|
212
|
+
options table above.
|
|
213
|
+
- An asset you just added is missing from `collection_assets` — wait a few seconds and read
|
|
214
|
+
again.
|
|
215
|
+
- A call returned every collection instead of one — the ID was `None`.
|
|
216
|
+
|
|
217
|
+
## Related
|
|
218
|
+
|
|
219
|
+
- Runnable example: `examples/organize-and-share-collections.py`
|
|
220
|
+
- [Search and manage assets](search-and-manage-assets.md) — finding the `asset_id` values to
|
|
221
|
+
add, and the expression syntax for dynamic collections.
|
|
222
|
+
- [Import and call the SDK](import-and-call.md)
|
|
@@ -99,6 +99,7 @@ URL building is local: no network call, no `api_secret`.
|
|
|
99
99
|
| Read, update, restore, or delete an asset; manage folders and presets | `api` — the Assets Admin API | [Search and manage assets](search-and-manage-assets.md) |
|
|
100
100
|
| Attach and query typed metadata fields | `api` + upload options | [Use structured metadata](use-structured-metadata.md) |
|
|
101
101
|
| Find visually similar assets | `api.visual_search` — needs the feature enabled | [Visual Search](https://cloudinary.com/documentation/visual_search.md) |
|
|
102
|
+
| Group assets into manual or dynamic collections and share them as public pages | `collections` — import explicitly, needs Collections on your plan | [Organize and share collections](organize-and-share-collections.md) |
|
|
102
103
|
|
|
103
104
|
## Analyze
|
|
104
105
|
|
|
@@ -10,9 +10,12 @@ Failures raise exceptions from `cloudinary.exceptions`, mapped from the HTTP sta
|
|
|
10
10
|
| `AuthorizationRequired` | 401 | Missing or wrong credentials, bad signature |
|
|
11
11
|
| `NotAllowed` | 403 | Valid credentials without permission for this operation |
|
|
12
12
|
| `NotFound` | 404 | No such asset, folder, or preset |
|
|
13
|
+
| `NotAcceptable` | 406 | The operation is not supported for this resource (for example, adding assets to a dynamic collection) |
|
|
13
14
|
| `AlreadyExists` | 409 | The resource exists (duplicate `external_id`, folder) |
|
|
14
15
|
| `RateLimited` | 420 / 429 | Hourly API limit reached, **or** an add-on you are not subscribed to |
|
|
16
|
+
| `Locked` | 423 | The asset is still processing |
|
|
15
17
|
| `GeneralError` | 500 / 503 | A server-side failure |
|
|
18
|
+
| `Error` | any other status | |
|
|
16
19
|
|
|
17
20
|
All of them subclass `cloudinary.exceptions.Error`, so one handler catches everything:
|
|
18
21
|
|
|
@@ -126,9 +129,7 @@ uploading a large video, or while an eager or add-on-driven transformation is st
|
|
|
126
129
|
running. This is transient: retry with backoff rather than treating it as a failure. For
|
|
127
130
|
long jobs, prefer `eager_async=True` with a `notification_url` over polling.
|
|
128
131
|
|
|
129
|
-
|
|
130
|
-
`Exception` rather than a `cloudinary.exceptions.Error` subclass — a handler that catches
|
|
131
|
-
only `cloudinary.exceptions.Error` will miss it.
|
|
132
|
+
It raises `cloudinary.exceptions.Locked`, so you can retry on that class specifically.
|
|
132
133
|
|
|
133
134
|
### A transformation is missing from the generated URL
|
|
134
135
|
|