cloudinary-cli 1.14.0__tar.gz → 1.14.1__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (71) hide show
  1. {cloudinary_cli-1.14.0/cloudinary_cli.egg-info → cloudinary_cli-1.14.1}/PKG-INFO +1 -1
  2. {cloudinary_cli-1.14.0 → cloudinary_cli-1.14.1}/cloudinary_cli/core/admin.py +2 -1
  3. {cloudinary_cli-1.14.0 → cloudinary_cli-1.14.1}/cloudinary_cli/utils/api_utils.py +34 -0
  4. cloudinary_cli-1.14.1/cloudinary_cli/version.py +1 -0
  5. {cloudinary_cli-1.14.0 → cloudinary_cli-1.14.1/cloudinary_cli.egg-info}/PKG-INFO +1 -1
  6. cloudinary_cli-1.14.1/test/test_cli_api.py +127 -0
  7. cloudinary_cli-1.14.0/cloudinary_cli/version.py +0 -1
  8. cloudinary_cli-1.14.0/test/test_cli_api.py +0 -41
  9. {cloudinary_cli-1.14.0 → cloudinary_cli-1.14.1}/LICENSE +0 -0
  10. {cloudinary_cli-1.14.0 → cloudinary_cli-1.14.1}/MANIFEST.in +0 -0
  11. {cloudinary_cli-1.14.0 → cloudinary_cli-1.14.1}/README.md +0 -0
  12. {cloudinary_cli-1.14.0 → cloudinary_cli-1.14.1}/cloudinary_cli/__init__.py +0 -0
  13. {cloudinary_cli-1.14.0 → cloudinary_cli-1.14.1}/cloudinary_cli/cli.py +0 -0
  14. {cloudinary_cli-1.14.0 → cloudinary_cli-1.14.1}/cloudinary_cli/cli_group.py +0 -0
  15. {cloudinary_cli-1.14.0 → cloudinary_cli-1.14.1}/cloudinary_cli/core/__init__.py +0 -0
  16. {cloudinary_cli-1.14.0 → cloudinary_cli-1.14.1}/cloudinary_cli/core/config.py +0 -0
  17. {cloudinary_cli-1.14.0 → cloudinary_cli-1.14.1}/cloudinary_cli/core/overrides.py +0 -0
  18. {cloudinary_cli-1.14.0 → cloudinary_cli-1.14.1}/cloudinary_cli/core/provisioning.py +0 -0
  19. {cloudinary_cli-1.14.0 → cloudinary_cli-1.14.1}/cloudinary_cli/core/search.py +0 -0
  20. {cloudinary_cli-1.14.0 → cloudinary_cli-1.14.1}/cloudinary_cli/core/uploader.py +0 -0
  21. {cloudinary_cli-1.14.0 → cloudinary_cli-1.14.1}/cloudinary_cli/core/utils.py +0 -0
  22. {cloudinary_cli-1.14.0 → cloudinary_cli-1.14.1}/cloudinary_cli/defaults.py +0 -0
  23. {cloudinary_cli-1.14.0 → cloudinary_cli-1.14.1}/cloudinary_cli/modules/__init__.py +0 -0
  24. {cloudinary_cli-1.14.0 → cloudinary_cli-1.14.1}/cloudinary_cli/modules/clone.py +0 -0
  25. {cloudinary_cli-1.14.0 → cloudinary_cli-1.14.1}/cloudinary_cli/modules/make.py +0 -0
  26. {cloudinary_cli-1.14.0 → cloudinary_cli-1.14.1}/cloudinary_cli/modules/migrate.py +0 -0
  27. {cloudinary_cli-1.14.0 → cloudinary_cli-1.14.1}/cloudinary_cli/modules/regen_derived.py +0 -0
  28. {cloudinary_cli-1.14.0 → cloudinary_cli-1.14.1}/cloudinary_cli/modules/sync.py +0 -0
  29. {cloudinary_cli-1.14.0 → cloudinary_cli-1.14.1}/cloudinary_cli/modules/upload_dir.py +0 -0
  30. {cloudinary_cli-1.14.0 → cloudinary_cli-1.14.1}/cloudinary_cli/samples/__init__.py +0 -0
  31. {cloudinary_cli-1.14.0 → cloudinary_cli-1.14.1}/cloudinary_cli/templates/html/media_library_widget +0 -0
  32. {cloudinary_cli-1.14.0 → cloudinary_cli-1.14.1}/cloudinary_cli/templates/html/product_gallery +0 -0
  33. {cloudinary_cli-1.14.0 → cloudinary_cli-1.14.1}/cloudinary_cli/templates/html/upload_widget +0 -0
  34. {cloudinary_cli-1.14.0 → cloudinary_cli-1.14.1}/cloudinary_cli/templates/html/video_player +0 -0
  35. {cloudinary_cli-1.14.0 → cloudinary_cli-1.14.1}/cloudinary_cli/templates/node/upload +0 -0
  36. {cloudinary_cli-1.14.0 → cloudinary_cli-1.14.1}/cloudinary_cli/templates/python/base +0 -0
  37. {cloudinary_cli-1.14.0 → cloudinary_cli-1.14.1}/cloudinary_cli/templates/python/explicit +0 -0
  38. {cloudinary_cli-1.14.0 → cloudinary_cli-1.14.1}/cloudinary_cli/templates/python/find_all_empty_folders +0 -0
  39. {cloudinary_cli-1.14.0 → cloudinary_cli-1.14.1}/cloudinary_cli/templates/python/upload +0 -0
  40. {cloudinary_cli-1.14.0 → cloudinary_cli-1.14.1}/cloudinary_cli/templates/ruby/upload +0 -0
  41. {cloudinary_cli-1.14.0 → cloudinary_cli-1.14.1}/cloudinary_cli/utils/__init__.py +0 -0
  42. {cloudinary_cli-1.14.0 → cloudinary_cli-1.14.1}/cloudinary_cli/utils/config_utils.py +0 -0
  43. {cloudinary_cli-1.14.0 → cloudinary_cli-1.14.1}/cloudinary_cli/utils/file_utils.py +0 -0
  44. {cloudinary_cli-1.14.0 → cloudinary_cli-1.14.1}/cloudinary_cli/utils/json_utils.py +0 -0
  45. {cloudinary_cli-1.14.0 → cloudinary_cli-1.14.1}/cloudinary_cli/utils/search_utils.py +0 -0
  46. {cloudinary_cli-1.14.0 → cloudinary_cli-1.14.1}/cloudinary_cli/utils/utils.py +0 -0
  47. {cloudinary_cli-1.14.0 → cloudinary_cli-1.14.1}/cloudinary_cli.egg-info/SOURCES.txt +0 -0
  48. {cloudinary_cli-1.14.0 → cloudinary_cli-1.14.1}/cloudinary_cli.egg-info/dependency_links.txt +0 -0
  49. {cloudinary_cli-1.14.0 → cloudinary_cli-1.14.1}/cloudinary_cli.egg-info/entry_points.txt +0 -0
  50. {cloudinary_cli-1.14.0 → cloudinary_cli-1.14.1}/cloudinary_cli.egg-info/not-zip-safe +0 -0
  51. {cloudinary_cli-1.14.0 → cloudinary_cli-1.14.1}/cloudinary_cli.egg-info/requires.txt +0 -0
  52. {cloudinary_cli-1.14.0 → cloudinary_cli-1.14.1}/cloudinary_cli.egg-info/top_level.txt +0 -0
  53. {cloudinary_cli-1.14.0 → cloudinary_cli-1.14.1}/requirements.txt +0 -0
  54. {cloudinary_cli-1.14.0 → cloudinary_cli-1.14.1}/setup.cfg +0 -0
  55. {cloudinary_cli-1.14.0 → cloudinary_cli-1.14.1}/setup.py +0 -0
  56. {cloudinary_cli-1.14.0 → cloudinary_cli-1.14.1}/test/__init__.py +0 -0
  57. {cloudinary_cli-1.14.0 → cloudinary_cli-1.14.1}/test/helper_test.py +0 -0
  58. {cloudinary_cli-1.14.0 → cloudinary_cli-1.14.1}/test/test_cli.py +0 -0
  59. {cloudinary_cli-1.14.0 → cloudinary_cli-1.14.1}/test/test_cli_config.py +0 -0
  60. {cloudinary_cli-1.14.0 → cloudinary_cli-1.14.1}/test/test_cli_samples.py +0 -0
  61. {cloudinary_cli-1.14.0 → cloudinary_cli-1.14.1}/test/test_cli_search_api.py +0 -0
  62. {cloudinary_cli-1.14.0 → cloudinary_cli-1.14.1}/test/test_cli_url.py +0 -0
  63. {cloudinary_cli-1.14.0 → cloudinary_cli-1.14.1}/test/test_cli_utils.py +0 -0
  64. {cloudinary_cli-1.14.0 → cloudinary_cli-1.14.1}/test/test_file_utils.py +0 -0
  65. {cloudinary_cli-1.14.0 → cloudinary_cli-1.14.1}/test/test_modules/__init__.py +0 -0
  66. {cloudinary_cli-1.14.0 → cloudinary_cli-1.14.1}/test/test_modules/test_cli_clone.py +0 -0
  67. {cloudinary_cli-1.14.0 → cloudinary_cli-1.14.1}/test/test_modules/test_cli_make.py +0 -0
  68. {cloudinary_cli-1.14.0 → cloudinary_cli-1.14.1}/test/test_modules/test_cli_sync.py +0 -0
  69. {cloudinary_cli-1.14.0 → cloudinary_cli-1.14.1}/test/test_modules/test_cli_upload_dir.py +0 -0
  70. {cloudinary_cli-1.14.0 → cloudinary_cli-1.14.1}/test/test_search_utils.py +0 -0
  71. {cloudinary_cli-1.14.0 → cloudinary_cli-1.14.1}/test/test_utils.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: cloudinary-cli
