#
#
# Этот модуль является единственным источником истины для всех
# конфигурационных параметров приложения. Использует Pydantic для типизации,
# валидации и загрузки настроек из переменных окружения.
# Заменяет устаревший `src/config.py`.
#
#
import os
from pathlib import Path
from pydantic import BaseModel, Field, validator
from dotenv import load_dotenv
from typing import List
#
#
# Загрузка переменных окружения из .env файла, если он существует.
load_dotenv()
#
#
#
BASE_DIR = Path(__file__).resolve().parent.parent.parent
#
#
# description: "Определяет CSS-селекторы для парсинга как строгий, типизированный контракт."
# invariant: "Все поля являются обязательными непустыми строками."
#
class ScraperSelectors(BaseModel):
#
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')
product_unavailable: str = Field(..., alias='PRODUCT_UNAVAILABLE')
#
#
# description: "Валидатор, проверяющий, что селекторы не являются пустыми строками."
#
#
@validator('*', pre=True, allow_reuse=True)
def validate_selectors(cls, v):
if not v or not v.strip():
raise ValueError('Селектор не может быть пустым')
return v.strip()
#
#
# description: "Главный класс конфигурации приложения. Собирает все настройки в одном месте, используя переменные окружения."
#
class Settings(BaseModel):
#
base_url: str = Field(default=os.getenv('PARSER_BASE_URL', 'https://elixirpeptide.ru'), description="Базовый URL сайта")
catalog_url: str = Field(default=os.getenv('PARSER_CATALOG_URL', 'https://elixirpeptide.ru/catalog/'), description="URL каталога товаров")
headers: dict = Field(default={'User-Agent': os.getenv('PARSER_USER_AGENT', 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36')})
#
#
output_dir: Path = Field(default=BASE_DIR / "price_data_final", description="Директория для сохранения результатов")
save_to_csv: bool = Field(default=os.getenv('PARSER_SAVE_TO_CSV', 'true').lower() == 'true')
save_to_db: bool = Field(default=os.getenv('PARSER_SAVE_TO_DB', 'true').lower() == 'true')
#
#
log_to_db: bool = Field(default=os.getenv('PARSER_LOG_TO_DB', 'true').lower() == 'true')
log_dir: Path = Field(default=BASE_DIR / "logs", description="Директория для сохранения логов")
#
#
request_timeout: int = Field(default=int(os.getenv('PARSER_TIMEOUT', 30)))
delay_between_requests: float = Field(default=float(os.getenv('PARSER_DELAY', 1.0)))
max_retries: int = Field(default=int(os.getenv('PARSER_RETRIES', 3)))
num_parser_threads: int = Field(default=int(os.getenv('PARSER_THREADS', 5)), description="Количество потоков для парсинга")
#
#
telegram_bot_token: str = Field(default=os.getenv('TELEGRAM_BOT_TOKEN', ''), description="Токен для Telegram бота")
telegram_chat_id: str = Field(default=os.getenv('TELEGRAM_CHAT_ID', ''), description="ID чата для отправки уведомлений")
#
#
rabbitmq_host: str = Field(default=os.getenv('RABBITMQ_HOST', 'localhost'))
rabbitmq_port: int = Field(default=int(os.getenv('RABBITMQ_PORT', 5672)))
rabbitmq_user: str = Field(default=os.getenv('RABBITMQ_USERNAME', 'guest'))
rabbitmq_password: str = Field(default=os.getenv('RABBITMQ_PASSWORD', 'guest'))
rabbitmq_vhost: str = Field(default=os.getenv('RABBITMQ_VIRTUAL_HOST', '/'))
rabbitmq_products_queue: str = Field(default=os.getenv('RABBITMQ_PRODUCTS_QUEUE', 'price_parser.products'))
rabbitmq_logs_queue: str = Field(default=os.getenv('RABBITMQ_LOGS_QUEUE', 'price_parser.logs'))
rabbitmq_exchange: str = Field(default=os.getenv('RABBITMQ_EXCHANGE', 'price_parser.exchange'))
#
#
selectors: ScraperSelectors = ScraperSelectors(
CATALOG_PRODUCT_LINK='.product-card h4 a.product-link',
VARIANT_LIST_ITEM='.product-version-select li, .product-variants-list .variant-item',
PRODUCT_PAGE_NAME='h1.product-h1',
ACTIVE_VOLUME='.product-version-select li.active, .variant-item.active',
PRICE_BLOCK='.price-value, .product-price .price, .product-sale-box .price span',
PRODUCT_UNAVAILABLE='.product-unavailable, .out-of-stock, .unavailable, .stock.status-0',
)
#
#
# description: "Вычисляемое свойство для получения полного пути к файлу базы данных."
#
#
@property
def db_path(self) -> Path:
return self.output_dir / 'parser_data.db'
#
#
# description: "Про��еряет ключевые параметры конфигурации на доступность и корректность."
# postconditions: "Возвращает список строк с описанием ошибок."
#
#
def validate_configuration(self) -> List[str]:
errors: List[str] = []
try:
self.output_dir.mkdir(parents=True, exist_ok=True)
except Exception as e:
errors.append(f"Не удается создать директорию {self.output_dir}: {e}")
try:
import requests
response = requests.head(self.base_url, timeout=self.request_timeout)
if response.status_code >= 400:
errors.append(f"Базовый URL недоступен: {self.base_url} (статус: {response.status_code})")
except Exception as e:
errors.append(f"Не удается подключиться к базовому URL {self.base_url}: {e}")
return errors
#
#
# Создаем единственный экземпляр настроек, который будет импортиров��ться
# и использоваться во всем приложении.
settings = Settings()
#
#
ENABLE_RABBITMQ_EXPORT = os.getenv("ENABLE_RABBITMQ_EXPORT", "false").lower() == "true"
ENABLE_CSV_EXPORT = settings.save_to_csv
ENABLE_DATABASE_EXPORT = settings.save_to_db
#
#