Skip to content

Commit

Permalink
Merge pull request #228 from cloudblue/LITE-29353-add-product-message…
Browse files Browse the repository at this point in the history
…s-to-export-sync-clone

LITE-29353: Added ProductMessage compatability on product export, sync and clone cmds
  • Loading branch information
jazz-jack authored Feb 9, 2024
2 parents 9b629fc + 3f4a38c commit c2db1f8
Show file tree
Hide file tree
Showing 17 changed files with 1,497 additions and 721 deletions.
15 changes: 15 additions & 0 deletions connect/cli/plugins/product/clone.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
GeneralSynchronizer,
ItemSynchronizer,
MediaSynchronizer,
MessageSynchronizer,
ParamsSynchronizer,
TemplatesSynchronizer,
)
Expand Down Expand Up @@ -134,6 +135,14 @@ def inject(self): # noqa: CCR001
is_clone=True,
)

synchronizer = MessageSynchronizer(
self.config.active.client,
self.progress,
self.stats,
)
synchronizer.open(input_file, 'Messages')
synchronizer.sync()

self.config.activate(self.source_account)
except ClientError as e:
raise ClickException(f'Error while cloning product: {str(e)}')
Expand Down Expand Up @@ -229,6 +238,12 @@ def clean_wb(self):
value = '-'
for row in range(2, ws.max_row + 1):
ws[f'C{row}'].value = value

ws = self.wb['Messages']
for row in range(2, ws.max_row + 1):
ws[f'A{row}'].value = ''
ws[f'B{row}'].value = 'create'

self.wb.save(f'{self.fs.root_path}/{self.product_id}/{self.product_id}.xlsx')

@staticmethod
Expand Down
13 changes: 11 additions & 2 deletions connect/cli/plugins/product/commands.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
GeneralSynchronizer,
ItemSynchronizer,
MediaSynchronizer,
MessageSynchronizer,
ParamsSynchronizer,
StaticResourcesSynchronizer,
TemplatesSynchronizer,
Expand Down Expand Up @@ -90,7 +91,7 @@ def cmd_list_products(config, query):
help='Directory where to store the export.',
)
@pass_config
def cmd_dump_products(config, product_id, output_file, output_path):
def cmd_export_products(config, product_id, output_file, output_path):
with console.progress() as progress:
outfile = dump_product(
config.active.client,
Expand Down Expand Up @@ -120,7 +121,7 @@ def cmd_sync_products(config, input_file):
product_id = synchronizer.open(input_file, 'General Information')

console.confirm(
'Are you sure you want to synchronize ' f'the product {product_id} ?',
f'Are you sure you want to synchronize the product {product_id} ?',
abort=True,
)
console.echo('')
Expand Down Expand Up @@ -148,6 +149,7 @@ def cmd_sync_products(config, input_file):
actions_sync,
media_sync,
config_values_sync,
messages_sync,
]
for task in sync_tasks:
try:
Expand Down Expand Up @@ -324,5 +326,12 @@ def item_sync(client, progress, input_file, stats):
synchronizer.save(input_file)


def messages_sync(client, progress, input_file, stats):
synchronizer = MessageSynchronizer(client, progress, stats)
synchronizer.open(input_file, 'Messages')
synchronizer.sync()
synchronizer.save(input_file)


def get_group():
return grp_product
46 changes: 42 additions & 4 deletions connect/cli/plugins/product/export.py
Original file line number Diff line number Diff line change
Expand Up @@ -174,11 +174,12 @@ def _setup_ws_header(ws, ws_type=None): # noqa: CCR001
elif ws_type == 'templates':
if cel.value == 'Content':
ws.column_dimensions[cel.column_letter].width = 100
if cel.value == 'Title':
elif cel.value == 'Title':
ws.column_dimensions[cel.column_letter].width = 50
elif ws_type == '_attributes':
if cel.column_letter in ['A', 'B', 'D']:
ws.column_dimensions[cel.column_letter].width = 100
elif ws_type == '_attributes' and cel.column_letter in ['A', 'B', 'D']:
ws.column_dimensions[cel.column_letter].width = 100
elif ws_type == 'messages' and cel.column_letter == 'D':
ws.column_dimensions[cel.column_letter].width = 70


def _calculate_commitment(item):
Expand Down Expand Up @@ -953,6 +954,42 @@ def _dump_translation_attr(wb, client, translation):
_setup_ws_header(attr_ws, '_attributes')


def _fill_product_message_row(ws, row_idx, msg):
ws.cell(row_idx, 1, value=msg['id'])
ws.cell(row_idx, 2, value='-')
ws.cell(row_idx, 3, value=msg['external_id'])
ws.cell(row_idx, 4, value=msg['value'])
ws.cell(row_idx, 5, value=msg['auto'])


