diff --git a/src/mail_order_bot/main.py b/src/mail_order_bot/main.py index 85023c1..4e41b9b 100644 --- a/src/mail_order_bot/main.py +++ b/src/mail_order_bot/main.py @@ -45,7 +45,7 @@ class MailOrderBot(ConfigManager): def execute(self): - # Получить список айдишников письма + #Получить список айдишников письма folder = self.config.get("folder") try: diff --git a/src/mail_order_bot/parsers/excel_parcer.py b/src/mail_order_bot/parsers/excel_parcer.py index ba7d218..23e4fb7 100644 --- a/src/mail_order_bot/parsers/excel_parcer.py +++ b/src/mail_order_bot/parsers/excel_parcer.py @@ -14,10 +14,7 @@ class ExcelFileParcer: def _parse_file(self, file_bytes): """Парсит вложение в формате эл таблиц""" - try: - df = pd.read_excel(file_bytes, sheet_name=self.sheet_name, header=None) - except Exception as e: - df = None + df = pd.read_excel(file_bytes, sheet_name=self.sheet_name, header=None) return df def set_value(self, sku, manufacturer, column, value): diff --git a/src/mail_order_bot/task_processor/__init__.py b/src/mail_order_bot/task_processor/__init__.py index f9b31cc..b0b5e64 100644 --- a/src/mail_order_bot/task_processor/__init__.py +++ b/src/mail_order_bot/task_processor/__init__.py @@ -1,2 +1,4 @@ from .processor import TaskProcessor -from .message import LogMessage, LogMessageLevel, LogMessageStorage \ No newline at end of file +from .message import LogMessage, LogMessageLevel, LogMessageStorage + +from .abstract_task import AbstractTask, pass_if_error, handle_errors \ No newline at end of file diff --git a/src/mail_order_bot/task_processor/abstract_task.py b/src/mail_order_bot/task_processor/abstract_task.py index b8db0f9..fd0c101 100644 --- a/src/mail_order_bot/task_processor/abstract_task.py +++ b/src/mail_order_bot/task_processor/abstract_task.py @@ -1,11 +1,60 @@ from abc import ABC, abstractmethod -from typing import Dict, Any +import logging +import functools from mail_order_bot.context import Context +logger = logging.getLogger(__name__) + + +def handle_errors(func): + """ + Декоратор для обработки ошибок в методе do класса AbstractTask. + Оборачивает выполнение метода в try-except, при ошибке устанавливает статус "error", + логирует ошибку и пробрасывает исключение дальше. + Применяется везде к методу do. + """ + @functools.wraps(func) + def wrapper(self, attachment) -> None: + file_name = attachment.get("name", "неизвестный файл") + + try: + # Выполняем метод do + return func(self, attachment) + + except Exception as e: + # При ошибке устанавливаем статус и логируем + if attachment: + attachment["status"] = "error" + logger.error(f"Ошибка при обработке файла {file_name} на стадии {self.STEP} \n{e}", exc_info=True) + # Пробрасываем исключение дальше + # raise + return wrapper + + +def pass_if_error(func): + """ + Декоратор для проверки статуса attachment перед выполнением метода do. + Если статус attachment["status"] != "ok", метод не выполняется. + Применяется опционально в конкретных классах, где нужна проверка статуса. + """ + @functools.wraps(func) + def wrapper(self, attachment) -> None: + # Проверяем статус перед выполнением + if attachment and attachment.get("status") != "ok": + file_name = attachment.get("name", "неизвестный файл") + logger.warning(f"Пропускаем шаг для файла {file_name}, статус {attachment.get('status')}") + return + + # Выполняем метод do + return func(self, attachment) + + return wrapper + class AbstractTask(): - RESULT_SECTION = "section" + STEP = "Название шага обработки" + """ Абстрактный базовый класс для всех хэндлеров. """ diff --git a/src/mail_order_bot/task_processor/handlers/__init__.py b/src/mail_order_bot/task_processor/handlers/__init__.py index bfb8db1..cdf0dac 100644 --- a/src/mail_order_bot/task_processor/handlers/__init__.py +++ b/src/mail_order_bot/task_processor/handlers/__init__.py @@ -1,5 +1,5 @@ -from .abcp.api_get_stock import APIGetStock +from .abcp._api_get_stock import APIGetStock from .delivery_time.local_store import DeliveryPeriodLocalStore from .delivery_time.from_config import DeliveryPeriodFromConfig from .notifications.test_notifier import TestNotifier diff --git a/src/mail_order_bot/task_processor/handlers/abcp/api_get_stock.py b/src/mail_order_bot/task_processor/handlers/abcp/_api_get_stock.py similarity index 99% rename from src/mail_order_bot/task_processor/handlers/abcp/api_get_stock.py rename to src/mail_order_bot/task_processor/handlers/abcp/_api_get_stock.py index 821638d..af02452 100644 --- a/src/mail_order_bot/task_processor/handlers/abcp/api_get_stock.py +++ b/src/mail_order_bot/task_processor/handlers/abcp/_api_get_stock.py @@ -24,7 +24,7 @@ class APIGetStock(AbstractTask): self.client_provider = AbcpProvider(login=client_login, password=client_password) def do(self, attachment) -> None: - + # order = attachment.get("order", None) for position in order.positions: # Получаем остатки из-под учетной записи клиента diff --git a/src/mail_order_bot/task_processor/handlers/abcp/api_save_order.py b/src/mail_order_bot/task_processor/handlers/abcp/api_save_order.py index ef975c2..1d0f543 100644 --- a/src/mail_order_bot/task_processor/handlers/abcp/api_save_order.py +++ b/src/mail_order_bot/task_processor/handlers/abcp/api_save_order.py @@ -6,7 +6,7 @@ """ import logging -from mail_order_bot.task_processor.abstract_task import AbstractTask +from ...abstract_task import AbstractTask, pass_if_error, handle_errors from mail_order_bot.abcp_api.abcp_provider import AbcpProvider from mail_order_bot.credential_provider import CredentialProvider @@ -14,47 +14,47 @@ from mail_order_bot.telegram.client import TelegramClient logger = logging.getLogger(__name__) + class SaveOrderToTelegram(AbstractTask): def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) + @pass_if_error + @handle_errors def do(self, attachment) -> None: client = TelegramClient() - try: - order = attachment["order"] - positions = order.positions - message = "\nОбработка заказа {указать название контрагента}\n" - message += f"\nПолучено {len(positions)} позиций от {order.client_id}\n" - message += "===============================\n" - for position in positions: - message += f"{position.sku} - {position.manufacturer} - {position.name} \n" - message += f"{position.asking_quantity} x {position.asking_price} = {position.total} \n" + order = attachment["order"] + positions = order.positions + message = "\nОбработка заказа {указать название контрагента}\n" + message += f"\nПолучено {len(positions)} позиций от {order.client_id}\n" + message += "===============================\n" + for position in positions: + message += f"{position.sku} - {position.manufacturer} - {position.name} \n" + message += f"{position.asking_quantity} x {position.asking_price} = {position.total} \n" - rejected = position.asking_quantity - position.order_quantity - if position.order_quantity == 0: - message += f"Отказ\n" - elif rejected: - message += (f"Отказ: {rejected}, запрошено, {position.asking_quantity}, " - f"отгружено {position.order_quantity}, профит {position.profit}\n") - else: - message += f"Позиция отгружена полностью, профит {position.profit}\n" - message += "-------------------------------\n" + rejected = position.asking_quantity - position.order_quantity + if position.order_quantity == 0: + message += f"Отказ\n" + elif rejected: + message += (f"Отказ: {rejected}, запрошено, {position.asking_quantity}, " + f"отгружено {position.order_quantity}, профит {position.profit}\n") + else: + message += f"Позиция отгружена полностью, профит {position.profit}\n" + message += "-------------------------------\n" - result = client.send_message(message) + result = client.send_message(message) - # Отправка экселя в телеграм - excel = attachment["excel"] - file = excel.get_file_bytes() + # Отправка экселя в телеграм + excel = attachment["excel"] + file = excel.get_file_bytes() - client.send_document( - document=file, - filename="document.xlsx" - ) - except Exception as e: - logger.error("Ошибка при отправке инфо по заказу в телеграм") - else: - logger.warning("Инфо по заказу отправлено в телеграм") + client.send_document( + document=file, + filename="document.xlsx" + ) + + logger.warning("Инфо по заказу отправлено в телеграм") #=============================== diff --git a/src/mail_order_bot/task_processor/handlers/delivery_time/from_config.py b/src/mail_order_bot/task_processor/handlers/delivery_time/from_config.py index 9c7f1b7..ced29c3 100644 --- a/src/mail_order_bot/task_processor/handlers/delivery_time/from_config.py +++ b/src/mail_order_bot/task_processor/handlers/delivery_time/from_config.py @@ -5,7 +5,7 @@ import logging -from mail_order_bot.task_processor.abstract_task import AbstractTask +from ...abstract_task import AbstractTask, pass_if_error, handle_errors logger = logging.getLogger(__name__) @@ -13,17 +13,9 @@ class DeliveryPeriodFromConfig(AbstractTask): def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) + @pass_if_error + @handle_errors def do(self, attachment) -> None: - try: - delivery_period = self.config.get("delivery_period") - - except Exception as e: - logger.error(f"Ошибка при получении срока доставки из конфига: {e}") - - else: - attachment["delivery_period"] = delivery_period - logger.warning(f"Срок доставки установлен из конфига - {delivery_period} (ч.)") - - - - + delivery_period = self.config.get("delivery_period") + attachment["delivery_period"] = delivery_period + logger.warning(f"Срок доставки установлен из конфига - {delivery_period} (ч.)") diff --git a/src/mail_order_bot/task_processor/handlers/delivery_time/from_subject.py b/src/mail_order_bot/task_processor/handlers/delivery_time/from_subject.py index 1d1bd69..bac2a60 100644 --- a/src/mail_order_bot/task_processor/handlers/delivery_time/from_subject.py +++ b/src/mail_order_bot/task_processor/handlers/delivery_time/from_subject.py @@ -2,7 +2,7 @@ Парсер срока доставки из темы письма """ -from mail_order_bot.task_processor.abstract_task import AbstractTask +from ...abstract_task import AbstractTask, pass_if_error, handle_errors import logging import re diff --git a/src/mail_order_bot/task_processor/handlers/delivery_time/local_store.py b/src/mail_order_bot/task_processor/handlers/delivery_time/local_store.py index 81692dc..4cb8796 100644 --- a/src/mail_order_bot/task_processor/handlers/delivery_time/local_store.py +++ b/src/mail_order_bot/task_processor/handlers/delivery_time/local_store.py @@ -5,7 +5,7 @@ import logging -from mail_order_bot.task_processor.abstract_task import AbstractTask +from ...abstract_task import AbstractTask, pass_if_error, handle_errors logger = logging.getLogger(__name__) diff --git a/src/mail_order_bot/task_processor/handlers/email/email_parcer.py b/src/mail_order_bot/task_processor/handlers/email/email_parcer.py index b58a012..1eece3a 100644 --- a/src/mail_order_bot/task_processor/handlers/email/email_parcer.py +++ b/src/mail_order_bot/task_processor/handlers/email/email_parcer.py @@ -4,7 +4,7 @@ """ import logging -from mail_order_bot.task_processor.abstract_task import AbstractTask +from ...abstract_task import AbstractTask, pass_if_error, handle_errors from mail_order_bot.email_client.utils import EmailUtils diff --git a/src/mail_order_bot/task_processor/handlers/email/email_reply_task.py b/src/mail_order_bot/task_processor/handlers/email/email_reply_task.py index 73abe35..e38e0bd 100644 --- a/src/mail_order_bot/task_processor/handlers/email/email_reply_task.py +++ b/src/mail_order_bot/task_processor/handlers/email/email_reply_task.py @@ -7,7 +7,7 @@ from email.utils import formatdate from email import encoders -from mail_order_bot.task_processor.abstract_task import AbstractTask +from ...abstract_task import AbstractTask, pass_if_error, handle_errors logger = logging.getLogger(__name__) @@ -19,40 +19,40 @@ class EmailReplyTask(AbstractTask): """Формирует ответ на входящее письмо с запросом на заказ°""" EMAIl = "zosimovaa@yandex.ru" #"noreply@zapchastiya.ru" + @pass_if_error + @handle_errors def do(self, attachment): - try: - email = self.context.data.get("email") + email = self.context.data.get("email") - if not email: - raise ValueError("В контексте нет входящего сообщения") + if not email: + raise ValueError("В контексте нет входящего сообщения") - email_from = self.context.data.get("email_from") - if not email_from: - raise ValueError("В контексте не определен адрес отправителя") + email_from = self.context.data.get("email_from") + if not email_from: + raise ValueError("В контексте не определен адрес отправителя") - reply_message = MIMEMultipart() + reply_message = MIMEMultipart() - email_subj = self.context.data.get("email_subj") + email_subj = self.context.data.get("email_subj") - reply_message["From"] = self.EMAIl - reply_message["To"] = email_from - #reply_message["Cc"] = self.config.get("reply_to", "") - reply_message["Subject"] = f"Re: {email_subj}" - reply_message["Date"] = formatdate(localtime=True) + reply_message["From"] = self.EMAIl + reply_message["To"] = email_from + #reply_message["Cc"] = self.config.get("reply_to", "") + reply_message["Subject"] = f"Re: {email_subj}" + reply_message["Date"] = formatdate(localtime=True) - body = "Автоматический ответ на создание заказа" - reply_message.attach(MIMEText(body, "plain", "utf-8")) + body = "Автоматический ответ на создание заказа" + reply_message.attach(MIMEText(body, "plain", "utf-8")) - self._attach_file(reply_message, attachment) + self._attach_file(reply_message, attachment) + + self.context.email_client.send_email(reply_message) + + logger.warning(f"Сформирован ответ на заказ на email") - self.context.email_client.send_email(reply_message) - except Exception as e: - logger.error(f"Ошибка при отправке ответа по заказу на email \n{e}") - else: - logger.warning(f"Сформирован ответ на заказ на email") def _attach_file(self, reply_message, attachment): """ diff --git a/src/mail_order_bot/task_processor/handlers/excel_parcers/excel_extractor.py b/src/mail_order_bot/task_processor/handlers/excel_parcers/excel_extractor.py index b36726f..41005c3 100644 --- a/src/mail_order_bot/task_processor/handlers/excel_parcers/excel_extractor.py +++ b/src/mail_order_bot/task_processor/handlers/excel_parcers/excel_extractor.py @@ -1,12 +1,7 @@ import logging -import pandas as pd from io import BytesIO - -from mail_order_bot.email_client import EmailUtils -#from mail_order_bot.task_processor.handlers.order_position import OrderPosition -from mail_order_bot.task_processor.abstract_task import AbstractTask - -from mail_order_bot.parsers.excel_parcer import ExcelFileParcer +from ...abstract_task import AbstractTask, pass_if_error, handle_errors +from ....parsers.excel_parcer import ExcelFileParcer logger = logging.getLogger(__name__) @@ -19,20 +14,10 @@ class ExcelExtractor(AbstractTask): super().__init__(*args, **kwargs) self.excel_config = self.config.get("excel", {}) + @pass_if_error + @handle_errors def do(self, attachment) -> None: - try: - file_bytes = BytesIO(attachment['bytes']) - excel_file = ExcelFileParcer(file_bytes, self.excel_config) - - except Exception as e: - logger.error(f"Не удалось распарсить файл: \n{e}") - attachment["excel"] = None - - else: - attachment["excel"] = excel_file - logger.warning(f"Произведен успешный парсинг файла") - - - - - + file_bytes = BytesIO(attachment['bytes']) + excel_file = ExcelFileParcer(file_bytes, self.excel_config) + attachment["excel"] = excel_file + logger.warning(f"Произведен успешный парсинг файла {attachment.get('name', 'неизвестный файл')}") diff --git a/src/mail_order_bot/task_processor/handlers/excel_parcers/order_extractor.py b/src/mail_order_bot/task_processor/handlers/excel_parcers/order_extractor.py index f68f710..f9349ea 100644 --- a/src/mail_order_bot/task_processor/handlers/excel_parcers/order_extractor.py +++ b/src/mail_order_bot/task_processor/handlers/excel_parcers/order_extractor.py @@ -2,7 +2,7 @@ import logging import pandas as pd from io import BytesIO from mail_order_bot.parsers.order_parcer import OrderParser -from mail_order_bot.task_processor.abstract_task import AbstractTask +from ...abstract_task import AbstractTask, pass_if_error, handle_errors from mail_order_bot.parsers.excel_parcer import ExcelFileParcer @@ -10,6 +10,7 @@ logger = logging.getLogger(__name__) class OrderExtractor(AbstractTask): + STEP = "Извлечение заказа" """ Хендлер для каждого вложения считывает эксель файл и сохраняет его контекст """ @@ -17,28 +18,24 @@ class OrderExtractor(AbstractTask): super().__init__(*args, **kwargs) self.excel_config = self.config.get("excel", {}) - + @pass_if_error + @handle_errors def do(self, attachment) -> None: # todo сделать проверку на наличие файла и его тип + delivery_period = attachment.get("delivery_period", 0) + mapping = self.excel_config.get("mapping") - try: - delivery_period = attachment.get("delivery_period", 0) - mapping = self.excel_config.get("mapping") + excel_file = attachment.get("excel") + client_id = self.config.get("client_id") - excel_file = attachment.get("excel") - client_id = self.config.get("client_id") + order_parcer = OrderParser(mapping, delivery_period, client_id) - order_parcer = OrderParser(mapping, delivery_period, client_id) + order_dataframe = excel_file.get_order_rows() + order = order_parcer.parse(order_dataframe) - order_dataframe = excel_file.get_order_rows() - order = order_parcer.parse(order_dataframe) + attachment["order"] = order - except Exception as e: - logger.error(f"Ошибка при парсинге заказа файла: \n{e}") - - else: - attachment["order"] = order - logger.warning(f"Обработан файл с заказом, извлечено позиций, {len(order.positions)}") + logger.warning(f"Файл заказа обработан успешно, извлечено {len(order.positions)} позиций") diff --git a/src/mail_order_bot/task_processor/handlers/excel_parcers/update_excel_file.py b/src/mail_order_bot/task_processor/handlers/excel_parcers/update_excel_file.py index 1efbad1..bd859c7 100644 --- a/src/mail_order_bot/task_processor/handlers/excel_parcers/update_excel_file.py +++ b/src/mail_order_bot/task_processor/handlers/excel_parcers/update_excel_file.py @@ -1,12 +1,5 @@ import logging -import pandas as pd -from io import BytesIO -# from mail_order_bot.task_processor.handlers.order_position import OrderPosition -from mail_order_bot.task_processor.abstract_task import AbstractTask - - -from mail_order_bot.order.auto_part_position import PositionStatus -from mail_order_bot.parsers.excel_parcer import ExcelFileParcer +from ...abstract_task import AbstractTask, pass_if_error, handle_errors logger = logging.getLogger(__name__) @@ -20,35 +13,31 @@ class UpdateExcelFile(AbstractTask): super().__init__(*args, **kwargs) self.excel_config = self.config.get("excel", {}) + @pass_if_error + @handle_errors def do(self, attachment) -> None: # todo сделать проверку на наличие файла и его тип + excel_file = attachment.get("excel") + order = attachment.get("order") + config = self.context.data.get("config", {}) + excel_config = config.get("excel", {}) + updatable_fields = excel_config.get("updatable_fields", {}) - try: - excel_file = attachment.get("excel") - order = attachment.get("order") - config = self.context.data.get("config", {}) - excel_config = config.get("excel", {}) - updatable_fields = excel_config.get("updatable_fields", {}) + for position in order.positions: - for position in order.positions: + sku = position.sku + manufacturer = position.manufacturer - sku = position.sku - manufacturer = position.manufacturer + for key, value in updatable_fields.items(): - for key, value in updatable_fields.items(): + if key == "ordered_quantity": + column = value + value = position.order_quantity + excel_file.set_value(sku, manufacturer, column, value) - if key == "ordered_quantity": - column = value - value = position.order_quantity - excel_file.set_value(sku, manufacturer, column, value) - - if key == "ordered_price": - column = value - value = position.order_price - excel_file.set_value(sku, manufacturer, column, value) - except Exception as e: - logger.error(f"Ошибка при правке excel файла: \n{e}") - - else: - logger.warning(f"Файл excel успешно обновлен") + if key == "ordered_price": + column = value + value = position.order_price + excel_file.set_value(sku, manufacturer, column, value) + logger.warning(f"Файла {attachment.get('name', 'неизвестный файл')} отредактирован") diff --git a/src/mail_order_bot/task_processor/handlers/stock_selectors/stock_selector.py b/src/mail_order_bot/task_processor/handlers/stock_selectors/stock_selector.py index a9fe4d7..3455928 100644 --- a/src/mail_order_bot/task_processor/handlers/stock_selectors/stock_selector.py +++ b/src/mail_order_bot/task_processor/handlers/stock_selectors/stock_selector.py @@ -10,7 +10,7 @@ from mail_order_bot.order.auto_part_position import AutoPartPosition, PositionSt from mail_order_bot.parsers.excel_parcer import ExcelFileParcer from decimal import Decimal -from mail_order_bot.task_processor.abstract_task import AbstractTask +from ...abstract_task import AbstractTask, pass_if_error, handle_errors from mail_order_bot.abcp_api.abcp_provider import AbcpProvider from mail_order_bot.credential_provider import CredentialProvider from mail_order_bot.order.auto_part_order import OrderStatus @@ -35,39 +35,37 @@ class StockSelector(AbstractTask): client_login, client_password = credential_provider.get_system_credentials() self.client_provider = AbcpProvider(login=client_login, password=client_password) + @pass_if_error + @handle_errors def do(self, attachment) -> None: # todo сделать проверку на наличие файла и его тип + order = attachment.get("order", None) + delivery_period = attachment.get("delivery_period") + for position in order.positions: - try: - order = attachment.get("order", None) - delivery_period = attachment.get("delivery_period") - for position in order.positions: + #1. Получаем остатки со складов + stock_data = self.client_provider.get_stock(position.sku, position.manufacturer) - #1. Получаем остатки со складов - stock_data = self.client_provider.get_stock(position.sku, position.manufacturer) - - #2. Из данных остатков выбираем оптимальное значение по стратегии - if stock_data["success"]: - stock_list = stock_data.get("data", []) - asking_price = position.asking_price - asking_quantity = position.asking_quantity + #2. Из данных остатков выбираем оптимальное значение по стратегии + if stock_data["success"]: + stock_list = stock_data.get("data", []) + asking_price = position.asking_price + asking_quantity = position.asking_quantity - optimal_stock_positions = self.get_optimal_stock(stock_list, asking_price, asking_quantity, delivery_period) + optimal_stock_positions = self.get_optimal_stock(stock_list, asking_price, asking_quantity, delivery_period) - # 3. Устанавливаем выбранное значение в позицию - if len(optimal_stock_positions): - position.set_order_item(optimal_stock_positions[0]) - else: - position.status = PositionStatus.NO_AVAILABLE_STOCK - # Мне не очень нравится управление статусами в этом месте, кажется что лучше это делать внутри AutoPartPosition + # 3. Устанавливаем выбранное значение в позицию + if len(optimal_stock_positions): + position.set_order_item(optimal_stock_positions[0]) else: - position.status = PositionStatus.STOCK_FAILED - except Exception as e: - logger.error(f"Ошибка при выборе позиции со складов: {e}") + position.status = PositionStatus.NO_AVAILABLE_STOCK + # Мне не очень нравится управление статусами в этом месте, кажется что лучше это делать внутри AutoPartPosition + else: + position.status = PositionStatus.STOCK_FAILED + + logger.warning("Определены оптимальные позиции со складов") - else: - logger.warning("Определены оптимальные позиции со складов") diff --git a/src/mail_order_bot/task_processor/processor.py b/src/mail_order_bot/task_processor/processor.py index b466ffb..9a4e697 100644 --- a/src/mail_order_bot/task_processor/processor.py +++ b/src/mail_order_bot/task_processor/processor.py @@ -90,7 +90,8 @@ class TaskProcessor: file_name = attachment["name"] logger.warning(f"Начата обработка файла: {file_name} =>") - attachment["log_messages"] = LogMessageStorage(file_name) + #attachment["log_messages"] = LogMessageStorage(file_name) + attachment["status"] = "ok" # Запустить обработку пайплайна for handler_name in pipeline: