cloudinary-cli 1.11.2__tar.gz → 1.13.0__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 (69) hide show
  1. {cloudinary_cli-1.11.2/cloudinary_cli.egg-info → cloudinary_cli-1.13.0}/PKG-INFO +1 -1
  2. {cloudinary_cli-1.11.2 → cloudinary_cli-1.13.0}/cloudinary_cli/core/search.py +2 -1
  3. {cloudinary_cli-1.11.2 → cloudinary_cli-1.13.0}/cloudinary_cli/modules/sync.py +6 -4
  4. {cloudinary_cli-1.11.2 → cloudinary_cli-1.13.0}/cloudinary_cli/templates/html/video_player +5 -6
  5. {cloudinary_cli-1.11.2 → cloudinary_cli-1.13.0}/cloudinary_cli/utils/api_utils.py +15 -6
  6. cloudinary_cli-1.13.0/cloudinary_cli/utils/search_utils.py +163 -0
  7. cloudinary_cli-1.13.0/cloudinary_cli/version.py +1 -0
  8. {cloudinary_cli-1.11.2 → cloudinary_cli-1.13.0/cloudinary_cli.egg-info}/PKG-INFO +1 -1
  9. {cloudinary_cli-1.11.2 → cloudinary_cli-1.13.0}/cloudinary_cli.egg-info/SOURCES.txt +2 -0
  10. {cloudinary_cli-1.11.2 → cloudinary_cli-1.13.0}/test/test_modules/test_cli_make.py +6 -0
  11. cloudinary_cli-1.13.0/test/test_search_utils.py +170 -0
  12. cloudinary_cli-1.11.2/cloudinary_cli/version.py +0 -1
  13. {cloudinary_cli-1.11.2 → cloudinary_cli-1.13.0}/LICENSE +0 -0
  14. {cloudinary_cli-1.11.2 → cloudinary_cli-1.13.0}/MANIFEST.in +0 -0
  15. {cloudinary_cli-1.11.2 → cloudinary_cli-1.13.0}/README.md +0 -0
  16. {cloudinary_cli-1.11.2 → cloudinary_cli-1.13.0}/cloudinary_cli/__init__.py +0 -0
  17. {cloudinary_cli-1.11.2 → cloudinary_cli-1.13.0}/cloudinary_cli/cli.py +0 -0
  18. {cloudinary_cli-1.11.2 → cloudinary_cli-1.13.0}/cloudinary_cli/cli_group.py +0 -0
  19. {cloudinary_cli-1.11.2 → cloudinary_cli-1.13.0}/cloudinary_cli/core/__init__.py +0 -0
  20. {cloudinary_cli-1.11.2 → cloudinary_cli-1.13.0}/cloudinary_cli/core/admin.py +0 -0
  21. {cloudinary_cli-1.11.2 → cloudinary_cli-1.13.0}/cloudinary_cli/core/config.py +0 -0
  22. {cloudinary_cli-1.11.2 → cloudinary_cli-1.13.0}/cloudinary_cli/core/overrides.py +0 -0
  23. {cloudinary_cli-1.11.2 → cloudinary_cli-1.13.0}/cloudinary_cli/core/provisioning.py +0 -0
  24. {cloudinary_cli-1.11.2 → cloudinary_cli-1.13.0}/cloudinary_cli/core/uploader.py +0 -0
  25. {cloudinary_cli-1.11.2 → cloudinary_cli-1.13.0}/cloudinary_cli/core/utils.py +0 -0
  26. {cloudinary_cli-1.11.2 → cloudinary_cli-1.13.0}/cloudinary_cli/defaults.py +0 -0
  27. {cloudinary_cli-1.11.2 → cloudinary_cli-1.13.0}/cloudinary_cli/modules/__init__.py +0 -0
  28. {cloudinary_cli-1.11.2 → cloudinary_cli-1.13.0}/cloudinary_cli/modules/clone.py +0 -0
  29. {cloudinary_cli-1.11.2 → cloudinary_cli-1.13.0}/cloudinary_cli/modules/make.py +0 -0
  30. {cloudinary_cli-1.11.2 → cloudinary_cli-1.13.0}/cloudinary_cli/modules/migrate.py +0 -0
  31. {cloudinary_cli-1.11.2 → cloudinary_cli-1.13.0}/cloudinary_cli/modules/regen_derived.py +0 -0
  32. {cloudinary_cli-1.11.2 → cloudinary_cli-1.13.0}/cloudinary_cli/modules/upload_dir.py +0 -0
  33. {cloudinary_cli-1.11.2 → cloudinary_cli-1.13.0}/cloudinary_cli/samples/__init__.py +0 -0
  34. {cloudinary_cli-1.11.2 → cloudinary_cli-1.13.0}/cloudinary_cli/templates/html/media_library_widget +0 -0
  35. {cloudinary_cli-1.11.2 → cloudinary_cli-1.13.0}/cloudinary_cli/templates/html/product_gallery +0 -0
  36. {cloudinary_cli-1.11.2 → cloudinary_cli-1.13.0}/cloudinary_cli/templates/html/upload_widget +0 -0
  37. {cloudinary_cli-1.11.2 → cloudinary_cli-1.13.0}/cloudinary_cli/templates/node/upload +0 -0
  38. {cloudinary_cli-1.11.2 → cloudinary_cli-1.13.0}/cloudinary_cli/templates/python/base +0 -0
  39. {cloudinary_cli-1.11.2 → cloudinary_cli-1.13.0}/cloudinary_cli/templates/python/explicit +0 -0
  40. {cloudinary_cli-1.11.2 → cloudinary_cli-1.13.0}/cloudinary_cli/templates/python/find_all_empty_folders +0 -0
  41. {cloudinary_cli-1.11.2 → cloudinary_cli-1.13.0}/cloudinary_cli/templates/python/upload +0 -0
  42. {cloudinary_cli-1.11.2 → cloudinary_cli-1.13.0}/cloudinary_cli/templates/ruby/upload +0 -0
  43. {cloudinary_cli-1.11.2 → cloudinary_cli-1.13.0}/cloudinary_cli/utils/__init__.py +0 -0
  44. {cloudinary_cli-1.11.2 → cloudinary_cli-1.13.0}/cloudinary_cli/utils/config_utils.py +0 -0
  45. {cloudinary_cli-1.11.2 → cloudinary_cli-1.13.0}/cloudinary_cli/utils/file_utils.py +0 -0
  46. {cloudinary_cli-1.11.2 → cloudinary_cli-1.13.0}/cloudinary_cli/utils/json_utils.py +0 -0
  47. {cloudinary_cli-1.11.2 → cloudinary_cli-1.13.0}/cloudinary_cli/utils/utils.py +0 -0
  48. {cloudinary_cli-1.11.2 → cloudinary_cli-1.13.0}/cloudinary_cli.egg-info/dependency_links.txt +0 -0
  49. {cloudinary_cli-1.11.2 → cloudinary_cli-1.13.0}/cloudinary_cli.egg-info/entry_points.txt +0 -0
  50. {cloudinary_cli-1.11.2 → cloudinary_cli-1.13.0}/cloudinary_cli.egg-info/not-zip-safe +0 -0
  51. {cloudinary_cli-1.11.2 → cloudinary_cli-1.13.0}/cloudinary_cli.egg-info/requires.txt +0 -0
  52. {cloudinary_cli-1.11.2 → cloudinary_cli-1.13.0}/cloudinary_cli.egg-info/top_level.txt +0 -0
  53. {cloudinary_cli-1.11.2 → cloudinary_cli-1.13.0}/requirements.txt +0 -0
  54. {cloudinary_cli-1.11.2 → cloudinary_cli-1.13.0}/setup.cfg +0 -0
  55. {cloudinary_cli-1.11.2 → cloudinary_cli-1.13.0}/setup.py +0 -0
  56. {cloudinary_cli-1.11.2 → cloudinary_cli-1.13.0}/test/__init__.py +0 -0
  57. {cloudinary_cli-1.11.2 → cloudinary_cli-1.13.0}/test/helper_test.py +0 -0
  58. {cloudinary_cli-1.11.2 → cloudinary_cli-1.13.0}/test/test_cli.py +0 -0
  59. {cloudinary_cli-1.11.2 → cloudinary_cli-1.13.0}/test/test_cli_api.py +0 -0
  60. {cloudinary_cli-1.11.2 → cloudinary_cli-1.13.0}/test/test_cli_config.py +0 -0
  61. {cloudinary_cli-1.11.2 → cloudinary_cli-1.13.0}/test/test_cli_samples.py +0 -0
  62. {cloudinary_cli-1.11.2 → cloudinary_cli-1.13.0}/test/test_cli_search_api.py +0 -0
  63. {cloudinary_cli-1.11.2 → cloudinary_cli-1.13.0}/test/test_cli_url.py +0 -0
  64. {cloudinary_cli-1.11.2 → cloudinary_cli-1.13.0}/test/test_cli_utils.py +0 -0
  65. {cloudinary_cli-1.11.2 → cloudinary_cli-1.13.0}/test/test_file_utils.py +0 -0
  66. {cloudinary_cli-1.11.2 → cloudinary_cli-1.13.0}/test/test_modules/__init__.py +0 -0
  67. {cloudinary_cli-1.11.2 → cloudinary_cli-1.13.0}/test/test_modules/test_cli_sync.py +0 -0
  68. {cloudinary_cli-1.11.2 → cloudinary_cli-1.13.0}/test/test_modules/test_cli_upload_dir.py +0 -0
  69. {cloudinary_cli-1.11.2 → cloudinary_cli-1.13.0}/test/test_utils.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: cloudinary-cli
