diff --git a/api/categories.py b/api/categories.py new file mode 100644 index 0000000..f47d26b --- /dev/null +++ b/api/categories.py @@ -0,0 +1,7 @@ +from flask import Blueprint, request + +type = Blueprint("type", __name__) + +@product.route('/types', method = ['GET']) +def GetTypes(): + return diff --git a/api/product.py b/api/product.py new file mode 100644 index 0000000..39dd18b --- /dev/null +++ b/api/product.py @@ -0,0 +1,5 @@ +from flask import Blueprint, request + +product = Blueprint("product", __name__) + +@product.route('') diff --git a/app.py b/app.py index 7290268..e441e3b 100644 --- a/app.py +++ b/app.py @@ -1,10 +1,14 @@ from sys import exit from flask import Flask from db.InitializeDB import init_database -from db.InitializeDBData import add_products +from json import dumps +from db.InitializeDBData import init_tables +from db.handlers.catigory_handler import GetAllCategories -init_database() +init_tables() -add_products() +categories = GetAllCategories() + +print(dumps(categories)) app = Flask(__name__) diff --git a/db/DBDataUtils.py b/db/DBDataUtils.py index 86e0771..f36a27b 100644 --- a/db/DBDataUtils.py +++ b/db/DBDataUtils.py @@ -2,14 +2,14 @@ import re import sqlite3 from loguru import logger as log -def add_category(conn, name, description=None, parent_id=None, image_path=None, page_number=1): +def add_category(conn, name, description=None, parent_id=None, image_path=None): cursor = conn.cursor() try: cursor.execute(''' - INSERT OR IGNORE INTO categories (name, description, parent_id, image_path, page_number) - VALUES (?, ?, ?, ?, ?) - ''', (name, description, parent_id, image_path, page_number)) + INSERT OR IGNORE INTO categories (name, description, parent_id, image_path) + VALUES (?, ?, ?, ?) + ''', (name, description, parent_id, image_path)) cursor.execute('SELECT id FROM categories WHERE name = ?', (name,)) result = cursor.fetchone() @@ -59,8 +59,8 @@ def parse_product_data(product_text): 'stiffness_class': stiffness_class } -def add_product(conn, product_text, category_id, image_path=None, page_number=1): - """Добавление товара с парсингом параметров, изображением и номером страницы""" +def add_product(conn, product_text, category_id, image_path=None): + """Добавление товара с парсингом параметров и изображением""" cursor = conn.cursor() product_data = parse_product_data(product_text) @@ -70,8 +70,8 @@ def add_product(conn, product_text, category_id, image_path=None, page_number=1) INSERT INTO products ( name, full_name, description, category_id, diameter, wall_thickness, length, color, - application_type, stiffness_class, image_path, page_number - ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) + application_type, stiffness_class, image_path + ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) ''', ( product_data['short_name'], product_data['full_name'], @@ -83,8 +83,7 @@ def add_product(conn, product_text, category_id, image_path=None, page_number=1) product_data['color'], product_data['application_type'], product_data['stiffness_class'], - image_path, - page_number + image_path )) product_id = cursor.lastrowid @@ -122,10 +121,9 @@ def get_categories_by_page(conn, page_number=1, items_per_page=9): cursor.execute(''' SELECT * FROM categories - WHERE page_number = ? ORDER BY id LIMIT ? OFFSET ? - ''', (page_number, items_per_page, offset)) + ''', (items_per_page, offset)) return cursor.fetchall() @@ -137,17 +135,16 @@ def get_products_by_page(conn, page_number=1, items_per_page=9, category_id=None if category_id: cursor.execute(''' SELECT * FROM products - WHERE page_number = ? AND category_id = ? + WHERE category_id = ? ORDER BY id LIMIT ? OFFSET ? - ''', (page_number, category_id, items_per_page, offset)) + ''', (category_id, items_per_page, offset)) else: cursor.execute(''' SELECT * FROM products - WHERE page_number = ? ORDER BY id LIMIT ? OFFSET ? - ''', (page_number, items_per_page, offset)) + ''', (items_per_page, offset)) return cursor.fetchall() diff --git a/db/InitializeDB.py b/db/InitializeDB.py index c5b5cf6..5afd332 100644 --- a/db/InitializeDB.py +++ b/db/InitializeDB.py @@ -13,7 +13,6 @@ def init_database(db_name='pipe_store.db'): description TEXT, parent_id INTEGER, image_path TEXT, - page_number INTEGER DEFAULT 1, -- Новое поле: номер страницы для пагинации created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, FOREIGN KEY (parent_id) REFERENCES categories (id) ) @@ -33,7 +32,6 @@ def init_database(db_name='pipe_store.db'): application_type TEXT, -- внутренняя/наружная stiffness_class TEXT, -- SN4, SN2 и т.д. image_path TEXT, - page_number INTEGER DEFAULT 1, -- Новое поле: номер страницы для пагинации created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, FOREIGN KEY (category_id) REFERENCES categories (id) ) @@ -63,8 +61,6 @@ def init_database(db_name='pipe_store.db'): cursor.execute('CREATE INDEX IF NOT EXISTS idx_products_category ON products(category_id)') cursor.execute('CREATE INDEX IF NOT EXISTS idx_products_diameter ON products(diameter)') cursor.execute('CREATE INDEX IF NOT EXISTS idx_products_length ON products(length)') - cursor.execute('CREATE INDEX IF NOT EXISTS idx_products_page ON products(page_number)') - cursor.execute('CREATE INDEX IF NOT EXISTS idx_categories_page ON categories(page_number)') cursor.execute('CREATE INDEX IF NOT EXISTS idx_product_filters_key_value ON product_filters(filter_key, value)') conn.commit() diff --git a/db/InitializeDBData.py b/db/InitializeDBData.py index 1beeef0..fe93abe 100644 --- a/db/InitializeDBData.py +++ b/db/InitializeDBData.py @@ -19,44 +19,35 @@ def init_tables(db_name='pipe_store.db'): log.info("Начинаем инициализацию базы категорий...") - # Добавляем категории с распределением по страницам - # Страница 1 main_category_id = add_category( conn, "Трубы ПВХ", "Трубы ПВХ для канализационных систем", - image_path="/images/categories/pvc_pipes_main.jpg", - page_number=1 + image_path="/images/categories/pvc_pipes_main.jpg" ) - # Страница 1 internal_category_id = add_category( conn, "Для внутренней канализации", "Трубы для внутренней канализационной системы", main_category_id, - image_path="/images/categories/internal_pipes.jpg", - page_number=1 + image_path="/images/categories/internal_pipes.jpg" ) - # Страница 1 external_category_id = add_category( conn, "Для наружной канализации", "Трубы для наружной канализационной системы", main_category_id, - image_path="/images/categories/external_pipes.jpg", - page_number=1 + image_path="/images/categories/external_pipes.jpg" ) - # Дополнительные категории для демонстрации пагинации (страница 2) add_category( conn, "Фитинги для ПВХ труб", "Соединительные элементы для ПВХ труб", main_category_id, - image_path="/images/categories/fittings.jpg", - page_number=2 + image_path="/images/categories/fittings.jpg" ) add_category( @@ -64,8 +55,7 @@ def init_tables(db_name='pipe_store.db'): "Колена и отводы", "Угловые соединения для ПВХ труб", main_category_id, - image_path="/images/categories/elbows.jpg", - page_number=2 + image_path="/images/categories/elbows.jpg" ) log.info("Категории успешно созданы") @@ -79,43 +69,40 @@ def init_tables(db_name='pipe_store.db'): log.info("Фильтры успешно добавлены") - # Данные продуктов с распределением по страницам + # Данные продуктов products_data = [ - # Страница 1 - внутренние трубы - {"text": "Труба ПВХ для ВНУТРЕННЕЙ канализации (цвет серый), с внутренней структурной стенкой толщиной 2,2 мм 110х2,2х500", "image": "/images/products/internal_110_500.jpg", "page": 1}, - {"text": "Труба ПВХ для ВНУТРЕННЕЙ канализации (цвет серый), с внутренней структурной стенкой толщиной 2,2 мм 110х2,2х1000", "image": "/images/products/internal_110_1000.jpg", "page": 1}, - {"text": "Труба ПВХ для ВНУТРЕННЕЙ канализации (цвет серый), с внутренней структурной стенкой толщиной 2,2 мм 110х2,2х1500", "image": "/images/products/internal_110_1500.jpg", "page": 1}, - {"text": "Труба ПВХ для ВНУТРЕННЕЙ канализации (цвет серый), с внутренней структурной стенкой толщиной 2,2 мм 110х2,2х2000", "image": "/images/products/internal_110_2000.jpg", "page": 1}, - {"text": "Труба ПВХ для ВНУТРЕННЕЙ канализации (цвет серый), с внутренней структурной стенкой толщиной 2,2 мм 110х2,2х3000", "image": "/images/products/internal_110_3000.jpg", "page": 1}, + # Внутренние трубы + {"text": "Труба ПВХ для ВНУТРЕННЕЙ канализации (цвет серый), с внутренней структурной стенкой толщиной 2,2 мм 110х2,2х500", "image": "/images/products/internal_110_500.jpg"}, + {"text": "Труба ПВХ для ВНУТРЕННЕЙ канализации (цвет серый), с внутренней структурной стенкой толщиной 2,2 мм 110х2,2х1000", "image": "/images/products/internal_110_1000.jpg"}, + {"text": "Труба ПВХ для ВНУТРЕННЕЙ канализации (цвет серый), с внутренней структурной стенкой толщиной 2,2 мм 110х2,2х1500", "image": "/images/products/internal_110_1500.jpg"}, + {"text": "Труба ПВХ для ВНУТРЕННЕЙ канализации (цвет серый), с внутренней структурной стенкой толщиной 2,2 мм 110х2,2х2000", "image": "/images/products/internal_110_2000.jpg"}, + {"text": "Труба ПВХ для ВНУТРЕННЕЙ канализации (цвет серый), с внутренней структурной стенкой толщиной 2,2 мм 110х2,2х3000", "image": "/images/products/internal_110_3000.jpg"}, - # Страница 1 - наружные трубы - {"text": "Труба ПВХ для НАРУЖНОЙ канализации (цвет оранжево-коричневый), с внутренней структурной стенкой, класс жесткости SN4 110х3,2х500", "image": "/images/products/external_110_sn4_500.jpg", "page": 1}, - {"text": "Труба ПВХ для НАРУЖНОЙ канализации (цвет оранжево-коричневый), с внутренней структурной стенкой, класс жесткости SN4 110х3,2х1000", "image": "/images/products/external_110_sn4_1000.jpg", "page": 1}, - {"text": "Труба ПВХ для НАРУЖНОЙ канализации (цвет оранжево-коричневый), с внутренней структурной стенкой, класс жесткости SN4 110х3,2х2000", "image": "/images/products/external_110_sn4_2000.jpg", "page": 1}, - - # Страница 2 - оставшиеся наружные трубы - {"text": "Труба ПВХ для НАРУЖНОЙ канализации (цвет оранжево-коричневый), с внутренней структурной стенкой, класс жесткости SN4 110х3,2х3000", "image": "/images/products/external_110_sn4_3000.jpg", "page": 2}, - {"text": "Труба ПВХ для НАРУЖНОЙ канализации (цвет оранжево-коричневый), с внутренней структурной стенкой, класс жесткости SN4 110х3,2х4000", "image": "/images/products/external_110_sn4_4000.jpg", "page": 2}, - {"text": "Труба ПВХ для НАРУЖНОЙ канализации (цвет оранжево-коричневый), с внутренней структурной стенкой, класс жесткости SN2 160х3,6х1000", "image": "/images/products/external_160_sn2_1000.jpg", "page": 2}, - {"text": "Труба ПВХ для НАРУЖНОЙ канализации (цвет оранжево-коричневый), с внутренней структурной стенкой, класс жесткости SN2 160х3,6х2000", "image": "/images/products/external_160_sn2_2000.jpg", "page": 2}, - {"text": "Труба ПВХ для НАРУЖНОЙ канализации (цвет оранжево-коричневый), с внутренней структурной стенкой, класс жесткости SN2 160х3,6х3000", "image": "/images/products/external_160_sn2_3000.jpg", "page": 2}, + # Наружные трубы + {"text": "Труба ПВХ для НАРУЖНОЙ канализации (цвет оранжево-коричневый), с внутренней структурной стенкой, класс жесткости SN4 110х3,2х500", "image": "/images/products/external_110_sn4_500.jpg"}, + {"text": "Труба ПВХ для НАРУЖНОЙ канализации (цвет оранжево-коричневый), с внутренней структурной стенкой, класс жесткости SN4 110х3,2х1000", "image": "/images/products/external_110_sn4_1000.jpg"}, + {"text": "Труба ПВХ для НАРУЖНОЙ канализации (цвет оранжево-коричневый), с внутренней структурной стенкой, класс жесткости SN4 110х3,2х2000", "image": "/images/products/external_110_sn4_2000.jpg"}, + {"text": "Труба ПВХ для НАРУЖНОЙ канализации (цвет оранжево-коричневый), с внутренней структурной стенкой, класс жесткости SN4 110х3,2х3000", "image": "/images/products/external_110_sn4_3000.jpg"}, + {"text": "Труба ПВХ для НАРУЖНОЙ канализации (цвет оранжево-коричневый), с внутренней структурной стенкой, класс жесткости SN4 110х3,2х4000", "image": "/images/products/external_110_sn4_4000.jpg"}, + {"text": "Труба ПВХ для НАРУЖНОЙ канализации (цвет оранжево-коричневый), с внутренней структурной стенкой, класс жесткости SN2 160х3,6х1000", "image": "/images/products/external_160_sn2_1000.jpg"}, + {"text": "Труба ПВХ для НАРУЖНОЙ канализации (цвет оранжево-коричневый), с внутренней структурной стенкой, класс жесткости SN2 160х3,6х2000", "image": "/images/products/external_160_sn2_2000.jpg"}, + {"text": "Труба ПВХ для НАРУЖНОЙ канализации (цвет оранжево-коричневый), с внутренней структурной стенкой, класс жесткости SN2 160х3,6х3000", "image": "/images/products/external_160_sn2_3000.jpg"}, ] added_products = 0 for product_info in products_data: product_text = product_info["text"] image_path = product_info["image"] - page_number = product_info["page"] if "ВНУТРЕННЕЙ" in product_text: category_id = internal_category_id else: category_id = external_category_id - product_id = add_product(conn, product_text, category_id, image_path, page_number) + product_id = add_product(conn, product_text, category_id, image_path) if product_id: added_products += 1 - log.info(f"Добавлен товар: {product_text[:50]}... (страница {page_number})") + log.info(f"Добавлен товар: {product_text[:50]}...") else: log.error(f"Ошибка при добавлении товара: {product_text}") diff --git a/db/handlers/catigory_handler.py b/db/handlers/catigory_handler.py new file mode 100644 index 0000000..1adae80 --- /dev/null +++ b/db/handlers/catigory_handler.py @@ -0,0 +1,23 @@ +import sqlite3 +from module.category import Category + +def GetAllCategories(db_name: str = 'pipe_store.db', parent_id: int = 1) -> list[Category]: + conn = sqlite3.connect(db_name) + conn.row_factory = sqlite3.Row + cursor = conn.cursor() + + categories = [] + + cursor.execute("SELECT id, name, description, image_path FROM categories WHERE parent_id = ?", [parent_id]) + + for row in cursor.fetchall(): + category = Category( + id = row["id"], + name = row["name"], + description= row["description"], + image_path= row["image_path"], + ) + categories.append(category.model_dump()) + + conn.close() + return categories diff --git a/db/handlers/product_handler.py b/db/handlers/product_handler.py new file mode 100644 index 0000000..5209127 --- /dev/null +++ b/db/handlers/product_handler.py @@ -0,0 +1,11 @@ +import sqlite3 +from module.product import Product + +def get_products(db_name: str = 'pipe_store.db', parent_id: int = 1) -> List[Product]: + conn = sqlite3.connect(db_name) + conn.row_factory = sqlite3.Row + transaction = conn.cursor() + + products = [] + + transaction.execute("SELECT ... FROM products") diff --git a/module/category.py b/module/category.py index 7d34008..c9bc154 100644 --- a/module/category.py +++ b/module/category.py @@ -2,15 +2,11 @@ from pydantic import BaseModel from typing import Optional from datetime import datetime - class Category(BaseModel): id: int name: str description: Optional[str] = None - parent_id: Optional[int] = None image_path: Optional[str] = None - page_number: int = 1 - created_at: datetime class Config: from_attributes = True @@ -22,6 +18,7 @@ class CategoryCreate(BaseModel): parent_id: Optional[int] = None image_path: Optional[str] = None page_number: int = 1 + created_at: datetime = None class CategoryUpdate(BaseModel): @@ -30,3 +27,4 @@ class CategoryUpdate(BaseModel): parent_id: Optional[int] = None image_path: Optional[str] = None page_number: Optional[int] = None + created_at: datetime = None diff --git a/requirements-dev.txt b/requirements-dev.txt new file mode 100644 index 0000000..918b4da --- /dev/null +++ b/requirements-dev.txt @@ -0,0 +1,5 @@ +python-lsp-server[all] +pyright +ruff-lsp +ruff + diff --git a/utils/loadDotENV.py b/utils/loadDotENV.py new file mode 100644 index 0000000..0d6c510 --- /dev/null +++ b/utils/loadDotENV.py @@ -0,0 +1,14 @@ +import os +from dotenv import load_dotenv +from loguru import logger as log + +DOTENV_PATH = '.env' + +def initializeENV() -> bool: + if os.path.exists(DOTENV_PATH): + load_dotenv(DOTENV_PATH) + log.info('.env is loaded') + return True + else: + log.error('.env isn`t loaded') + return False diff --git a/utils/mail.py b/utils/mail.py new file mode 100644 index 0000000..0d4d822 --- /dev/null +++ b/utils/mail.py @@ -0,0 +1,127 @@ +import os +import smtplib +import logging +from email.mime.multipart import MIMEMultipart +from email.mime.text import MIMEText +from email.mime.application import MIMEApplication +from pathlib import Path + +# Настройка логирования +logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s') +logger = logging.getLogger(__name__) + +def send_pdf_to_email(recipient_email, pdf_filename=None, subject="PDF документ", body_text=None): + + # Настройки Яндекс.Почты + SMTP_SERVER = "smtp.yandex.ru" + SMTP_PORT = 587 + SENDER_EMAIL = "w0ron.black@yandex.ru" + + # Получаем пароль из переменной окружения + SENDER_PASSWORD = os.environ.get('EMAIL_PASS') + + if not SENDER_PASSWORD: + logger.error("Не найдена переменная окружения EMAIL_PASS") + return False + + # Путь к папке с PDF файлами + data_folder = Path("./data") + + if not data_folder.exists(): + logger.error(f"Папка {data_folder.absolute()} не существует") + return False + + # Определяем файл для отправки + if pdf_filename: + pdf_path = data_folder / pdf_filename + if not pdf_path.exists(): + logger.error(f"Файл {pdf_filename} не найден в папке {data_folder}") + return False + else: + # Ищем первый PDF файл в папке + pdf_files = list(data_folder.glob("*.pdf")) + if not pdf_files: + logger.error("В папке нет PDF файлов") + return False + pdf_path = pdf_files[0] + logger.info(f"Найден файл: {pdf_path.name}") + + # Проверяем размер файла (Яндекс ограничивает вложения до 30 МБ) + file_size_mb = pdf_path.stat().st_size / (1024 * 1024) + if file_size_mb > 30: + logger.error(f"Файл слишком большой: {file_size_mb:.2f} МБ (максимум 30 МБ)") + return False + + # Текст письма по умолчанию + if body_text is None: + body_text = f""" + Здравствуйте! + + Во вложении находится PDF документ: {pdf_path.name} + + С уважением, + Отправитель + """ + + # Создаем сообщение + msg = MIMEMultipart() + msg['From'] = SENDER_EMAIL + msg['To'] = recipient_email + msg['Subject'] = subject + msg['Reply-To'] = SENDER_EMAIL + + # Добавляем текст письма с правильной кодировкой + msg.attach(MIMEText(body_text, 'plain', 'utf-8')) + + try: + # Добавляем PDF файл + with open(pdf_path, 'rb') as file: + pdf_attachment = MIMEApplication(file.read(), _subtype='pdf') + pdf_attachment.add_header( + 'Content-Disposition', + 'attachment', + filename=('utf-8', '', pdf_path.name) # Поддержка русских имен файлов + ) + msg.attach(pdf_attachment) + + # Подключаемся к серверу Яндекс.Почты + logger.info("Подключение к серверу Яндекс.Почты...") + server = smtplib.SMTP(SMTP_SERVER, SMTP_PORT) + server.starttls() + server.login(SENDER_EMAIL, SENDER_PASSWORD) + + # Отправляем письмо + server.send_message(msg) + server.quit() + + logger.info(f"✅ Письмо успешно отправлено на {recipient_email}") + logger.info(f"📎 Файл: {pdf_path.name} ({file_size_mb:.2f} МБ)") + return True + + except smtplib.SMTPAuthenticationError: + logger.error("❌ Ошибка аутентификации. Проверьте пароль в переменной EMAIL_PASS") + return False + except smtplib.SMTPException as e: + logger.error(f"❌ Ошибка SMTP: {e}") + return False + except Exception as e: + logger.error(f"❌ Неожиданная ошибка: {e}") + return False + +def send_pdf_to_multiple(recipient_emails, pdf_filename=None, subject="PDF документ", body_text=None): + """ + Отправляет PDF файл нескольким получателям + + Args: + recipient_emails (list): Список email получателей + pdf_filename (str, optional): Имя PDF файла + subject (str): Тема письма + body_text (str): Текст письма + + Returns: + dict: Словарь с результатами для каждого получателя + """ + results = {} + for email in recipient_emails: + results[email] = send_pdf_to_email(email, pdf_filename, subject, body_text) + return results