# # # Этот модуль определяет все Pydantic-модели, которые служат контрактами данных # в приложении. Они обеспечивают валидацию, типизацию и четкую структуру # для продуктов, логов и сообщений RabbitMQ. # # import logging from pydantic import BaseModel, Field, HttpUrl from pydantic.functional_validators import model_validator from datetime import datetime from typing import List, Optional import uuid logger = logging.getLogger(__name__) # # # description: "Модель данных для одного варианта продукта." # invariant: "`name`, `price`, `url` являются обязательными. `price` всегда `int` > 0." # class ProductVariant(BaseModel): # name: str = Field(..., description="Название продукта.") volume: Optional[str] = Field(None, description="Объем или вариант продукта (например, '50мл', '10 капсул'). Может быть пустым, если не применимо.") price: int = Field(..., description="Цена продукта в числовом формате. Должна быть положительной, если товар в наличии, иначе может быть 0.") is_in_stock: bool = Field(..., description="Наличие товара.") @model_validator(mode='after') def validate_price_based_on_stock(self) -> 'ProductVariant': if not self.is_in_stock and self.price != 0: logger.warning(f"[CONTRACT_VIOLATION] Product '{self.name}' (URL: {self.url}) is out of stock but has a non-zero price ({self.price}). Setting price to 0.") self.price = 0 elif self.is_in_stock and self.price <= 0: raise ValueError("Price must be greater than 0 for in-stock products.") return self url: HttpUrl = Field(..., description="Полный URL страницы варианта продукта.") is_in_stock: bool = Field(..., description="Наличие товара.") # # # description: "Модель данных для записи лога, используемая при сохранении в БД или отправке в RabbitMQ." # invariant: "Все поля являются обязательными." # class LogRecordModel(BaseModel): # run_id: str = Field(..., description="Уникальный идентификатор текущего запуска парсера.") timestamp: datetime = Field(..., description="Время создания лог-записи.") level: str = Field(..., description="Уровень логирования (e.g., INFO, ERROR, DEBUG).") message: str = Field(..., description="Текст лог-сообщения.") # # # # description: "Базовая модель для всех сообщений, отправляемых в RabbitMQ." # invariant: "Все сообщения имеют уникальный ID, timestamp и источник." # class RabbitMQMessage(BaseModel): # message_id: str = Field(default_factory=lambda: str(uuid.uuid4()), description="Уникальный идентификатор сообщения.") timestamp: datetime = Field(default_factory=datetime.utcnow, description="Время создания сообщения.") source: str = Field(default="price_parser", description="Источник сообщения.") # # # description: "Модель сообщения с данными о продуктах для отправки в RabbitMQ." # invariant: "Содержит список продуктов и метаданные о запуске." # class ProductDataMessage(RabbitMQMessage): # products: List[ProductVariant] = Field(..., description="Список продуктов для обработки.") run_id: str = Field(..., description="Идентификатор запуска парсера.") total_count: int = Field(..., description="Общее количество продуктов в сообщении.") # # # description: "Модель сообщения с логами для отправки в RabbitMQ." # invariant: "Содержит список записей логов и метаданные о запуске." # class LogMessage(RabbitMQMessage): # log_records: List[LogRecordModel] = Field(..., description="Список записей логов.") run_id: str = Field(..., description="Идентификатор запуска парсера.") # #