Skip to content

Commit 11875d7

Browse files
Refactor code, normalize parameters
1 parent 9a89159 commit 11875d7

2 files changed

Lines changed: 70 additions & 63 deletions

File tree

cloudinary_cli/modules/clone.py

Lines changed: 41 additions & 57 deletions
Original file line numberDiff line numberDiff line change
@@ -1,103 +1,87 @@
11
from click import command, option, style
2-
from cloudinary_cli.utils.utils import normalize_list_params, \
3-
print_help_and_exit
2+
from cloudinary_cli.utils.utils import normalize_list_params, print_help_and_exit
43
import cloudinary
54
from cloudinary_cli.utils.utils import run_tasks_concurrently
65
from cloudinary_cli.utils.api_utils import upload_file
7-
from cloudinary_cli.utils.config_utils import load_config
6+
from cloudinary_cli.utils.config_utils import load_config, get_cloudinary_config, config_to_dict
87
from cloudinary_cli.defaults import logger
9-
from cloudinary_cli.core.search import execute_single_request, \
10-
handle_auto_pagination
8+
from cloudinary_cli.core.search import execute_single_request, handle_auto_pagination
119

1210
DEFAULT_MAX_RESULTS = 500
1311

1412

1513
@command("clone",
16-
short_help="""Clone assets from one account to another.""",
14+
short_help="""Clone assets from one product environment to another.""",
1715
help="""
1816
\b
19-
Clone assets from one environment to another with/without tags and/or context (structured metadata is not currently supported).
17+
Clone assets from one product environment to another with/without tags and/or context (structured metadata is not currently supported).
2018
Source will be your `CLOUDINARY_URL` environemnt variable but you also can specify a different source using `-c/-C` option.
2119
Cloning restricted assets is also not supported currently.
2220
Format: cld clone -T <target_environment> <command options>
2321
`<target_environment>` can be a CLOUDINARY_URL or a saved config (see `config` command)
2422
e.g. cld clone -T cloudinary://<api_key>:<api_secret>@<cloudname> -f tags,context
2523
""")
26-
@option("-T", "--target",
24+
@option("-t", "--target",
2725
help="Tell the CLI the target environemnt to run the command on.")
28-
@option("-F", "--force", is_flag=True,
26+
@option("-f", "--force", is_flag=True,
2927
help="Skip confirmation.")
30-
@option("-O", "--overwrite", is_flag=True, default=False,
28+
@option("-o", "--overwrite", is_flag=True, default=False,
3129
help="Specify whether to overwrite existing assets.")
3230
@option("-w", "--concurrent_workers", type=int, default=30,
3331
help="Specify the number of concurrent network threads.")
34-
@option("-f", "--fields", multiple=True,
32+
@option("-fi", "--fields", multiple=True,
3533
help="Specify whether to copy tags and context.")
3634
@option("-se", "--search_exp", default="",
3735
help="Define a search expression.")
3836
@option("--async", "async_", is_flag=True, default=False,
3937
help="Generate asynchronously.")
4038
@option("-nu", "--notification_url",
4139
help="Webhook notification URL.")
42-
def clone(target, force, overwrite, concurrent_workers, fields, search_exp,
43-
async_, notification_url):
40+
def clone(target, force, overwrite, concurrent_workers, fields, search_exp, async_, notification_url):
4441
if not target:
4542
print_help_and_exit()
4643

47-
target_config = cloudinary.Config()
48-
is_cloudinary_url = False
49-
if target.startswith("cloudinary://"):
50-
is_cloudinary_url = True
51-
parsed_url = target_config._parse_cloudinary_url(target)
52-
elif target in load_config():
53-
parsed_url = target_config._parse_cloudinary_url(load_config().get(target))
54-
else:
55-
logger.error("The specified config does not exist or the "
56-
"CLOUDINARY_URL scheme provided is invalid "
57-
"(expecting to start with 'cloudinary://').")
44+
target_config = get_cloudinary_config(target)
45+
if not target_config:
46+
logger.error("The specified config does not exist or the CLOUDINARY_URL scheme provided is invalid"
47+
" (expecting to start with 'cloudinary://').")
5848
return False
5949

60-
target_config._setup_from_parsed_url(parsed_url)
61-
target_config_dict = {k: v for k, v in target_config.__dict__.items()
62-
if not k.startswith("_")}
63-
if is_cloudinary_url:
64-
try:
65-
cloudinary.api.ping(**target_config_dict)
66-
except Exception as e:
67-
logger.error(f"{e}. Please double-check your Cloudinary URL.")
68-
return False
69-
70-
source_cloudname = cloudinary.config().cloud_name
71-
target_cloudname = target_config.cloud_name
72-
if source_cloudname == target_cloudname:
73-
logger.info("Target environment cannot be the "
74-
"same as source environment.")
75-
return True
76-
77-
copy_fields = normalize_list_params(fields)
78-
search = cloudinary.search.Search().expression(search_exp)
79-
search.fields(['tags', 'context', 'access_control',
80-
'secure_url', 'display_name'])
81-
search.max_results(DEFAULT_MAX_RESULTS)
82-
res = execute_single_request(search, fields_to_keep="")
83-
res = handle_auto_pagination(res, search, force, fields_to_keep="")
50+
if cloudinary.config().cloud_name == target_config.cloud_name:
51+
logger.error("Target environment cannot be the same as source environment.")
52+
return False
53+
54+
source_assets = search_assets(force, search_exp)
8455

8556
upload_list = []
86-
for r in res.get('resources'):
87-
updated_options, asset_url = process_metadata(r, overwrite, async_,
88-
notification_url,
89-
copy_fields)
90-
updated_options.update(target_config_dict)
57+
for r in source_assets.get('resources'):
58+
updated_options, asset_url = process_metadata(r, overwrite, async_, notification_url,
59+
normalize_list_params(fields))
60+
updated_options.update(config_to_dict(target_config))
9161
upload_list.append((asset_url, {**updated_options}))
9262

93-
logger.info(style(f'Copying {len(upload_list)} asset(s) to '
94-
f'{target_cloudname}', fg="blue"))
95-
run_tasks_concurrently(upload_file, upload_list,
96-
concurrent_workers)
63+
if not upload_list:
64+
logger.error(style(f'No assets found in {cloudinary.config().cloud_name}', fg="red"))
65+
return False
66+
67+
logger.info(style(f'Copying {len(upload_list)} asset(s) from {cloudinary.config().cloud_name} to {target_config.cloud_name}', fg="blue"))
68+
69+
run_tasks_concurrently(upload_file, upload_list, concurrent_workers)
9770

9871
return True
9972

10073

74+
def search_assets(force, search_exp):
75+
search = cloudinary.search.Search().expression(search_exp)
76+
search.fields(['tags', 'context', 'access_control', 'secure_url', 'display_name'])
77+
search.max_results(DEFAULT_MAX_RESULTS)
78+
79+
res = execute_single_request(search, fields_to_keep="")
80+
res = handle_auto_pagination(res, search, force, fields_to_keep="")
81+
82+
return res
83+
84+
10185
def process_metadata(res, overwrite, async_, notification_url, copy_fields=""):
10286
cloned_options = {}
10387
asset_url = res.get('secure_url')

cloudinary_cli/utils/config_utils.py

Lines changed: 29 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -44,16 +44,31 @@ def refresh_cloudinary_config(cloudinary_url):
4444

4545
def verify_cloudinary_url(cloudinary_url):
4646
refresh_cloudinary_config(cloudinary_url)
47-
try:
48-
api.ping()
49-
except Exception as e:
50-
log_exception(e, f"Invalid Cloudinary URL: {cloudinary_url}")
47+
return ping_cloudinary()
48+
49+
50+
def get_cloudinary_config(target):
51+
target_config = cloudinary.Config()
52+
if target.startswith("cloudinary://"):
53+
parsed_url = target_config._parse_cloudinary_url(target)
54+
elif target in load_config():
55+
parsed_url = target_config._parse_cloudinary_url(load_config().get(target))
56+
else:
5157
return False
52-
return True
5358

59+
target_config._setup_from_parsed_url(parsed_url)
60+
61+
if not ping_cloudinary(**config_to_dict(target_config)):
62+
logger.error(f"Invalid Cloudinary config: {target}")
63+
return False
64+
65+
return target_config
66+
67+
def config_to_dict(config):
68+
return {k: v for k, v in config.__dict__.items() if not k.startswith("_")}
5469

5570
def show_cloudinary_config(cloudinary_config):
56-
obfuscated_config = {k: v for k, v in cloudinary_config.__dict__.items() if not k.startswith("_")}
71+
obfuscated_config = config_to_dict(cloudinary_config)
5772

5873
if "api_secret" in obfuscated_config:
5974
api_secret = obfuscated_config["api_secret"]
@@ -96,6 +111,14 @@ def is_valid_cloudinary_config():
96111
def initialize():
97112
migrate_old_config()
98113

114+
def ping_cloudinary(**options):
115+
try:
116+
api.ping(**options)
117+
except Exception as e:
118+
logger.error(f"Failed to ping Cloudinary: {e}")
119+
return False
120+
121+
return True
99122

100123
def _verify_file_path(file):
101124
os.makedirs(os.path.dirname(file), exist_ok=True)

0 commit comments

Comments
 (0)