10 Commits

19 changed files with 274 additions and 235 deletions

3
.gitignore vendored
View File

@@ -1,5 +1,6 @@
venv .venv
.vscode .vscode
__pycache__ __pycache__
.env .env
.cursorignore .cursorignore
logs/

66
docker-compose.yml Normal file
View File

@@ -0,0 +1,66 @@
version: '3.8'
services:
mail_order_bot:
build:
context: .
dockerfile: Dockerfile
args:
GIT_REPO_URL: https://git.lesha.spb.ru/alex/mail_order_bot
GIT_BRANCH: master
container_name: mail_order_bot
restart: unless-stopped
# Монтирование .env файла для секретов
env_file:
- .env
# Дополнительные переменные окружения
environment:
- PYTHONUNBUFFERED=1
# Монтирование volumes (если нужно)
volumes:
- .//app/data # для хранения данных
- ./logs:/app/logs # для логов
# Если приложение использует сеть
# ports:
# - "8000:8000"
# Если нужны другие сервисы (БД, Redis и т.д.)
# depends_on:
# - postgres
# - redis
# Настройки логирования
logging:
driver: "json-file"
options:
max-size: "10m"
max-file: "3"
# Раскомментируйте, если нужны дополнительные сервисы
#
# postgres:
# image: postgres:15-alpine
# container_name: mail_order_bot_db
# restart: unless-stopped
# environment:
# POSTGRES_DB: mail_order_bot
# POSTGRES_USER: ${POSTGRES_USER}
# POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
# volumes:
# - postgres_/var/lib/postgresql/data
# ports:
# - "5432:5432"
#
# redis:
# image: redis:7-alpine
# container_name: mail_order_bot_redis
# restart: unless-stopped
# ports:
# - "6379:6379"
# volumes:
# postgres_

32
dockerfile Normal file
View File