3
- Version: 1.11.2
3
+ Version: 1.13.0
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
@@ -6,6 +6,7 @@ from cloudinary_cli.defaults import logger
6
6
  from cloudinary_cli.utils.json_utils import write_json_to_file, print_json
7
7
  from cloudinary_cli.utils.utils import write_json_list_to_csv, confirm_action, whitelist_keys, \
8
8
  normalize_list_params
9
+ from cloudinary_cli.utils.search_utils import parse_aggregate
9
10
 
10
11
  DEFAULT_MAX_RESULTS = 500
11
12
 
@@ -100,7 +101,7 @@ def _perform_search(query, with_field, fields, sort_by, aggregate, max_results,
100
101
  if sort_by:
101
102
  search.sort_by(*sort_by)
102
103
  if aggregate:
103
- search.aggregate(aggregate)
104
+ search.aggregate(parse_aggregate(aggregate))
104
105
  if next_cursor:
105
106
  search.next_cursor(next_cursor)
106
107
  if ttl:
@@ -38,17 +38,19 @@ _SYNC_META_FILE = '.cld-sync'
38
38
  help="Specify the batch size for deleting remote assets.")
39
39
  @option("-fm", "--folder-mode", type=Choice(['fixed', 'dynamic'], case_sensitive=False),
40
40
  help="Specify folder mode explicitly. By default uses cloud mode configured in your cloud.", hidden=True)
41
+ @option("-st", "--status", type=Choice(['all', 'active', 'pending'], case_sensitive=False),
42
+ help="Specify asset status. Server default: active.", default=None)
41
43
  @option("-o", "--optional_parameter", multiple=True, nargs=2, help="Pass optional parameters as raw strings.")
42
44
  @option("-O", "--optional_parameter_parsed", multiple=True, nargs=2,
43
45
  help="Pass optional parameters as interpreted strings.")
44
46
  @option("--dry-run", is_flag=True, help="Simulate the sync operation without making any changes.")
45
47
  def sync(local_folder, cloudinary_folder, push, pull, include_hidden, concurrent_workers, force, keep_unique,
46
- deletion_batch_size, folder_mode, optional_parameter, optional_parameter_parsed, dry_run):
48
+ deletion_batch_size, folder_mode, status, optional_parameter, optional_parameter_parsed, dry_run):
47
49
  if push == pull:
48
50
  raise UsageError("Please use either the '--push' OR '--pull' options")
49
51
 
50
52
  sync_dir = SyncDir(local_folder, cloudinary_folder, include_hidden, concurrent_workers, force, keep_unique,
51
- deletion_batch_size, folder_mode, optional_parameter, optional_parameter_parsed, dry_run)
53
+ deletion_batch_size, folder_mode, status, optional_parameter, optional_parameter_parsed, dry_run)
52
54
  result = True
53
55
  if push:
54
56
  result = sync_dir.push()
@@ -63,7 +65,7 @@ def sync(local_folder, cloudinary_folder, push, pull, include_hidden, concurrent
63
65
 
64
66
  class SyncDir:
65
67
  def __init__(self, local_dir, remote_dir, include_hidden, concurrent_workers, force, keep_deleted,
66
- deletion_batch_size, folder_mode, optional_parameter, optional_parameter_parsed, dry_run):
68
+ deletion_batch_size, folder_mode, status, optional_parameter, optional_parameter_parsed, dry_run):
67
69
  self.local_dir = local_dir
68
70
  self.remote_dir = remote_dir.strip('/')
69
71
  self.user_friendly_remote_dir = self.remote_dir if self.remote_dir else '/'
@@ -100,7 +102,7 @@ class SyncDir:
100
102
  logger.info(f"Cloudinary folder '{self.user_friendly_remote_dir}' does not exist "
101
103
  f"({self.folder_mode} folder mode).")
102
104
  else:
103
- raw_remote_files = query_cld_folder(self.remote_dir, self.folder_mode)
105
+ raw_remote_files = query_cld_folder(self.remote_dir, self.folder_mode, status)
104
106
  if len(raw_remote_files):
105
107
  logger.info(
106
108
  f"Found {len(raw_remote_files)} items in Cloudinary folder '{self.user_friendly_remote_dir}' "
@@ -1,15 +1,14 @@
1
1
  <!DOCTYPE html>
2
2
 
3
3
  <head>
4
- <link href="https://unpkg.com/cloudinary-video-player/dist/cld-video-player.min.css" rel="stylesheet">
4
+ <link rel="stylesheet"
5
+ href="https://cdn.jsdelivr.net/npm/cloudinary-video-player@latest/dist/cld-video-player.min.css">
5
6
  </head>
6
7
 
7
8
  <body>
8
- <link href="https://unpkg.com/cloudinary-video-player/dist/cld-video-player.min.css" rel="stylesheet">
9
- <script src="https://unpkg.com/cloudinary-core/cloudinary-core-shrinkwrap.min.js" type="text/javascript"></script>
10
- <script src="https://unpkg.com/cloudinary-video-player/dist/cld-video-player.min.js" type="text/javascript"></script>
11
- <script src="https://cdnjs.cloudflare.com/ajax/libs/dashjs/2.6.3/dash.all.min.js" type="text/javascript"></script>
12
- <script src="https://cdnjs.cloudflare.com/ajax/libs/videojs-contrib-dash/2.9.2/videojs-dash.min.js" type="text/javascript"></script>
9
+
10
+ <script
11
+ src="https://cdn.jsdelivr.net/npm/cloudinary-video-player@latest/dist/cld-video-player.light.min.js"></script>
13
12
 
14
13
  <video
15
14
  style="min-width:100vw;min-height:100vh;max-width:100vw;max-height:100vh"
@@ -21,18 +21,22 @@ PAGINATION_MAX_RESULTS = 500
21
21
  _cursor_fields = {"resource": "derived_next_cursor"}
22
22
 
23
23
 
24
- def query_cld_folder(folder, folder_mode):
24
+ def query_cld_folder(folder, folder_mode, status=None):
25
25
  files = {}
26
26
 
27
27
  folder = folder.strip('/') # omit redundant leading slash and duplicate trailing slashes in query
28
28
  folder_key = "asset_folder" if folder_mode == "dynamic" else "folder"
29
29
  folder_query = f"{folder}/*" if folder else "*"
30
+ status_value = "(active OR pending)" if status == "all" else status
31
+ status_query = f" AND status:{status_value}" if status_value else ""
30
32
 
31
- expression = Search().expression(f"{folder_key}:\"{folder_query}\"").with_field("image_analysis").max_results(500)
33
+ search = Search().expression(f"{folder_key}:\"{folder_query}\"{status_query}").with_field("image_analysis").max_results(500)
34
+
35
+ logger.debug(f"Search expression: {search.to_json()}")
32
36
 
33
37
  next_cursor = True
34
38
  while next_cursor:
35
- res = expression.execute()
39
+ res = search.execute()
36
40
 
37
41
  for asset in res['resources']:
38
42
  rel_path = _relative_path(asset, folder)
@@ -58,7 +62,7 @@ def query_cld_folder(folder, folder_mode):
58
62
  }
