""" Pydantic models for request/response validation and serialization. """ from pydantic import BaseModel, Field, HttpUrl from typing import Optional from datetime import datetime from enum import Enum # --- Enums --- class InspectionStatus(str, Enum): RUNNING = "running" COMPLETED = "completed" ERROR = "error" class Severity(str, Enum): CRITICAL = "critical" MAJOR = "major" MINOR = "minor" INFO = "info" class CategoryName(str, Enum): HTML_CSS = "html_css" ACCESSIBILITY = "accessibility" SEO = "seo" PERFORMANCE_SECURITY = "performance_security" # --- Request --- class StartInspectionRequest(BaseModel): url: HttpUrl accessibility_standard: str = Field( default="wcag_2.1_aa", description="접근성 검사 표준 (wcag_2.0_a, wcag_2.0_aa, wcag_2.1_aa, wcag_2.2_aa, kwcag_2.1, kwcag_2.2)", ) # --- Core Data Models --- class Issue(BaseModel): code: str category: str severity: Severity message: str element: Optional[str] = None line: Optional[int] = None suggestion: str wcag_criterion: Optional[str] = None # KWCAG mapping fields (populated when standard is kwcag_2.1 or kwcag_2.2) kwcag_criterion: Optional[str] = None kwcag_name: Optional[str] = None kwcag_principle: Optional[str] = None kwcag_criteria_all: Optional[list[str]] = None class CategoryResult(BaseModel): score: int = Field(ge=0, le=100) grade: str total_issues: int critical: int = 0 major: int = 0 minor: int = 0 info: int = 0 issues: list[Issue] = [] # Category-specific fields wcag_level: Optional[str] = None meta_info: Optional[dict] = None sub_scores: Optional[dict] = None metrics: Optional[dict] = None class IssueSummary(BaseModel): total_issues: int critical: int major: int minor: int info: int # --- Response Models --- class StartInspectionResponse(BaseModel): inspection_id: str status: str = "running" url: str stream_url: str class InspectionResult(BaseModel): inspection_id: str url: str status: InspectionStatus created_at: datetime completed_at: Optional[datetime] = None duration_seconds: Optional[float] = None overall_score: int = Field(ge=0, le=100) grade: str categories: dict[str, CategoryResult] summary: IssueSummary accessibility_standard: Optional[str] = None class InspectionResultResponse(BaseModel): """Response model for GET /api/inspections/{id} (without nested issues).""" inspection_id: str url: str status: InspectionStatus created_at: datetime completed_at: Optional[datetime] = None duration_seconds: Optional[float] = None overall_score: int = Field(ge=0, le=100) grade: str categories: dict[str, CategoryResult] summary: IssueSummary accessibility_standard: Optional[str] = None class IssueListResponse(BaseModel): inspection_id: str total: int filters: dict issues: list[Issue] class InspectionListItem(BaseModel): inspection_id: str url: str created_at: datetime overall_score: int grade: str total_issues: int class PaginatedResponse(BaseModel): items: list[InspectionListItem] total: int page: int limit: int total_pages: int class TrendDataPoint(BaseModel): inspection_id: str created_at: datetime overall_score: int html_css: int accessibility: int seo: int performance_security: int class TrendResponse(BaseModel): url: str data_points: list[TrendDataPoint] class HealthResponse(BaseModel): status: str timestamp: str services: dict[str, str] # --- Utility functions --- def calculate_grade(score: int) -> str: """Calculate letter grade from numeric score.""" if score >= 90: return "A+" if score >= 80: return "A" if score >= 70: return "B" if score >= 60: return "C" if score >= 50: return "D" return "F" def calculate_overall_score(categories: dict[str, CategoryResult]) -> int: """Calculate overall score as simple average of category scores.""" scores = [cat.score for cat in categories.values()] if not scores: return 0 return round(sum(scores) / len(scores))