gemini-cli refactor
This commit is contained in:
@@ -1,116 +1,107 @@
|
||||
# ANCHOR: Database_Module
|
||||
# Семантика: Инкапсуляция всей логики взаимодействия с базой данных SQLite.
|
||||
# Этот модуль отвечает за схему, сохранение данных и логирование в БД.
|
||||
# <MODULE name="core.database" semantics="database_interaction_logic" />
|
||||
# <DESIGN_NOTE>
|
||||
# Инкапсулирует всю логику взаимодействия с базой данных SQLite.
|
||||
# Отвечает за управление соединениями, создание схемы, сохранение данных и запись логов.
|
||||
# </DESIGN_NOTE>
|
||||
|
||||
# <IMPORTS>
|
||||
import logging
|
||||
import sqlite3
|
||||
from datetime import datetime
|
||||
from pathlib import Path
|
||||
from typing import List, Dict, Optional
|
||||
from .models import LogRecordModel
|
||||
# </IMPORTS>
|
||||
|
||||
from core.models import ProductVariant, LogRecordModel # [FIX] Импорт моделей
|
||||
|
||||
# [CONTRACT] DatabaseManager
|
||||
# @description: Контекстный менеджер для управления соединением с SQLite.
|
||||
# @pre: `db_path` должен быть валидным путем `Path`.
|
||||
# @post: Гарантирует открытие и закрытие соединения с БД.
|
||||
# <MAIN_CONTRACT for="DatabaseManager">
|
||||
# description: "Контекстный менеджер для безопасного управления соединением с SQLite."
|
||||
# preconditions: "`db_path` должен быть валидным путем `Path`."
|
||||
# postconditions: "Гарантирует корректное открытие и закрытие соединения с БД."
|
||||
# </MAIN_CONTRACT>
|
||||
class DatabaseManager:
|
||||
"""[CONTEXT_MANAGER] Управляет соединением с базой данных SQLite."""
|
||||
# <INIT name="__init__">
|
||||
def __init__(self, db_path: Path):
|
||||
self.db_path = db_path
|
||||
self.conn: Optional[sqlite3.Connection] = None
|
||||
self.logger = logging.getLogger(self.__class__.__name__)
|
||||
# </INIT>
|
||||
|
||||
# <ACTION name="__enter__">
|
||||
def __enter__(self):
|
||||
# [ACTION] Открытие соединения при входе в контекст
|
||||
self.logger.debug(f"[STATE] Открытие соединения с БД: {self.db_path}")
|
||||
self.logger.debug(f"[INIT:DatabaseManager] Открытие соединения с БД: {self.db_path}")
|
||||
try:
|
||||
self.conn = sqlite3.connect(self.db_path)
|
||||
self.conn.row_factory = sqlite3.Row # Для удобного доступа к данным по именам колонок
|
||||
self.logger.debug("[COHERENCE_CHECK_PASSED] Соединение с БД установлено.")
|
||||
self.conn.row_factory = sqlite3.Row
|
||||
self.logger.debug("[INIT:DatabaseManager] [COHERENCE_CHECK_PASSED] Соединение с БД установлено.")
|
||||
return self.conn
|
||||
except sqlite3.Error as e:
|
||||
self.logger.critical(f"[CRITICAL] Ошибка подключения к БД: {e}", exc_info=True)
|
||||
self.logger.critical(f"[INIT:DatabaseManager] [CRITICAL] Ошибка подключения к БД: {e}", exc_info=True)
|
||||
raise ConnectionError(f"Не удалось подключиться к базе данных {self.db_path}") from e
|
||||
# </ACTION>
|
||||
|
||||
# <ACTION name="__exit__">
|
||||
def __exit__(self, exc_type, exc_val, exc_tb):
|
||||
# [ACTION] Закрытие соединения при выходе из контекста
|
||||
if self.conn:
|
||||
self.conn.close()
|
||||
self.logger.debug("[STATE] Соединение с БД закрыто.")
|
||||
self.logger.debug("[CLEANUP:DatabaseManager] Соединение с БД закрыто.")
|
||||
if exc_type:
|
||||
self.logger.error(f"[ERROR] Исключение в контекстном менеджере БД: {exc_type.__name__}: {exc_val}", exc_info=True)
|
||||
# [COHERENCE_CHECK_FAILED] Ошибка внутри контекста
|
||||
return False # Пробрасываем исключение
|
||||
self.logger.error(f"[ERROR:DatabaseManager] Исключение в контекстном менеджере БД: {exc_type.__name__}: {exc_val}", exc_info=True)
|
||||
# </ACTION>
|
||||
|
||||
# <HELPER name="close">
|
||||
def close(self):
|
||||
"""[HELPER] Явное закрытие соединения, если менеджер используется вне 'with'."""
|
||||
if self.conn:
|
||||
self.conn.close()
|
||||
self.conn = None
|
||||
self.logger.debug("[STATE] Соединение с БД явно закрыто.")
|
||||
self.logger.debug("[CLEANUP:DatabaseManager] Соединение с БД явно закрыто.")
|
||||
# </HELPER>
|
||||
|
||||
# [CONTRACT] DatabaseLogHandler (перенесен в models.py и адаптирован)
|
||||
# @description: Обработчик логирования, который записывает логи в SQLite базу данных.
|
||||
# @pre: `db_manager` должен быть инициализирован и подключен.
|
||||
# @post: Записи логов сохраняются в таблицу `logs`.
|
||||
# <MAIN_CONTRACT for="DatabaseLogHandler">
|
||||
# description: "Обработчик логирования, который записывает логи в таблицу `logs` в SQLite."
|
||||
# preconditions: "`db_manager` должен быть инициализирован."
|
||||
# postconditions: "Записи логов сохраняются в базу данных."
|
||||
# </MAIN_CONTRACT>
|
||||
class DatabaseLogHandler(logging.Handler):
|
||||
# ... (код класса DatabaseLogHandler) ...
|
||||
# <INIT name="__init__">
|
||||
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] Инициализация логгера для обработчика
|
||||
# </INIT>
|
||||
|
||||
# <ACTION name="emit">
|
||||
def emit(self, record: logging.LogRecord):
|
||||
# [ACTION] Запись лог-записи в БД
|
||||
try:
|
||||
# Используем менеджер контекста для безопасного взаимодействия с БД
|
||||
# Примечание: В DatabaseLogHandler обычно не используется with, т.к. он должен быть "легким"
|
||||
# и работать с существующим соединением, которое управляется извне (через db_manager.conn)
|
||||
# или создает временное (что неэффективно).
|
||||
# В данном случае, db_manager должен предоставить уже открытое соединение.
|
||||
# Если db_manager не передает активное соединение, нужно его получить.
|
||||
# Для простоты, пока будем использовать прямое подключение в emit, но в реальном продакшене
|
||||
# это место лучше оптимизировать (например, через пул соединений или одно соединение в db_manager).
|
||||
|
||||
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,
|
||||
timestamp=datetime.fromtimestamp(record.created),
|
||||
level=record.levelname,
|
||||
message=self.format(record) # Используем форматтер для полного сообщения
|
||||
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)
|
||||
(log_entry.run_id, log_entry.timestamp.isoformat(), 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)
|
||||
# </ACTION>
|
||||
|
||||
# [CONTRACT] init_database
|
||||
# @description: Инициализирует схему базы данных (создает таблицы, если они не существуют).
|
||||
# @pre: `db_path` должен быть валидным путем `Path`.
|
||||
# @post: Таблицы `products` и `logs` существуют в БД.
|
||||
# @side_effects: Создает директорию для БД, если ее нет.
|
||||
# <CONTRACT for="init_database">
|
||||
# description: "Инициализирует схему базы данных, создавая таблицы `products` и `logs`, если они не существуют."
|
||||
# preconditions: "`db_path` должен быть валидным путем `Path`."
|
||||
# side_effects: "Создает директорию для БД, если она не существует."
|
||||
# </CONTRACT>
|
||||
# <ACTION name="init_database">
|
||||
def init_database(db_path: Path, run_id: str):
|
||||
log_prefix = f"init_database(id={run_id})"
|
||||
logging.info(f"{log_prefix} - Инициализация базы данных: {db_path}")
|
||||
log_prefix = f"[ACTION:init_database(id={run_id})]"
|
||||
logging.info(f"{log_prefix} Инициализация базы данных: {db_path}")
|
||||
try:
|
||||
# [ACTION] Создаем родительскую директорию, если она не существует.
|
||||
db_path.parent.mkdir(parents=True, exist_ok=True)
|
||||
# [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,
|
||||
@@ -118,134 +109,68 @@ def init_database(db_path: Path, run_id: str):
|
||||
name TEXT NOT NULL,
|
||||
volume TEXT,
|
||||
price INTEGER NOT NULL,
|
||||
url TEXT,
|
||||
is_in_stock BOOLEAN,
|
||||
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
|
||||
timestamp TEXT NOT NULL,
|
||||
level TEXT NOT NULL,
|
||||
message TEXT NOT NULL
|
||||
)
|
||||
""")
|
||||
con.commit()
|
||||
logging.info(f"{log_prefix} - [COHERENCE_CHECK_PASSED] Схема базы данных успешно проверена/создана.")
|
||||
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)
|
||||
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
|
||||
# </ACTION>
|
||||
|
||||
# [CONTRACT] save_data_to_db
|
||||
# @description: Сохраняет список объектов ProductVariant (представленных как словари) в таблицу `products`.
|
||||
# @pre:
|
||||
# - `data` должен быть списком словарей, каждый из которых соответствует ProductVariant.
|
||||
# - `db_path` должен указывать на существующую и инициализированную БД.
|
||||
# @post: Данные из `data` вставлены в таблицу `products`.
|
||||
# <CONTRACT for="save_data_to_db">
|
||||
# description: "Сохраняет список словарей с данными о продуктах в таблицу `products`."
|
||||
# preconditions:
|
||||
# - "`data` должен быть списком словарей, соответствующих модели `ProductVariant`."
|
||||
# - "`db_path` должен указывать на существующую и инициализированную БД."
|
||||
# postconditions: "Данные вставлены в таблицу. Возвращает True в случае успеха."
|
||||
# </CONTRACT>
|
||||
# <ACTION name="save_data_to_db">
|
||||
def save_data_to_db(data: List[Dict], db_path: Path, run_id: str) -> bool:
|
||||
"""
|
||||
[ENHANCED] Сохраняет данные в базу данных с улучшенной обработкой ошибок.
|
||||
|
||||
Args:
|
||||
data: Список словарей с данными для сохранения
|
||||
db_path: Путь к файлу базы данных
|
||||
run_id: Идентификатор запуска для логирования
|
||||
|
||||
Returns:
|
||||
bool: True если сохранение прошло успешно, False в противном случае
|
||||
"""
|
||||
log_prefix = f"save_data_to_db(id={run_id})"
|
||||
|
||||
# [ENHANCEMENT] Валидация входных данных
|
||||
log_prefix = f"[ACTION:save_data_to_db(id={run_id})]"
|
||||
if not data:
|
||||
logging.warning(f"{log_prefix} - [CONTRACT_VIOLATION] Данные для сохранения отсутствуют. Пропуск сохранения.")
|
||||
logging.warning(f"{log_prefix} [CONTRACT_VIOLATION] Данные для сохранения отсутствуют.")
|
||||
return False
|
||||
|
||||
if not isinstance(data, list):
|
||||
logging.error(f"{log_prefix} - [TYPE_ERROR] Данные должны быть списком, получено: {type(data)}")
|
||||
return False
|
||||
|
||||
logging.info(f"{log_prefix} - Начало сохранения {len(data)} записей в БД: {db_path}")
|
||||
|
||||
# [PRECONDITION] Проверка формата данных (хотя ProductVariant.model_dump() должен гарантировать)
|
||||
required_fields = ['name', 'volume', 'price']
|
||||
if not all(isinstance(item, dict) and all(k in item for k in required_fields) for item in data):
|
||||
logging.error(f"{log_prefix} - [CONTRACT_VIOLATION] Некорректный формат данных для сохранения в БД.", extra={"sample_data": data[:1]})
|
||||
return False
|
||||
|
||||
logging.info(f"{log_prefix} Начало сохранения {len(data)} записей в БД: {db_path}")
|
||||
|
||||
try:
|
||||
# [ENHANCEMENT] Проверка существования файла БД
|
||||
if not db_path.exists():
|
||||
logging.warning(f"{log_prefix} - Файл БД не существует: {db_path}")
|
||||
return False
|
||||
|
||||
# [CONTEXT_MANAGER] Используем with-statement для безопасного соединения и коммита
|
||||
with sqlite3.connect(db_path) as con:
|
||||
cur = con.cursor()
|
||||
products_to_insert = []
|
||||
skipped_count = 0
|
||||
products_to_insert = [
|
||||
(run_id, item['name'], item['volume'], item['price'], str(item['url']), item['is_in_stock'])
|
||||
for item in data
|
||||
]
|
||||
|
||||
for i, item in enumerate(data):
|
||||
# [ENHANCEMENT] Детальная валидация каждого элемента
|
||||
try:
|
||||
# Проверка типов данных
|
||||
if not isinstance(item['name'], str) or not item['name'].strip():
|
||||
logging.warning(f"{log_prefix} - [INVALID_NAME] Элемент {i}: некорректное имя '{item.get('name')}'")
|
||||
skipped_count += 1
|
||||
continue
|
||||
|
||||
if not isinstance(item['volume'], str):
|
||||
logging.warning(f"{log_prefix} - [INVALID_VOLUME] Элемент {i}: некорректный объем '{item.get('volume')}'")
|
||||
skipped_count += 1
|
||||
continue
|
||||
|
||||
# Преобразование к int и обработка возможных ошибок приведения типа
|
||||
try:
|
||||
price_int = int(item['price'])
|
||||
if price_int <= 0:
|
||||
logging.warning(f"{log_prefix} - [INVALID_PRICE] Элемент {i}: некорректная цена {price_int}")
|
||||
skipped_count += 1
|
||||
continue
|
||||
except (ValueError, TypeError) as e:
|
||||
logging.error(f"{log_prefix} - [DATA_CLEANUP_FAILED] Некорректное значение цены для '{item.get('name')}': {item.get('price')}. Пропуск записи. Ошибка: {e}")
|
||||
skipped_count += 1
|
||||
continue # Пропускаем эту запись, но продолжаем для остальных
|
||||
|
||||
products_to_insert.append(
|
||||
(run_id, item['name'], item['volume'], price_int)
|
||||
)
|
||||
|
||||
except KeyError as e:
|
||||
logging.error(f"{log_prefix} - [MISSING_FIELD] Элемент {i} не содержит обязательное поле: {e}")
|
||||
skipped_count += 1
|
||||
continue
|
||||
|
||||
if products_to_insert:
|
||||
cur.executemany(
|
||||
"INSERT INTO products (run_id, name, volume, price) VALUES (?, ?, ?, ?)",
|
||||
"INSERT INTO products (run_id, name, volume, price, url, is_in_stock) VALUES (?, ?, ?, ?, ?, ?)",
|
||||
products_to_insert
|
||||
)
|
||||
con.commit()
|
||||
logging.info(f"{log_prefix} - [COHERENCE_CHECK_PASSED] {len(products_to_insert)} записей успешно сохранено в базу данных.")
|
||||
if skipped_count > 0:
|
||||
logging.warning(f"{log_prefix} - Пропущено {skipped_count} некорректных записей.")
|
||||
logging.info(f"{log_prefix} [COHERENCE_CHECK_PASSED] {len(products_to_insert)} записей успешно сохранено.")
|
||||
return True
|
||||
else:
|
||||
logging.warning(f"{log_prefix} - После фильтрации не осталось валидных записей для сохранения.")
|
||||
logging.warning(f"{log_prefix} Нет <20><>алидных записей для сохранения.")
|
||||
return False
|
||||
|
||||
except sqlite3.Error as e:
|
||||
logging.error(f"{log_prefix} - [COHERENCE_CHECK_FAILED] Ошибка SQLite при сохранении данных: {e}", exc_info=True)
|
||||
return False
|
||||
except PermissionError as e:
|
||||
logging.error(f"{log_prefix} - [PERMISSION_ERROR] Нет прав на запись в БД {db_path}: {e}")
|
||||
logging.error(f"{log_prefix} [COHERENCE_CHECK_FAILED] Ошибка SQLite при сохранении данных: {e}", exc_info=True)
|
||||
return False
|
||||
except Exception as e:
|
||||
logging.critical(f"{log_prefix} - [CRITICAL] Непредвиденная ошибка при сохранении данных в БД: {e}", exc_info=True)
|
||||
logging.critical(f"{log_prefix} [CRITICAL] Непредвиденная ошибка при сохранении данных в БД: {e}", exc_info=True)
|
||||
return False
|
||||
# </ACTION>
|
||||
|
||||
# [REFACTORING_COMPLETE] Дублированные функции удалены, улучшена обработка ошибок
|
||||
# <COHERENCE_CHECK status="PASSED" description="Модуль базы данных полностью стр<D182><D180>ктурирован и размечен." />
|
||||
|
||||
Reference in New Issue
Block a user