no message

This commit is contained in:
2026-01-10 19:53:17 +03:00
parent f96b0a076b
commit 7b5bba2a17
17 changed files with 286 additions and 153 deletions

View File

@@ -0,0 +1 @@
from .main import MailOrderBotException

View File

@@ -1,4 +1,6 @@
# Настройки обработки ================================================================= # Настройки обработки =================================================================
folder: "spareparts"
clients: clients:
lesha.spb@gmail.com: lesha.spb@gmail.com:
enabled: true enabled: true
@@ -6,8 +8,8 @@ clients:
pipeline: pipeline:
- ExcelExtractor - ExcelExtractor
- OrderExtractor
- DeliveryPeriodFromConfig - DeliveryPeriodFromConfig
- OrderExtractor
- StockSelector - StockSelector
- UpdateExcelFile - UpdateExcelFile
- SaveOrderToTelegram - SaveOrderToTelegram
@@ -39,7 +41,6 @@ clients:
update_interval: 1 update_interval: 1
work_interval: 60 work_interval: 60
email_dir: "spareparts" email_dir: "spareparts"
# Логирование ================================================================= # Логирование =================================================================
log: log:
version: 1 version: 1

View File

@@ -1,3 +1,27 @@
"""
Структура контекста
email : Электронное письмо в формате EmailObject
config : Конфиг для текущего клиента
attachments : [ Список вложений
{
name : Имя файла
isOrder : Признак что файл является валидным файлом заказа
bytes : содержимое файла в формате BytesIO
deliveryPeriod: Int
sheet : Распарсенный лист в формате ExcelFileParcer
order : Файл заказа в формате AutopartOrder
log : Лог сообщений по обработке файла в формате LogMessage
}
]
status:
"""
import threading import threading
from typing import Any, Dict from typing import Any, Dict
import logging import logging

View File