59
63
  # use := when switch to python 3.8
60
64
  next_cursor = res.get('next_cursor')
61
- expression.next_cursor(next_cursor)
65
+ search.next_cursor(next_cursor)
62
66
 
63
67
  return files
64
68
 
@@ -78,7 +82,12 @@ def _display_path(asset):
78
82
  if asset.get("display_name") is None:
79
83
  return ""
80
84
 
81
- return "/".join([asset.get("asset_folder", ""), ".".join(filter(None, [asset["display_name"], asset.get("format", None)]))])
85
+ if asset["resource_type"] == "raw" or asset["type"] == 'fetch':
86
+ normalized_display_name = asset["display_name"]
87
+ else:
88
+ normalized_display_name = ".".join(filter(None, [asset["display_name"], asset.get("format", None)]))
89
+
90
+ return "/".join([asset.get("asset_folder", ""), normalized_display_name])
82
91
 
83
92
 
84
93
  def _relative_display_path(asset, folder):
@@ -129,7 +138,7 @@ def upload_file(file_path, options, uploaded=None, failed=None):
129
138
  disp_path = _display_path(result)
130
139
  if "batch_id" in result:
131
140
  starting_msg = "Uploading"
132
- disp_str = f"asynchnously with batch_id: {result['batch_id']}"
141
+ disp_str = f"asynchronously with batch_id: {result['batch_id']}"
133
142
  else:
134
143
  starting_msg = "Successfully uploaded"
135
144
  disp_str = f"as {result['public_id']}" if not disp_path \
@@ -0,0 +1,163 @@
1
+ import json
2
+ from cloudinary.utils import build_array
3
+
4
+
5
+ def parse_aggregate(agg_input):
6
+ """
7
+ Parses an aggregator definition or list of definitions into structured aggregator objects.
8
+
9
+ Accepts:
10
+ - Full JSON (if a string starts with '{')
11
+ - Transformation-style string (if a string contains ':')
12
+ - Simple aggregate string
13
+ - A list (or tuple) of any of the above
14
+
15
+ :param agg_input: Aggregator definition(s) as a string or list of strings.
16
+ :type agg_input: str or list or dict
17
+ :return: List of parsed aggregator objects.
18
+ :rtype: list
19
+ """
20
+ agg_list = build_array(agg_input)
21
+ parsed_aggregators = []
22
+
23
+ for agg in agg_list:
24
+ if isinstance(agg, str):
25
+ s = agg.strip()
26
+
27
+ if s.startswith("{"):
28
+ parsed = parse_json_aggregate(s)
29
+ else:
30
+ parsed = parse_aggregate_string(s)
31
+
32
+ parsed_aggregators.append(parsed)
33
+ else:
34
+ parsed_aggregators.append(agg)
35
+
36
+ return parsed_aggregators
37
+
38
+
39
+ def parse_json_aggregate(s):
40
+ """
41
+ Parses a JSON aggregator string.
42
+
43
+ :param s: JSON aggregator string.
44
+ :type s: str
45
+ :return: Parsed aggregator object.
46
+ :rtype: dict
47
+ :raises: ValueError if JSON is invalid or missing the required 'type' key.
48
+ """
49
+ try:
50
+ agg_obj = json.loads(s)
51
+ except json.JSONDecodeError as e:
52
+ raise ValueError("Invalid JSON provided for aggregate: " + str(e))
53
+
54
+ if not (isinstance(agg_obj, dict) and "type" in agg_obj):
55
+ raise ValueError("Full JSON aggregate must be an object with a 'type' key.")
56
+
57
+ return agg_obj
58
+
59
+
60
+ def parse_aggregate_string(s):
61
+ """
62
+ Parses a transformation-style aggregator string into a structured aggregator.
63
+
64
+ Expected format:
65
+ "agg_type:range1,range2,..."
66
+ where each range is in the format "<key>_<from>-<to>".
67
+
68
+ If the string does not contain a colon, it is returned as-is.
69
+
70
+ :param s: Aggregator string.
71
+ :type s: str
72
+ :return: Aggregator object (dict) if colon is present, else the original string.
73
+ """
74
+ if ":" not in s:
75
+ return s
76
+
77
+ try:
78
+ agg_type, range_str = s.split(":", 1)
79
+ except ValueError:
80
+ raise ValueError("Aggregator string must contain a colon separating type and ranges.")
81
+
82
+ agg_type = agg_type.strip()
83
+ ranges = []
84
+
85
+ for part in range_str.split(","):
86
+ part = part.strip()
87
+ if not part:
88
+ continue
89
+
90
+ range_dict = parse_range_definition(part)
91
+ ranges.append(range_dict)
92
+
93
+ result = {"type": agg_type, "ranges": ranges}
94
+ return result
95
+
96
+
97
+ def parse_range_definition(part):
98
+ """
99
+ Parses a single range definition in the format "<key>_<range_value>".
100
+
101
+ :param part: Range definition string.
102
+ :type part: str
103
+ :return: Dict with 'key' and parsed 'from' and/or 'to' values.
104
+ """
105
+ if "_" not in part:
106
+ raise ValueError("Range definition '{}' must contain an underscore separating key and value.".format(part))
107
+
108
+ key, value = part.split("_", 1)
109
+ key = key.strip()
110
+ value = value.strip()
111
+
112
+ if "-" not in value:
113
+ raise ValueError("Range value in '{}' must contain a dash (-) separating from and to values.".format(part))
114
+
115
+ from_val, to_val = parse_range_bounds(value, part)
116
+ range_dict = {"key": key}
117
+
118
+ if from_val is not None:
119
+ range_dict["from"] = from_val
120
+
121
+ if to_val is not None:
122
+ range_dict["to"] = to_val
123
+
124
+ return range_dict
125
+
126
+
127
+ def parse_range_bounds(value, part):
128
+ """
129
+ Parses a range value in the format "from-to", where either may be omitted.
130
+ Returns numeric values (int if whole number, else float) or None.
131
+
132
+ :param value: Range value string.
133
+ :type value: str
134
+ :param part: Original range definition string.
135
+ :type part: str
136
+ :return: Tuple (from_val, to_val) as numbers or None.
137
+ """
138
+ parts = value.split("-", 1)
139
+ from_val = parse_numeric_value(parts[0], "from", part)
140
+ to_val = parse_numeric_value(parts[1], "to", part)
141
+
142
+ return from_val, to_val
143
+
144
+ def parse_numeric_value(value, label, part):
145
+ """
146
+ Parses a numeric value (int or float) or returns None if the value is empty.
147
+
148
+ :param value: The string to parse.
149
+ :type value: str
150
+ :param label: The label ('from' or 'to') for error messages.
151
+ :type label: str
152
+ :param part: The original range definition string for error context.
153
+ :type part: str
154
+ :return: Parsed numeric value (int or float) or None.
155
+ :rtype: int, float, or None
156
+ :raises ValueError: If the value is not a valid number.
157
+ """
158
+ value = value.strip() if value else value
159
+ try:
160
+ num = float(value) if value else None
161
+ return int(num) if num is not None and num.is_integer() else num
162
+ except ValueError:
163
+ raise ValueError(f"Invalid numeric value for '{label}' in range '{part}'.")
@@ -0,0 +1 @@
1
+ __version__ = "1.13.0"
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: cloudinary-cli
3
- Version: 1.11.2
3
+ Version: 1.13.0
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
@@ -46,6 +46,7 @@ cloudinary_cli/utils/api_utils.py
46
46
  cloudinary_cli/utils/config_utils.py
47
47
  cloudinary_cli/utils/file_utils.py
48
48
  cloudinary_cli/utils/json_utils.py
