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.
- {cloudinary_cli-1.11.2/cloudinary_cli.egg-info → cloudinary_cli-1.13.0}/PKG-INFO +1 -1
- {cloudinary_cli-1.11.2 → cloudinary_cli-1.13.0}/cloudinary_cli/core/search.py +2 -1
- {cloudinary_cli-1.11.2 → cloudinary_cli-1.13.0}/cloudinary_cli/modules/sync.py +6 -4
- {cloudinary_cli-1.11.2 → cloudinary_cli-1.13.0}/cloudinary_cli/templates/html/video_player +5 -6
- {cloudinary_cli-1.11.2 → cloudinary_cli-1.13.0}/cloudinary_cli/utils/api_utils.py +15 -6
- cloudinary_cli-1.13.0/cloudinary_cli/utils/search_utils.py +163 -0
- cloudinary_cli-1.13.0/cloudinary_cli/version.py +1 -0
- {cloudinary_cli-1.11.2 → cloudinary_cli-1.13.0/cloudinary_cli.egg-info}/PKG-INFO +1 -1
- {cloudinary_cli-1.11.2 → cloudinary_cli-1.13.0}/cloudinary_cli.egg-info/SOURCES.txt +2 -0
- {cloudinary_cli-1.11.2 → cloudinary_cli-1.13.0}/test/test_modules/test_cli_make.py +6 -0
- cloudinary_cli-1.13.0/test/test_search_utils.py +170 -0
- cloudinary_cli-1.11.2/cloudinary_cli/version.py +0 -1
- {cloudinary_cli-1.11.2 → cloudinary_cli-1.13.0}/LICENSE +0 -0
- {cloudinary_cli-1.11.2 → cloudinary_cli-1.13.0}/MANIFEST.in +0 -0
- {cloudinary_cli-1.11.2 → cloudinary_cli-1.13.0}/README.md +0 -0
- {cloudinary_cli-1.11.2 → cloudinary_cli-1.13.0}/cloudinary_cli/__init__.py +0 -0
- {cloudinary_cli-1.11.2 → cloudinary_cli-1.13.0}/cloudinary_cli/cli.py +0 -0
- {cloudinary_cli-1.11.2 → cloudinary_cli-1.13.0}/cloudinary_cli/cli_group.py +0 -0
- {cloudinary_cli-1.11.2 → cloudinary_cli-1.13.0}/cloudinary_cli/core/__init__.py +0 -0
- {cloudinary_cli-1.11.2 → cloudinary_cli-1.13.0}/cloudinary_cli/core/admin.py +0 -0
- {cloudinary_cli-1.11.2 → cloudinary_cli-1.13.0}/cloudinary_cli/core/config.py +0 -0
- {cloudinary_cli-1.11.2 → cloudinary_cli-1.13.0}/cloudinary_cli/core/overrides.py +0 -0
- {cloudinary_cli-1.11.2 → cloudinary_cli-1.13.0}/cloudinary_cli/core/provisioning.py +0 -0
- {cloudinary_cli-1.11.2 → cloudinary_cli-1.13.0}/cloudinary_cli/core/uploader.py +0 -0
- {cloudinary_cli-1.11.2 → cloudinary_cli-1.13.0}/cloudinary_cli/core/utils.py +0 -0
- {cloudinary_cli-1.11.2 → cloudinary_cli-1.13.0}/cloudinary_cli/defaults.py +0 -0
- {cloudinary_cli-1.11.2 → cloudinary_cli-1.13.0}/cloudinary_cli/modules/__init__.py +0 -0
- {cloudinary_cli-1.11.2 → cloudinary_cli-1.13.0}/cloudinary_cli/modules/clone.py +0 -0
- {cloudinary_cli-1.11.2 → cloudinary_cli-1.13.0}/cloudinary_cli/modules/make.py +0 -0
- {cloudinary_cli-1.11.2 → cloudinary_cli-1.13.0}/cloudinary_cli/modules/migrate.py +0 -0
- {cloudinary_cli-1.11.2 → cloudinary_cli-1.13.0}/cloudinary_cli/modules/regen_derived.py +0 -0
- {cloudinary_cli-1.11.2 → cloudinary_cli-1.13.0}/cloudinary_cli/modules/upload_dir.py +0 -0
- {cloudinary_cli-1.11.2 → cloudinary_cli-1.13.0}/cloudinary_cli/samples/__init__.py +0 -0
- {cloudinary_cli-1.11.2 → cloudinary_cli-1.13.0}/cloudinary_cli/templates/html/media_library_widget +0 -0
- {cloudinary_cli-1.11.2 → cloudinary_cli-1.13.0}/cloudinary_cli/templates/html/product_gallery +0 -0
- {cloudinary_cli-1.11.2 → cloudinary_cli-1.13.0}/cloudinary_cli/templates/html/upload_widget +0 -0
- {cloudinary_cli-1.11.2 → cloudinary_cli-1.13.0}/cloudinary_cli/templates/node/upload +0 -0
- {cloudinary_cli-1.11.2 → cloudinary_cli-1.13.0}/cloudinary_cli/templates/python/base +0 -0
- {cloudinary_cli-1.11.2 → cloudinary_cli-1.13.0}/cloudinary_cli/templates/python/explicit +0 -0
- {cloudinary_cli-1.11.2 → cloudinary_cli-1.13.0}/cloudinary_cli/templates/python/find_all_empty_folders +0 -0
- {cloudinary_cli-1.11.2 → cloudinary_cli-1.13.0}/cloudinary_cli/templates/python/upload +0 -0
- {cloudinary_cli-1.11.2 → cloudinary_cli-1.13.0}/cloudinary_cli/templates/ruby/upload +0 -0
- {cloudinary_cli-1.11.2 → cloudinary_cli-1.13.0}/cloudinary_cli/utils/__init__.py +0 -0
- {cloudinary_cli-1.11.2 → cloudinary_cli-1.13.0}/cloudinary_cli/utils/config_utils.py +0 -0
- {cloudinary_cli-1.11.2 → cloudinary_cli-1.13.0}/cloudinary_cli/utils/file_utils.py +0 -0
- {cloudinary_cli-1.11.2 → cloudinary_cli-1.13.0}/cloudinary_cli/utils/json_utils.py +0 -0
- {cloudinary_cli-1.11.2 → cloudinary_cli-1.13.0}/cloudinary_cli/utils/utils.py +0 -0
- {cloudinary_cli-1.11.2 → cloudinary_cli-1.13.0}/cloudinary_cli.egg-info/dependency_links.txt +0 -0
- {cloudinary_cli-1.11.2 → cloudinary_cli-1.13.0}/cloudinary_cli.egg-info/entry_points.txt +0 -0
- {cloudinary_cli-1.11.2 → cloudinary_cli-1.13.0}/cloudinary_cli.egg-info/not-zip-safe +0 -0
- {cloudinary_cli-1.11.2 → cloudinary_cli-1.13.0}/cloudinary_cli.egg-info/requires.txt +0 -0
- {cloudinary_cli-1.11.2 → cloudinary_cli-1.13.0}/cloudinary_cli.egg-info/top_level.txt +0 -0
- {cloudinary_cli-1.11.2 → cloudinary_cli-1.13.0}/requirements.txt +0 -0
- {cloudinary_cli-1.11.2 → cloudinary_cli-1.13.0}/setup.cfg +0 -0
- {cloudinary_cli-1.11.2 → cloudinary_cli-1.13.0}/setup.py +0 -0
- {cloudinary_cli-1.11.2 → cloudinary_cli-1.13.0}/test/__init__.py +0 -0
- {cloudinary_cli-1.11.2 → cloudinary_cli-1.13.0}/test/helper_test.py +0 -0
- {cloudinary_cli-1.11.2 → cloudinary_cli-1.13.0}/test/test_cli.py +0 -0
- {cloudinary_cli-1.11.2 → cloudinary_cli-1.13.0}/test/test_cli_api.py +0 -0
- {cloudinary_cli-1.11.2 → cloudinary_cli-1.13.0}/test/test_cli_config.py +0 -0
- {cloudinary_cli-1.11.2 → cloudinary_cli-1.13.0}/test/test_cli_samples.py +0 -0
- {cloudinary_cli-1.11.2 → cloudinary_cli-1.13.0}/test/test_cli_search_api.py +0 -0
- {cloudinary_cli-1.11.2 → cloudinary_cli-1.13.0}/test/test_cli_url.py +0 -0
- {cloudinary_cli-1.11.2 → cloudinary_cli-1.13.0}/test/test_cli_utils.py +0 -0
- {cloudinary_cli-1.11.2 → cloudinary_cli-1.13.0}/test/test_file_utils.py +0 -0
- {cloudinary_cli-1.11.2 → cloudinary_cli-1.13.0}/test/test_modules/__init__.py +0 -0
- {cloudinary_cli-1.11.2 → cloudinary_cli-1.13.0}/test/test_modules/test_cli_sync.py +0 -0
- {cloudinary_cli-1.11.2 → cloudinary_cli-1.13.0}/test/test_modules/test_cli_upload_dir.py +0 -0
- {cloudinary_cli-1.11.2 → cloudinary_cli-1.13.0}/test/test_utils.py +0 -0
|
@@ -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
|
|
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
|
-
|
|
9
|
-
<script
|
|
10
|
-
|
|
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
|
-
|
|
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 =
|
|
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
|
-
|
|
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
|
-
|
|
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"
|
|
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"
|
|
@@ -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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{cloudinary_cli-1.11.2 → cloudinary_cli-1.13.0}/cloudinary_cli/templates/html/media_library_widget
RENAMED
|
File without changes
|
{cloudinary_cli-1.11.2 → cloudinary_cli-1.13.0}/cloudinary_cli/templates/html/product_gallery
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{cloudinary_cli-1.11.2 → cloudinary_cli-1.13.0}/cloudinary_cli.egg-info/dependency_links.txt
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|