3
- Version: 1.14.0
3
+ Version: 1.14.1
4
4
  Summary: A command line interface for Cloudinary with full API support
5
5
  Home-page: https://github.com/cloudinary/cloudinary-cli
6
6
  Author: Cloudinary, Brian Luk
@@ -21,7 +21,8 @@ Format: cld <cli options> admin <command options> <method> <method parameters>
21
21
  help="Pass optional parameters as interpreted strings.")
22
22
  @option("-A", "--auto_paginate", is_flag=True, help="Will auto paginate Admin API calls.", default=False)
23
23
  @option("-ff", "--filter_fields", multiple=True, help="Filter fields to return when using auto pagination.")
24
- @option("-F", "--force", is_flag=True, help="Skip confirmation when running --auto-paginate.")
24
+ @option("-F", "--force", is_flag=True,
25
+ help="Skip confirmations for auto pagination and destructive bulk API methods.")
25
26
  @option("-ls", "--ls", is_flag=True, help="List all available methods in the Admin API.")
26
27
  @option("--save", nargs=1, help="Save output to a file.")
27
28
  @option("-d", "--doc", is_flag=True, help="Open the Admin API reference in a browser.")
@@ -20,6 +20,37 @@ PAGINATION_MAX_RESULTS = 500
20
20
 
