Skip to content

Commit 74041cb

Browse files
TalLevAmiclaude
andauthored
Fix admin -A -ff when all results fit on one page
* Fix `admin -A -ff` when all results fit on one page `cld admin resources -A -ff public_id` returned every field when the first response had no cursor. `handle_auto_pagination` returned before it applied the filter. Without `-A`, `-ff` was ignored with no message. - Apply the field filter to each list in a single-page response. - Log a warning when `-ff` is given without `-A`. - State in the `-ff` help text that it requires `-A`. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Keep lists without the requested fields in a single-page -ff result On a single page, -ff filtered every list in the response. A list with none of the requested fields, such as versions, became [{}]. Now the CLI filters a list only when one or more of its items has a requested field. --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
1 parent e472eba commit 74041cb

3 files changed

Lines changed: 52 additions & 7 deletions

File tree

‎cloudinary_cli/core/admin.py‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@
2121
@option("-O", "--optional_parameter_parsed", multiple=True, nargs=2,
2222
help="Pass an optional parameter and parse its value as JSON or a boolean. e.g. -O context '{\"alt\": \"cat\"}'")
2323
@option("-A", "--auto_paginate", is_flag=True, help="Will auto paginate Admin API calls.", default=False)
24-
@option("-ff", "--filter_fields", multiple=True, help="Filter fields to return when using auto pagination.")
24+
@option("-ff", "--filter_fields", multiple=True, help="Filter fields to return. Requires -A/--auto_paginate.")
2525
@option("-F", "--force", is_flag=True,
2626
help="Skip confirmations for auto pagination and destructive bulk API methods.")
2727
@option("-ls", "--ls", is_flag=True, help="List all available methods in the Admin API.")

‎cloudinary_cli/utils/api_utils.py‎

Lines changed: 13 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414
populate_duplicate_name)
1515
from cloudinary_cli.utils.json_utils import print_json, write_json_to_file
1616
from cloudinary_cli.utils.utils import log_exception, confirm_action, get_command_params, merge_responses, \
17-
normalize_list_params, ConfigurationError, print_api_help, duplicate_values, should_dump_responses
17+
normalize_list_params, ConfigurationError, print_api_help, duplicate_values, should_dump_responses, whitelist_keys
1818
import re
1919
from cloudinary.utils import is_remote_url
2020

@@ -391,6 +391,8 @@ def handle_api_command(
391391

392392
if auto_paginate:
393393
res = handle_auto_pagination(res, func, args, kwargs, force, filter_fields)
394+
elif filter_fields:
395+
logger.warning("-ff/--filter_fields has no effect without -A/--auto_paginate.")
394396

395397
if return_data:
396398
return res
@@ -404,7 +406,17 @@ def handle_api_command(
404406
def handle_auto_pagination(res, func, args, kwargs, force, filter_fields):
405407
cursor_field = _cursor_fields.get(func.__name__, "next_cursor")
406408

409+
fields_to_keep = []
410+
if filter_fields:
411+
fields_to_keep = normalize_list_params(filter_fields)
412+
407413
if cursor_field not in res:
414+
# A single page: the pagination field is unknown, so filter only the lists that have a requested field.
415+
for key, value in res.items():
416+
if isinstance(value, list) and any(isinstance(item, dict) and item.keys() & set(fields_to_keep)
417+
for item in value):
418+
res[key] = whitelist_keys(value, fields_to_keep)
419+
408420
return res
409421

410422
if not force:
@@ -417,10 +429,6 @@ def handle_auto_pagination(res, func, args, kwargs, force, filter_fields):
417429
else:
418430
logger.info("Continuing. You may use the -F flag to force auto_pagination.")
419431

420-
fields_to_keep = []
421-
if filter_fields:
422-
fields_to_keep = normalize_list_params(filter_fields)
423-
424432
kwargs['max_results'] = PAGINATION_MAX_RESULTS
425433

426434
all_results = res

‎test/test_cli_api.py‎

Lines changed: 38 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
from click.testing import CliRunner
77

88
from cloudinary_cli.cli import cli
9-
from test.helper_test import api_response_mock, uploader_response_mock, URLLIB3_REQUEST, \
9+
from test.helper_test import api_response_mock, uploader_response_mock, http_response_mock, URLLIB3_REQUEST, \
1010
CONFIG_PRESENT, REQUIRES_CONFIG
1111

1212
API_MOCK_RESPONSE = api_response_mock()
@@ -134,3 +134,40 @@ def test_admin_resources_read_with_force_no_prompt(self, http_mock, confirm_mock
134134
self.assertEqual(0, result.exit_code, result.output)
135135
self.assertFalse(confirm_mock.called, "Read commands must not prompt regardless of --force")
136136
self.assertTrue(http_mock.called)
137+
138+
139+
class TestAdminFilterFields(unittest.TestCase):
140+
runner = CliRunner()
141+
142+
@unittest.skipUnless(CONFIG_PRESENT, REQUIRES_CONFIG)
143+
@patch(URLLIB3_REQUEST)
144+
def test_admin_filter_fields_single_page(self, http_mock):
145+
http_mock.return_value = http_response_mock(
146+
'{"resources": [{"public_id": "p1", "bytes": 1}, {"public_id": "p2", "bytes": 2}]}',
147+
{"x-featureratelimit-remaining": '0'})
148+
result = self.runner.invoke(cli, ['admin', 'resources', '-A', '-ff', 'public_id'])
149+
150+
self.assertEqual(0, result.exit_code, result.output)
151+
self.assertIn('"public_id": "p1"', result.output)
152+
self.assertNotIn('"bytes"', result.output)
153+
154+
@unittest.skipUnless(CONFIG_PRESENT, REQUIRES_CONFIG)
155+
@patch(URLLIB3_REQUEST)
156+
def test_admin_filter_fields_without_auto_paginate_warns(self, http_mock):
157+
http_mock.return_value = API_MOCK_RESPONSE
158+
result = self.runner.invoke(cli, ['admin', 'resources', '-ff', 'public_id'])
159+
160+
self.assertEqual(0, result.exit_code, result.output)
161+
self.assertIn('-ff/--filter_fields has no effect without -A/--auto_paginate', result.output)
162+
163+
@unittest.skipUnless(CONFIG_PRESENT, REQUIRES_CONFIG)
164+
@patch(URLLIB3_REQUEST)
165+
def test_admin_filter_fields_single_page_keeps_other_lists(self, http_mock):
166+
http_mock.return_value = http_response_mock(
167+
'{"derived": [{"id": "d1", "bytes": 1}], "versions": [{"version_id": "v1", "size": 2}]}',
168+
{"x-featureratelimit-remaining": '0'})
169+
result = self.runner.invoke(cli, ['admin', 'resource', 'sample', '-A', '-ff', 'id'])
170+
171+
self.assertEqual(0, result.exit_code, result.output)
172+
self.assertNotIn('"bytes"', result.output)
173+
self.assertIn('"version_id": "v1"', result.output)

0 commit comments

Comments
 (0)