apply patch

This commit is contained in:
2025-07-03 21:03:21 +03:00
parent 54827a5152
commit 0ddd9f0683
6 changed files with 527 additions and 88 deletions

View File

@@ -6,63 +6,238 @@ import logging
import sqlite3
from datetime import datetime
from pathlib import Path
from typing import List, Dict
from typing import List, Dict, Optional
# Контракты для функций здесь остаются такими же, как в предыдущей версии.
from src.core.models import ProductVariant, LogRecordModel # [FIX] Импорт моделей
class DatabaseLogHandler(logging.Handler):
# ... (код класса DatabaseLogHandler без изменений) ...
def __init__(self, db_path: Path, run_id: str):
super().__init__()
# [CONTRACT] DatabaseManager
# @description: Контекстный менеджер для управления соединением с SQLite.
# @pre: `db_path` должен быть валидным путем `Path`.
# @post: Гарантирует открытие и закрытие соединения с БД.
class DatabaseManager:
"""[CONTEXT_MANAGER] Управляет соединением с базой данных SQLite."""
def __init__(self, db_path: Path):
self.db_path = db_path
self.conn: Optional[sqlite3.Connection] = None
self.logger = logging.getLogger(self.__class__.__name__)
def __enter__(self):
# [ACTION] Открытие соединения при входе в контекст
self.logger.debug(f"[STATE] Открытие соединения с БД: {self.db_path}")
try:
self.conn = sqlite3.connect(self.db_path)
self.conn.row_factory = sqlite3.Row # Для удобного доступа к данным по именам колонок
self.logger.debug("[COHERENCE_CHECK_PASSED] Соединение с БД установлено.")
return self.conn
except sqlite3.Error as e:
self.logger.critical(f"[CRITICAL] Ошибка подключения к БД: {e}", exc_info=True)
raise ConnectionError(f"Не удалось подключиться к базе данных {self.db_path}") from e
def __exit__(self, exc_type, exc_val, exc_tb):
# [ACTION] Закрытие соединения при выходе из контекста
if self.conn:
self.conn.close()
self.logger.debug("[STATE] Соединение с БД закрыто.")
if exc_type:
self.logger.error(f"[ERROR] Исключение в контекстном менеджере БД: {exc_type.__name__}: {exc_val}", exc_info=True)
# [COHERENCE_CHECK_FAILED] Ошибка внутри контекста
return False # Пробрасываем исключение
def close(self):
"""[HELPER] Явное закрытие соединения, если менеджер используется вне 'with'."""
if self.conn:
self.conn.close()
self.conn = None
self.logger.debug("[STATE] Соединение с БД явно закрыто.")
# [CONTRACT] DatabaseLogHandler (перенесен в models.py и адаптирован)
# @description: Обработчик логирования, который записывает логи в SQLite базу данных.
# @pre: `db_manager` должен быть инициализирован и подключен.
# @post: Записи логов сохраняются в таблицу `logs`.
class DatabaseLogHandler(logging.Handler):
# ... (код класса DatabaseLogHandler) ...
def __init__(self, db_manager: DatabaseManager, run_id: str):
super().__init__()
self.db_manager = db_manager
self.run_id = run_id
self.logger = logging.getLogger(self.__class__.__name__) # [INIT] Инициализация логгера для обработчика
def emit(self, record: logging.LogRecord):
# [ACTION] Запись лог-записи в БД
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}")
# Используем менеджер контекста для безопасного взаимодействия с БД
# Примечание: В DatabaseLogHandler обычно не используется with, т.к. он должен быть "легким"
# и работать с существующим соединением, которое управляется извне (через db_manager.conn)
# или создает временное (что неэффективно).
# В данном случае, db_manager должен предоставить уже открытое соединение.
# Если db_manager не передает активное соединение, нужно его получить.
# Для простоты, пока будем использовать прямое подключение в emit, но в реальном продакшене
# это место лучше оптимизировать (например, через пул соединений или одно соединение в db_manager).
def init_database(db_path: Path, request_id: str):
# ... (код функции init_database без изменений) ...
log_prefix = f"init_database(id={request_id})"
with sqlite3.connect(self.db_manager.db_path) as con:
cur = con.cursor()
log_time = datetime.fromtimestamp(record.created)
# Создаем модель лог-записи для валидации
log_entry = LogRecordModel(
run_id=self.run_id,
timestamp=log_time,
level=record.levelname,
message=self.format(record) # Используем форматтер для полного сообщения
)
cur.execute(
"INSERT INTO logs (run_id, timestamp, level, message) VALUES (?, ?, ?, ?)",
(log_entry.run_id, log_entry.timestamp, log_entry.level, log_entry.message)
)
con.commit()
# [COHERENCE_CHECK_PASSED] Лог успешно записан.
except Exception as e:
# [ERROR_HANDLER] Логирование ошибок записи логов (очень важно)
# print() используется, потому что обычный логгер может вызвать рекурсию
print(f"CRITICAL: [COHERENCE_CHECK_FAILED] Не удалось записать лог в базу данных: {e}", flush=True)
# [CONTRACT] init_database
# @description: Инициализирует схему базы данных (создает таблицы, если они не существуют).
# @pre: `db_path` должен быть валидным путем `Path`.
# @post: Таблицы `products` и `logs` существуют в БД.
# @side_effects: Создает директорию для БД, если ее нет.
def init_database(db_path: Path, run_id: str):
log_prefix = f"init_database(id={run_id})"
logging.info(f"{log_prefix} - Инициализация базы данных: {db_path}")
try:
# [ACTION] Создаем родительскую директорию, если она не существует.
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()
# [CONTEXT_MANAGER] Используем with-statement для соединения с БД
with sqlite3.connect(db_path) as con:
cur = con.cursor()
# [ACTION] Создание таблицы products
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
)
""")
# [ACTION] Создание таблицы logs
cur.execute("""
CREATE TABLE IF NOT EXISTS logs (
id INTEGER PRIMARY KEY AUTOINCREMENT,
run_id TEXT NOT NULL,
timestamp TEXT NOT NULL, -- Changed to TEXT for ISO format from datetime
level TEXT NOT NULL,
message TEXT NOT NULL
)
""")
con.commit()
logging.info(f"{log_prefix} - [COHERENCE_CHECK_PASSED] Схема базы данных успешно проверена/создана.")
except sqlite3.Error as e:
logging.error(f"{log_prefix} - [COHERENCE_CHECK_FAILED] Ошибка SQLite при инициализации БД: {e}", exc_info=True)
raise ConnectionError(f"Ошибка БД при инициализации: {e}") from e
except Exception as e:
logging.error(f"{log_prefix} - [COHERENCE_CHECK_FAILED] Ошибка при инициализации БД: {e}")
logging.critical(f"{log_prefix} - [CRITICAL] Непредвиденная ошибка при инициализации БД: {e}", exc_info=True)
raise
# [CONTRACT] save_data_to_db
# @description: Сохраняет список объектов ProductVariant (представленных как словари) в таблицу `products`.
# @pre:
# - `data` должен быть списком словарей, каждый из которых соответствует ProductVariant.
# - `db_path` должен указывать на существующую и инициализированную БД.
# @post: Данные из `data` вставлены в таблицу `products`.
def save_data_to_db(data: List[Dict], db_path: Path, run_id: str):
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}")
# [PRECONDITION] Проверка формата данных (хотя ProductVariant.model_dump() должен гарантировать)
if not all(isinstance(item, dict) and all(k in item for k in ['name', 'volume', 'price']) for item in data):
logging.error(f"{log_prefix} - [CONTRACT_VIOLATION] Некорректный формат данных для сохранения в БД.", extra={"sample_data": data[:1]})
raise ValueError("Данные для сохранения в БД не соответствуют ожидаемому формату ProductVariant.")
try:
# [CONTEXT_MANAGER] Используем with-statement для безопасного соединения и коммита
with sqlite3.connect(db_path) as con:
cur = con.cursor()
products_to_insert = []
for item in data:
# Преобразование к int и обработка возможных ошибок приведения типа
try:
price_int = int(item['price'])
except (ValueError, TypeError) as e:
logging.error(f"{log_prefix} - [DATA_CLEANUP_FAILED] Некорректное значение цены для '{item.get('name')}': {item.get('price')}. Пропуск записи. Ошибка: {e}")
# [COHERENCE_CHECK_FAILED] Данные не соответствуют схеме
continue # Пропускаем эту запись, но продолжаем для остальных
products_to_insert.append(
(run_id, item['name'], item['volume'], price_int)
)
if products_to_insert:
cur.executemany(
"INSERT INTO products (run_id, name, volume, price) VALUES (?, ?, ?, ?)",
products_to_insert
)
con.commit()
logging.info(f"{log_prefix} - [COHERENCE_CHECK_PASSED] {len(products_to_insert)} записей успешно сохранено в базу данных.")
else:
logging.warning(f"{log_prefix} - После фильтрации не осталось валидных записей для сохранения.")
except sqlite3.Error as e:
logging.error(f"{log_prefix} - [COHERENCE_CHECK_FAILED] Ошибка SQLite при сохранении данных: {e}", exc_info=True)
raise ConnectionError(f"Ошибка БД при сохранении: {e}") from e
except Exception as e:
logging.critical(f"{log_prefix} - [CRITICAL] Непредвиденная ошибка при сохранении данных в БД: {e}", exc_info=True)
raise
# [CONTRACT] save_data_to_db
# @description: Сохраняет список объектов ProductVariant (представленных как словари) в таблицу `products`.
# @pre:
# - `data` должен быть списком словарей, каждый из которых соответствует ProductVariant.
# - `db_path` должен указывать на существующую и инициализированную БД.
# @post: Данные из `data` вставлены в таблицу `products`.
def save_data_to_db(data: List[Dict], db_path: Path, run_id: str):
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}")
# [PRECONDITION] Проверка формата данных (хотя ProductVariant.model_dump() должен гарантировать)
if not all(isinstance(item, dict) and all(k in item for k in ['name', 'volume', 'price']) for item in data):
logging.error(f"{log_prefix} - [CONTRACT_VIOLATION] Некорректный формат данных для сохранения в БД.", extra={"sample_data": data[:1]})
raise ValueError("Данные для сохранения в БД не соответствуют ожидаемому формату ProductVariant.")
try:
# [CONTEXT_MANAGER] Используем with-statement для безопасного соединения и коммита
with sqlite3.connect(db_path) as con:
cur = con.cursor()
products_to_insert = []
for item in data:
# Преобразование к int и обработка возможных ошибок приведения типа
try:
price_int = int(item['price'])
except (ValueError, TypeError) as e:
logging.error(f"{log_prefix} - [DATA_CLEANUP_FAILED] Некорректное значение цены для '{item.get('name')}': {item.get('price')}. Пропуск записи. Ошибка: {e}")
# [COHERENCE_CHECK_FAILED] Данные не соответствуют схеме
continue # Пропускаем эту запись, но продолжаем для остальных
products_to_insert.append(
(run_id, item['name'], item['volume'], price_int)
)
if products_to_insert:
cur.executemany(
"INSERT INTO products (run_id, name, volume, price) VALUES (?, ?, ?, ?)",
products_to_insert
)
con.commit()
logging.info(f"{log_prefix} - [COHERENCE_CHECK_PASSED] {len(products_to_insert)} записей успешно сохранено в базу данных.")
else:
logging.warning(f"{log_prefix} - После фильтрации не осталось валидных записей для сохранения.")
except sqlite3.Error as e:
logging.error(f"{log_prefix} - [COHERENCE_CHECK_FAILED] Ошибка SQLite при сохранении данных: {e}", exc_info=True)
raise ConnectionError(f"Ошибка БД при сохранении: {e}") from e
except Exception as e:
logging.critical(f"{log_prefix} - [CRITICAL] Непредвиденная ошибка при сохранении данных в БД: {e}", exc_info=True)
raise
def save_data_to_db(data: List[Dict], db_path: Path, run_id: str):