21
21
  _cursor_fields = {"resource": "derived_next_cursor"}
22
22
 
23
+ # Selector-style destructive bulk Admin API methods.
24
+ # These can match an open-ended set of assets without the caller enumerating each target,
25
+ # so an interactive confirmation is required (bypassable via --force).
26
+ DESTRUCTIVE_BULK_API_METHODS = {
27
+ "delete_all_resources",
28
+ "delete_resources_by_prefix",
29
+ "delete_resources_by_tag",
30
+ }
31
+
32
+ # Server-enforced cap on how many resources a single destructive bulk call can affect
33
+ # (NConfig.max_resource_count_for_delete). Used purely for clearer prompt wording.
34
+ MAX_DESTRUCTIVE_BULK_PER_CALL = 1000
35
+
36
+
37
+ def is_destructive_bulk_api_method(method_name):
38
+ return method_name in DESTRUCTIVE_BULK_API_METHODS
39
+
40
+
41
+ def confirm_destructive_bulk_api_method(api_name, method_name, force=False):
42
+ if force or not is_destructive_bulk_api_method(method_name):
43
+ return True
44
+
45
+ display_api_name = api_name.capitalize()
46
+ if not confirm_action(
47
+ f"This will delete up to {MAX_DESTRUCTIVE_BULK_PER_CALL} matching assets via "
48
+ f"{display_api_name} API method '{method_name}'. Continue? (y/N)"):
49
+ logger.info("Stopping.")
50
+ return False
51
+
52
+ return True
53
+
23
54
 
24
55
  def query_cld_folder(folder, folder_mode, status=None):
25
56
  files = {}
@@ -311,6 +342,9 @@ def handle_api_command(
311
342
  log_exception(e)
312
343
  return False
313
344
 
345
+ if not confirm_destructive_bulk_api_method(api_name, func.__name__, force):
346
+ return False
347
+
314
348
  if not is_valid_cloudinary_config():
315
349
  raise ConfigurationError("No Cloudinary configuration found.")
316
350
 
@@ -0,0 +1 @@
1
+ __version__ = "1.14.1"
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: cloudinary-cli
3
- Version: 1.14.0
3
+ Version: 1.14.1
4
4
  Summary: A command line interface for Cloudinary with full API support
5
5
  Home-page: https://github.com/cloudinary/cloudinary-cli
6
6
  Author: Cloudinary, Brian Luk
@@ -0,0 +1,127 @@
1
+ import os
2
+ import unittest
3
+ from unittest.mock import patch
4
+
5
+ import cloudinary.provisioning
6
+ from click.testing import CliRunner
7
+
8
+ from cloudinary_cli.cli import cli
9
+ from test.helper_test import api_response_mock, uploader_response_mock, URLLIB3_REQUEST
10
+
11
+ API_MOCK_RESPONSE = api_response_mock()
12
+ UPLOAD_MOCK_RESPONSE = uploader_response_mock()
13
+
14
+ CONFIRM_ACTION_PATCH = "cloudinary_cli.utils.api_utils.confirm_action"
15
+
16
+
17
+ class TestCLIApi(unittest.TestCase):
18
+ runner = CliRunner()
19
+
20
+ @patch(URLLIB3_REQUEST)
21
+ def test_admin(self, mocker):
22
+ mocker.return_value = API_MOCK_RESPONSE
23
+ result = self.runner.invoke(cli, ['ping'])
24
+
25
+ self.assertEqual(0, result.exit_code, result.output)
26
+ self.assertIn('"foo": "bar"', result.output)
27
+
28
+ @patch(URLLIB3_REQUEST)
29
+ def test_upload(self, mocker):
30
+ mocker.return_value = UPLOAD_MOCK_RESPONSE
31
+ result = self.runner.invoke(cli, ['upload', os.path.abspath(__file__)])
32
+
33
+ self.assertEqual(0, result.exit_code, result.output)
34
+ self.assertIn('"foo": "bar"', result.output)
35
+
36
+ @patch(URLLIB3_REQUEST)
37
+ @unittest.skipUnless(cloudinary.provisioning.account_config().account_id, "requires account_id")
38
+ def test_provisioning(self, mocker):
39
+ mocker.return_value = API_MOCK_RESPONSE
40
+ result = self.runner.invoke(cli, ['provisioning', 'users'])
41
+
42
+ self.assertEqual(0, result.exit_code, result.output)
43
+ self.assertIn('"foo": "bar"', result.output)
44
+
45
+
46
+ class TestDestructiveBulkConfirmation(unittest.TestCase):
47
+ runner = CliRunner()
48
+
49
+ @patch(CONFIRM_ACTION_PATCH, return_value=False)
50
+ @patch(URLLIB3_REQUEST)
51
+ def test_delete_all_resources_decline_skips_call(self, http_mock, confirm_mock):
52
+ http_mock.return_value = API_MOCK_RESPONSE
53
+ result = self.runner.invoke(cli, ['admin', 'delete_all_resources'])
54
+
55
+ self.assertEqual(0, result.exit_code, result.output)
56
+ confirm_mock.assert_called_once()
57
+ self.assertFalse(http_mock.called, "SDK should not be called when user declines")
58
+
59
+ @patch(CONFIRM_ACTION_PATCH, return_value=True)
60
+ @patch(URLLIB3_REQUEST)
61
+ def test_delete_all_resources_accept_calls_sdk(self, http_mock, confirm_mock):
62
+ http_mock.return_value = API_MOCK_RESPONSE
63
+ result = self.runner.invoke(cli, ['admin', 'delete_all_resources'])
64
+
65
+ self.assertEqual(0, result.exit_code, result.output)
66
+ confirm_mock.assert_called_once()
67
+ self.assertTrue(http_mock.called, "SDK should be called when user accepts")
68
+
69
+ @patch(CONFIRM_ACTION_PATCH, return_value=False)
70
+ @patch(URLLIB3_REQUEST)
71
+ def test_delete_all_resources_force_skips_prompt(self, http_mock, confirm_mock):
72
+ http_mock.return_value = API_MOCK_RESPONSE
73
+ result = self.runner.invoke(cli, ['admin', '-F', 'delete_all_resources'])
74
+
75
+ self.assertEqual(0, result.exit_code, result.output)
76
+ self.assertFalse(confirm_mock.called, "--force should bypass the confirmation prompt")
77
+ self.assertTrue(http_mock.called, "SDK should be called when --force is set")
78
+
79
+ @patch(CONFIRM_ACTION_PATCH, return_value=False)
80
+ @patch(URLLIB3_REQUEST)
81
+ def test_delete_resources_by_tag_decline_skips_call(self, http_mock, confirm_mock):
82
+ http_mock.return_value = API_MOCK_RESPONSE
83
+ result = self.runner.invoke(cli, ['admin', 'delete_resources_by_tag', 'mytag'])
84
+
85
+ self.assertEqual(0, result.exit_code, result.output)
86
+ confirm_mock.assert_called_once()
87
+ self.assertFalse(http_mock.called, "SDK should not be called when user declines")
88
+
89
+ @patch(CONFIRM_ACTION_PATCH, return_value=False)
90
+ @patch(URLLIB3_REQUEST)
91
+ def test_delete_resources_explicit_ids_no_prompt(self, http_mock, confirm_mock):
92
+ http_mock.return_value = API_MOCK_RESPONSE
93
+ result = self.runner.invoke(cli, ['admin', 'delete_resources', 'public_id1', 'public_id2'])
94
+
95
+ self.assertEqual(0, result.exit_code, result.output)
96
+ self.assertFalse(confirm_mock.called, "Explicit-ID delete must not prompt")
97
+ self.assertTrue(http_mock.called, "SDK should be called for explicit-ID delete")
98
+
99
+ @patch(CONFIRM_ACTION_PATCH, return_value=False)
100
+ @patch(URLLIB3_REQUEST)
101
+ def test_uploader_add_tag_no_prompt(self, http_mock, confirm_mock):
102
+ http_mock.return_value = UPLOAD_MOCK_RESPONSE
103
+ result = self.runner.invoke(cli, ['uploader', 'add_tag', 'mytag', 'public_id1'])
104
+
105
+ self.assertEqual(0, result.exit_code, result.output)
106
+ self.assertFalse(confirm_mock.called, "Non-destructive bulk methods must not prompt")
107
+ self.assertTrue(http_mock.called, "SDK should be called for non-destructive bulk methods")
108
+
109
+ @patch(CONFIRM_ACTION_PATCH, return_value=False)
110
+ @patch(URLLIB3_REQUEST)
111
+ def test_admin_resources_read_no_prompt(self, http_mock, confirm_mock):
112
+ http_mock.return_value = API_MOCK_RESPONSE
113
+ result = self.runner.invoke(cli, ['admin', 'resources'])
114
+
115
+ self.assertEqual(0, result.exit_code, result.output)
116
+ self.assertFalse(confirm_mock.called, "Read commands must not prompt")
117
+ self.assertTrue(http_mock.called, "SDK should be called for read commands")
118
+
119
+ @patch(CONFIRM_ACTION_PATCH, return_value=False)
120
+ @patch(URLLIB3_REQUEST)
121
+ def test_admin_resources_read_with_force_no_prompt(self, http_mock, confirm_mock):
122
+ http_mock.return_value = API_MOCK_RESPONSE
123
+ result = self.runner.invoke(cli, ['admin', '-F', 'resources'])
124
+
125
+ self.assertEqual(0, result.exit_code, result.output)
126
+ self.assertFalse(confirm_mock.called, "Read commands must not prompt regardless of --force")
127
+ self.assertTrue(http_mock.called)
@@ -1 +0,0 @@
1
- __version__ = "1.14.0"
@@ -1,41 +0,0 @@
1
- import os
2
- import unittest
3
- from unittest.mock import patch
4
-
5
- import cloudinary.provisioning
6
- from click.testing import CliRunner
7
-
8
- from cloudinary_cli.cli import cli
9
- from test.helper_test import api_response_mock, uploader_response_mock, URLLIB3_REQUEST
10
-
11
- API_MOCK_RESPONSE = api_response_mock()
12
- UPLOAD_MOCK_RESPONSE = uploader_response_mock()
13
-
14
-
15
- class TestCLIApi(unittest.TestCase):
16
- runner = CliRunner()
17
-
18
- @patch(URLLIB3_REQUEST)
19
- def test_admin(self, mocker):
20
- mocker.return_value = API_MOCK_RESPONSE
21
- result = self.runner.invoke(cli, ['ping'])
22
-
23
- self.assertEqual(0, result.exit_code, result.output)
24
- self.assertIn('"foo": "bar"', result.output)
25
-
26
- @patch(URLLIB3_REQUEST)
27
- def test_upload(self, mocker):
28
- mocker.return_value = UPLOAD_MOCK_RESPONSE
29
- result = self.runner.invoke(cli, ['upload', os.path.abspath(__file__)])
30
-
31
- self.assertEqual(0, result.exit_code, result.output)
32
- self.assertIn('"foo": "bar"', result.output)
33
-
34
- @patch(URLLIB3_REQUEST)
35
- @unittest.skipUnless(cloudinary.provisioning.account_config().account_id, "requires account_id")
36
- def test_provisioning(self, mocker):
37
- mocker.return_value = API_MOCK_RESPONSE
38
- result = self.runner.invoke(cli, ['provisioning', 'users'])
39
-
40
- self.assertEqual(0, result.exit_code, result.output)
41
- self.assertIn('"foo": "bar"', result.output)
File without changes