# # # Этот модуль является единственным источником истины для всех # конфигурационных параметров приложения. Использует 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 # #