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
4 changes: 2 additions & 2 deletions cloudinary/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -450,7 +450,7 @@ def update(public_id, **options):
params = only(options, "moderation_status", "raw_convert",
"quality_override", "ocr",
"categorization", "detection", "similarity_search",
"background_removal", "notification_url")
"background_removal", "notification_url", "batch_id")
if "tags" in options:
params["tags"] = ",".join(utils.build_array(options["tags"]))
if "face_coordinates" in options:
Expand Down Expand Up @@ -1567,7 +1567,7 @@ def __delete_resource_params(options, **params):
:internal
"""
p = dict(transformations=utils.build_eager(options.get('transformations')),
**only(options, "keep_original", "next_cursor", "invalidate", "batch_id"))
**only(options, "keep_original", "next_cursor", "invalidate", "batch_id", "notification_url"))
p.update(params)
return p

Expand Down
51 changes: 46 additions & 5 deletions cloudinary/uploader.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,9 @@ def is_appengine_sandbox():

UPLOAD_LARGE_CHUNK_SIZE = 20000000

# The tags, context and metadata endpoints accept these params but omit them from signature
_NON_SIGNABLE_NOTIFICATION_PARAMS = ("notification_url", "batch_id")


def upload(file, **options):
"""
Expand Down Expand Up @@ -378,14 +381,19 @@ def destroy(public_id, **options):
:param options: Additional options for the deletion.
:keyword str type: The storage type (upload, private, authenticated).
:keyword bool invalidate: Invalidate cached copies on the CDN if True.
:keyword str notification_url: A URL to notify when the deletion is completed.
:keyword str batch_id: Correlation key for the completion notification, used to address it
when polling.
:return: The result of the API call.
:rtype: dict
"""
params = {
"timestamp": utils.now(),
"type": options.get("type"),
"invalidate": options.get("invalidate"),
"public_id": public_id
"public_id": public_id,
"notification_url": options.get("notification_url"),
"batch_id": options.get("batch_id")
}
return call_api("destroy", params, **options)

Expand All @@ -407,6 +415,9 @@ def rename(from_public_id, to_public_id, **options):
:keyword str to_type: Change the resource to the specified upload type.
:keyword dict context: Set or update contextual metadata.
:keyword dict metadata: Set or update structured metadata.
:keyword str notification_url: A URL to notify when the rename is completed.
:keyword str batch_id: Correlation key for the completion notification, used to address it
when polling.
:return: The result of the API call.
:rtype: dict
"""
Expand All @@ -419,7 +430,9 @@ def rename(from_public_id, to_public_id, **options):
"to_public_id": to_public_id,
"to_type": options.get("to_type"),
"context": options.get("context"),
"metadata": options.get("metadata")
"metadata": options.get("metadata"),
"notification_url": options.get("notification_url"),
"batch_id": options.get("batch_id")
}
return call_api("rename", params, **options)

Expand All @@ -438,6 +451,9 @@ def update_metadata(metadata, public_ids, **options):
:keyword str resource_type: The resource type (image, raw, video). Default="image".
:keyword str type: The storage type (upload, private, authenticated).
:keyword bool clear_invalid: If True, remove keys that are not valid.
:keyword str notification_url: A URL to notify when the update is completed.
:keyword str batch_id: Correlation key for the completion notification, used to address it
when polling.
:return: A list of public IDs that were updated.
:rtype: dict
"""
Expand All @@ -446,8 +462,11 @@ def update_metadata(metadata, public_ids, **options):
"metadata": utils.encode_context(metadata),
"public_ids": utils.build_array(public_ids),
"type": options.get("type"),
"clear_invalid": options.get("clear_invalid")
"clear_invalid": options.get("clear_invalid"),
"notification_url": options.get("notification_url"),
"batch_id": options.get("batch_id")
}
options["non_signable"] = _NON_SIGNABLE_NOTIFICATION_PARAMS
return call_api("metadata", params, **options)


Expand Down Expand Up @@ -509,6 +528,8 @@ def generate_sprite(tag=None, urls=None, **options):
:param urls: List of URLs to create a sprite from (only if tag not set).
:type urls: list[str], optional
:param options: Additional sprite configuration.
:keyword str public_id: The public ID to assign to the generated sprite. When omitted, the
server derives one from the tag or URLs. Cannot be used with mode=download.
:return: Dictionary with metadata and URLs of generated sprite resources.
:rtype: dict
"""
Expand Down Expand Up @@ -543,6 +564,8 @@ def multi(tag=None, urls=None, **options):
:param urls: A list of image URLs (if no tag is set).
:type urls: list[str], optional
:param options: Additional multi-configuration options.
:keyword str public_id: The public ID to assign to the generated file. When omitted, the server
derives one from the tag or URLs. Cannot be used with mode=download.
:return: Dictionary with metadata and URLs of the generated file.
:rtype: dict
"""
Expand Down Expand Up @@ -575,6 +598,8 @@ def explode(public_id, **options):
:param public_id: The public ID of the file to explode.
:type public_id: str
:param options: Additional explode options (format, notification_url, batch_id, transformation).
:keyword str type: The storage type of the file to explode (upload, private, authenticated).
Default=upload.
:keyword str batch_id: Correlation key for the completion notification, used to address it
when polling.
:return: The result of the API call.
Expand All @@ -584,6 +609,7 @@ def explode(public_id, **options):
"timestamp": utils.now(),
"public_id": public_id,
"format": options.get("format"),
"type": options.get("type"),
"notification_url": options.get("notification_url"),
"batch_id": options.get("batch_id"),
"transformation": utils.generate_transformation_string(**options)[0]
Expand Down Expand Up @@ -705,6 +731,9 @@ def call_tags_api(tag, command, public_ids=None, **options):
:param public_ids: A list of asset public IDs.
:type public_ids: list[str], optional
:param options: Additional options (e.g., type).
:keyword str notification_url: A URL to notify when the update is completed.
:keyword str batch_id: Correlation key for the completion notification, used to address it
when polling.
:return: The result of the API call.
:rtype: dict
"""
Expand All @@ -713,8 +742,11 @@ def call_tags_api(tag, command, public_ids=None, **options):
"tag": tag,
"public_ids": utils.build_array(public_ids),
"command": command,
"type": options.get("type")
"type": options.get("type"),
"notification_url": options.get("notification_url"),
"batch_id": options.get("batch_id")
}
options["non_signable"] = _NON_SIGNABLE_NOTIFICATION_PARAMS
return call_api("tags", params, **options)


