This commit is contained in:
2025-07-03 19:56:10 +03:00
commit 54827a5152
11 changed files with 575 additions and 0 deletions

89
src/core/database.py Normal file
View File

@@ -0,0 +1,89 @@
# ANCHOR: Database_Module
# Семантика: Инкапсуляция всей логики взаимодействия с базой данных SQLite.
# Этот модуль отвечает за схему, сохранение данных и логирование в БД.
import logging
import sqlite3
from datetime import datetime
from pathlib import Path
from typing import List, Dict
# Контракты для функций здесь остаются такими же, как в предыдущей версии.
class DatabaseLogHandler(logging.Handler):
# ... (код класса DatabaseLogHandler без изменений) ...
def __init__(self, db_path: Path, run_id: str):
super().__init__()
self.db_path = db_path
self.run_id = run_id
def emit(self, record: logging.LogRecord):
try:
con = sqlite3.connect(self.db_path)
cur = con.cursor()
log_time = datetime.fromtimestamp(record.created)
cur.execute(
"INSERT INTO logs (run_id, timestamp, level, message) VALUES (?, ?, ?, ?)",
(self.run_id, log_time, record.levelname, self.format(record))
)
con.commit()
con.close()
except Exception as e:
print(f"CRITICAL: Failed to write log to database: {e}")
def init_database(db_path: Path, request_id: str):
# ... (код функции init_database без изменений) ...
log_prefix = f"init_database(id={request_id})"
logging.info(f"{log_prefix} - Инициализация базы данных: {db_path}")
try:
db_path.parent.mkdir(parents=True, exist_ok=True)
con = sqlite3.connect(db_path)
cur = con.cursor()
cur.execute("""
CREATE TABLE IF NOT EXISTS products (
id INTEGER PRIMARY KEY AUTOINCREMENT,
run_id TEXT NOT NULL,
name TEXT NOT NULL,
volume TEXT,
price INTEGER NOT NULL,
parsed_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
)
""")
cur.execute("""
CREATE TABLE IF NOT EXISTS logs (
id INTEGER PRIMARY KEY AUTOINCREMENT,
run_id TEXT NOT NULL,
timestamp TIMESTAMP NOT NULL,
level TEXT NOT NULL,
message TEXT NOT NULL
)
""")
con.commit()
con.close()
logging.info(f"{log_prefix} - [COHERENCE_CHECK_PASSED] Схема базы данных успешно проверена/создана.")
except Exception as e:
logging.error(f"{log_prefix} - [COHERENCE_CHECK_FAILED] Ошибка при инициализации БД: {e}")
raise
def save_data_to_db(data: List[Dict], db_path: Path, run_id: str):
# ... (код функции save_data_to_db без изменений) ...
log_prefix = f"save_data_to_db(id={run_id})"
if not data:
logging.warning(f"{log_prefix} - [CONTRACT_VIOLATION] Данные для сохранения отсутствуют.")
return
logging.info(f"{log_prefix} - Начало сохранения {len(data)} записей в БД: {db_path}")
try:
con = sqlite3.connect(db_path)
cur = con.cursor()
products_to_insert = [
(run_id, item['name'], item['volume'], int(item['price'])) for item in data
]
cur.executemany(
"INSERT INTO products (run_id, name, volume, price) VALUES (?, ?, ?, ?)",
products_to_insert
)
con.commit()
con.close()
logging.info(f"{log_prefix} - [COHERENCE_CHECK_PASSED] Данные успешно сохранены в базу данных.")
except Exception as e:
logging.error(f"{log_prefix} - [COHERENCE_CHECK_FAILED] Ошибка при сохранении в БД: {e}")

View File

@@ -0,0 +1,32 @@
# ANCHOR: Logging_Config_Module
# Семантика: Конфигурация системы логирования.
import logging
from typing import Optional
from .database import DatabaseLogHandler, DatabaseManager
from .settings import settings
def setup_logging(run_id: str, db_manager: Optional[DatabaseManager] = None):
"""
[CONTRACT]
@description: Настраивает логирование. Теперь принимает db_manager как зависимость.
"""
log_format = '[%(asctime)s] [%(levelname)s] :: %(message)s'
logging.basicConfig(
level=logging.INFO,
format=log_format,
datefmt='%Y-%m-%d %H:%M:%S',
force=True # Перезаписывает любую существующую конфигурацию
)
if settings.log_to_db and db_manager:
try:
root_logger = logging.getLogger('')
db_handler = DatabaseLogHandler(db_manager, run_id)
db_handler.setLevel(logging.DEBUG)
db_handler.setFormatter(logging.Formatter(log_format))
root_logger.addHandler(db_handler)
logging.info("Обработчик логов для записи в базу данных успешно добавлен.")
except Exception as e:
logging.error(f"Не удалось инициализировать обработчик логов для БД: {e}")
logging.info("Система логирования инициализирована.")

66
src/core/settings.py Normal file
View File

@@ -0,0 +1,66 @@
# [FILE] src/core/settings.py
# [REFACTORING_NOTE] Этот файл заменяет старый src/config.py, используя Pydantic.
# ANCHOR: Configuration_Module
# Семантика: Этот модуль является единственным источником истины для всех
# конфигурационных параметров приложения. Использует Pydantic для типизации и валидации.
from pathlib import Path
from pydantic import BaseModel, Field
class ScraperSelectors(BaseModel):
"""
[CONTRACT]
@description: Определяет CSS-селекторы для парсинга как строгий, типизированный контракт.
@invariant: Все поля являются обязательными строками.
"""
# [CONFIG] Используем Field с alias, чтобы Pydantic мог инициализировать
# модель из словаря с ключами в верхнем регистре, как было раньше.
catalog_product_link: str = Field(..., alias='CATALOG_PRODUCT_LINK')
variant_list_item: str = Field(..., alias='VARIANT_LIST_ITEM')
product_page_name: str = Field(..., alias='PRODUCT_PAGE_NAME')
active_volume: str = Field(..., alias='ACTIVE_VOLUME')
price_block: str = Field(..., alias='PRICE_BLOCK')
class Settings(BaseModel):
"""
[MAIN-CONTRACT]
@description: Главный класс конфигурации приложения. Собирает все настройки в одном месте.
"""
# [CONFIG] Основные настройки парсера
base_url: str = 'https://elixirpeptide.ru'
catalog_url: str = 'https://elixirpeptide.ru/catalog/'
headers: dict = {
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36'
}
# [CONFIG] Настройки вывода
output_dir: Path = Path('price_data_final')
save_to_csv: bool = True
save_to_db: bool = True
# [CONFIG] Настройки логирования
log_to_db: bool = True
# [CONFIG] Вложенная модель с селекторами
# Мы инициализируем ее прямо здесь, передавая словарь со значениями.
selectors: ScraperSelectors = ScraperSelectors(
CATALOG_PRODUCT_LINK='.product-card h4 a.product-link',
VARIANT_LIST_ITEM='.product-version-select li',
PRODUCT_PAGE_NAME='h1.product-h1',
ACTIVE_VOLUME='.product-version-select li.active',
PRICE_BLOCK='.product-sale-box .price span',
)
@property
def db_path(self) -> Path:
"""
[HELPER] Вычисляемое свойство для пути к базе данных.
Гарантирует, что путь всегда будет актуальным, если изменится output_dir.
"""
return self.output_dir / 'parser_data.db'
# [SINGLETON] Создаем единственный экземпляр настроек, который будет использоваться
# во всем приложении. Это стандартная практика для работы с конфигурацией.
settings = Settings()
# [REFACTORING_COMPLETE] Этот модуль готов к использованию.