Files
peptide-parcer/src/core/models.py
2025-07-19 01:04:04 +03:00

89 lines
5.4 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# <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="Все основные модели данных определены, типизированы и структурированы." />