49
+ cloudinary_cli/utils/search_utils.py
49
50
  cloudinary_cli/utils/utils.py
50
51
  test/__init__.py
51
52
  test/helper_test.py
@@ -57,6 +58,7 @@ test/test_cli_search_api.py
57
58
  test/test_cli_url.py
58
59
  test/test_cli_utils.py
59
60
  test/test_file_utils.py
61
+ test/test_search_utils.py
60
62
  test/test_utils.py
61
63
  test/test_modules/__init__.py
62
64
  test/test_modules/test_cli_make.py
@@ -41,6 +41,12 @@ class TestCLIMake(unittest.TestCase):
41
41
  self.assertIn('upload_widget', result.output)
42
42
  self.assertIn(f"cloudName: '{cloudinary.Config().cloud_name}'", result.output)
43
43
 
44
+ def test_cli_make_video_player(self):
45
+ result = self.runner.invoke(cli, ["make", "video_player"])
46
+
47
+ self.assertEqual(0, result.exit_code)
48
+ self.assertIn('demo-player', result.output)
49
+
44
50
  def test_cli_make_base_python(self):
45
51
  result = self.runner.invoke(cli, ["make", "base", "python"])
46
52
 
@@ -0,0 +1,170 @@
1
+ import unittest
2
+ from cloudinary_cli.utils.search_utils import (
3
+ parse_aggregate,
4
+ parse_json_aggregate,
5
+ parse_aggregate_string,
6
+ parse_range_definition,
7
+ parse_range_bounds
8
+ )
9
+
10
+
11
+ class TestAggregateParsing(unittest.TestCase):
12
+
13
+ # --- Tests for parse_json_aggregate ---
14
+
15
+ def test_parse_json_aggregate_valid(self):
16
+ s = '{"type": "bytes", "ranges": [{"key": "tiny", "to": 500}]}'
17
+ result = parse_json_aggregate(s)
18
+ expected = {"type": "bytes", "ranges": [{"key": "tiny", "to": 500}]}
19
+ self.assertEqual(expected, result)
20
+
21
+ def test_parse_json_aggregate_invalid_json(self):
22
+ s = '{"type": "bytes", "ranges": [{"key": "tiny", "to": 500}' # missing closing ]
23
+ with self.assertRaises(ValueError):
24
+ parse_json_aggregate(s)
25
+
26
+ def test_parse_json_aggregate_missing_type(self):
27
+ s = '{"ranges": [{"key": "tiny", "to": 500}]}'
28
+ with self.assertRaises(ValueError):
29
+ parse_json_aggregate(s)
30
+
31
+ # --- Tests for parse_aggregate_string ---
32
+
33
+ def test_parse_aggregate_string_valid(self):
34
+ s = "bytes:tiny_-500,medium_501-1999,big_2000-"
35
+ result = parse_aggregate_string(s)
36
+ expected = {
37
+ "type": "bytes",
38
+ "ranges": [
39
+ {"key": "tiny", "to": 500},
40
+ {"key": "medium", "from": 501, "to": 1999},
41
+ {"key": "big", "from": 2000}
42
+ ]
43
+ }
44
+ self.assertEqual(expected, result)
45
+
46
+ def test_parse_aggregate_string_no_colon(self):
47
+ s = "format"
48
+ result = parse_aggregate_string(s)
49
+ self.assertEqual(s, result)
50
+
51
+ # --- Tests for parse_aggregate (supports list and non-string inputs) ---
52
+
53
+ def test_parse_aggregate_simple_string(self):
54
+ s = "format"
55
+ result = parse_aggregate(s)
56
+ self.assertEqual([s], result)
57
+
58
+ def test_parse_aggregate_json(self):
59
+ s = '{"type": "bytes", "ranges": [{"key": "tiny", "to": 500}]}'
60
+ result = parse_aggregate(s)
61
+ expected = [{"type": "bytes", "ranges": [{"key": "tiny", "to": 500}]}]
62
+ self.assertEqual(expected, result)
63
+
64
+ def test_parse_aggregate_transformation_string(self):
65
+ s = "bytes:tiny_-500,medium_501-1999,big_2000-"
66
+ result = parse_aggregate(s)
67
+ expected = [{
68
+ "type": "bytes",
69
+ "ranges": [
70
+ {"key": "tiny", "to": 500},
71
+ {"key": "medium", "from": 501, "to": 1999},
72
+ {"key": "big", "from": 2000}
73
+ ]
74
+ }]
75
+ self.assertEqual(expected, result)
76
+
77
+ def test_parse_aggregate_list_input(self):
78
+ input_list = [
79
+ "format",
80
+ "bytes:tiny_-500,medium_501-1999,big_2000-"
81
+ ]
82
+ result = parse_aggregate(input_list)
83
+ expected = [
84
+ "format",
85
+ {
86
+ "type": "bytes",
87
+ "ranges": [
88
+ {"key": "tiny", "to": 500},
89
+ {"key": "medium", "from": 501, "to": 1999},
90
+ {"key": "big", "from": 2000}
91
+ ]
92
+ }
93
+ ]
94
+ self.assertEqual(expected, result)
95
+
96
+ def test_parse_aggregate_non_string(self):
97
+ # If a non-string (e.g. dict) is passed, build_array wraps it, and it is returned as is.
98
+ d = {"type": "custom", "value": 123}
99
+ result = parse_aggregate(d)
100
+ self.assertEqual([d], result)
101
+
102
+ # --- Tests for parse_range_definition ---
103
+
104
+ def test_parse_range_definition_valid_tiny(self):
105
+ part = "tiny_-500"
106
+ result = parse_range_definition(part)
107
+ expected = {"key": "tiny", "to": 500}
108
+ self.assertEqual(expected, result)
109
+
110
+ def test_parse_range_definition_valid_medium(self):
111
+ part = "medium_501-1999"
112
+ result = parse_range_definition(part)
113
+ expected = {"key": "medium", "from": 501, "to": 1999}
114
+ self.assertEqual(expected, result)
115
+
116
+ def test_parse_range_definition_valid_big(self):
117
+ part = "big_2000-"
118
+ result = parse_range_definition(part)
119
+ expected = {"key": "big", "from": 2000}
120
+ self.assertEqual(expected, result)
121
+
122
+ def test_parse_range_definition_missing_underscore(self):
123
+ part = "big2000-"
124
+ with self.assertRaises(ValueError):
125
+ parse_range_definition(part)
126
+
127
+ def test_parse_range_definition_missing_dash(self):
128
+ part = "big_2000"
129
+ with self.assertRaises(ValueError):
130
+ parse_range_definition(part)
131
+
132
+ # --- Tests for parse_range_bounds ---
133
+
134
+ def test_parse_range_bounds_whole_numbers(self):
135
+ value = "501-1999"
136
+ result = parse_range_bounds(value, "test")
137
+ expected = (501, 1999)
138
+ self.assertEqual(expected, result)
139
+
140
+ def test_parse_range_bounds_floats(self):
141
+ value = "24.5-29.97"
142
+ result = parse_range_bounds(value, "test")
143
+ expected = (24.5, 29.97)
144
+ self.assertEqual(expected, result)
145
+
146
+ def test_parse_range_bounds_empty_from(self):
147
+ value = "-500"
148
+ result = parse_range_bounds(value, "test")
149
+ expected = (None, 500)
150
+ self.assertEqual(expected, result)
151
+
152
+ def test_parse_range_bounds_empty_to(self):
153
+ value = "2000-"
154
+ result = parse_range_bounds(value, "test")
155
+ expected = (2000, None)
156
+ self.assertEqual(expected, result)
157
+
158
+ def test_parse_range_bounds_invalid_from(self):
159
+ value = "abc-100"
160
+ with self.assertRaises(ValueError):
161
+ parse_range_bounds(value, "test")
162
+
163
+ def test_parse_range_bounds_invalid_to(self):
164
+ value = "100-abc"
165
+ with self.assertRaises(ValueError):
166
+ parse_range_bounds(value, "test")
167
+
168
+
169
+ if __name__ == '__main__':
170
+ unittest.main()
@@ -1 +0,0 @@
1
- __version__ = "1.11.2"
File without changes