cloudinary-cli 1.12.0__tar.gz → 1.14.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 (71) hide show
  1. {cloudinary_cli-1.12.0/cloudinary_cli.egg-info → cloudinary_cli-1.14.0}/PKG-INFO +34 -7
  2. {cloudinary_cli-1.12.0 → cloudinary_cli-1.14.0}/README.md +33 -6
  3. {cloudinary_cli-1.12.0 → cloudinary_cli-1.14.0}/cloudinary_cli/cli_group.py +9 -3
  4. {cloudinary_cli-1.12.0 → cloudinary_cli-1.14.0}/cloudinary_cli/core/__init__.py +1 -1
  5. {cloudinary_cli-1.12.0 → cloudinary_cli-1.14.0}/cloudinary_cli/core/overrides.py +6 -1
  6. {cloudinary_cli-1.12.0 → cloudinary_cli-1.14.0}/cloudinary_cli/core/search.py +2 -1
  7. cloudinary_cli-1.14.0/cloudinary_cli/modules/clone.py +245 -0
  8. {cloudinary_cli-1.12.0 → cloudinary_cli-1.14.0}/cloudinary_cli/modules/sync.py +1 -1
  9. {cloudinary_cli-1.12.0 → cloudinary_cli-1.14.0}/cloudinary_cli/templates/html/video_player +5 -6
  10. cloudinary_cli-1.14.0/cloudinary_cli/utils/search_utils.py +163 -0
  11. {cloudinary_cli-1.12.0 → cloudinary_cli-1.14.0}/cloudinary_cli/utils/utils.py +22 -0
  12. cloudinary_cli-1.14.0/cloudinary_cli/version.py +1 -0
  13. {cloudinary_cli-1.12.0 → cloudinary_cli-1.14.0/cloudinary_cli.egg-info}/PKG-INFO +34 -7
  14. {cloudinary_cli-1.12.0 → cloudinary_cli-1.14.0}/cloudinary_cli.egg-info/SOURCES.txt +3 -0
  15. cloudinary_cli-1.14.0/test/test_modules/test_cli_clone.py +317 -0
  16. {cloudinary_cli-1.12.0 → cloudinary_cli-1.14.0}/test/test_modules/test_cli_make.py +6 -0
  17. cloudinary_cli-1.14.0/test/test_search_utils.py +170 -0
  18. cloudinary_cli-1.12.0/cloudinary_cli/modules/clone.py +0 -112
  19. cloudinary_cli-1.12.0/cloudinary_cli/version.py +0 -1
  20. {cloudinary_cli-1.12.0 → cloudinary_cli-1.14.0}/LICENSE +0 -0
  21. {cloudinary_cli-1.12.0 → cloudinary_cli-1.14.0}/MANIFEST.in +0 -0
  22. {cloudinary_cli-1.12.0 → cloudinary_cli-1.14.0}/cloudinary_cli/__init__.py +0 -0
  23. {cloudinary_cli-1.12.0 → cloudinary_cli-1.14.0}/cloudinary_cli/cli.py +0 -0
  24. {cloudinary_cli-1.12.0 → cloudinary_cli-1.14.0}/cloudinary_cli/core/admin.py +0 -0
  25. {cloudinary_cli-1.12.0 → cloudinary_cli-1.14.0}/cloudinary_cli/core/config.py +0 -0
  26. {cloudinary_cli-1.12.0 → cloudinary_cli-1.14.0}/cloudinary_cli/core/provisioning.py +0 -0
  27. {cloudinary_cli-1.12.0 → cloudinary_cli-1.14.0}/cloudinary_cli/core/uploader.py +0 -0
  28. {cloudinary_cli-1.12.0 → cloudinary_cli-1.14.0}/cloudinary_cli/core/utils.py +0 -0
  29. {cloudinary_cli-1.12.0 → cloudinary_cli-1.14.0}/cloudinary_cli/defaults.py +0 -0
  30. {cloudinary_cli-1.12.0 → cloudinary_cli-1.14.0}/cloudinary_cli/modules/__init__.py +0 -0
  31. {cloudinary_cli-1.12.0 → cloudinary_cli-1.14.0}/cloudinary_cli/modules/make.py +0 -0
  32. {cloudinary_cli-1.12.0 → cloudinary_cli-1.14.0}/cloudinary_cli/modules/migrate.py +0 -0
  33. {cloudinary_cli-1.12.0 → cloudinary_cli-1.14.0}/cloudinary_cli/modules/regen_derived.py +0 -0
  34. {cloudinary_cli-1.12.0 → cloudinary_cli-1.14.0}/cloudinary_cli/modules/upload_dir.py +0 -0
  35. {cloudinary_cli-1.12.0 → cloudinary_cli-1.14.0}/cloudinary_cli/samples/__init__.py +0 -0
  36. {cloudinary_cli-1.12.0 → cloudinary_cli-1.14.0}/cloudinary_cli/templates/html/media_library_widget +0 -0
  37. {cloudinary_cli-1.12.0 → cloudinary_cli-1.14.0}/cloudinary_cli/templates/html/product_gallery +0 -0
  38. {cloudinary_cli-1.12.0 → cloudinary_cli-1.14.0}/cloudinary_cli/templates/html/upload_widget +0 -0
  39. {cloudinary_cli-1.12.0 → cloudinary_cli-1.14.0}/cloudinary_cli/templates/node/upload +0 -0
  40. {cloudinary_cli-1.12.0 → cloudinary_cli-1.14.0}/cloudinary_cli/templates/python/base +0 -0
  41. {cloudinary_cli-1.12.0 → cloudinary_cli-1.14.0}/cloudinary_cli/templates/python/explicit +0 -0
  42. {cloudinary_cli-1.12.0 → cloudinary_cli-1.14.0}/cloudinary_cli/templates/python/find_all_empty_folders +0 -0
  43. {cloudinary_cli-1.12.0 → cloudinary_cli-1.14.0}/cloudinary_cli/templates/python/upload +0 -0
  44. {cloudinary_cli-1.12.0 → cloudinary_cli-1.14.0}/cloudinary_cli/templates/ruby/upload +0 -0
  45. {cloudinary_cli-1.12.0 → cloudinary_cli-1.14.0}/cloudinary_cli/utils/__init__.py +0 -0
  46. {cloudinary_cli-1.12.0 → cloudinary_cli-1.14.0}/cloudinary_cli/utils/api_utils.py +0 -0
  47. {cloudinary_cli-1.12.0 → cloudinary_cli-1.14.0}/cloudinary_cli/utils/config_utils.py +0 -0
  48. {cloudinary_cli-1.12.0 → cloudinary_cli-1.14.0}/cloudinary_cli/utils/file_utils.py +0 -0
  49. {cloudinary_cli-1.12.0 → cloudinary_cli-1.14.0}/cloudinary_cli/utils/json_utils.py +0 -0
  50. {cloudinary_cli-1.12.0 → cloudinary_cli-1.14.0}/cloudinary_cli.egg-info/dependency_links.txt +0 -0
  51. {cloudinary_cli-1.12.0 → cloudinary_cli-1.14.0}/cloudinary_cli.egg-info/entry_points.txt +0 -0
  52. {cloudinary_cli-1.12.0 → cloudinary_cli-1.14.0}/cloudinary_cli.egg-info/not-zip-safe +0 -0
  53. {cloudinary_cli-1.12.0 → cloudinary_cli-1.14.0}/cloudinary_cli.egg-info/requires.txt +0 -0
  54. {cloudinary_cli-1.12.0 → cloudinary_cli-1.14.0}/cloudinary_cli.egg-info/top_level.txt +0 -0
  55. {cloudinary_cli-1.12.0 → cloudinary_cli-1.14.0}/requirements.txt +0 -0
  56. {cloudinary_cli-1.12.0 → cloudinary_cli-1.14.0}/setup.cfg +0 -0
  57. {cloudinary_cli-1.12.0 → cloudinary_cli-1.14.0}/setup.py +0 -0
  58. {cloudinary_cli-1.12.0 → cloudinary_cli-1.14.0}/test/__init__.py +0 -0
  59. {cloudinary_cli-1.12.0 → cloudinary_cli-1.14.0}/test/helper_test.py +0 -0
  60. {cloudinary_cli-1.12.0 → cloudinary_cli-1.14.0}/test/test_cli.py +0 -0
  61. {cloudinary_cli-1.12.0 → cloudinary_cli-1.14.0}/test/test_cli_api.py +0 -0
  62. {cloudinary_cli-1.12.0 → cloudinary_cli-1.14.0}/test/test_cli_config.py +0 -0
  63. {cloudinary_cli-1.12.0 → cloudinary_cli-1.14.0}/test/test_cli_samples.py +0 -0
  64. {cloudinary_cli-1.12.0 → cloudinary_cli-1.14.0}/test/test_cli_search_api.py +0 -0
  65. {cloudinary_cli-1.12.0 → cloudinary_cli-1.14.0}/test/test_cli_url.py +0 -0
  66. {cloudinary_cli-1.12.0 → cloudinary_cli-1.14.0}/test/test_cli_utils.py +0 -0
  67. {cloudinary_cli-1.12.0 → cloudinary_cli-1.14.0}/test/test_file_utils.py +0 -0
  68. {cloudinary_cli-1.12.0 → cloudinary_cli-1.14.0}/test/test_modules/__init__.py +0 -0
  69. {cloudinary_cli-1.12.0 → cloudinary_cli-1.14.0}/test/test_modules/test_cli_sync.py +0 -0
  70. {cloudinary_cli-1.12.0 → cloudinary_cli-1.14.0}/test/test_modules/test_cli_upload_dir.py +0 -0
  71. {cloudinary_cli-1.12.0 → cloudinary_cli-1.14.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.12.0
3
+ Version: 1.14.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
@@ -57,7 +57,7 @@ _**Note:** you can copy and paste your account environment variable from the Acc
57
57
  api_secret: ***************<LAST_4_DIGITS>
58
58
  private_cdn: <True|False>
59
59
  ```
60
-
60
+
61
61
  If you get an error message when running `cld config`, you may need to add your Python installation to your $PATH. To do so, you can run `PATH="$PATH:/Library/Python/Versions/3.8/bin"` in your terminal, and add `export PATH="$PATH:/Library/Python/Versions/3.8/bin"` to your `/.bash_profile` or `~/.zshrc`.
62
62
 
63
63
  ## Quickstart
@@ -77,11 +77,38 @@ cld admin # Lists Admin API methods.
77
77
  cld uploader # Lists Upload API methods.
78
78
  ```
79
79
 
80
+ ## Docker Usage
81
+
82
+ The Cloudinary CLI is also available as a Docker image, perfect for containerized environments, CI/CD pipelines, or when you prefer not to install Python locally.
83
+
84
+ ### Quick Docker Examples
85
+
86
+ ```sh
87
+ # Check configuration
88
+ docker run --rm -e CLOUDINARY_URL cloudinary/cli config
89
+
90
+ # Upload files from your local machine
91
+ docker run --rm -e CLOUDINARY_URL \
92
+ -v /path/to/your/images:/app/images \
93
+ cloudinary/cli uploader upload /app/images/sample.jpg
94
+
95
+ # Search for assets
96
+ docker run --rm -e CLOUDINARY_URL \
97
+ cloudinary/cli search "resource_type:image" --max_results 5
98
+
99
+ # Interactive mode for multiple commands
100
+ docker run --rm -it -e CLOUDINARY_URL --entrypoint /bin/bash cloudinary/cli
101
+ ```
102
+
103
+ **Docker Image**: `cloudinary/cli` • **Base**: Debian 12 • **Size**: ~175MB • **Multi-arch**: amd64, arm64
104
+
105
+ For comprehensive Docker usage, examples, and troubleshooting, see [DOCKER.md](DOCKER.md).
106
+
80
107
  ## Upload API
81
108
 
82
109
  Enables you to run any methods that can be called through the upload API.
83
110
 
84
- You can find documentation for each of the Upload API methods at [https://cloudinary.com/documentation/image_upload_api_reference](https://cloudinary.com/documentation/image_upload_api_reference).
111
+ You can find documentation for each of the Upload API methods at [https://cloudinary.com/documentation/image_upload_api_reference](https://cloudinary.com/documentation/image_upload_api_reference).
85
112
 
86
113
  The basic syntax using the Upload API is as follows:
87
114
 
@@ -152,7 +179,7 @@ cld [cli options] url [command options] public_id [transformation]
152
179
 
153
180
  For details, see the [Cloudinary CLI documentation](https://cloudinary.com/documentation/cloudinary_cli#url).
154
181
 
155
- Example: generate a URL that displays the image in your media library that has the public ID of 'sample', with a width of 500 pixels and transformed using the cartoonify effect, then open this URL in a browser.
182
+ Example: generate a URL that displays the image in your media library that has the public ID of 'sample', with a width of 500 pixels and transformed using the cartoonify effect, then open this URL in a browser.
156
183
 
157
184
  ```
158
185
  cld url -rt image -t upload -o sample w_500,e_cartoonify
@@ -187,9 +214,9 @@ Uploads a folder of assets, maintaining the folder structure.
187
214
  cld [cli options] upload_dir [command options] [local_folder]
188
215
  ```
189
216
 
190
- For details, see the [Cloudinary CLI documentation](https://cloudinary.com/documentation/cloudinary_cli#upload_dir).
217
+ For details, see the [Cloudinary CLI documentation](https://cloudinary.com/documentation/cloudinary_cli#upload_dir).
191
218
 
192
- Example: upload the local folder, my_images, and all its contents and sub-folders to your Cloudinary folder my_images_on_cloudinary.
219
+ Example: upload the local folder, my_images, and all its contents and sub-folders to your Cloudinary folder my_images_on_cloudinary.
193
220
 
194
221
  ```
195
222
  cld upload_dir -f my_images_on_cloudinary my_images
@@ -227,7 +254,7 @@ A configuration is a reference to a specified Cloudinary account or cloud name v
227
254
 
228
255
  The `config` command displays the current configuration and lets you manage additional configurations.
229
256
 
230
- You can specify the environment variable of additional Cloudinary accounts either explicitly (`-c` option) or as a saved configuration (`-C` option).
257
+ You can specify the environment variable of additional Cloudinary accounts either explicitly (`-c` option) or as a saved configuration (`-C` option).
231
258
 
232
259
  For example, using the `-c` option:
233
260
 
@@ -32,7 +32,7 @@ _**Note:** you can copy and paste your account environment variable from the Acc
32
32
  api_secret: ***************<LAST_4_DIGITS>
33
33
  private_cdn: <True|False>
34
34
  ```
35
-
35
+
36
36
  If you get an error message when running `cld config`, you may need to add your Python installation to your $PATH. To do so, you can run `PATH="$PATH:/Library/Python/Versions/3.8/bin"` in your terminal, and add `export PATH="$PATH:/Library/Python/Versions/3.8/bin"` to your `/.bash_profile` or `~/.zshrc`.
37
37
 
38
38
  ## Quickstart
@@ -52,11 +52,38 @@ cld admin # Lists Admin API methods.
52
52
  cld uploader # Lists Upload API methods.
53
53
  ```
54
54
 
55
+ ## Docker Usage
56
+
57
+ The Cloudinary CLI is also available as a Docker image, perfect for containerized environments, CI/CD pipelines, or when you prefer not to install Python locally.
58
+
59
+ ### Quick Docker Examples
60
+
61
+ ```sh
62
+ # Check configuration
63
+ docker run --rm -e CLOUDINARY_URL cloudinary/cli config
64
+
65
+ # Upload files from your local machine
66
+ docker run --rm -e CLOUDINARY_URL \
67
+ -v /path/to/your/images:/app/images \
68
+ cloudinary/cli uploader upload /app/images/sample.jpg
69
+
70
+ # Search for assets
71
+ docker run --rm -e CLOUDINARY_URL \
72
+ cloudinary/cli search "resource_type:image" --max_results 5
73
+
74
+ # Interactive mode for multiple commands
75
+ docker run --rm -it -e CLOUDINARY_URL --entrypoint /bin/bash cloudinary/cli
76
+ ```
77
+
78
+ **Docker Image**: `cloudinary/cli` • **Base**: Debian 12 • **Size**: ~175MB • **Multi-arch**: amd64, arm64
79
+
80
+ For comprehensive Docker usage, examples, and troubleshooting, see [DOCKER.md](DOCKER.md).
81
+
55
82
  ## Upload API
56
83
 
57
84
  Enables you to run any methods that can be called through the upload API.
58
85
 
59
- You can find documentation for each of the Upload API methods at [https://cloudinary.com/documentation/image_upload_api_reference](https://cloudinary.com/documentation/image_upload_api_reference).
86
+ You can find documentation for each of the Upload API methods at [https://cloudinary.com/documentation/image_upload_api_reference](https://cloudinary.com/documentation/image_upload_api_reference).
60
87
 
61
88
  The basic syntax using the Upload API is as follows:
62
89
 
@@ -127,7 +154,7 @@ cld [cli options] url [command options] public_id [transformation]
127
154
 
128
155
  For details, see the [Cloudinary CLI documentation](https://cloudinary.com/documentation/cloudinary_cli#url).
129
156
 
130
- Example: generate a URL that displays the image in your media library that has the public ID of 'sample', with a width of 500 pixels and transformed using the cartoonify effect, then open this URL in a browser.
157
+ Example: generate a URL that displays the image in your media library that has the public ID of 'sample', with a width of 500 pixels and transformed using the cartoonify effect, then open this URL in a browser.
131
158
 
132
159
  ```
133
160
  cld url -rt image -t upload -o sample w_500,e_cartoonify
@@ -162,9 +189,9 @@ Uploads a folder of assets, maintaining the folder structure.
162
189
  cld [cli options] upload_dir [command options] [local_folder]
163
190
  ```
164
191
 
165
- For details, see the [Cloudinary CLI documentation](https://cloudinary.com/documentation/cloudinary_cli#upload_dir).
192
+ For details, see the [Cloudinary CLI documentation](https://cloudinary.com/documentation/cloudinary_cli#upload_dir).
166
193
 
167
- Example: upload the local folder, my_images, and all its contents and sub-folders to your Cloudinary folder my_images_on_cloudinary.
194
+ Example: upload the local folder, my_images, and all its contents and sub-folders to your Cloudinary folder my_images_on_cloudinary.
168
195
 
169
196
  ```
170
197
  cld upload_dir -f my_images_on_cloudinary my_images
@@ -202,7 +229,7 @@ A configuration is a reference to a specified Cloudinary account or cloud name v
202
229
 
203
230
  The `config` command displays the current configuration and lets you manage additional configurations.
204
231
 
205
- You can specify the environment variable of additional Cloudinary accounts either explicitly (`-c` option) or as a saved configuration (`-C` option).
232
+ You can specify the environment variable of additional Cloudinary accounts either explicitly (`-c` option) or as a saved configuration (`-C` option).
206
233
 
207
234
  For example, using the `-c` option:
208
235
 
@@ -14,7 +14,7 @@ from cloudinary_cli.version import __version__ as cli_version
14
14
  CONTEXT_SETTINGS = dict(max_content_width=shutil.get_terminal_size()[0], terminal_width=shutil.get_terminal_size()[0])
15
15
 
16
16
 
17
- @click.group(context_settings=CONTEXT_SETTINGS)
17
+ @click.group(context_settings=CONTEXT_SETTINGS, invoke_without_command=True)
18
18
  @click.help_option()
19
19
  @click.version_option(cli_version, prog_name="Cloudinary CLI",
20
20
  message=f"%(prog)s, version %(version)s\n"
@@ -24,10 +24,11 @@ CONTEXT_SETTINGS = dict(max_content_width=shutil.get_terminal_size()[0], termina
24
24
  help="""Tell the CLI which account to run the command on by specifying an account environment variable."""
25
25
  )
26
26
  @click.option("-C", "--config_saved",
27
- help="""Tell the CLI which account to run the command on by specifying a saved configuration - see
27
+ help="""Tell the CLI which account to run the command on by specifying a saved configuration - see
28
28
  `config` command.""")
29
29
  @click_log.simple_verbosity_option(logger)
30
- def cli(config, config_saved):
30
+ @click.pass_context
31
+ def cli(ctx, config, config_saved):
31
32
  if config:
32
33
  refresh_cloudinary_config(config)
33
34
  elif config_saved:
@@ -40,4 +41,9 @@ def cli(config, config_saved):
40
41
  if not is_valid_cloudinary_config():
41
42
  logger.warning("No Cloudinary configuration found.")
42
43
 
44
+ # If no subcommand was invoked, show help and exit with code 0
45
+ if ctx.invoked_subcommand is None:
46
+ click.echo(ctx.get_help())
47
+ ctx.exit(0)
48
+
43
49
  return True
@@ -8,7 +8,7 @@ from cloudinary_cli.core.provisioning import provisioning
8
8
  from cloudinary_cli.core.utils import url, utils
9
9
  from cloudinary_cli.core.overrides import resolve_command
10
10
 
11
- setattr(click.MultiCommand, "resolve_command", resolve_command)
11
+ setattr(click.Group, "resolve_command", resolve_command)
12
12
 
13
13
  commands = [
14
14
  config,
@@ -1,14 +1,19 @@
1
- from click.parser import split_opt
2
1
  from click.utils import make_str
3
2
  from cloudinary import api, uploader
4
3
  from cloudinary.uploader import upload as original_upload
5
4
  from cloudinary.utils import cloudinary_url as original_cloudinary_url
5
+ from cloudinary_cli.utils.utils import split_opt
6
6
 
7
7
 
8
8
  # overrides click.MultiCommand.resolve_command
9
9
  def resolve_command(self, ctx, args):
10
10
  # Patch the `resolve_command` function to enable simple commands (eg. cld resource)
11
11
  # Only core commands from API and modules are registered (eg. cld admin)
12
+
13
+ # Handle empty args (when CLI is invoked with no arguments)
14
+ if not args:
15
+ return None, None, []
16
+
12
17
  cmd_name = make_str(args[0])
13
18
  original_cmd_name = cmd_name
14
19
 
@@ -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:
@@ -0,0 +1,245 @@
1
+ from click import command, option, style, argument
2
+ from cloudinary_cli.utils.utils import normalize_list_params, print_help_and_exit
3
+ import cloudinary
4
+ from cloudinary.auth_token import _digest
5
+ from cloudinary_cli.utils.utils import run_tasks_concurrently
6
+ from cloudinary_cli.utils.api_utils import upload_file
7
+ from cloudinary_cli.utils.config_utils import get_cloudinary_config, config_to_dict
8
+ from cloudinary_cli.defaults import logger
9
+ from cloudinary_cli.core.search import execute_single_request, handle_auto_pagination
10
+ import time
11
+ import re
12
+
13
+ DEFAULT_MAX_RESULTS = 500
14
+ ALLOWED_TYPE_VALUES = ("upload", "private", "authenticated")
15
+
16
+
17
+ @command("clone",
18
+ short_help="""Clone assets from one product environment to another.""",
19
+ help="""
20
+ \b
21
+ Clone assets from one product environment to another with/without tags and/or context (structured metadata is not currently supported).
22
+ Source will be your `CLOUDINARY_URL` environment variable but you also can specify a different source using the `-c/-C` option.
23
+ Format: cld clone <target_environment> <command options>
24
+ `<target_environment>` can be a CLOUDINARY_URL or a saved config (see `config` command)
25
+ Example 1 (Copy all assets including tags and context using CLOUDINARY URL):
26
+ cld clone cloudinary://<api_key>:<api_secret>@<cloudname> -fi tags,context
27
+ Example 2 (Copy all assets with a specific tag via a search expression using a saved config):
28
+ cld clone <config_name> -se "tags:<tag_name>"
29
+ """)
30
+ @argument("target")
31
+ @option("-F", "--force", is_flag=True,
32
+ help="Skip confirmation.")
33
+ @option("-ow", "--overwrite", is_flag=True, default=False,
34
+ help="Specify whether to overwrite existing assets.")
35
+ @option("-w", "--concurrent_workers", type=int, default=30,
36
+ help="Specify the number of concurrent network threads.")
37
+ @option("-fi", "--fields", multiple=True,
38
+ help=("Specify whether to copy tags and/or context. "
39
+ "Valid options: `tags,context`."))
40
+ @option("-se", "--search_exp", default="",
41
+ help="Define a search expression to filter the assets to clone.")
42
+ @option("--async", "async_", is_flag=True, default=False,
43
+ help="Clone the assets asynchronously.")
44
+ @option("-nu", "--notification_url",
45
+ help="Webhook notification URL.")
46
+ @option("-ue", "--url_expiry", type=int, default=3600,
47
+ help=("URL expiration duration in seconds. Only relevant if cloning "
48
+ "restricted assets with an auth_key configured. "
49
+ "If you do not provide an auth_key, "
50
+ "a private download URL is generated which may incur additional "
51
+ "bandwidth costs."))
52
+ def clone(target, force, overwrite, concurrent_workers, fields,
53
+ search_exp, async_, notification_url, url_expiry):
54
+ target_config, auth_token = _validate_clone_inputs(target)
55
+ if not target_config:
56
+ return False
57
+
58
+ source_assets = search_assets(search_exp, force)
59
+ if not source_assets:
60
+ return False
61
+ if not isinstance(source_assets, dict) or not source_assets.get('resources'):
62
+ logger.error(style(f"No asset(s) found in {cloudinary.config().cloud_name}", fg="red"))
63
+ return False
64
+
65
+ upload_list = _prepare_upload_list(
66
+ source_assets, target_config, overwrite, async_,
67
+ notification_url, auth_token, url_expiry, fields
68
+ )
69
+
70
+ logger.info(style(f"Copying {len(upload_list)} asset(s) from "
71
+ f"{cloudinary.config().cloud_name} to "
72
+ f"{target_config.cloud_name}", fg="blue"))
73
+
74
+ run_tasks_concurrently(upload_file, upload_list, concurrent_workers)
75
+
76
+ return True
77
+
78
+
79
+ def _validate_clone_inputs(target):
80
+ if not target:
81
+ print_help_and_exit()
82
+
83
+ target_config = get_cloudinary_config(target)
84
+ if not target_config:
85
+ logger.error("The specified config does not exist or the "
86
+ "CLOUDINARY_URL scheme provided is invalid "
87
+ "(expecting to start with 'cloudinary://').")
88
+ return None, None
89
+
90
+ if cloudinary.config().cloud_name == target_config.cloud_name:
91
+ logger.error("Target environment cannot be the same "
92
+ "as source environment.")
93
+ return None, None
94
+
95
+ auth_token = cloudinary.config().auth_token
96
+ if auth_token:
97
+ # It is important to validate auth_token if provided as this prevents
98
+ # customer from having to re-run the command as well as
99
+ # saving Admin API calls and time.
100
+ try:
101
+ cloudinary.utils.generate_auth_token(acl="/image/*")
102
+ except Exception as e:
103
+ logger.error(f"{e} - auth_token validation failed. "
104
+ "Please double-check your auth_token parameters.")
105
+ return None, None
106
+
107
+ return target_config, auth_token
108
+
109
+
110
+ def _prepare_upload_list(source_assets, target_config, overwrite, async_,
111
+ notification_url, auth_token, url_expiry, fields):
112
+ upload_list = []
113
+ for r in source_assets.get('resources'):
114
+ updated_options, asset_url = process_metadata(r, overwrite, async_,
115
+ notification_url,
116
+ auth_token, url_expiry,
117
+ normalize_list_params(fields))
118
+ updated_options.update(config_to_dict(target_config))
119
+ upload_list.append((asset_url, {**updated_options}))
120
+ return upload_list
121
+
122
+
123
+ def search_assets(search_exp, force):
124
+ search_exp = _normalize_search_expression(search_exp)
125
+ if not search_exp:
126
+ return False
127
+
128
+ search = cloudinary.search.Search().expression(search_exp)
129
+ search.fields(['tags', 'context', 'access_control',
130
+ 'secure_url', 'display_name', 'format'])
131
+ search.max_results(DEFAULT_MAX_RESULTS)
132
+
133
+ res = execute_single_request(search, fields_to_keep="")
134
+ res = handle_auto_pagination(res, search, force, fields_to_keep="")
135
+
136
+ return res
137
+
138
+
139
+ def _normalize_search_expression(search_exp):
140
+ """
141
+ Ensures the search expression has a valid 'type' filter.
142
+
143
+ - If no expression is given, a default is created.
144
+ - If 'type' filters exist, they are validated.
145
+ - If no 'type' filters exist, the default is appended.
146
+ """
147
+ default_types_str = " OR ".join(f"type:{t}" for t in ALLOWED_TYPE_VALUES)
148
+
149
+ if not search_exp:
150
+ return default_types_str
151
+
152
+ # Use a simple regex to find all 'type' filters
153
+ found_types = re.findall(r"\btype\s*[:=]\s*(\w+)", search_exp)
154
+
155
+ if not found_types:
156
+ # No 'type' filter found, so append the default
157
+ return f"{search_exp} AND ({default_types_str})"
158
+
159
+ # A 'type' filter was found, so validate it
160
+ invalid_types = {t for t in found_types if t not in ALLOWED_TYPE_VALUES}
161
+
162
+ if invalid_types:
163
+ error_msg = ", ".join(f"type:{t}" for t in invalid_types)
164
+ logger.error(
165
+ f"Unsupported type(s) in search expression: {error_msg}. "
166
+ f"Only {', '.join(ALLOWED_TYPE_VALUES)} types allowed."
167
+ )
168
+ return None
169
+
170
+ # All found types are valid, so return the original expression
171
+ return search_exp
172
+
173
+
174
+ def process_metadata(res, overwrite, async_, notification_url, auth_token, url_expiry, copy_fields=None):
175
+ if copy_fields is None:
176
+ copy_fields = []
177
+ asset_url = _get_asset_url(res, auth_token, url_expiry)
178
+ cloned_options = _build_cloned_options(res, overwrite, async_, notification_url, copy_fields)
179
+
180
+ return cloned_options, asset_url
181
+
182
+
183
+ def _get_asset_url(res, auth_token, url_expiry):
184
+ if not (isinstance(res.get('access_control'), list) and
185
+ len(res.get('access_control')) > 0 and
186
+ isinstance(res['access_control'][0], dict) and
187
+ res['access_control'][0].get("access_type") == "token"):
188
+ return res.get('secure_url')
189
+
190
+ reso_type = res.get('resource_type')
191
+ del_type = res.get('type')
192
+ pub_id = res.get('public_id')
193
+ file_format = res.get('format')
194
+
195
+ if auth_token:
196
+ # Raw assets already have the format in the public_id
197
+ pub_id_format = pub_id if reso_type == "raw" else f"{pub_id}.{file_format}"
198
+ return cloudinary.utils.cloudinary_url(
199
+ pub_id_format,
200
+ type=del_type,
201
+ resource_type=reso_type,
202
+ auth_token={"duration": url_expiry},
203
+ secure=True,
204
+ sign_url=True
205
+ )
206
+
207
+ # Use private url if no auth_token provided
208
+ return cloudinary.utils.private_download_url(
209
+ pub_id,
210
+ file_format,
211
+ resource_type=reso_type,
212
+ type=del_type,
213
+ expires_at=int(time.time()) + url_expiry
214
+ )
215
+
216
+
217
+ def _build_cloned_options(res, overwrite, async_, notification_url, copy_fields):
218
+ # 1. Start with mandatory options
219
+ cloned_options = {
220
+ 'overwrite': overwrite,
221
+ 'async': async_,
222
+ }
223
+
224
+ # 2. Copy fields from source asset. Some are standard, others are from user input.
225
+ fields_to_copy = {'public_id', 'type', 'resource_type', 'access_control'}.union(copy_fields)
226
+ cloned_options.update({field: res.get(field) for field in fields_to_copy})
227
+
228
+ # 3. Handle fields that are added only if they have a truthy value
229
+ if res.get('display_name'):
230
+ cloned_options['display_name'] = res['display_name']
231
+
232
+ # This is required to put the asset in the correct asset_folder
233
+ # when copying from a fixed to DF (dynamic folder) cloud as if
234
+ # you just pass a `folder` param to a DF cloud, it will append
235
+ # this to the `public_id` and we don't want this.
236
+ if res.get('folder'):
237
+ cloned_options['asset_folder'] = res['folder']
238
+ elif res.get('asset_folder'):
239
+ cloned_options['asset_folder'] = res['asset_folder']
240
+
241
+ if notification_url:
242
+ cloned_options['notification_url'] = notification_url
243
+
244
+ # 4. Clean up any None values before returning
245
+ return {k: v for k, v in cloned_options.items() if v is not None}
@@ -300,7 +300,7 @@ class SyncDir:
300
300
 
301
301
  def _local_candidates(self, candidate_path):
302
302
  filename, extension = path.splitext(candidate_path)
303
- r = re.compile(f"({candidate_path}|{filename} \(\d+\){extension})")
303
+ r = re.compile(f"({candidate_path}|{filename} \\(\\d+\\){extension})")
304
304
  # sort local files by base name (without ext) for accurate results.
305
305
  return dict(sorted({f: self.local_files[f]["etag"] for f in filter(r.match, self.local_files.keys())}.items(),
306
306
  key=lambda f: path.splitext(f[0])[0]))
@@ -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"