Skip to content

Commit 2b6fd28

Browse files
improve scripts based on discussions
1 parent a2cf5a1 commit 2b6fd28

4 files changed

Lines changed: 161 additions & 129 deletions

File tree

cloudinary_cli/modules/clone.py

Lines changed: 10 additions & 90 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,12 @@
11
from click import command, option, style, argument
22
from cloudinary_cli.utils.utils import normalize_list_params, print_help_and_exit
33
import cloudinary
4+
from cloudinary_cli.utils.clone.metadata import clone_metadata
45
from cloudinary.auth_token import _digest
56
from cloudinary_cli.utils.utils import run_tasks_concurrently
6-
from cloudinary_cli.utils.api_utils import upload_file, handle_api_command
7+
from cloudinary_cli.utils.api_utils import upload_file
78
from cloudinary_cli.utils.json_utils import print_json
8-
from cloudinary_cli.utils.config_utils import get_cloudinary_config, config_to_dict, config_to_tuple_list
9+
from cloudinary_cli.utils.config_utils import get_cloudinary_config, config_to_dict
910
from cloudinary_cli.defaults import logger
1011
from cloudinary_cli.core.search import execute_single_request, handle_auto_pagination
1112
import time
@@ -55,27 +56,19 @@ def clone(target, force, overwrite, concurrent_workers, fields,
5556
target_config, auth_token = _validate_clone_inputs(target)
5657
if not target_config:
5758
return False
58-
59+
if 'metadata' in normalize_list_params(fields):
60+
metadata_clone = clone_metadata(target_config)
61+
if not metadata_clone:
62+
logger.error(style(f"The operation has been aborted due to your answer.", fg="red"))
63+
return False
64+
else:
65+
logger.info(style(f"Metadata cloned successfully from {cloudinary.config().cloud_name} to {target_config.cloud_name}. We will now proceed with cloning the assets.", fg="green"))
5966
source_assets = search_assets(search_exp, force)
6067
if not source_assets:
6168
return False
6269
if not isinstance(source_assets, dict) or not source_assets.get('resources'):
6370
logger.error(style(f"No asset(s) found in {cloudinary.config().cloud_name}", fg="red"))
6471
return False
65-
66-
if 'metadata' in normalize_list_params(fields):
67-
source_metadata = list_metadata_items("metadata_fields")
68-
if source_metadata.get('metadata_fields'):
69-
target_metadata = list_metadata_items("metadata_fields", config_to_tuple_list(target_config))
70-
fields_compare = compare_create_metadata_items(source_metadata, target_metadata, config_to_tuple_list(target_config), key="metadata_fields")
71-
source_metadata_rules = list_metadata_items("metadata_rules")
72-
if source_metadata_rules.get('metadata_rules'):
73-
target_metadata_rules = list_metadata_items("metadata_rules", config_to_tuple_list(target_config))
74-
rules_compare = compare_create_metadata_items(source_metadata_rules,target_metadata_rules, config_to_tuple_list(target_config), key="metadata_rules", id_field="name")
75-
else:
76-
logger.info(style(f"No metadata rules found in {cloudinary.config().cloud_name}", fg="yellow"))
77-
else:
78-
logger.info(style(f"No metadata found in {cloudinary.config().cloud_name}", fg="yellow"))
7972

8073
upload_list = _prepare_upload_list(
8174
source_assets, target_config, overwrite, async_,
@@ -150,79 +143,6 @@ def search_assets(search_exp, force):
150143

151144
return res
152145

153-
def list_metadata_items(method_key, *options):
154-
api_method_name = 'list_' + method_key
155-
params = [api_method_name]
156-
if options:
157-
options = options[0]
158-
res = handle_api_command(params, (), options, None, None, None,
159-
doc_url="", api_instance=cloudinary.api,
160-
api_name="admin",
161-
auto_paginate=True,
162-
force=True, return_data=True)
163-
res.get(method_key).sort(key=lambda x: x["external_id"])
164-
165-
return res
166-
167-
168-
def create_metadata_item(api_method_name, item, *options):
169-
params = (api_method_name, item)
170-
if options:
171-
options = options[0]
172-
res = handle_api_command(params, (), options, None, None, None,
173-
doc_url="", api_instance=cloudinary.api,
174-
api_name="admin",
175-
return_data=True)
176-
177-
return res
178-
179-
180-
def deep_diff(obj_source, obj_target):
181-
diffs = {}
182-
for k in set(obj_source.keys()).union(obj_target.keys()):
183-
if obj_source.get(k) != obj_target.get(k):
184-
diffs[k] = {"json_source": obj_source.get(k), "json_target": obj_target.get(k)}
185-
186-
return diffs
187-
188-
189-
def compare_create_metadata_items(json_source, json_target, target_config, key, id_field = "external_id"):
190-
list_source = {item[id_field]: item for item in json_source.get(key, [])}
191-
list_target = {item[id_field]: item for item in json_target.get(key, [])}
192-
193-
only_in_source = list(list_source.keys() - list_target.keys())
194-
common = list_source.keys() & list_target.keys()
195-
196-
if not len(only_in_source):
197-
logger.info(style(f"{(' '.join(key.split('_')))} in {dict(target_config)['cloud_name']} and in {cloudinary.config().cloud_name} are identical. No {(' '.join(key.split('_')))} will be cloned", fg="yellow"))
198-
else:
199-
logger.info(style(f"Copying {len(only_in_source)} {(' '.join(key.split('_')))} from {cloudinary.config().cloud_name} to {dict(target_config)['cloud_name']}", fg="blue"))
200-
201-
for key_field in only_in_source:
202-
if key == 'metadata_fields':
203-
try:
204-
res = create_metadata_item('add_metadata_field', list_source[key_field],target_config)
205-
logger.info(style(f"Successfully created {(' '.join(key.split('_')))} {key_field} to {dict(target_config)['cloud_name']}", fg="green"))
206-
except Exception as e:
207-
logger.error(style(f"Error when creating {(' '.join(key.split('_')))} {key_field} to {dict(target_config)['cloud_name']}", fg="red"))
208-
else:
209-
try:
210-
res = create_metadata_item('add_metadata_rule', list_source[key_field],target_config)
211-
logger.info(style(f"Successfully created {(' '.join(key.split('_')))} {key_field} to {dict(target_config)['cloud_name']}", fg="green"))
212-
except Exception as e:
213-
logger.error(style(f"Error when creating {(' '.join(key.split('_')))} {key_field} to {dict(target_config)['cloud_name']}", fg="red"))
214-
215-
216-
diffs = {}
217-
for id_ in common:
218-
if list_source[id_] != list_target[id_]:
219-
diffs[id_] = deep_diff(list_source[id_], list_target[id_])
220-
221-
return {
222-
"only_in_json_source": only_in_source,
223-
"differences": diffs
224-
}
225-
226146
def _normalize_search_expression(search_exp):
227147
"""
228148
Ensures the search expression has a valid 'type' filter.
Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
1+
import cloudinary
2+
from cloudinary_cli.utils.config_utils import config_to_dict
3+
from cloudinary_cli.utils.api_utils import handle_auto_pagination
4+
from cloudinary_cli.defaults import logger
5+
from cloudinary_cli.utils.utils import confirm_action
6+
from click import style
7+
8+
def clone_metadata(config):
9+
"""
10+
Clone metadata from the source to the destination.
11+
"""
12+
target_config = config_to_dict(config)
13+
source_metadata = list_metadata_items("metadata_fields")
14+
if source_metadata.get('metadata_fields'):
15+
target_metadata = list_metadata_items("metadata_fields", **target_config)
16+
fields_compare = compare_create_metadata_items(source_metadata, target_metadata, key="metadata_fields", **target_config)
17+
if not fields_compare:
18+
return False
19+
else:
20+
source_metadata_rules = list_metadata_items("metadata_rules")
21+
if source_metadata_rules.get('metadata_rules'):
22+
target_metadata_rules = list_metadata_items("metadata_rules", **target_config)
23+
rules_compare = compare_create_metadata_items(source_metadata_rules,target_metadata_rules, key="metadata_rules", id_field="name", **target_config)
24+
if not rules_compare:
25+
return False
26+
else:
27+
logger.info(style(f"No metadata rules found in {cloudinary.config().cloud_name}", fg="yellow"))
28+
else:
29+
logger.info(style(f"No metadata found in {cloudinary.config().cloud_name}", fg="yellow"))
30+
31+
return True # Return True to indicate that the metadata was cloned successfully or False if there were no items to clone.
32+
33+
def list_metadata_items(method_key, **options):
34+
if method_key == 'metadata_fields':
35+
res = cloudinary.api.list_metadata_fields(**options)
36+
res = handle_auto_pagination(res, cloudinary.api.list_metadata_fields, options, None, force=True, filter_fields="")
37+
else:
38+
res = cloudinary.api.list_metadata_rules(**options)
39+
res = handle_auto_pagination(res, cloudinary.api.list_metadata_rules, options, None, force=True, filter_fields="")
40+
41+
return res
42+
43+
def create_metadata_items(api_method_name, item, **options):
44+
if api_method_name == 'add_metadata_field':
45+
res = cloudinary.api.add_metadata_field(item, **options)
46+
else:
47+
res = cloudinary.api.add_metadata_rule(item, **options)
48+
return res
49+
50+
def deep_diff(obj_source, obj_target):
51+
diffs = {}
52+
for k in set(obj_source.keys()).union(obj_target.keys()):
53+
if obj_source.get(k) != obj_target.get(k):
54+
diffs[k] = {"json_source": obj_source.get(k), "json_target": obj_target.get(k)}
55+
56+
return diffs
57+
58+
59+
def compare_create_metadata_items(json_source, json_target, key, id_field = "external_id", **options):
60+
list_source = {item[id_field]: item for item in json_source.get(key, [])}
61+
list_target = {item[id_field]: item for item in json_target.get(key, [])}
62+
63+
only_in_source = list(list_source.keys() - list_target.keys())
64+
common = list_source.keys() & list_target.keys()
65+
66+
if not len(only_in_source):
67+
logger.info(style(f"{(' '.join(key.split('_')))} in `{dict(options)['cloud_name']}` and in `{cloudinary.config().cloud_name}` are identical. No {(' '.join(key.split('_')))} will be cloned", fg="yellow"))
68+
if not confirm_action(
69+
f"If you had some {key} in the target environment, "
70+
f"new values from the source environment won't be cloned.\n"
71+
f"Would you like to still proceed with the cloning of assets? (y/N).\n"):
72+
logger.info("Stopping.")
73+
return False
74+
else:
75+
logger.info("Continuing.")
76+
else:
77+
logger.info(style(f"{only_in_source} are only in `{dict(options)['cloud_name']}` and will be cloned to `{cloudinary.config().cloud_name}`.", fg="blue"))
78+
if not confirm_action(
79+
f"You have a {key} mismatch between the source and target environment.\n"
80+
f"Confirming this action will create the missing {key} and their values.\n"
81+
f"If you currently have some {key} in the target environment, "
82+
f"new values from the source environment won't be cloned.\n"
83+
f"Continue? (y/N)"):
84+
logger.info("Stopping.")
85+
return False
86+
else:
87+
logger.info("Continuing.")
88+
logger.info(style(f"Copying {len(only_in_source)} {(' '.join(key.split('_')))} from {cloudinary.config().cloud_name} to {dict(options)['cloud_name']}", fg="blue"))
89+
for key_field in only_in_source:
90+
if key == 'metadata_fields':
91+
try:
92+
res = create_metadata_items('add_metadata_field', list_source[key_field], **options)
93+
logger.info(style(f"Successfully created {(' '.join(key.split('_')))[:-1]} `{res.get('label')}` to {dict(options)['cloud_name']}", fg="green"))
94+
except Exception as e:
95+
logger.error(style(f"Error when creating {(' '.join(key.split('_')))[:-1]} `{res.get('label')}`` to {dict(options)['cloud_name']}", fg="red"))
96+
else:
97+
try:
98+
res = create_metadata_items('add_metadata_rule', list_source[key_field],**options)
99+
logger.info(style(f"Successfully created {(' '.join(key.split('_')))[:-1]} `{res.get('name')}` to {dict(options)['cloud_name']}", fg="green"))
100+
except Exception as e:
101+
logger.error(style(f"Error when creating {(' '.join(key.split('_')))[:-1]} `{res.get('name')}` to {dict(options)['cloud_name']}", fg="red"))
102+
103+
# for Phase 3
104+
#diffs = {}
105+
#for id_ in common:
106+
# if list_source[id_] != list_target[id_]:
107+
# diffs[id_] = deep_diff(list_source[id_], list_target[id_])
108+
109+
#return {
110+
# "only_in_json_source": only_in_source,
111+
# "differences": diffs
112+
#}
113+
return True # Return True to indicate that the metadata items were compared and created successfully.

cloudinary_cli/utils/config_utils.py

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -67,9 +67,6 @@ def get_cloudinary_config(target):
6767
def config_to_dict(config):
6868
return {k: v for k, v in config.__dict__.items() if not k.startswith("_")}
6969

70-
def config_to_tuple_list(config):
71-
return [(k, v) for k, v in config.__dict__.items() if not k.startswith("_")]
72-
7370
def show_cloudinary_config(cloudinary_config):
7471
obfuscated_config = config_to_dict(cloudinary_config)
7572

0 commit comments

Comments
 (0)