@@ -14,11 +14,17 @@ import smtplib
import logging import logging
from mail_order_bot import MailOrderBotException
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
# from .objects import EmailMessage, EmailAttachment # from .objects import EmailMessage, EmailAttachment
class EmailClientException(MailOrderBotException):
pass
class EmailClient: class EmailClient:
def __init__(self, imap_host: str, smtp_host: str, email: str, password: str, def __init__(self, imap_host: str, smtp_host: str, email: str, password: str,
@@ -32,7 +38,7 @@ class EmailClient:
self.imap_conn = None self.imap_conn = None
def connect(self): def connect(self):
"""Установkение IMAP соединения""" """Установление IMAP соединения"""
if self.imap_conn is None: if self.imap_conn is None:
self.imap_conn = imaplib.IMAP4_SSL(self.imap_host, self.imap_port) self.imap_conn = imaplib.IMAP4_SSL(self.imap_host, self.imap_port)
self.imap_conn.login(self.email, self.password) self.imap_conn.login(self.email, self.password)
@@ -58,6 +64,7 @@ class EmailClient:
def get_emails_id(self, folder: str = "INBOX", only_unseen: bool = True) -> List[int]: def get_emails_id(self, folder: str = "INBOX", only_unseen: bool = True) -> List[int]:
"""Получить список новых электронных писем.""" """Получить список новых электронных писем."""
try:
self.connect() self.connect()
self.imap_conn.select(folder, readonly=False) self.imap_conn.select(folder, readonly=False)
# Ищем письма # Ищем письма
@@ -69,8 +76,16 @@ class EmailClient:
return [] return []
email_ids = messages[0].split() email_ids = messages[0].split()
except Exception as e:
logger.error(e)
raise
else:
return email_ids return email_ids
def get_email(self, email_id, mark_as_read: bool = True): def get_email(self, email_id, mark_as_read: bool = True):
"""Получить список новых электронных писем.""" """Получить список новых электронных писем."""
self.connect() self.connect()

View File

@@ -7,6 +7,7 @@ import os
from dotenv import load_dotenv from dotenv import load_dotenv
from email_client import EmailClient from email_client import EmailClient
from mail_order_bot.task_processor.abstract_task import AbstractTask
from task_processor import TaskProcessor from task_processor import TaskProcessor
from mail_order_bot.context import Context from mail_order_bot.context import Context
@@ -14,6 +15,8 @@ from mail_order_bot.context import Context
logger = logging.getLogger() logger = logging.getLogger()
class MailOrderBotException(Exception):
pass
class MailOrderBot(ConfigManager): class MailOrderBot(ConfigManager):
def __init__(self, *agrs, **kwargs): def __init__(self, *agrs, **kwargs):
@@ -33,35 +36,45 @@ class MailOrderBot(ConfigManager):
self.context = Context() self.context = Context()
self.context.email_client = self.email_client self.context.email_client = self.email_client
# Обработчик писем # Обработчик писем
#self.email_processor = TaskProcessor("./configs") #self.email_processor = TaskProcessor("./configs")
config = self.config.get("clients") config = self.config.get("clients")
self.email_processor = TaskProcessor(config) self.email_processor = TaskProcessor(config)
logger.warning("MailOrderBot инициализирован") logger.warning("MailOrderBot инициализирован")
def execute(self): def execute(self):
# Получить список айдишников письма # Получить список айдишников письма
logger.critical("Запуск приложения critical !!!!!!!!") folder = self.config.get("folder")
unread_email_ids = self.email_client.get_emails_id(folder="spareparts") try:
unread_email_ids = self.email_client.get_emails_id(folder=folder)
logger.info(f"Новых писем - {len(unread_email_ids)}") logger.info(f"Новых писем - {len(unread_email_ids)}")
# Обработать каждое письмо по идентификатору # Обработать каждое письмо по идентификатору
for email_id in unread_email_ids: for email_id in unread_email_ids:
logger.debug(f"==================================================") try:
logger.debug(f"Обработка письма с идентификатором {email_id}") logger.info(f"Обработка письма с идентификатором {email_id}")
# Получить письмо по идентификатору и запустить его обработку # Получить письмо по идентификатору и запустить его обработку
email = self.email_client.get_email(email_id, mark_as_read=False) email = self.email_client.get_email(email_id, mark_as_read=False)
self.email_processor.process_email(email) self.email_processor.process_email(email)
except MailOrderBotException as e:
logger.error(f"Произошла ошибка {e}")
except MailOrderBotException as e:
logger.error(f"Произошла ошибка {e}")
except Exception as e:
logger.error(f"Произошла непредвиденная ошибка {e}")
logger = logging.getLogger() logger = logging.getLogger()
async def main(): async def main():
logger.critical("Запуск приложения")
app = MailOrderBot("config.yml") app = MailOrderBot("config.yml")
await app.start() await app.start()

View File

@@ -1 +1,2 @@
from .processor import TaskProcessor from .processor import TaskProcessor
from .message import LogMessage, LogMessageLevel, LogMessageStorage

View File

@@ -23,3 +23,6 @@ class AbstractTask():
""" """
raise NotImplementedError raise NotImplementedError
def get_name(self) -> str:
pass

View File

@@ -2,8 +2,8 @@
from .attachment_handler.attachment_handler import AttachmentHandler from .attachment_handler.attachment_handler import AttachmentHandler
from .abcp.api_get_stock import APIGetStock from .abcp.api_get_stock import APIGetStock
from .destination_time.local_store import DeliveryPeriodLocalStore from .delivery_time.local_store import DeliveryPeriodLocalStore
from .destination_time.from_config import DeliveryPeriodFromConfig from .delivery_time.from_config import DeliveryPeriodFromConfig
from .notifications.test_notifier import TestNotifier from .notifications.test_notifier import TestNotifier
from .excel_parcers.excel_extractor import ExcelExtractor from .excel_parcers.excel_extractor import ExcelExtractor
from .excel_parcers.order_extractor import OrderExtractor from .excel_parcers.order_extractor import OrderExtractor

View File

@@ -1,29 +0,0 @@
"""
Извлекает вложения из имейла и складывает их в контекст
Использует EmailUtils
"""
import logging
from mail_order_bot.task_processor.abstract_task import AbstractTask
from mail_order_bot.email_client.utils import EmailUtils
logger = logging.getLogger(__name__)
class AttachmentHandler(AbstractTask):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
def do(self) -> None:
try:
email = self.context.data["email"]
attachments = EmailUtils.extract_attachments(email)
except Exception as e:
logger.error(e)
self.context.data["error"] = str(e)
else:
self.context.data["attachments"] = attachments
logger.info(f"Извлечено вложений: {len(attachments)} ")

View File

@@ -18,8 +18,7 @@ class DeliveryPeriodFromConfig(AbstractTask):
for attachment in attachments: for attachment in attachments:
delivery_period = self.config.get("delivery_period", 0) delivery_period = self.config.get("delivery_period", 0)
attachment["delivery_period"] = delivery_period attachment["delivery_period"] = delivery_period
logger.info(f"Срок доставки для файла {attachment["name"]} установлен из конфига - {delivery_period}")
logger.info(f"Доставка только с локального склада, срок 1 день.")

View File

@@ -26,6 +26,8 @@ class DeliveryPeriodFromSubject(AbstractTask):
- Срок переводится в часы (умножается на 24) - Срок переводится в часы (умножается на 24)
""" """
# Получаем тему письма # Получаем тему письма
try:
email_subj = self.context.data.get("email_subj", "") email_subj = self.context.data.get("email_subj", "")
if not email_subj: if not email_subj:
logger.warning("Тема письма не найдена в контексте") logger.warning("Тема письма не найдена в контексте")
@@ -43,8 +45,11 @@ class DeliveryPeriodFromSubject(AbstractTask):
attachments = self.context.data.get("attachments", []) attachments = self.context.data.get("attachments", [])
for attachment in attachments: for attachment in attachments:
attachment["delivery_time"] = delivery_time attachment["delivery_time"] = delivery_time
logger.debug(f"Срок доставки для файла {attachment["name"]} установлен как {delivery_time}")
except Exception as e:
logger.error(e)
logger.debug(f"Срок доставки сохранен в {len(attachments)} вложений")
def _parse_delivery_period(self, subject: str) -> int: def _parse_delivery_period(self, subject: str) -> int:
""" """

View File

@@ -17,8 +17,4 @@ class DeliveryPeriodLocalStore(AbstractTask):
attachments = self.context.data["attachments"] attachments = self.context.data["attachments"]
for attachment in attachments: for attachment in attachments:
attachment["delivery_period"] = 0 attachment["delivery_period"] = 0
logger.info(f"Доставка только с локального склада, срок 1 день.") logger.info(f"Срок доставки для файла {attachment["name"]} - только из наличия")

View File

@@ -6,15 +6,23 @@ import logging
from mail_order_bot.task_processor.abstract_task import AbstractTask from mail_order_bot.task_processor.abstract_task import AbstractTask
from mail_order_bot.email_client.utils import EmailUtils from mail_order_bot.email_client.utils import EmailUtils
from mail_order_bot import MailOrderBotException
from mail_order_bot.task_processor import LogMessage, LogMessageLevel
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
class EmailParcerException(MailOrderBotException):
pass
class EmailParcer(AbstractTask): class EmailParcer(AbstractTask):
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs) super().__init__(*args, **kwargs)
def do(self) -> None: def do(self) -> None:
# Определить клиента # Определить клиента
try:
email = self.context.data.get("email", None) email = self.context.data.get("email", None)
if email is not None: if email is not None:
email_body = EmailUtils.extract_body(email) email_body = EmailUtils.extract_body(email)
@@ -36,9 +44,24 @@ class EmailParcer(AbstractTask):
self.context.data["client"] = client self.context.data["client"] = client
attachments = EmailUtils.extract_attachments(email) attachments = EmailUtils.extract_attachments(email)
self.context.data["attachments"] = attachments self.context.data["attachments"] = attachments
logger.info(f"Извлечено вложений: {len(attachments)} ") logger.info(f"Извлечено вложений: {len(attachments)} ")
except Exception as e:
logger.error(e)
self.context.data["error"].add(
LogMessage(
handler="EmailParcer",
level=LogMessageLevel.ERROR,
message="Возникла ошибка при парсинге письма",
error_data=str(e)
)
)
#raise EmailParcerException(f"Ошибка при парсинге письма {e}") from e

View File

@@ -1,24 +1,29 @@
import logging
import smtplib
from email.mime.multipart import MIMEMultipart from email.mime.multipart import MIMEMultipart
from email.mime.text import MIMEText from email.mime.text import MIMEText
from email.mime.base import MIMEBase from email.mime.base import MIMEBase
from email.utils import formatdate from email.utils import formatdate
from email import encoders from email import encoders
from abc import ABC, abstractmethod
import os
from mail_order_bot import MailOrderBotException
from mail_order_bot.task_processor.abstract_task import AbstractTask from mail_order_bot.task_processor.abstract_task import AbstractTask
logger = logging.getLogger(__name__)
class EmailReplyTaskException(MailOrderBotException):
pass
class EmailReplyTask(AbstractTask): class EmailReplyTask(AbstractTask):
"""Формирует ответ на входящее письмо с запросом на заказ°""" """Формирует ответ на входящее письмо с запросом на заказ°"""
EMAIl = "zosimovaa@yandex.ru" #"noreply@zapchastiya.ru" EMAIl = "zosimovaa@yandex.ru" #"noreply@zapchastiya.ru"
def do(self): def do(self):
try:
email = self.context.data.get("email") email = self.context.data.get("email")
if not email: if not email:
raise ValueError("В контексте нет входящего сообщения") raise ValueError("В контексте нет входящего сообщения")
@@ -45,6 +50,8 @@ class EmailReplyTask(AbstractTask):
self._attach_file(reply_message, attachment) self._attach_file(reply_message, attachment)
self.context.email_client.send_email(reply_message) self.context.email_client.send_email(reply_message)
except Exception as e:
pass
def _attach_file(self, reply_message, attachment): def _attach_file(self, reply_message, attachment):
""" """

View File

@@ -1,6 +1,8 @@
import logging import logging
import pandas as pd import pandas as pd
from io import BytesIO 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.handlers.order_position import OrderPosition
from mail_order_bot.task_processor.abstract_task import AbstractTask from mail_order_bot.task_processor.abstract_task import AbstractTask
@@ -22,14 +24,20 @@ class ExcelExtractor(AbstractTask):
# todo сделать проверку на наличие файла и его тип # todo сделать проверку на наличие файла и его тип
attachments = self.context.data.get("attachments", []) attachments = self.context.data.get("attachments", [])
for attachment in attachments:
file_bytes = BytesIO(attachment['bytes'])
for attachment in attachments:
try:
file_bytes = BytesIO(attachment['bytes'])
excel_file = ExcelFileParcer(file_bytes, self.excel_config) excel_file = ExcelFileParcer(file_bytes, self.excel_config)
except Exception as e:
logger.warning(f"Не удалось прочитать файл {attachment['name']}: {e}")
attachment["excel"] = None
else:
attachment["excel"] = excel_file attachment["excel"] = excel_file

View File

@@ -0,0 +1,44 @@
from enum import Enum
class LogMessageLevel(Enum):
SUCCESS = "SUCCESS"
WARNING = "WARNING"
ERROR = "ERROR"
class LogMessage:
def __init__(self, handler=None, level=None, message=None, error_data=None):
self.handler = handler
self.level = level
self.message = message
self.error_data = error_data
def __str__(self):
return self.message
class LogMessageStorage:
def __init__(self, filename=None):
self.filename = filename
self.messages = []
def append(self, message):
self.messages.append(message)
def check_errors(self) -> bool:
fatal_statuses = [message.level == LogMessageLevel.ERROR for message in self.messages]
return bool(sum(fatal_statuses))
def get_messages_log(self) -> str:
response = ""
if self.filename is not None:
response += f" Лог обработки файла: {self.filename}"
for message in self.messages:
if len(response):
response += "\n"
response += f"{message.handler} [{message.level}]: {message.message}"
if message.error_data is not None:
response += f"\n{message.error_data}"

View File

@@ -1,3 +1,35 @@
"""
Общая логика обработки писем следующая
1. Общая часть
- скачиваем письмо
- складываем в контекст
- обработчик и парсим данные - тело, тема, отправитель
2. Запускаем паплайн
- прогоняем обработчик для каждого вложения
- каждый обработчик для вложения докидывает результат своей работы
- каждый обработчик анализирует общий лог на наличие фатальных ошибок. Если есть - пропускаем шаг.
Последний обработчик направляет лог ошибок на администратора
Ограничения:
- каждое вложение воспринимается как "отдельное письмо", т.е. если клиент в одном письме направит несколько вложений,
то они будут обрабатываться как отдельные письма, и на каждое будет дан ответ (если он требуется).
Исключительные ситуации:
- При невозможности создать заказ - пересылаем письмо на администратора с логом обработки вложения
- Вложения, которые не являются файлами заказа игнорируем.
todo
[ ] Нужен класс, который будет хранить сообщения от обработчиков
- метод для добавления сообщения
- метод для проверки фатальных ошибок
- метод для извлечения лога
"""
import os import os
import yaml import yaml
import logging import logging
@@ -9,9 +41,10 @@ from mail_order_bot.email_client.utils import EmailUtils
from enum import Enum from enum import Enum
from mail_order_bot.task_processor.handlers import * from mail_order_bot.task_processor.handlers import *
from mail_order_bot.task_processor.handlers import AttachmentHandler
from mail_order_bot.task_processor.handlers.email.email_parcer import EmailParcer from mail_order_bot.task_processor.handlers.email.email_parcer import EmailParcer
from mail_order_bot import MailOrderBotException
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@@ -38,34 +71,26 @@ class TaskProcessor:
self.context.clear() self.context.clear()
self.context.data["email"] = email self.context.data["email"] = email
try:
# Парсинг письма # Парсинг письма
email_parcer = EmailParcer() email_parcer = EmailParcer()
email_parcer.do() email_parcer.do()
email_from = self.context.data.get("email_from") email_sender = self.context.data.get("email_from")
#client = EmailUtils.extract_domain(email_from)
#self.context.data["client"] = client
try:
# Определить конфиг для пайплайна # Определить конфиг для пайплайна
config = self._load_config(email_from) config = self._load_config(email_sender)
self.context.data["config"] = config self.context.data["config"] = config
# Запустить обработку пайплайна # Запустить обработку пайплайна
pipeline = config["pipeline"] pipeline = config["pipeline"]
for stage in pipeline: for handler_name in pipeline:
handler_name = stage
logger.info(f"Processing handler: {handler_name}") logger.info(f"Processing handler: {handler_name}")
task = globals()[handler_name]() task = globals()[handler_name]()
task.do() task.do()
except FileNotFoundError: except Exception as e:
logger.error(f"Конфиг для клиента {email_from} не найден") logger.error(f"Произошла ошибка: {e}")
for attachment in self.context.data["attachments"]:
print(attachment["order"].__dict__)
#except Exception as e:
# logger.error(f"Произошла другая ошибка: {e}")
def _load_config(self, email_from) -> Dict[str, Any]: def _load_config(self, email_from) -> Dict[str, Any]:
@@ -73,11 +98,8 @@ class TaskProcessor:
return self.config[email_from] return self.config[email_from]
email_from_domain = EmailUtils.extract_domain(email_from) email_from_domain = EmailUtils.extract_domain(email_from)
if email_from_domain in self.config: if email_from_domain in self.config:
return self.config[email_from_domain] return self.config[email_from_domain]
raise FileNotFoundError raise FileNotFoundError
#path = os.path.join(self.configs_path, client + '.yml')
#with open(path, 'r', encoding='utf-8') as f:
# return yaml.safe_load(f)