@@ -0,0 +1,32 @@
# Используем официальный образ Python
FROM python:3.12-slim
# Устанавливаем git для клонирования репозитория
RUN apt-get update && apt-get install -y git && rm -rf /var/lib/apt/lists/*
# Создаем рабочую директорию
WORKDIR /app
# Клонируем репозиторий
ARG GIT_REPO_URL=https://git.lesha.spb.ru/alex/mail_order_bot
ARG GIT_BRANCH=master
RUN git clone --branch ${GIT_BRANCH} ${GIT_REPO_URL} .
# Устанавливаем зависимости из requirements.txt (если есть)
RUN if [ -f requirements.txt ]; then pip install --no-cache-dir -r requirements.txt; fi
# Устанавливаем пакет и его зависимости из pyproject.toml
RUN pip install --no-cache-dir -e .
# Альтернативный вариант для production (без editable mode):
# RUN pip install --no-cache-dir .
# Устанавливаем переменные окружения для Python
ENV PYTHONUNBUFFERED=1
ENV PYTHONDONTWRITEBYTECODE=1
# Команда запуска приложения (замените на вашу)
# CMD ["python", "-m", "mail_order_bot"]
WORKDIR /app/src/mail_order_bot
CMD ["python", "/app/src/mail_order_bot/main.py"]

View File

@@ -5,7 +5,7 @@ build-backend = "setuptools.build_meta"
[project] [project]
name = "MailOrderBot" name = "MailOrderBot"
description = "Config manager for building applications" description = "Config manager for building applications"
version = "1.0.2" version = "1.0.4"
authors = [ authors = [
{ name = "Aleksei Zosimov", email = "lesha.spb@gmail.com" } { name = "Aleksei Zosimov", email = "lesha.spb@gmail.com" }
] ]
@@ -13,14 +13,20 @@ readme = "README.md"
requires-python = ">=3.12" requires-python = ">=3.12"
dependencies = [ dependencies = [
"python-dotenv>=1.0.0" "python-dotenv>=1.0.0",
"config_manager @ git+https://git.lesha.spb.ru/alex/config_manager.git@master"
] ]
dynamic = ["version"]
[tool.setuptools.packages.find] [tool.setuptools.packages.find]
where = ["src"] where = ["src"]
[project.urls] [project.urls]
Homepage = "https://git.lesha.spb.ru/alex/config_manager" Homepage = "https://git.lesha.spb.ru/alex/mail_order_bot"
Documentation = "https://git.lesha.spb.ru/alex/config_manager" Documentation = "https://git.lesha.spb.ru/alex/mail_order_bot"
Repository = "https://git.lesha.spb.ru/alex/config_manager" Repository = "https://git.lesha.spb.ru/alex/mail_order_bot"
[tool.pytest.ini_options]
addopts = [
"--import-mode=importlib",
]

View File

@@ -1,14 +0,0 @@
[build-system]
requires = ["setuptools>=75.3.0"]
build-backend = "setuptools.build_meta"
[project]
name = "MailOrderBot"
requires-python = ">=3.12"
dependencies = [
"python-dotenv>=1.0.0"
]
dynamic = ["version"]
[tool.setuptools.packages.find]
where = ["src"]

View File

@@ -0,0 +1,11 @@
Metadata-Version: 2.4
Name: MailOrderBot
Version: 1.0.2
Summary: Config manager for building applications
Author-email: Aleksei Zosimov <lesha.spb@gmail.com>
Project-URL: Homepage, https://git.lesha.spb.ru/alex/config_manager
Project-URL: Documentation, https://git.lesha.spb.ru/alex/config_manager
Project-URL: Repository, https://git.lesha.spb.ru/alex/config_manager
Requires-Python: >=3.12
Description-Content-Type: text/markdown
Requires-Dist: python-dotenv>=1.0.0

View File

@@ -0,0 +1,17 @@
README.md
pyproject.toml
src/MailOrderBot.egg-info/PKG-INFO
src/MailOrderBot.egg-info/SOURCES.txt
src/MailOrderBot.egg-info/dependency_links.txt
src/MailOrderBot.egg-info/requires.txt
src/MailOrderBot.egg-info/top_level.txt
src/mail_order_bot/__init__.py
src/mail_order_bot/main.py
src/mail_order_bot/email_client/__init__.py
src/mail_order_bot/email_client/email_client.py
src/mail_order_bot/email_client/email_objects.py
src/mail_order_bot/excel_processor/configurable_parser.py
src/mail_order_bot/excel_processor/excel_parser.py
src/mail_order_bot/excel_processor/excel_processor.py
src/mail_order_bot/excel_processor/order_position.py
src/mail_order_bot/excel_processor/parser_factory.py

View File

@@ -0,0 +1 @@

View File

@@ -0,0 +1 @@
python-dotenv>=1.0.0

View File

@@ -0,0 +1 @@
mail_order_bot

View File

@@ -1,100 +0,0 @@
# === Раздел с общими конфигурационными параметрами ===
runtime:
symbols: ["BTC_USDT", "ETH_USDT", "USDD_USDT", "TRX_USDT", "BTT_USDT", "NFT_USDT", "XRP_USDT",
"ETH_BTC", "XRP_BTC", "TRX_BTC", "LTC_BTC", "EOS_BTC", "XMR_BTC", "DOGE_BTC",
"NFT_TRX", "ETH_TRX", "JST_TRX", "XRP_TRX",
"ETHBULL_USDT", "BULL_USDT", "BEAR_USDT", "ADABULL_USDT"]
updateTimeout: 45
errorTimeout: 10
orderbook:
levels: [ 0.0, 0.2, 0.4, 0.6, 0.8,
1.0, 1.2, 1.4, 1.6, 1.8,
2.0, 2.2, 2.4, 2.6, 2.8,
3.0, 3.3, 3.6, 3.9,
4.2, 4.5, 4.8,
5.1, 5.4, 5.7, 100 ]
trades:
depth: 300
# === Database params ===
db:
#host: 185.117.118.107
host: 92.53.127.143
port: 59000
database: rt5_dev
# === Логирование ===
log:
version: 1
disable_existing_loggers: False
formatters:
standard:
format: '%(asctime)s %(name)30s [%(levelname)8s]: %(message)s'
telegram:
format: '%(message)s'
handlers:
console:
level: DEBUG
formatter: standard
class: logging.StreamHandler
stream: ext://sys.stdout # Default is stderr
file:
level: DEBUG
formatter: standard
class: logging.handlers.RotatingFileHandler
filename: logs/log.log
mode: a
maxBytes: 500000
backupCount: 15
telegram:
level: CRITICAL
formatter: telegram
class: logging_telegram_handler.TelegramHandler
chat_id: 211945135
alias: "PDC"
# -- Логгеры --
loggers:
'':
handlers: [console, file]
level: ERROR
propagate: False
__main__:
handlers: [console, file, telegram]
level: WARNING
propagate: False
basic_application:
handlers: [console, file, telegram]
level: INFO
config_manager:
level: INFO
log_manager:
level: INFO
poloniex.public:
level: ERROR
controllers.abstract:
level: ERROR
controllers.trades:
level: ERROR
controllers.orderbook:
level: ERROR
clickhouse_connector.clickhouse_connector:
level: ERROR

View File

@@ -1,8 +1,9 @@
# === Раздел с общими конфигурационными параметрами === # === Раздел с общими конфигурационными параметрами ===
runtime: 5
update_interval: 1 update_interval: 10
work_interval: 3 work_interval: 30
email_dir: "spareparts"
# === Логирование === # === Логирование ===
log: log:
@@ -11,7 +12,7 @@ log:
formatters: formatters:
standard: standard:
format: '%(asctime)s %(name)30s [%(levelname)8s]: %(message)s' format: '%(asctime)s %(module)15s [%(levelname)8s]: %(message)s'
telegram: telegram:
format: '%(message)s' format: '%(message)s'
@@ -29,28 +30,29 @@ log:
filename: logs/log.log filename: logs/log.log
mode: a mode: a
maxBytes: 500000 maxBytes: 500000
backupCount: 15 backupCount: 10
#telegram: telegram:
# level: CRITICAL level: CRITICAL
# formatter: telegram formatter: telegram
# class: logging_telegram_handler.TelegramHandler class: logging_telegram_handler.TelegramHandler
# chat_id: 211945135 chat_id: 211945135
# alias: "PDC" alias: "Mail order bot"
# -- Логгеры -- # -- Логгеры --
loggers: loggers:
'': '':
handlers: [console, file] handlers: [console, file, telegram]
level: DEBUG level: INFO
propagate: False propagate: False
__main__: __main__:
handlers: [console, file] handlers: [console, file, telegram]
level: WARNING level: INFO
propagate: False propagate: False
config_manager: config_manager:
handlers: [console, file] handlers: [console, file]
level: DEBUG level: DEBUG

View File

@@ -1,19 +1,2 @@
from .email_client import EmailClient from .client import EmailClient
from .email_objects import EmailMessage, EmailAttachment from .objects import EmailMessage, EmailAttachment
__all__ = ['EmailClient', 'EmailMessage', 'EmailAttachment']
def test_email_client():
email_client = EmailClient(
imap_host='imap.yandex.ru',
smtp_host='smtp.yandex.ru',
email='zosimovaa@yandex.ru',
password='test'
)
assert email_client is not None
email_client.close()
pytest.main()
if __name__ == "__main__":
test_email_client()

View File

@@ -1,17 +1,17 @@
import imaplib
import smtplib
import re import re
from email.mime.text import MIMEText
from email.mime.multipart import MIMEMultipart
from email.mime.base import MIMEBase
from email import encoders
import email
from email.header import decode_header
from datetime import datetime from datetime import datetime
from typing import List, Optional from typing import List, Optional
from dataclasses import dataclass from dataclasses import dataclass
import email
from email import encoders
from email.mime.text import MIMEText
from email.mime.multipart import MIMEMultipart
from email.mime.base import MIMEBase
from email.header import decode_header
import imaplib
import smtplib
from .email_objects import EmailMessage, EmailAttachment from .objects import EmailMessage, EmailAttachment
class EmailClient: class EmailClient:
@@ -109,27 +109,16 @@ class EmailClient:
07.10.2025, 16:01, Имя (email@example.com): 07.10.2025, 16:01, Имя (email@example.com):
Кому: ... Кому: ...
""" """
# Ищем первую секцию пересылаемого сообщения (по структуре письма) # Ищем email внутри скобок после строки "Пересылаемое сообщение"
match = re.search( pattern = r"Пересылаемое сообщение.*?\((.*?)\)"
r"-{8,}\\s*Пересылаемое сообщение\\s*-{8,}.*?(\\d{2}\\.\\d{2}\\.\\d{4},\\s*\\d{2}:\\d{2},.*?)\\(([^\\)]+)\\):", match = re.search(pattern, body, re.DOTALL)
body, re.DOTALL)
emails = []
if match: if match:
emails.append(match.group(2)) # email из первой строки пересыла return match.group(1)
# Ищем все email в первой пересылаемой секции (например, в "Кому:") return None
forwarded_section = re.search(
r"^-{8,}.*?Пересылаемое сообщение.*?:$(.*?)(?:^[-=]{5,}|\\Z)",
body, re.MULTILINE | re.DOTALL)
if forwarded_section:
addresses = re.findall(r"\\b([\\w\\.-]+@[\\w\\.-]+)\\b", forwarded_section.group(1))
for addr in addresses:
if addr not in emails:
emails.append(addr)
return emails
def _extract_body(self, msg: email.message.Message) -> str: def _extract_body(self, msg: email.message.Message) -> str:
""" """
Извлечь текст письма. Извлечь текст письма из любого типа содержимого, кроме вложений.
Args: Args:
msg: Объект письма msg: Объект письма
@@ -141,25 +130,36 @@ class EmailClient:
if msg.is_multipart(): if msg.is_multipart():
for part in msg.walk(): for part in msg.walk():
content_type = part.get_content_type()
content_disposition = str(part.get("Content-Disposition", "")) content_disposition = str(part.get("Content-Disposition", ""))
# Пропускаем вложения
# Ищем текстовые части без вложений if "attachment" in content_disposition.lower():
if content_type == "text/plain" and "attachment" not in content_disposition: continue
try: try:
charset = part.get_content_charset() or 'utf-8' charset = part.get_content_charset() or 'utf-8'
body += part.get_payload(decode=True).decode(charset, errors='ignore') payload = part.get_payload(decode=True)
except: if payload:
pass body_piece = payload.decode(charset, errors='ignore')
body += body_piece
except Exception:
pass
else: else:
try: try:
charset = msg.get_content_charset() or 'utf-8' charset = msg.get_content_charset() or 'utf-8'
body = msg.get_payload(decode=True).decode(charset, errors='ignore') payload = msg.get_payload(decode=True)
except: if payload:
body = payload.decode(charset, errors='ignore')
except Exception:
pass pass
return body return body
def __extract_email(self, text: str) -> str:
match = re.search(r'<([^<>]+)>', text)
if match:
return match.group(1)
return None
def _extract_attachments(self, msg: email.message.Message) -> List[EmailAttachment]: def _extract_attachments(self, msg: email.message.Message) -> List[EmailAttachment]:
""" """
Извлечь вложения из письма. Извлечь вложения из письма.
@@ -239,6 +239,8 @@ class EmailClient:
from_addr = self._decode_header(msg.get("From", "")) from_addr = self._decode_header(msg.get("From", ""))
subject = self._decode_header(msg.get("Subject", "")) subject = self._decode_header(msg.get("Subject", ""))
from_email = self.__extract_email(from_addr)
# Получаем дату # Получаем дату
date_str = msg.get("Date", "") date_str = msg.get("Date", "")
try: try:
@@ -254,6 +256,7 @@ class EmailClient:
# Извлекаем тело письма # Извлекаем тело письма
body = self._extract_body(msg) body = self._extract_body(msg)
#print(body)
first_sender = self._extract_first_sender(body) first_sender = self._extract_first_sender(body)
# Извлекаем вложения # Извлекаем вложения
@@ -262,6 +265,7 @@ class EmailClient:
# Создаем объект письма # Создаем объект письма
email_obj = EmailMessage( email_obj = EmailMessage(
from_addr=from_addr, from_addr=from_addr,
from_email=from_email,
subj=subject, subj=subject,
dt=dt, dt=dt,
body=body, body=body,

View File

@@ -14,6 +14,7 @@ class EmailAttachment:
class EmailMessage: class EmailMessage:
"""Класс для представления электронного письма""" """Класс для представления электронного письма"""
from_addr: str from_addr: str
from_email: str
subj: str subj: str
dt: datetime dt: datetime
body: str body: str

View File

@@ -0,0 +1,57 @@
from config_manager import ConfigManager
from dotenv import load_dotenv
import asyncio
import logging
import os
from dotenv import load_dotenv
from email_client import EmailClient
logger = logging.getLogger()
class MailOrderBot(ConfigManager):
def __init__(self, *agrs, **kwargs):
super().__init__(*agrs, **kwargs)
self.email_client = EmailClient(
imap_host=os.getenv('IMAP_HOST'),
smtp_host=os.getenv('SMTP_HOST'),
email=os.getenv('EMAIL_USER'),
password=os.getenv('EMAIL_PASSWORD'),
imap_port=os.getenv('IMAP_PORT'),
smtp_port=os.getenv('SMTP_PORT')
)
def execute(self):
logger.debug(f"Check emails for new orders")
emails = self.email_client.get_emails(folder="spareparts", only_unseen=True, mark_as_read=True)
for email in emails:
logger.info(email.subj)
logger.info(email.from_addr)
logger.info(email.dt)
logger.info(email.body)
logger.info(email.first_sender)
logger.info('--------------------------------')
logger.critical("mail checked")
logger = logging.getLogger()
async def main():
app = MailOrderBot("config.yml")
await app.start()
#await asyncio.sleep(200)
#await app.stop()
if __name__ == "__main__":
if os.environ.get("APP_ENV") != "PRODUCTION":
logger.warning("Non production environment")
load_dotenv()
asyncio.run(main())

View File

@@ -1,31 +0,0 @@
from config_manager.config_manager import ConfigManager
import asyncio
import logging
import os
os.chdir(os.path.dirname(__file__))
logger = logging.getLogger()
class MailOrderBot(ConfigManager):
def __init__(self, *agrs, **kwargs):
super().__init__(*agrs, **kwargs)
def execute(self):
print("run")
async def main():
app = MailOrderBot("config.yaml") # Можно config.json или config.yaml
task = asyncio.create_task(app.start())
await asyncio.sleep(5)
app.stop()
await task
logger.info("Work finished.")
if __name__ == "__main__":
asyncio.run(main())

View File

@@ -1,13 +1,13 @@
import os import os
from dotenv import load_dotenv from dotenv import load_dotenv
import sys
sys.path.append('./src')
load_dotenv()
from mail_order_bot.email_client import EmailClient from mail_order_bot.email_client import EmailClient
if __name__ == "__main__": if __name__ == "__main__":
print(__name__)
# подгружаем переменные окружения
load_dotenv()
email_client = EmailClient( email_client = EmailClient(
imap_host=os.getenv('IMAP_HOST'), imap_host=os.getenv('IMAP_HOST'),
smtp_host=os.getenv('SMTP_HOST'), smtp_host=os.getenv('SMTP_HOST'),
@@ -16,12 +16,13 @@ if __name__ == "__main__":
imap_port=os.getenv('IMAP_PORT'), imap_port=os.getenv('IMAP_PORT'),
smtp_port=os.getenv('SMTP_PORT') smtp_port=os.getenv('SMTP_PORT')
) )
emails = email_client.get_emails(folder='spareparts', only_unseen=True, mark_as_read=True) emails = email_client.get_emails(folder='spareparts', only_unseen=True, mark_as_read=False)
for email in emails: for email in emails:
print(email.subj) print(email.subj)
print(email.from_addr) print(email.from_addr)
print(email.from_email)
print(email.dt) print(email.dt)
print(email.body)
print(email.first_sender) print(email.first_sender)
print('--------------------------------') print('--------------------------------')