Skip to content

Commit afa59c0

Browse files
Add support for search_folders command
1 parent 9d36e09 commit afa59c0

File tree

3 files changed

+83
-32
lines changed

3 files changed

+83
-32
lines changed

cloudinary_cli/core/__init__.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
from cloudinary_cli.core.admin import admin
44
from cloudinary_cli.core.config import config
5-
from cloudinary_cli.core.search import search
5+
from cloudinary_cli.core.search import search, search_folders
66
from cloudinary_cli.core.uploader import uploader
77
from cloudinary_cli.core.provisioning import provisioning
88
from cloudinary_cli.core.utils import url, utils
@@ -13,6 +13,7 @@
1313
commands = [
1414
config,
1515
search,
16+
search_folders,
1617
admin,
1718
uploader,
1819
provisioning,

cloudinary_cli/core/search.py

Lines changed: 73 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import cloudinary
22
from click import command, argument, option, launch
3+
from functools import wraps
34

45
from cloudinary_cli.defaults import logger
56
from cloudinary_cli.utils.json_utils import write_json_to_file, print_json
@@ -9,45 +10,86 @@
910
DEFAULT_MAX_RESULTS = 500
1011

1112

13+
def shared_options(func):
14+
@option("-f", "--with_field", multiple=True, help="Specify which non-default asset attributes to include "
15+
"in the result as a comma separated list.")
16+
@option("-fi", "--fields", multiple=True, help="Specify which asset attributes to include in the result "
17+
"(together with a subset of the default attributes) as a comma separated"
18+
" list. This overrides any value specified for with_field.")
19+
@option("-s", "--sort_by", nargs=2, help="Sort search results by (field, <asc|desc>).")
20+
@option("-a", "--aggregate", nargs=1,
21+
help="Specify the attribute for which an aggregation count should be calculated and returned.")
22+
@option("-n", "--max_results", nargs=1, default=10,
23+
help="The maximum number of results to return. Default: 10, maximum: 500.")
24+
@option("-c", "--next_cursor", nargs=1, help="Continue a search using an existing cursor.")
25+
@option("-A", "--auto_paginate", is_flag=True, help="Return all results. Will call Admin API multiple times.")
26+
@option("-F", "--force", is_flag=True, help="Skip confirmation when running --auto-paginate.")
27+
@option("-ff", "--filter_fields", multiple=True, help="Specify which attributes to show in the response. "
28+
"None of the others will be shown.")
29+
@option("-sq", "--search-query", is_flag=True, help="Show the search request query.", hidden=True)
30+
@option("--json", nargs=1, help="Save JSON output to a file. Usage: --json <filename>")
31+
@option("--csv", nargs=1, help="Save CSV output to a file. Usage: --csv <filename>")
32+
@wraps(func)
33+
def wrapper(*args, **kwargs):
34+
return func(*args, **kwargs)
35+
36+
return wrapper
37+
38+
1239
@command("search",
13-
short_help="Run the admin API search method.",
40+
short_help="Run the Admin API search method.",
1441
help="""\b
15-
Run the admin API search method.
42+
Run the Admin API search method.
1643
Format: cld <cli options> search <command options> <Lucene query syntax string>
1744
e.g. cld search cat AND tags:kitten -s public_id desc -f context -f tags -n 10
1845
""")
1946
@argument("query", nargs=-1)
20-
@option("-f", "--with_field", multiple=True, help="Specify which non-default asset attributes to include "
21-
"in the result as a comma separated list. ")
22-
@option("-fi", "--fields", multiple=True, help="Specify which asset attributes to include in the result "
23-
"(together with a subset of the default attributes) as a comma separated"
24-
" list. This overrides any value specified for with_field.")
25-
@option("-s", "--sort_by", nargs=2, help="Sort search results by (field, <asc|desc>).")
26-
@option("-a", "--aggregate", nargs=1,
27-
help="Specify the attribute for which an aggregation count should be calculated and returned.")
28-
@option("-n", "--max_results", nargs=1, default=10,
29-
help="The maximum number of results to return. Default: 10, maximum: 500.")
30-
@option("-c", "--next_cursor", nargs=1, help="Continue a search using an existing cursor.")
31-
@option("-A", "--auto_paginate", is_flag=True, help="Return all results. Will call Admin API multiple times.")
32-
@option("-F", "--force", is_flag=True, help="Skip confirmation when running --auto-paginate.")
33-
@option("-ff", "--filter_fields", multiple=True, help="Specify which attributes to show in the response. "
34-
"None of the others will be shown.")
47+
@shared_options
3548
@option("-t", "--ttl", nargs=1, default=300, help="Set the Search URL TTL in seconds. Default: 300.")
3649
@option("-u", "--url", is_flag=True, help="Build a signed search URL.")
37-
@option("-sq", "--search-query", is_flag=True, help="Show the search request query.", hidden=True)
38-
@option("--json", nargs=1, help="Save JSON output to a file. Usage: --json <filename>")
39-
@option("--csv", nargs=1, help="Save CSV output to a file. Usage: --csv <filename>")
4050
@option("-d", "--doc", is_flag=True, help="Open Search API documentation page.")
4151
def search(query, with_field, fields, sort_by, aggregate, max_results, next_cursor,
4252
auto_paginate, force, filter_fields, ttl, url, search_query, json, csv, doc):
53+
search_instance = cloudinary.search.Search()
54+
doc_url = "https://cloudinary.com/documentation/search_api"
55+
result_field = 'resources'
56+
return _perform_search(query, with_field, fields, sort_by, aggregate, max_results, next_cursor,
57+
auto_paginate, force, filter_fields, ttl, url, search_query, json, csv, doc,
58+
search_instance, doc_url, result_field)
59+
60+
61+
@command("search_folders",
62+
short_help="Run the Admin API search folders method.",
63+
help="""\b
64+
Run the Admin API search folders method.
65+
Format: cld <cli options> search_folders <command options> <Lucene query syntax string>
66+
e.g. cld search_folders name:folder AND path:my_parent AND created_at>4w
67+
""")
68+
@argument("query", nargs=-1)
69+
@shared_options
70+
@option("-d", "--doc", is_flag=True, help="Open Search Folders API documentation page.")
71+
def search_folders(query, with_field, fields, sort_by, aggregate, max_results, next_cursor,
72+
auto_paginate, force, filter_fields, search_query, json, csv, doc):
73+
search_instance = cloudinary.search_folders.SearchFolders()
74+
doc_url = "https://cloudinary.com/documentation/admin_api#search_folders"
75+
result_field = 'folders'
76+
return _perform_search(query, with_field, fields, sort_by, aggregate, max_results, next_cursor,
77+
auto_paginate, force, filter_fields, 300, False, search_query, json, csv, doc,
78+
search_instance, doc_url, result_field)
79+
80+
81+
def _perform_search(query, with_field, fields, sort_by, aggregate, max_results, next_cursor,
82+
auto_paginate, force, filter_fields, ttl, url, search_query, json, csv, doc,
83+
search_instance, doc_url, result_field):
84+
"""Shared logic for running a search."""
4385
if doc:
44-
return launch("https://cloudinary.com/documentation/search_api")
86+
return launch(doc_url)
4587

4688
fields_to_keep = []
4789
if filter_fields:
4890
fields_to_keep = tuple(normalize_list_params(filter_fields)) + tuple(normalize_list_params(with_field))
4991

50-
search = cloudinary.search.Search().expression(" ".join(query))
92+
search = search_instance.expression(" ".join(query))
5193

5294
if auto_paginate:
5395
max_results = DEFAULT_MAX_RESULTS
@@ -74,32 +116,32 @@ def search(query, with_field, fields, sort_by, aggregate, max_results, next_curs
74116
print_json(search.as_dict())
75117
return True
76118

77-
res = execute_single_request(search, fields_to_keep)
119+
res = execute_single_request(search, fields_to_keep, result_field)
78120

79121
if auto_paginate:
80-
res = handle_auto_pagination(res, search, force, fields_to_keep)
122+
res = handle_auto_pagination(res, search, force, fields_to_keep, result_field)
81123

82124
print_json(res)
83125

84126
if json:
85-
write_json_to_file(res['resources'], json)
127+
write_json_to_file(res[result_field], json)
86128
logger.info(f"Saved search JSON to '{json}' file")
87129

88130
if csv:
89-
write_json_list_to_csv(res['resources'], csv, fields_to_keep)
131+
write_json_list_to_csv(res[result_field], csv, fields_to_keep)
90132
logger.info(f"Saved search to '{csv}.csv' file")
91133

92134

93-
def execute_single_request(expression, fields_to_keep):
135+
def execute_single_request(expression, fields_to_keep, result_field='resources'):
94136
res = expression.execute()
95137

96138
if fields_to_keep:
97-
res['resources'] = whitelist_keys(res['resources'], fields_to_keep)
139+
res[result_field] = whitelist_keys(res[result_field], fields_to_keep)
98140

99141
return res
100142

101143

102-
def handle_auto_pagination(res, expression, force, fields_to_keep):
144+
def handle_auto_pagination(res, expression, force, fields_to_keep, result_field='resources'):
103145
if 'next_cursor' not in res:
104146
return res
105147

@@ -119,9 +161,9 @@ def handle_auto_pagination(res, expression, force, fields_to_keep):
119161
while 'next_cursor' in res.keys():
120162
expression.next_cursor(res['next_cursor'])
121163

122-
res = execute_single_request(expression, fields_to_keep)
164+
res = execute_single_request(expression, fields_to_keep, result_field)
123165

124-
all_results['resources'] += res['resources']
166+
all_results[result_field] += res[result_field]
125167
all_results['time'] += res['time']
126168

127169
all_results.pop('next_cursor', None) # it is empty by now

test/test_cli_search_api.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,3 +36,11 @@ def test_search_url(self):
3636
self.assertIn('eyJleHByZXNzaW9uIjoiY2F0IiwibWF4X3Jlc3VsdHMiOjEwfQ==', result.output)
3737
self.assertIn('1000', result.output)
3838
self.assertIn('NEXT_CURSOR', result.output)
39+
40+
@patch(URLLIB3_REQUEST)
41+
def test_search_folders(self, mocker):
42+
mocker.return_value = API_MOCK_RESPONSE
43+
result = self.runner.invoke(cli, ['search_folders', 'cat_folder'])
44+
45+
self.assertEqual(0, result.exit_code)
46+
self.assertIn('"foo": "bar"', result.output)

0 commit comments

Comments
 (0)