def _dump_product_messages(ws, client, product_id, progress):
_setup_ws_header(ws, 'messages')

row_idx = 2

messages = client.products[product_id].messages.all()
count = messages.count()

action_validation = DataValidation(
type='list',
formula1='"-,create,update,delete"',
allow_blank=False,
)

if count > 0:
ws.add_data_validation(action_validation)

task = progress.add_task('Processing message', total=count)

for msg in messages:
progress.update(task, description=f'Processing message {msg["id"]}', advance=1)
_fill_product_message_row(ws, row_idx, msg)
action_validation.add(f'B{row_idx}')
row_idx += 1

progress.update(task, completed=count)


def dump_product( # noqa: CCR001
client,
product_id,
Expand Down Expand Up @@ -1019,6 +1056,7 @@ def dump_product( # noqa: CCR001
_dump_actions(wb.create_sheet('Actions'), client, product_id, progress)
_dump_configuration(wb.create_sheet('Configuration'), client, product_id, progress)
_dump_translations(wb, client, product_id, progress)
_dump_product_messages(wb.create_sheet('Messages'), client, product_id, progress)
wb.save(output_file)

except ClientError as error:
Expand Down
1 change: 1 addition & 0 deletions connect/cli/plugins/product/sync/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
from connect.cli.plugins.product.sync.general import GeneralSynchronizer # noqa: F401
from connect.cli.plugins.product.sync.items import ItemSynchronizer # noqa: F401
from connect.cli.plugins.product.sync.media import MediaSynchronizer # noqa: F401
from connect.cli.plugins.product.sync.messages import MessageSynchronizer # noqa: F401
from connect.cli.plugins.product.sync.params import ParamsSynchronizer # noqa: F401
from connect.cli.plugins.product.sync.static_resources import ( # noqa: F401
StaticResourcesSynchronizer,
Expand Down
2 changes: 1 addition & 1 deletion connect/cli/plugins/product/sync/items.py
Original file line number Diff line number Diff line change
Expand Up @@ -135,7 +135,7 @@ def sync(self): # noqa: CCR001
field = 'ID' if data.id else 'MPN'
value = data.id if data.id else data.mpn
self._mstats.error(
f'Cannot update item: item with {field} `{value}` '
f'Cannot delete item: item with {field} `{value}` '
f'the item does not exist.',
row_idx,
)
Expand Down
175 changes: 175 additions & 0 deletions connect/cli/plugins/product/sync/messages.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,175 @@
# -*- coding: utf-8 -*-

# This file is part of the Ingram Micro Cloud Blue Connect connect-cli.
# Copyright (c) 2019-2024 Ingram Micro. All Rights Reserved.

from collections import namedtuple

from connect.client.rql import R
from connect.client import ClientError, R

from connect.cli.core.http import handle_http_error
from connect.cli.plugins.shared.base import ProductSynchronizer
from connect.cli.plugins.shared.constants import MESSAGES_HEADERS


fields = (v.replace(' ', '_').lower() for v in MESSAGES_HEADERS.values())

_RowData = namedtuple('RowData', fields)


class MessageSynchronizer(ProductSynchronizer):
def __init__(self, client, progress, stats):
self._mstats = stats['Messages']
super().__init__(client, progress)

def _get_message(self, message_id):
try:
res = self._client.products[self._product_id].messages[message_id].get()
except ClientError as error:
if error.status_code == 404:
return
handle_http_error(error)
return res

def _create_message(self, data):
try:
payload = {'external_id': data.external_id, 'value': data.value, 'auto': data.auto}
res = self._client.products[self._product_id].messages.create(payload)
except ClientError as error:
handle_http_error(error)
return res

def _update_message(self, data):
try:
payload = {'external_id': data.external_id, 'value': data.value, 'auto': data.auto}
res = self._client.products[self._product_id].messages[data.id].update(payload)
except ClientError as error:
handle_http_error(error)
return res

def _delete_message(self, message_id):
try:
res = self._client.products[self._product_id].messages[message_id].delete()
except ClientError as error:
handle_http_error(error)
return res

def _process_create(self, row_idx, data, task):
rql = R().external_id.eq(data.external_id)
message = self._client.products[self._product_id].messages.filter(rql).first()
if message:
self._mstats.error(
f'Cannot create message: message with external_id `{data.external_id}`'
f' already exists with ID `{message["id"]}`.',
row_idx,
)
else:
self._progress.update(
task,
description=f'Creating message {data.external_id}',
)
try:
new_message = self._create_message(data)
self._mstats.created()
self._update_sheet_row(self._ws, row_idx, new_message)
except Exception as e:
self._mstats.error(str(e), row_idx)

def _process_update(self, row_idx, data, task):
if not self._get_message(data.id):
self._mstats.error(
f'Cannot update message: message with ID `{data.id}` ' 'does not exist.',
row_idx,
)
else:
self._progress.update(
task,
description=f'Updating message {data.id}',
)
try:
updated_message = self._update_message(
data,
)
self._mstats.updated()
self._update_sheet_row(self._ws, row_idx, updated_message)
except Exception as e:
self._mstats.error(str(e), row_idx)

def _process_delete(self, row_idx, data, task):
if not self._get_message(data.id):
self._mstats.error(
f'Cannot delete message: message with ID `{data.id}` ' 'does not exist.',
row_idx,
)
else:
self._progress.update(
task,
description=f'Deleting message {data.id}',
)
try:
self._delete_message(data.id)
self._mstats.deleted()
for c in range(1, self._ws.max_column + 1):
self._ws.cell(row_idx, c, value='')
except Exception as e:
self._mstats.error(str(e), row_idx)

def sync(self): # noqa: CCR001
self._ws = self._wb['Messages']
task = self._progress.add_task('Processing messages', total=self._ws.max_row - 1)
for row_idx in range(2, self._ws.max_row + 1):
data = _RowData(*[self._ws.cell(row_idx, col_idx).value for col_idx in range(1, 6)])
self._progress.update(
task,
description=f'Processing message {data.id}',
advance=1,
)
if data.action == '-':
self._mstats.skipped()
continue

row_errors = self._validate_row(data)
if row_errors:
self._mstats.error(row_errors, row_idx)
continue

if data.action == 'create':
self._process_create(row_idx, data, task)
elif data.action == 'update':
self._process_update(row_idx, data, task)
elif data.action == 'delete':
self._process_delete(row_idx, data, task)

self._progress.update(task, completed=self._ws.max_row - 1)

def _validate_row(self, row): # noqa: CCR001
errors = []

if row.action == 'create' and row.id:
errors.append(
'the `ID` must not be specified for the `create` action.',
)
return errors
if row.action in ('update', 'delete') and not row.id:
errors.append(
'the `ID` must be specified for the `update` or `delete` actions.',
)
return errors
if (
row.action in ('create', 'update', 'delete')
and row.external_id is None
or row.value is None
):
errors.append(
'the `External ID` and `Value` must be specified for the `create`, `update` or `delete` actions.',
)
return errors

@staticmethod
def _update_sheet_row(ws, row_idx, message):
ws.cell(row_idx, 1, value=message['id'])
ws.cell(row_idx, 2, value='-')
ws.cell(row_idx, 3, value=message['external_id'])
ws.cell(row_idx, 4, value=message['value'])
ws.cell(row_idx, 5, value=message['auto'])
8 changes: 8 additions & 0 deletions connect/cli/plugins/shared/constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -113,3 +113,11 @@
'M': 'Created',
'N': 'Updated',
}

MESSAGES_HEADERS = {
'A': 'ID',
'B': 'Action',
'C': 'External ID',
'D': 'Value',
'E': 'Auto',
}
7 changes: 7 additions & 0 deletions connect/cli/plugins/shared/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
CONFIGURATION_HEADERS,
ITEMS_COLS_HEADERS,
MEDIA_COLS_HEADERS,
MESSAGES_HEADERS,
PARAMS_COLS_HEADERS,
STATIC_LINK_HEADERS,
TEMPLATES_HEADERS,
Expand Down Expand Up @@ -47,6 +48,8 @@ def get_col_limit_by_ws_type(ws_type):
return 'N'
elif ws_type == '_attributes':
return 'F'
elif ws_type == 'messages':
return 'E'
return 'Z'


Expand All @@ -73,6 +76,8 @@ def get_ws_type_by_worksheet_name(ws_name):
return 'actions'
elif ws_name == 'Translations':
return 'translations'
elif ws_name == 'Messages':
return 'messages'
return None


Expand All @@ -95,6 +100,8 @@ def get_col_headers_by_ws_type(ws_type):
return ACTIONS_HEADERS
elif ws_type == 'translations':
return TRANSLATION_HEADERS
elif ws_type == 'messages':
return MESSAGES_HEADERS


def wait_for_autotranslation(client, progress, translation, wait_seconds=1, max_counts=5):
Expand Down
Loading

0 comments on commit c2db1f8

Please sign in to comment.