Skip to content

Commit c2db1f8

Browse files
authored
Merge pull request #228 from cloudblue/LITE-29353-add-product-messages-to-export-sync-clone
LITE-29353: Added ProductMessage compatability on product export, sync and clone cmds
2 parents 9b629fc + 3f4a38c commit c2db1f8

File tree

17 files changed

+1497
-721
lines changed

17 files changed

+1497
-721
lines changed

connect/cli/plugins/product/clone.py

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
GeneralSynchronizer,
1313
ItemSynchronizer,
1414
MediaSynchronizer,
15+
MessageSynchronizer,
1516
ParamsSynchronizer,
1617
TemplatesSynchronizer,
1718
)
@@ -134,6 +135,14 @@ def inject(self): # noqa: CCR001
134135
is_clone=True,
135136
)
136137

138+
synchronizer = MessageSynchronizer(
139+
self.config.active.client,
140+
self.progress,
141+
self.stats,
142+
)
143+
synchronizer.open(input_file, 'Messages')
144+
synchronizer.sync()
145+
137146
self.config.activate(self.source_account)
138147
except ClientError as e:
139148
raise ClickException(f'Error while cloning product: {str(e)}')
@@ -229,6 +238,12 @@ def clean_wb(self):
229238
value = '-'
230239
for row in range(2, ws.max_row + 1):
231240
ws[f'C{row}'].value = value
241+
242+
ws = self.wb['Messages']
243+
for row in range(2, ws.max_row + 1):
244+
ws[f'A{row}'].value = ''
245+
ws[f'B{row}'].value = 'create'
246+
232247
self.wb.save(f'{self.fs.root_path}/{self.product_id}/{self.product_id}.xlsx')
233248

234249
@staticmethod

connect/cli/plugins/product/commands.py

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
GeneralSynchronizer,
2222
ItemSynchronizer,
2323
MediaSynchronizer,
24+
MessageSynchronizer,
2425
ParamsSynchronizer,
2526
StaticResourcesSynchronizer,
2627
TemplatesSynchronizer,
@@ -90,7 +91,7 @@ def cmd_list_products(config, query):
9091
help='Directory where to store the export.',
9192
)
9293
@pass_config
93-
def cmd_dump_products(config, product_id, output_file, output_path):
94+
def cmd_export_products(config, product_id, output_file, output_path):
9495
with console.progress() as progress:
9596
outfile = dump_product(
9697
config.active.client,
@@ -120,7 +121,7 @@ def cmd_sync_products(config, input_file):
120121
product_id = synchronizer.open(input_file, 'General Information')
121122

122123
console.confirm(
123-
'Are you sure you want to synchronize ' f'the product {product_id} ?',
124+
f'Are you sure you want to synchronize the product {product_id} ?',
124125
abort=True,
125126
)
126127
console.echo('')
@@ -148,6 +149,7 @@ def cmd_sync_products(config, input_file):
148149
actions_sync,
149150
media_sync,
150151
config_values_sync,
152+
messages_sync,
151153
]
152154
for task in sync_tasks:
153155
try:
@@ -324,5 +326,12 @@ def item_sync(client, progress, input_file, stats):
324326
synchronizer.save(input_file)
325327

326328

329+
def messages_sync(client, progress, input_file, stats):
330+
synchronizer = MessageSynchronizer(client, progress, stats)
331+
synchronizer.open(input_file, 'Messages')
332+
synchronizer.sync()
333+
synchronizer.save(input_file)
334+
335+
327336
def get_group():
328337
return grp_product

connect/cli/plugins/product/export.py

Lines changed: 42 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -174,11 +174,12 @@ def _setup_ws_header(ws, ws_type=None): # noqa: CCR001
174174
elif ws_type == 'templates':
175175
if cel.value == 'Content':
176176
ws.column_dimensions[cel.column_letter].width = 100
177-
if cel.value == 'Title':
177+
elif cel.value == 'Title':
178178
ws.column_dimensions[cel.column_letter].width = 50
179-
elif ws_type == '_attributes':
180-
if cel.column_letter in ['A', 'B', 'D']:
181-
ws.column_dimensions[cel.column_letter].width = 100
179+
elif ws_type == '_attributes' and cel.column_letter in ['A', 'B', 'D']:
180+
ws.column_dimensions[cel.column_letter].width = 100
181+
elif ws_type == 'messages' and cel.column_letter == 'D':
182+
ws.column_dimensions[cel.column_letter].width = 70
182183

183184

184185
def _calculate_commitment(item):
@@ -953,6 +954,42 @@ def _dump_translation_attr(wb, client, translation):
953954
_setup_ws_header(attr_ws, '_attributes')
954955

955956

957+
def _fill_product_message_row(ws, row_idx, msg):
958+
ws.cell(row_idx, 1, value=msg['id'])
959+
ws.cell(row_idx, 2, value='-')
960+
ws.cell(row_idx, 3, value=msg['external_id'])
961+
ws.cell(row_idx, 4, value=msg['value'])
962+
ws.cell(row_idx, 5, value=msg['auto'])
963+
964+
965+
def _dump_product_messages(ws, client, product_id, progress):
966+
_setup_ws_header(ws, 'messages')
967+
968+
row_idx = 2
969+
970+
messages = client.products[product_id].messages.all()
971+
count = messages.count()
972+
973+
action_validation = DataValidation(
974+
type='list',
975+
formula1='"-,create,update,delete"',
976+
allow_blank=False,
977+
)
978+
979+
if count > 0:
980+
ws.add_data_validation(action_validation)
981+
982+
task = progress.add_task('Processing message', total=count)
983+
984+
for msg in messages:
985+
progress.update(task, description=f'Processing message {msg["id"]}', advance=1)
986+
_fill_product_message_row(ws, row_idx, msg)
987+
action_validation.add(f'B{row_idx}')
988+
row_idx += 1
989+
990+
progress.update(task, completed=count)
991+
992+
956993
def dump_product( # noqa: CCR001
957994
client,
958995
product_id,
@@ -1019,6 +1056,7 @@ def dump_product( # noqa: CCR001
10191056
_dump_actions(wb.create_sheet('Actions'), client, product_id, progress)
10201057
_dump_configuration(wb.create_sheet('Configuration'), client, product_id, progress)
10211058
_dump_translations(wb, client, product_id, progress)
1059+
_dump_product_messages(wb.create_sheet('Messages'), client, product_id, progress)
10221060
wb.save(output_file)
10231061

10241062
except ClientError as error:

connect/cli/plugins/product/sync/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
from connect.cli.plugins.product.sync.general import GeneralSynchronizer # noqa: F401
77
from connect.cli.plugins.product.sync.items import ItemSynchronizer # noqa: F401
88
from connect.cli.plugins.product.sync.media import MediaSynchronizer # noqa: F401
9+
from connect.cli.plugins.product.sync.messages import MessageSynchronizer # noqa: F401
910
from connect.cli.plugins.product.sync.params import ParamsSynchronizer # noqa: F401
1011
from connect.cli.plugins.product.sync.static_resources import ( # noqa: F401
1112
StaticResourcesSynchronizer,

connect/cli/plugins/product/sync/items.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -135,7 +135,7 @@ def sync(self): # noqa: CCR001
135135
field = 'ID' if data.id else 'MPN'
136136
value = data.id if data.id else data.mpn
137137
self._mstats.error(
138-
f'Cannot update item: item with {field} `{value}` '
138+
f'Cannot delete item: item with {field} `{value}` '
139139
f'the item does not exist.',
140140
row_idx,
141141
)
Lines changed: 175 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,175 @@
1+
# -*- coding: utf-8 -*-
2+
3+
# This file is part of the Ingram Micro Cloud Blue Connect connect-cli.
4+
# Copyright (c) 2019-2024 Ingram Micro. All Rights Reserved.
5+
6+
from collections import namedtuple
7+
8+
from connect.client.rql import R
9+
from connect.client import ClientError, R
10+
11+
from connect.cli.core.http import handle_http_error
12+
from connect.cli.plugins.shared.base import ProductSynchronizer
13+
from connect.cli.plugins.shared.constants import MESSAGES_HEADERS
14+
15+
16+
fields = (v.replace(' ', '_').lower() for v in MESSAGES_HEADERS.values())
17+
18+
_RowData = namedtuple('RowData', fields)
19+
20+
21+
class MessageSynchronizer(ProductSynchronizer):
22+
def __init__(self, client, progress, stats):
23+
self._mstats = stats['Messages']
24+
super().__init__(client, progress)
25+
26+
def _get_message(self, message_id):
27+
try:
28+
res = self._client.products[self._product_id].messages[message_id].get()
29+
except ClientError as error:
30+
if error.status_code == 404:
31+
return
32+
handle_http_error(error)
33+
return res
34+
35+
def _create_message(self, data):
36+
try:
37+
payload = {'external_id': data.external_id, 'value': data.value, 'auto': data.auto}
38+
res = self._client.products[self._product_id].messages.create(payload)
39+
except ClientError as error:
40+
handle_http_error(error)
41+
return res
42+
43+
def _update_message(self, data):
44+
try:
45+
payload = {'external_id': data.external_id, 'value': data.value, 'auto': data.auto}
46+
res = self._client.products[self._product_id].messages[data.id].update(payload)
47+
except ClientError as error:
48+
handle_http_error(error)
49+
return res
50+
51+
def _delete_message(self, message_id):
52+
try:
53+
res = self._client.products[self._product_id].messages[message_id].delete()
54+
except ClientError as error:
55+
handle_http_error(error)
56+
return res
57+
58+
def _process_create(self, row_idx, data, task):
59+
rql = R().external_id.eq(data.external_id)
60+
message = self._client.products[self._product_id].messages.filter(rql).first()
61+
if message:
62+
self._mstats.error(
63+
f'Cannot create message: message with external_id `{data.external_id}`'
64+
f' already exists with ID `{message["id"]}`.',
65+
row_idx,
66+
)
67+
else:
68+
self._progress.update(
69+
task,
70+
description=f'Creating message {data.external_id}',
71+
)
72+
try:
73+
new_message = self._create_message(data)
74+
self._mstats.created()
75+
self._update_sheet_row(self._ws, row_idx, new_message)
76+
except Exception as e:
77+
self._mstats.error(str(e), row_idx)
78+
79+
def _process_update(self, row_idx, data, task):
80+
if not self._get_message(data.id):
81+
self._mstats.error(
82+
f'Cannot update message: message with ID `{data.id}` ' 'does not exist.',
83+
row_idx,
84+
)
85+
else:
86+
self._progress.update(
87+
task,
88+
description=f'Updating message {data.id}',
89+
)
90+
try:
91+
updated_message = self._update_message(
92+
data,
93+
)
94+
self._mstats.updated()
95+
self._update_sheet_row(self._ws, row_idx, updated_message)
96+
except Exception as e:
97+
self._mstats.error(str(e), row_idx)
98+
99+
def _process_delete(self, row_idx, data, task):
100+
if not self._get_message(data.id):
101+
self._mstats.error(
102+
f'Cannot delete message: message with ID `{data.id}` ' 'does not exist.',
103+
row_idx,
104+
)
105+
else:
106+
self._progress.update(
107+
task,
108+
description=f'Deleting message {data.id}',
109+
)
110+
try:
111+
self._delete_message(data.id)
112+
self._mstats.deleted()
113+
for c in range(1, self._ws.max_column + 1):
114+
self._ws.cell(row_idx, c, value='')
115+
except Exception as e:
116+
self._mstats.error(str(e), row_idx)
117+
118+
def sync(self): # noqa: CCR001
119+
self._ws = self._wb['Messages']
120+
task = self._progress.add_task('Processing messages', total=self._ws.max_row - 1)
121+
for row_idx in range(2, self._ws.max_row + 1):
122+
data = _RowData(*[self._ws.cell(row_idx, col_idx).value for col_idx in range(1, 6)])
123+
self._progress.update(
124+
task,
125+
description=f'Processing message {data.id}',
126+
advance=1,
127+
)
128+
if data.action == '-':
129+
self._mstats.skipped()
130+
continue
131+
132+
row_errors = self._validate_row(data)
133+
if row_errors:
134+
self._mstats.error(row_errors, row_idx)
135+
continue
136+
137+
if data.action == 'create':
138+
self._process_create(row_idx, data, task)
139+
elif data.action == 'update':
140+
self._process_update(row_idx, data, task)
141+
elif data.action == 'delete':
142+
self._process_delete(row_idx, data, task)
143+
144+
self._progress.update(task, completed=self._ws.max_row - 1)
145+
146+
def _validate_row(self, row): # noqa: CCR001
147+
errors = []
148+
149+
if row.action == 'create' and row.id:
150+
errors.append(
151+
'the `ID` must not be specified for the `create` action.',
152+
)
153+
return errors
154+
if row.action in ('update', 'delete') and not row.id:
155+
errors.append(
156+
'the `ID` must be specified for the `update` or `delete` actions.',
157+
)
158+
return errors
159+
if (
160+
row.action in ('create', 'update', 'delete')
161+
and row.external_id is None
162+
or row.value is None
163+
):
164+
errors.append(
165+
'the `External ID` and `Value` must be specified for the `create`, `update` or `delete` actions.',
166+
)
167+
return errors
168+
169+
@staticmethod
170+
def _update_sheet_row(ws, row_idx, message):
171+
ws.cell(row_idx, 1, value=message['id'])
172+
ws.cell(row_idx, 2, value='-')
173+
ws.cell(row_idx, 3, value=message['external_id'])
174+
ws.cell(row_idx, 4, value=message['value'])
175+
ws.cell(row_idx, 5, value=message['auto'])

connect/cli/plugins/shared/constants.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -113,3 +113,11 @@
113113
'M': 'Created',
114114
'N': 'Updated',
115115
}
116+
117+
MESSAGES_HEADERS = {
118+
'A': 'ID',
119+
'B': 'Action',
120+
'C': 'External ID',
121+
'D': 'Value',
122+
'E': 'Auto',
123+
}

connect/cli/plugins/shared/utils.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
CONFIGURATION_HEADERS,
2020
ITEMS_COLS_HEADERS,
2121
MEDIA_COLS_HEADERS,
22+
MESSAGES_HEADERS,
2223
PARAMS_COLS_HEADERS,
2324
STATIC_LINK_HEADERS,
2425
TEMPLATES_HEADERS,
@@ -47,6 +48,8 @@ def get_col_limit_by_ws_type(ws_type):
4748
return 'N'
4849
elif ws_type == '_attributes':
4950
return 'F'
51+
elif ws_type == 'messages':
52+
return 'E'
5053
return 'Z'
5154

5255

@@ -73,6 +76,8 @@ def get_ws_type_by_worksheet_name(ws_name):
7376
return 'actions'
7477
elif ws_name == 'Translations':
7578
return 'translations'
79+
elif ws_name == 'Messages':
80+
return 'messages'
7681
return None
7782

7883

@@ -95,6 +100,8 @@ def get_col_headers_by_ws_type(ws_type):
95100
return ACTIONS_HEADERS
96101
elif ws_type == 'translations':
97102
return TRANSLATION_HEADERS
103+
elif ws_type == 'messages':
104+
return MESSAGES_HEADERS
98105

99106

100107
def wait_for_autotranslation(client, progress, translation, wait_seconds=1, max_counts=5):

0 commit comments

Comments
 (0)