Skip to content

Commit 3e4436a

Browse files
Merge branch 'master' into fix/sync-duplicate-name-match
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2 parents 3dc1ae1 + f6913ce commit 3e4436a

23 files changed

Lines changed: 338 additions & 34 deletions

‎cloudinary_cli/__init__.py‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,8 @@
11
from cloudinary_cli.version import __version__
2+
from cloudinary_cli.utils.env_config import import_sdk
3+
4+
# Must run before any other `import cloudinary`, so that an invalid CLOUDINARY_URL does not stop the CLI.
5+
import_sdk()
26

37
import cloudinary
48

‎cloudinary_cli/cli_group.py‎

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
#!/usr/bin/env python3
2+
import difflib
23
import platform
34
import shutil
45

@@ -13,7 +14,21 @@
1314
CONTEXT_SETTINGS = dict(max_content_width=shutil.get_terminal_size()[0], terminal_width=shutil.get_terminal_size()[0])
1415

1516

16-
@click.group(context_settings=CONTEXT_SETTINGS, invoke_without_command=True)
17+
class SuggestingGroup(click.Group):
18+
"""Suggests similar command names when the command name is not known."""
19+
20+
def resolve_command(self, ctx, args):
21+
try:
22+
return super().resolve_command(ctx, args)
23+
except click.UsageError as e:
24+
names = [name for name in self.list_commands(ctx) if not self.get_command(ctx, name).hidden]
25+
matches = difflib.get_close_matches(args[0], names) if args else []
26+
if matches:
27+
e.message += f"\nDid you mean: {', '.join(matches)}?"
28+
raise
29+
30+
31+
@click.group(cls=SuggestingGroup, context_settings=CONTEXT_SETTINGS, invoke_without_command=True)
1732
@click.help_option()
1833
@click.version_option(cli_version, prog_name="Cloudinary CLI",
1934
message=f"%(prog)s, version %(version)s\n"

‎cloudinary_cli/core/admin.py‎

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,11 +16,12 @@
1616
\t cld admin resources max_results=10 -o tags sample
1717
""")
1818
@argument("params", nargs=-1)
19-
@option("-o", "--optional_parameter", multiple=True, nargs=2, help="Pass optional parameters as raw strings.")
19+
@option("-o", "--optional_parameter", multiple=True, nargs=2,
20+
help="Pass an optional parameter as a string, with no parsing. e.g. -o tags a,b")
2021
@option("-O", "--optional_parameter_parsed", multiple=True, nargs=2,
21-
help="Pass optional parameters as interpreted strings.")
22+
help="Pass an optional parameter and parse its value as JSON or a boolean. e.g. -O context '{\"alt\": \"cat\"}'")
2223
@option("-A", "--auto_paginate", is_flag=True, help="Will auto paginate Admin API calls.", default=False)
23-
@option("-ff", "--filter_fields", multiple=True, help="Filter fields to return when using auto pagination.")
24+
@option("-ff", "--filter_fields", multiple=True, help="Filter fields to return. Requires -A/--auto_paginate.")
2425
@option("-F", "--force", is_flag=True,
2526
help="Skip confirmations for auto pagination and destructive bulk API methods.")
2627
@option("-ls", "--ls", is_flag=True, help="List all available methods in the Admin API.")

‎cloudinary_cli/core/provisioning.py‎

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,10 @@
1+
import sys
2+
13
from click import command, argument, option
24
import cloudinary.provisioning
35

46
from cloudinary_cli.utils.api_utils import handle_api_command
7+
from cloudinary_cli.utils.env_config import env_config_error
58

69

710
@command("provisioning",
@@ -12,13 +15,19 @@
1215
\te.g. cld provisioning sub_accounts
1316
""")
1417
@argument("params", nargs=-1)
15-
@option("-o", "--optional_parameter", multiple=True, nargs=2, help="Pass optional parameters as raw strings.")
18+
@option("-o", "--optional_parameter", multiple=True, nargs=2,
19+
help="Pass an optional parameter as a string, with no parsing. e.g. -o prefix demo")
1620
@option("-O", "--optional_parameter_parsed", multiple=True, nargs=2,
17-
help="Pass optional parameters as interpreted strings.")
21+
help="Pass an optional parameter and parse its value as JSON or a boolean. e.g. -O enabled true")
1822
@option("-ls", "--ls", is_flag=True, help="List all available methods in the Provisioning API.")
1923
@option("--save", nargs=1, help="Save output to a file.")
2024
@option("-d", "--doc", is_flag=True, help="Open the Provisioning API reference in a browser.")
2125
def provisioning(params, optional_parameter, optional_parameter_parsed, ls, save, doc):
26+
# -c, -C and the saved default do not replace CLOUDINARY_ACCOUNT_URL, so an invalid one is always an error here.
27+
account_url_error = env_config_error("CLOUDINARY_ACCOUNT_URL")
28+
if account_url_error:
29+
sys.exit(account_url_error)
30+
2231
return handle_api_command(params, optional_parameter, optional_parameter_parsed, ls, save, doc,
2332
doc_url="https://cloudinary.com/documentation/provisioning_api",
2433
api_instance=cloudinary.provisioning,

‎cloudinary_cli/core/search.py‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -130,8 +130,8 @@ def _perform_search(query, with_field, fields, sort_by, aggregate, max_results,
130130
logger.info(f"Saved search JSON to '{json}' file")
131131

132132
if csv:
133-
write_json_list_to_csv(res[result_field], csv, fields_to_keep)
134-
logger.info(f"Saved search to '{csv}.csv' file")
133+
csv = write_json_list_to_csv(res[result_field], csv, fields_to_keep)
134+
logger.info(f"Saved search to '{csv}' file")
135135

136136

137137
def execute_single_request(expression, fields_to_keep, result_field='resources'):

‎cloudinary_cli/core/uploader.py‎

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,9 +20,10 @@
2020
\t cld uploader rename flowers secret_flowers -o to_type private
2121
""")
2222
@argument("params", nargs=-1)
23-
@option("-o", "--optional_parameter", multiple=True, nargs=2, help="Pass optional parameters as raw strings.")
23+
@option("-o", "--optional_parameter", multiple=True, nargs=2,
24+
help="Pass an optional parameter as a string, with no parsing. e.g. -o tags a,b")
2425
@option("-O", "--optional_parameter_parsed", multiple=True, nargs=2,
25-
help="Pass optional parameters as interpreted strings.")
26+
help="Pass an optional parameter and parse its value as JSON or a boolean. e.g. -O context '{\"alt\": \"cat\"}'")
2627
@option("-ls", "--ls", is_flag=True, help="List all available methods in the Upload API.")
2728
@option("--save", nargs=1, help="Save output to a file.")
2829
@option("-d", "--doc", is_flag=True, help="Open the Upload API reference in a browser.")

‎cloudinary_cli/core/utils.py‎

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
from click import command, argument, option, Choice, echo, launch
2+
import cloudinary
23
from cloudinary import utils as cld_utils
34

45
from cloudinary_cli.core.overrides import cloudinary_url
@@ -14,9 +15,10 @@
1415

1516
@command("utils", help="Call Cloudinary utility methods.")
1617
@argument("params", nargs=-1)
17-
@option("-o", "--optional_parameter", multiple=True, nargs=2, help="Pass optional parameters as raw strings.")
18+
@option("-o", "--optional_parameter", multiple=True, nargs=2,
19+
help="Pass an optional parameter as a string, with no parsing. e.g. -o tags a,b")
1820
@option("-O", "--optional_parameter_parsed", multiple=True, nargs=2,
19-
help="Pass optional parameters as interpreted strings.")
21+
help="Pass an optional parameter and parse its value as JSON or a boolean. e.g. -O sign_url true")
2022
@option("-ls", "--ls", is_flag=True, help="List all available utility methods.")
2123
def utils(params, optional_parameter, optional_parameter_parsed, ls):
2224
if ls or len(params) < 1:
@@ -50,8 +52,13 @@ def url(public_id, transformation, resource_type, delivery_type, open_in_browser
5052
elif delivery_type == "list":
5153
public_id += ".json"
5254

55+
# Use HTTPS unless the configuration sets `secure` explicitly.
56+
secure = cloudinary.config().secure
57+
if secure is None:
58+
secure = True
59+
5360
res = cloudinary_url(public_id, resource_type=resource_type,
54-
raw_transformation=transformation, type=delivery_type, sign_url=sign)
61+
raw_transformation=transformation, type=delivery_type, sign_url=sign, secure=secure)
5562
echo(res)
5663

5764
if open_in_browser:

‎cloudinary_cli/modules/migrate.py‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@
1717
@argument("upload_mapping")
1818
@argument("file")
1919
@option("-d", "--delimiter", default="\n", help="The separator used between the URLs. Default: New line")
20-
@option("-v", "--verbose", is_flag=True)
20+
@option("-v", "--verbose", is_flag=True, help="Log each migrated URL.")
2121
def migrate(upload_mapping, file, delimiter, verbose):
2222
if not os.path.exists(file):
2323
logger.error(f"Migration file: '{file}' does not exist")

‎cloudinary_cli/modules/regen_derived.py‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,10 @@ def regen_derived(trans_str, eager_notification_url,
7070
logger.info("No derived assets are using this transformation.")
7171
exit()
7272

73+
if trans_details.get('next_cursor'):
74+
logger.warning(f"Only the first {len(derived_resources)} derived assets will be regenerated. "
75+
f"Use -A to regenerate all of them.")
76+
7377
is_named = trans_details.get('named')
7478
eager_trans = normalise_trans_name(trans_str) if is_named else trans_str
7579

‎cloudinary_cli/modules/sync.py‎

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -41,9 +41,10 @@
4141
help="Specify folder mode explicitly. By default uses cloud mode configured in your cloud.", hidden=True)
4242
@option("-st", "--status", type=Choice(['all', 'active', 'pending'], case_sensitive=False),
4343
help="Specify asset status. Server default: active.", default=None)
44-
@option("-o", "--optional_parameter", multiple=True, nargs=2, help="Pass optional parameters as raw strings.")
44+
@option("-o", "--optional_parameter", multiple=True, nargs=2,
45+
help="Pass an optional parameter as a string, with no parsing. e.g. -o tags a,b")
4546
@option("-O", "--optional_parameter_parsed", multiple=True, nargs=2,
46-
help="Pass optional parameters as interpreted strings.")
47+
help="Pass an optional parameter and parse its value as JSON or a boolean. e.g. -O context '{\"alt\": \"cat\"}'")
4748
@option("--dry-run", is_flag=True, help="Simulate the sync operation without making any changes.")
4849
def sync(local_folder, cloudinary_folder, push, pull, include_hidden, concurrent_workers, force, keep_unique,
4950
deletion_batch_size, folder_mode, status, optional_parameter, optional_parameter_parsed, dry_run):
@@ -226,7 +227,8 @@ def pull(self):
226227
if not files_to_pull:
227228
return True
228229

229-
logger.info(f"Preparing to download {len(files_to_pull)} items from Cloudinary folder ")
230+
logger.info(f"Preparing to download {len(files_to_pull)} items "
231+
f"from Cloudinary folder '{self.user_friendly_remote_dir}'")
230232

231233
if self.dry_run:
232234
logger.info("Dry run mode enabled. The following files would be downloaded:")

0 commit comments

Comments
 (0)