""" Pydantic модели для 152-ФЗ Scanner. Описывают структуры данных: результаты проверок, отчеты, сканирование. """ from __future__ import annotations from datetime import datetime from enum import Enum from typing import Any, Optional from pydantic import BaseModel, Field class CheckStatus(str, Enum): PASS = "pass" # ✅ Проверка пройдена FAIL = "fail" # ❌ Проверка не пройдена SKIP = "skip" # ⚠️ Проверка пропущена ERROR = "error" # 🔴 Ошибка при проверке class RiskLevel(str, Enum): LOW = "low" # 🟢 Низкий риск MEDIUM = "medium" # 🟡 Средний риск HIGH = "high" # 🔴 Высокий риск CRITICAL = "critical" # 🚫 Критический риск class CheckResult(BaseModel): """Результат одной проверки.""" name: str # Имя проверки (например, "cookie_banner") title: str # Название для отображения (например, "Cookie-баннер") status: CheckStatus # Статус проверки risk_level: RiskLevel = RiskLevel.LOW # Уровень риска при провале message: str = "" # Описание результата details: dict[str, Any] = {} # Детали для отладки/отчета class SiteScan(BaseModel): """Результат сканирования одного сайта.""" url: str # URL сайта scanned_at: datetime # Время сканирования checks: list[CheckResult] = [] # Результаты всех проверок score: int = 100 # Общий балл (0-100) risks: list[CheckResult] = [] # Проваленные проверки (только fail) critical_risks: list[CheckResult] = [] # Критические провалы is_compliant: bool = True # Соответствует ли 152-ФЗ summary: str = "" # Краткое резюме def _calculate_score(self): """Вычисляет итоговый балл и статус соответствия.""" risk_weights = { RiskLevel.LOW: 5, RiskLevel.MEDIUM: 10, RiskLevel.HIGH: 20, RiskLevel.CRITICAL: 30, } score = 100 risks = [] critical_risks = [] for check in self.checks: if check.status == CheckStatus.FAIL: penalty = risk_weights.get(check.risk_level, 5) score -= penalty risks.append(check) if check.risk_level == RiskLevel.CRITICAL: critical_risks.append(check) self.score = max(0, score) self.risks = risks self.critical_risks = critical_risks self.is_compliant = self.score >= 70 # Формируем краткое резюме fail_count = sum(1 for c in self.checks if c.status == CheckStatus.FAIL) error_count = sum(1 for c in self.checks if c.status == CheckStatus.ERROR) skip_count = sum(1 for c in self.checks if c.status == CheckStatus.SKIP) if self.is_compliant: emoji = "🟢" if self.score >= 90 else "🟡" self.summary = f"{emoji} Сайт частично соответствует 152-ФЗ (балл: {self.score}/100). Найдено нарушений: {fail_count}." else: emoji = "🔴" if self.critical_risks else "🟠" self.summary = f"{emoji} Сайт НЕ соответствует 152-ФЗ (балл: {self.score}/100). Критических нарушений: {len(self.critical_risks)}. Требуется доработка." return self class ScanRequest(BaseModel): """Запрос на сканирование (через API).""" url: str = Field(..., description="URL сайта для сканирования") checks: Optional[list[str]] = None # Какие проверки запустить (все если None) class CSVScanRequest(BaseModel): """Запрос на сканирование CSV-файла.""" urls: list[str] = Field(..., description="Список URL для сканирования") class ScanResponse(BaseModel): """Ответ API — результат сканирования.""" scan_id: str # Уникальный ID сканирования status: str # pending / completed / error url: Optional[str] = None # URL (если одно) urls_count: Optional[int] = None # Количество URL (если CSV) results: Optional[list[SiteScan]] = None # Результаты error: Optional[str] = None # Ошибка (если есть) created_at: datetime # Время создания запроса class PDFReport(BaseModel): """Данные для генерации PDF-отчета.""" site_scan: SiteScan # Результаты сканирования generated_at: datetime # Время генерации company_name: Optional[str] = None # Название компании (если известно) contact_email: Optional[str] = None # Email для связи scanner_version: str = "1.0.0" # Версия сканера