Skip to content

Commit 44a157a

Browse files
Add support for related assets APIs
1 parent 32ab0b0 commit 44a157a

File tree

3 files changed

+106
-22
lines changed

3 files changed

+106
-22
lines changed

cloudinary/api.py

Lines changed: 45 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -190,7 +190,7 @@ def _prepare_asset_details_params(**options):
190190
"""
191191
return only(options, "exif", "faces", "colors", "image_metadata", "media_metadata", "cinemagraph_analysis",
192192
"pages", "phash", "coordinates", "max_results", "quality_analysis", "derived_next_cursor",
193-
"accessibility_analysis", "versions")
193+
"accessibility_analysis", "versions", "related", "related_next_cursor")
194194

195195

196196
def update(public_id, **options):
@@ -295,6 +295,50 @@ def delete_derived_by_transformation(public_ids, transformations,
295295
return call_api("delete", uri, params, **options)
296296

297297

298+
def add_related_assets(public_id, assets_to_relate, resource_type="image", type="upload", **options):
299+
"""
300+
Relates an asset to other assets by public IDs.
301+
302+
:param public_id: The public ID of the asset to update.
303+
:type public_id: str
304+
:param assets_to_relate: The array of up to 10 fully_qualified_public_ids given as resource_type/type/public_id.
305+
:type assets_to_relate: list[str]
306+
:param type: The upload type. Defaults to "upload".
307+
:type type: str
308+
:param resource_type: The type of the resource. Defaults to "image".
309+
:type resource_type: str
310+
:param options: Additional options.
311+
:type options: dict, optional
312+
:return: The result of the command.
313+
:rtype: dict
314+
"""
315+
uri = ["resources", "related_assets", resource_type, type, public_id]
316+
params = {"assets_to_relate": utils.build_array(assets_to_relate)}
317+
return call_json_api("post", uri, params, **options)
318+
319+
320+
def delete_related_assets(public_id, assets_to_unrelate, resource_type="image", type="upload", **options):
321+
"""
322+
Unrelates an asset from other assets by public IDs.
323+
324+
:param public_id: The public ID of the asset to update.
325+
:type public_id: str
326+
:param assets_to_unrelate: The array of up to 10 fully_qualified_public_ids given as resource_type/type/public_id.
327+
:type assets_to_unrelate: list[str]
328+
:param type: The upload type.
329+
:type type: str
330+
:param resource_type: The type of the resource: defaults to "image".
331+
:type resource_type: str
332+
:param options: Additional options.
333+
:type options: dict, optional
334+
:return: The result of the command.
335+
:rtype: dict
336+
"""
337+
uri = ["resources", "related_assets", resource_type, type, public_id]
338+
params = {"assets_to_unrelate": utils.build_array(assets_to_unrelate)}
339+
return call_json_api("delete", uri, params, **options)
340+
341+
298342
def tags(**options):
299343
resource_type = options.pop("resource_type", "image")
300344
uri = ["tags", resource_type]

cloudinary/utils.py

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1579,3 +1579,19 @@ def unique(collection, key=None):
15791579
to_return[key(element)] = element
15801580

15811581
return list(to_return.values())
1582+
1583+
1584+
def fq_public_id(public_id, resource_type="image", type="upload"):
1585+
"""
1586+
Returns the fully qualified public id of form resource_type/type/public_id.
1587+
1588+
:param public_id: The public ID of the asset.
1589+
:type public_id: str
1590+
:param resource_type: The type of the asset. Defaults to "image".
1591+
:type resource_type: str
1592+
:param type: The upload type. Defaults to "upload".
1593+
:type type: str
1594+
1595+
:return:
1596+
"""
1597+
return "{resource_type}/{type}/{public_id}".format(resource_type=resource_type, type=type, public_id=public_id)

test/test_api.py

Lines changed: 45 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99

1010
import cloudinary
1111
from cloudinary import api, uploader, utils
12+
from cloudinary.utils import fq_public_id
1213
from test.helper_test import SUFFIX, TEST_IMAGE, get_uri, get_params, get_list_param, get_param, TEST_DOC, get_method, \
1314
UNIQUE_TAG, api_response_mock, ignore_exception, cleanup_test_resources_by_tag, cleanup_test_transformation, \
1415
cleanup_test_resources, UNIQUE_TEST_FOLDER, EVAL_STR, get_json_body
@@ -461,6 +462,33 @@ def test09_delete_resources_tuple(self, mocker):
461462
self.assertIn(API_TEST_ID, param)
462463
self.assertIn(API_TEST_ID2, param)
463464

465+
@patch('urllib3.request.RequestMethods.request')
466+
@unittest.skipUnless(cloudinary.config().api_secret, "requires api_key/api_secret")
467+
def test_add_related_assets(self, mocker):
468+
""" should allow adding related assets """
469+
mocker.return_value = MOCK_RESPONSE
470+
api.add_related_assets(API_TEST_ID, [fq_public_id(API_TEST_ID2), fq_public_id(API_TEST_ID3)])
471+
args, kargs = mocker.call_args
472+
self.assertEqual(args[0], 'POST')
473+
self.assertTrue(get_uri(args).endswith('/resources/related_assets/image/upload/' + API_TEST_ID))
474+
param = get_json_body(mocker)['assets_to_relate']
475+
self.assertIn(fq_public_id(API_TEST_ID2), param)
476+
self.assertIn(fq_public_id(API_TEST_ID3), param)
477+
478+
@patch('urllib3.request.RequestMethods.request')
479+
@unittest.skipUnless(cloudinary.config().api_secret, "requires api_key/api_secret")
480+
def test_delete_related_assets(self, mocker):
481+
""" should allow deleting related assets """
482+
mocker.return_value = MOCK_RESPONSE
483+
api.delete_related_assets(API_TEST_ID, [fq_public_id(API_TEST_ID2), fq_public_id(API_TEST_ID3)])
484+
args, kargs = mocker.call_args
485+
self.assertEqual(args[0], 'DELETE')
486+
self.assertTrue(get_uri(args).endswith('/resources/related_assets/image/upload/' + API_TEST_ID))
487+
param = get_json_body(mocker)['assets_to_unrelate']
488+
self.assertIn(fq_public_id(API_TEST_ID2), param)
489+
self.assertIn(fq_public_id(API_TEST_ID3), param)
490+
491+
464492
@patch('urllib3.request.RequestMethods.request')
465493
@unittest.skipUnless(cloudinary.config().api_secret, "requires api_key/api_secret")
466494
def test10_tags(self, mocker):
@@ -869,11 +897,11 @@ def test_delete_folder(self, mocker):
869897
def test_root_folders_allows_next_cursor_and_max_results_parameter(self, mocker):
870898
""" should allow next_cursor and max_results parameters """
871899
mocker.return_value = MOCK_RESPONSE
872-
900+
873901
api.root_folders(next_cursor=NEXT_CURSOR, max_results=10)
874-
902+
875903
args, kwargs = mocker.call_args
876-
904+
877905
self.assertTrue("next_cursor" in get_params(args))
878906
self.assertTrue("max_results" in get_params(args))
879907

@@ -882,11 +910,11 @@ def test_root_folders_allows_next_cursor_and_max_results_parameter(self, mocker)
882910
def test_subfolders_allows_next_cursor_and_max_results_parameter(self, mocker):
883911
""" should allow next_cursor and max_results parameters """
884912
mocker.return_value = MOCK_RESPONSE
885-
913+
886914
api.subfolders(API_TEST_ID, next_cursor=NEXT_CURSOR, max_results=10)
887-
915+
888916
args, kwargs = mocker.call_args
889-
917+
890918
self.assertTrue("next_cursor" in get_params(args))
891919
self.assertTrue("max_results" in get_params(args))
892920

@@ -996,26 +1024,22 @@ def test_update_access_control(self, mocker):
9961024
self.assertEqual(exp_acl, params["access_control"])
9971025

9981026
@patch('urllib3.request.RequestMethods.request')
999-
def test_cinemagraph_analysis_resource(self, mocker):
1000-
""" should allow the user to pass cinemagraph_analysis in the resource function """
1027+
def test_various_resource_parameters(self, mocker):
1028+
""" should allow the user to pass various parameters to the resource function """
10011029
mocker.return_value = MOCK_RESPONSE
10021030

1003-
api.resource(API_TEST_ID, cinemagraph_analysis=True)
1004-
1005-
params = get_params(mocker.call_args[0])
1006-
1007-
self.assertIn("cinemagraph_analysis", params)
1008-
1009-
@patch('urllib3.request.RequestMethods.request')
1010-
def test_accessibility_analysis_resource(self, mocker):
1011-
""" should allow the user to pass accessibility_analysis in the resource function """
1012-
mocker.return_value = MOCK_RESPONSE
1031+
options = {
1032+
"cinemagraph_analysis": True,
1033+
"accessibility_analysis": True,
1034+
"related": True,
1035+
"related_next_cursor": NEXT_CURSOR,
1036+
}
10131037

1014-
api.resource(API_TEST_ID, accessibility_analysis=True)
1038+
api.resource(TEST_IMAGE, **options)
10151039

10161040
params = get_params(mocker.call_args[0])
1017-
1018-
self.assertIn("accessibility_analysis", params)
1041+
for param in options.keys():
1042+
self.assertIn(param, params)
10191043

10201044
@patch('urllib3.request.RequestMethods.request')
10211045
def test_api_url_escapes_special_characters(self, mocker):

0 commit comments

Comments
 (0)