Skip to content

Commit c77e64e

Browse files
Add support for fields parameter in Search and Admin APIs
1 parent c5160e7 commit c77e64e

File tree

5 files changed

+72
-19
lines changed

5 files changed

+72
-19
lines changed

cloudinary/api.py

Lines changed: 37 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -77,32 +77,30 @@ def resources(**options):
7777
uri = ["resources", resource_type]
7878
if upload_type:
7979
uri.append(upload_type)
80-
params = only(options, "next_cursor", "max_results", "prefix", "tags",
81-
"context", "moderations", "direction", "start_at", "metadata")
80+
params = __list_resources_params(**options)
81+
params.update(only(options, "prefix", "start_at"))
8282
return call_api("get", uri, params, **options)
8383

8484

8585
def resources_by_tag(tag, **options):
8686
resource_type = options.pop("resource_type", "image")
8787
uri = ["resources", resource_type, "tags", tag]
88-
params = only(options, "next_cursor", "max_results", "tags",
89-
"context", "moderations", "direction", "metadata")
88+
params = __list_resources_params(**options)
9089
return call_api("get", uri, params, **options)
9190

9291

9392
def resources_by_moderation(kind, status, **options):
9493
resource_type = options.pop("resource_type", "image")
9594
uri = ["resources", resource_type, "moderations", kind, status]
96-
params = only(options, "next_cursor", "max_results", "tags",
97-
"context", "moderations", "direction", "metadata")
95+
params = __list_resources_params(**options)
9896
return call_api("get", uri, params, **options)
9997

10098

10199
def resources_by_ids(public_ids, **options):
102100
resource_type = options.pop("resource_type", "image")
103101
upload_type = options.pop("type", "upload")
104102
uri = ["resources", resource_type, upload_type]
105-
params = dict(only(options, "tags", "moderations", "context"), public_ids=public_ids)
103+
params = dict(__resources_params(**options), public_ids=public_ids)
106104
return call_api("get", uri, params, **options)
107105

108106

@@ -118,7 +116,7 @@ def resources_by_asset_folder(asset_folder, **options):
118116
:rtype: Response
119117
"""
120118
uri = ["resources", "by_asset_folder"]
121-
params = only(options, "max_results", "tags", "moderations", "context", "next_cursor")
119+
params = __list_resources_params(**options)
122120
params["asset_folder"] = asset_folder
123121
return call_api("get", uri, params, **options)
124122

@@ -138,7 +136,7 @@ def resources_by_asset_ids(asset_ids, **options):
138136
:rtype: Response
139137
"""
140138
uri = ["resources", 'by_asset_ids']
141-
params = dict(only(options, "tags", "moderations", "context"), asset_ids=asset_ids)
139+
params = dict(__resources_params(**options), asset_ids=asset_ids)
142140
return call_api("get", uri, params, **options)
143141

144142

@@ -160,14 +158,42 @@ def resources_by_context(key, value=None, **options):
160158
"""
161159
resource_type = options.pop("resource_type", "image")
162160
uri = ["resources", resource_type, "context"]
163-
params = only(options, "next_cursor", "max_results", "tags",
164-
"context", "moderations", "direction", "metadata")
161+
params = __list_resources_params(**options)
165162
params["key"] = key
166163
if value is not None:
167164
params["value"] = value
168165
return call_api("get", uri, params, **options)
169166

170167

168+
def __resources_params(**options):
169+
"""
170+
Prepares optional parameters for resources_* API calls.
171+
172+
:param options: Additional options
173+
:return: Optional parameters
174+
175+
:internal
176+
"""
177+
params = only(options, "tags", "context", "metadata", "moderations")
178+
params["fields"] = options.get("fields") and utils.encode_list(utils.build_array(options["fields"]))
179+
return params
180+
181+
182+
def __list_resources_params(**options):
183+
"""
184+
Prepares optional parameters for resources_* API calls.
185+
186+
:param options: Additional options
187+
:return: Optional parameters
188+
189+
:internal
190+
"""
191+
resources_params = __resources_params(**options)
192+
resources_params.update(only(options, "next_cursor", "max_results", "direction"))
193+
194+
return resources_params
195+
196+
171197
def visual_search(image_url=None, image_asset_id=None, text=None, image_file=None, **options):
172198
"""
173199
Find images based on their visual content.

cloudinary/search.py

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,8 @@
33