Expand All @@ -731,6 +763,9 @@ def call_context_api(context, command, public_ids=None, **options):
:param public_ids: A list of asset public IDs.
:type public_ids: list[str], optional
:param options: Additional options (e.g., type).
:keyword str notification_url: A URL to notify when the update is completed.
:keyword str batch_id: Correlation key for the completion notification, used to address it
when polling.
:return: The result of the API call.
:rtype: dict
"""
Expand All @@ -739,8 +774,11 @@ def call_context_api(context, command, public_ids=None, **options):
"context": utils.encode_context(context),
"public_ids": utils.build_array(public_ids),
"command": command,
"type": options.get("type")
"type": options.get("type"),
"notification_url": options.get("notification_url"),
"batch_id": options.get("batch_id")
}
options["non_signable"] = _NON_SIGNABLE_NOTIFICATION_PARAMS
return call_api("context", params, **options)


Expand Down Expand Up @@ -778,6 +816,7 @@ def text(text, **options):

_SLIDESHOW_PARAMS = [
"notification_url",
"batch_id",
"public_id",
"overwrite",
"upload_preset",
Expand All @@ -791,6 +830,8 @@ def create_slideshow(**options):
:param options: Additional parameters for the slideshow creation.
:keyword str resource_type: The resource type, defaults to "video".
:keyword str notification_url: A URL to be notified when the processing is completed.
:keyword str batch_id: Correlation key for the completion notification, used to address it
when polling.
:keyword str public_id: The public ID to assign to the generated slideshow.
:keyword bool overwrite: Whether to overwrite the slideshow if public_id already exists.
:keyword str upload_preset: An upload preset to apply to the slideshow creation.
Expand Down
15 changes: 14 additions & 1 deletion cloudinary/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -629,6 +629,16 @@ def json_body(params, headers=None):


def sign_request(params, options):
"""
Signs Upload API request parameters.

Keys listed in options["non_signable"] are still sent, but left out of the signature.

:param params: Params to sign and send.
:param options: Additional options, including non_signable.
:return: The params, with signature and api_key added.
:internal
"""
api_key = options.get("api_key", cloudinary.config().api_key)
if not api_key:
raise ValueError("Must supply api_key")
Expand All @@ -637,9 +647,11 @@ def sign_request(params, options):
raise ValueError("Must supply api_secret")
signature_algorithm = options.get("signature_algorithm", cloudinary.config().signature_algorithm)
signature_version = options.get("signature_version", cloudinary.config().signature_version)
non_signable = options.get("non_signable") or ()

params = cleanup_params(params)
params["signature"] = api_sign_request(params, api_secret, signature_algorithm, signature_version)
params_to_sign = params if not non_signable else dict((k, v) for k, v in params.items() if k not in non_signable)
params["signature"] = api_sign_request(params_to_sign, api_secret, signature_algorithm, signature_version)
params["api_key"] = api_key

return params
Expand Down Expand Up @@ -1254,6 +1266,7 @@ def build_multi_and_sprite_params(**options):
"notification_url": options.get("notification_url"),
"tag": tag,
"urls": urls,
"public_id": options.get("public_id"),
"transformation": generate_transformation_string(fetch_format=options.get("format"), **options)[0]
}
return params
Expand Down
9 changes: 6 additions & 3 deletions test/test_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -522,9 +522,11 @@ def test09c_delete_resources_by_transformations(self, mocker):
""" should allow deleting resources by transformations """
mocker.return_value = MOCK_RESPONSE

api.delete_resources(['api_test', 'api_test2'], transformations=['c_crop,w_100'])
api.delete_resources(['api_test', 'api_test2'], transformations=['c_crop,w_100'],
notification_url="http://example.com")
self.assertEqual(get_method(mocker), 'DELETE')
self.assertEqual(get_param(mocker, 'transformations'), 'c_crop,w_100')
self.assertEqual(get_param(mocker, 'notification_url'), "http://example.com")

api.delete_all_resources(transformations=['c_crop,w_100', {"crop": "scale", "width": 107}])
self.assertEqual(get_method(mocker), 'DELETE')
Expand Down Expand Up @@ -824,11 +826,12 @@ def test20_manual_moderation(self):
@patch(URLLIB3_REQUEST)
@unittest.skipUnless(cloudinary.config().api_secret, "requires api_key/api_secret")
def test21_notification_url(self, mocker):
""" should support notification_url param """
""" should support notification_url and batch_id params """
mocker.return_value = MOCK_RESPONSE
api.update("api_test", notification_url="http://example.com")
api.update("api_test", notification_url="http://example.com", batch_id="batch_1")
notification_url = get_param(mocker, 'notification_url')
self.assertEqual(notification_url, "http://example.com")
self.assertEqual(get_param(mocker, 'batch_id'), "batch_1")

@patch(URLLIB3_REQUEST)
@unittest.skipUnless(cloudinary.config().api_secret, "requires api_key/api_secret")
Expand Down
Loading
Loading