89 lines
5.4 KiB
Python
89 lines
5.4 KiB
Python
# <MODULE name="core.models" semantics="data_contracts" />
|
||
# <DESIGN_NOTE>
|
||
# Этот модуль определяет все Pydantic-модели, которые служат контрактами данных
|
||
# в приложении. Они обеспечивают валидацию, типизацию и четкую структуру
|
||
# для продуктов, логов и сообщений RabbitMQ.
|
||
# </DESIGN_NOTE>
|
||
|
||
# <IMPORTS>
|
||
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__)
|
||
# </IMPORTS>
|
||
|
||
# <MAIN_CONTRACT for="ProductVariant">
|
||
# description: "Модель данных для одного варианта продукта."
|
||
# invariant: "`name`, `price`, `url` являются обязательными. `price` всегда `int` > 0."
|
||
# </MAIN_CONTRACT>
|
||
class ProductVariant(BaseModel):
|
||
# <STATE name="product_variant_fields">
|
||
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="Наличие товара.")
|
||
# </STATE>
|
||
|
||
# <MAIN_CONTRACT for="LogRecordModel">
|
||
# description: "Модель данных для записи лога, используемая при сохранении в БД или отправке в RabbitMQ."
|
||
# invariant: "Все поля являются обязательными."
|
||
# </MAIN_CONTRACT>
|
||
class LogRecordModel(BaseModel):
|
||
# <STATE name="log_record_fields">
|
||
run_id: str = Field(..., description="Уникальный идентификатор текущего запуска парсера.")
|
||
timestamp: datetime = Field(..., description="Время создания лог-записи.")
|
||
level: str = Field(..., description="Уровень логирования (e.g., INFO, ERROR, DEBUG).")
|
||
message: str = Field(..., description="Текст лог-сообщения.")
|
||
# </STATE>
|
||
|
||
# <MODULE name="rabbitmq_models" semantics="message_contracts_for_rabbitmq" />
|
||
|
||
# <MAIN_CONTRACT for="RabbitMQMessage">
|
||
# description: "Базовая модель для всех сообщений, отправляемых в RabbitMQ."
|
||
# invariant: "Все сообщения имеют уникальный ID, timestamp и источник."
|
||
# </MAIN_CONTRACT>
|
||
class RabbitMQMessage(BaseModel):
|
||
# <STATE name="rabbitmq_base_fields">
|
||
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="Источник сообщения.")
|
||
# </STATE>
|
||
|
||
# <MAIN_CONTRACT for="ProductDataMessage">
|
||
# description: "Модель сообщения с данными о продуктах для отправки в RabbitMQ."
|
||
# invariant: "Содержит список продуктов и метаданные о запуске."
|
||
# </MAIN_CONTRACT>
|
||
class ProductDataMessage(RabbitMQMessage):
|
||
# <STATE name="product_data_message_fields">
|
||
products: List[ProductVariant] = Field(..., description="Список продуктов для обработки.")
|
||
run_id: str = Field(..., description="Идентификатор запуска парсера.")
|
||
total_count: int = Field(..., description="Общее количество продуктов в сообщении.")
|
||
# </STATE>
|
||
|
||
# <MAIN_CONTRACT for="LogMessage">
|
||
# description: "Модель сообщения с логами для отправки в RabbitMQ."
|
||
# invariant: "Содержит список записей логов и метаданные о запуске."
|
||
# </MAIN_CONTRACT>
|
||
class LogMessage(RabbitMQMessage):
|
||
# <STATE name="log_message_fields">
|
||
log_records: List[LogRecordModel] = Field(..., description="Список записей логов.")
|
||
run_id: str = Field(..., description="Идентификатор запуска парсера.")
|
||
# </STATE>
|
||
|
||
# <COHERENCE_CHECK status="PASSED" description="Все основные модели данных определены, типизированы и структурированы." />
|