44
import cloudinary
55
from cloudinary.api_client.call_api import call_json_api
6-
from cloudinary.utils import unique, unsigned_download_url_prefix, build_distribution_domain, base64url_encode, \
7-
json_encode, compute_hex_hash, SIGNATURE_SHA256
6+
from cloudinary.utils import (unique, build_distribution_domain, base64url_encode, json_encode, compute_hex_hash,
7+
SIGNATURE_SHA256, build_array)
88

99

1010
class Search(object):
@@ -16,6 +16,7 @@ class Search(object):
1616
'sort_by': lambda x: next(iter(x)),
1717
'aggregate': None,
1818
'with_field': None,
19+
'fields': None,
1920
}
2021

2122
_ttl = 300 # Used for search URLs
@@ -57,6 +58,11 @@ def with_field(self, value):
5758
self._add("with_field", value)
5859
return self
5960

61+
def fields(self, value):
62+
"""Request which fields to return in the result set."""
63+
self._add("fields", value)
64+
return self
65+
6066
def ttl(self, ttl):
6167
"""
6268
Sets the time to live of the search URL.
@@ -133,5 +139,5 @@ def endpoint(self, endpoint):
133139
def _add(self, name, value):
134140
if name not in self.query:
135141
self.query[name] = []
136-
self.query[name].append(value)
142+
self.query[name].extend(build_array(value))
137143
return self

cloudinary/utils.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -186,7 +186,7 @@ def compute_hex_hash(s, algorithm=SIGNATURE_SHA1):
186186

187187

188188
def build_array(arg):
189-
if isinstance(arg, list):
189+
if isinstance(arg, (list, tuple)):
190190
return arg
191191
elif arg is None:
192192
return []
@@ -602,6 +602,7 @@ def normalize_params(params):
602602

603603
return dict([(k, __bool_string(v)) for (k, v) in params.items() if v is not None and not v == ""])
604604

605+
605606
def sign_request(params, options):
606607
api_key = options.get("api_key", cloudinary.config().api_key)
607608
if not api_key:

test/test_api.py

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -205,6 +205,24 @@ def test05_resources_prefix(self, mocker):
205205
self.assertTrue(get_params(mocker)['context'])
206206
self.assertTrue(get_params(mocker)['tags'])
207207

208+
@patch(URLLIB3_REQUEST)
209+
@unittest.skipUnless(cloudinary.config().api_secret, "requires api_key/api_secret")
210+
def test06_resources_fields(self, mocker):
211+
""" should allow listing resources and returning only specified fields """
212+
mocker.return_value = MOCK_RESPONSE
213+
api.resources(fields=["tags", "secure_url"], type="upload")
214+
215+
self.assertTrue(get_uri(mocker).endswith('/resources/image/upload'))
216+
self.assertEqual(get_params(mocker)['fields'], "tags,secure_url")
217+
218+
api.resources(fields="context,url", type="upload")
219+
220+
self.assertEqual(get_params(mocker)['fields'], "context,url")
221+
222+
api.resources(fields="", type="upload")
223+
224+
self.assertNotIn('fields', get_params(mocker))
225+
208226
@patch(URLLIB3_REQUEST)
209227
@unittest.skipUnless(cloudinary.config().api_secret, "requires api_key/api_secret")
210228
def test06_resources_tag(self, mocker):
@@ -979,8 +997,6 @@ def test_delete_folder(self, mocker):
979997

980998
api.delete_folder(UNIQUE_TEST_FOLDER)
981999

982-
983-
9841000
self.assertEqual("DELETE", get_method(mocker))
9851001
self.assertTrue(get_uri(mocker).endswith('/folders/' + UNIQUE_TEST_FOLDER))
9861002

test/test_search.py

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -212,10 +212,13 @@ def test_should_not_duplicate_values(self, mocker):
212212
.sort_by('created_at') \
213213
.aggregate('format') \
214214
.aggregate('format') \
215-
.aggregate('resource_type') \
215+
.aggregate(['resource_type', 'type']) \
216216
.with_field('context') \
217217
.with_field('context') \
218218
.with_field('tags') \
219+
.fields(('tags', 'context')) \
220+
.fields('metadata') \
221+
.fields('tags') \
219222
.execute()
220223

221224
_, args = mocker.call_args
@@ -226,8 +229,9 @@ def test_should_not_duplicate_values(self, mocker):
226229
{'created_at': 'desc'},
227230
{'public_id': 'asc'},
228231
],
229-
'aggregate': ['format', 'resource_type'],
232+
'aggregate': ['format', 'resource_type', 'type'],
230233
'with_field': ['context', 'tags'],
234+
'fields': ['tags', 'context', 'metadata'],
231235
})
232236

233237
def test_should_build_search_url(self):

0 commit comments

Comments
 (0)