Files
ss-tools/backend/src/models/report.py
busya c6189876b3 chore(lint): apply ruff --fix (4443 auto-fixes)
Auto-fixed categories:
- F401: unused imports removed
- I001: import blocks sorted
- W293: trailing whitespace stripped
- UP035: deprecated typing imports replaced
- SIM: simplify suggestions applied
- ARG: unused args prefixed with underscore
- T201: print statements removed
- F841: unused variables removed
- RUF059: unpacked variables prefixed

Remaining ~1500 unfixable errors (C901, B904, N806, E402) require manual work.

Backend smoke tests: 13/13 passed.
2026-05-14 11:20:17 +03:00

239 lines
8.8 KiB
Python

# #region ReportModels [C:3] [TYPE Module] [SEMANTICS pydantic, report, model, schema, task, task-type]
# @BRIEF Canonical report schemas for unified task reporting across heterogeneous task types.
# @LAYER: Domain
# @PRE: Pydantic library and task manager models are available.
# @POST: Provides validated schemas for cross-plugin reporting and UI consumption.
# @SIDE_EFFECT: None (schema definition).
# @DATA_CONTRACT: Model[TaskReport, ReportCollection, ReportDetailView]
# @RELATION DEPENDS_ON -> [TaskModels]
# @INVARIANT: Canonical report fields are always present for every report item.
from datetime import datetime
from enum import Enum
from typing import Any
from pydantic import BaseModel, Field, field_validator, model_validator
# #region TaskType [C:3] [TYPE Class] [SEMANTICS enum, type, task]
# @INVARIANT: Must contain valid generic task type mappings.
# @RELATION DEPENDS_ON -> ReportModels
# @BRIEF Supported normalized task report types.
class TaskType(str, Enum):
LLM_VERIFICATION = "llm_verification"
BACKUP = "backup"
MIGRATION = "migration"
DOCUMENTATION = "documentation"
CLEAN_RELEASE = "clean_release"
UNKNOWN = "unknown"
# #endregion TaskType
# #region ReportStatus [C:3] [TYPE Class] [SEMANTICS enum, status, task]
# @INVARIANT: TaskStatus enum mapping logic holds.
# @BRIEF Supported normalized report status values.
# @RELATION DEPENDS_ON -> ReportModels
class ReportStatus(str, Enum):
SUCCESS = "success"
FAILED = "failed"
IN_PROGRESS = "in_progress"
PARTIAL = "partial"
# #endregion ReportStatus
# #region ErrorContext [C:3] [TYPE Class] [SEMANTICS error, context, payload]
# @INVARIANT: The properties accurately describe error state.
# @BRIEF Error and recovery context for failed/partial reports.
#
# @TEST_CONTRACT: ErrorContextModel ->
# {
# required_fields: {
# message: str
# },
# optional_fields: {
# code: str,
# next_actions: list[str]
# }
# }
# @TEST_FIXTURE: basic_error -> {"message": "Connection timeout", "code": "ERR_504", "next_actions": ["retry"]}
# @TEST_EDGE: missing_message -> {"code": "ERR_504"}
# @RELATION DEPENDS_ON -> ReportModels
class ErrorContext(BaseModel):
code: str | None = None
message: str
next_actions: list[str] = Field(default_factory=list)
# #endregion ErrorContext
# #region TaskReport [C:3] [TYPE Class] [SEMANTICS report, model, summary]
# @INVARIANT: Must represent canonical task record attributes.
# @BRIEF Canonical normalized report envelope for one task execution.
#
# @TEST_CONTRACT: TaskReportModel ->
# {
# required_fields: {
# report_id: str,
# task_id: str,
# task_type: TaskType,
# status: ReportStatus,
# updated_at: datetime,
# summary: str
# },
# invariants: [
# "report_id is a non-empty string",
# "task_id is a non-empty string",
# "summary is a non-empty string"
# ]
# }
# @TEST_FIXTURE: valid_task_report ->
# {
# report_id: "rep-123",
# task_id: "task-456",
# task_type: "migration",
# status: "success",
# updated_at: "2026-02-26T12:00:00Z",
# summary: "Migration completed successfully"
# }
# @TEST_EDGE: empty_report_id -> {"report_id": " ", "task_id": "task-456", "task_type": "migration", "status": "success", "updated_at": "2026-02-26T12:00:00Z", "summary": "Done"}
# @TEST_EDGE: empty_summary -> {"report_id": "rep-123", "task_id": "task-456", "task_type": "migration", "status": "success", "updated_at": "2026-02-26T12:00:00Z", "summary": ""}
# @TEST_EDGE: invalid_task_type -> {"report_id": "rep-123", "task_id": "task-456", "task_type": "invalid_type", "status": "success", "updated_at": "2026-02-26T12:00:00Z", "summary": "Done"}
# @TEST_INVARIANT: non_empty_validators -> verifies: [empty_report_id, empty_summary]
# @RELATION DEPENDS_ON -> ReportModels
class TaskReport(BaseModel):
report_id: str
task_id: str
task_type: TaskType
status: ReportStatus
started_at: datetime | None = None
updated_at: datetime
summary: str
details: dict[str, Any] | None = None
validation_record: dict[str, Any] | None = None # Extended for US2
error_context: ErrorContext | None = None
source_ref: dict[str, Any] | None = None
@field_validator("report_id", "task_id", "summary")
@classmethod
def _non_empty_str(cls, value: str) -> str:
if not isinstance(value, str) or not value.strip():
raise ValueError("Value must be a non-empty string")
return value.strip()
# #endregion TaskReport
# #region ReportQuery [C:3] [TYPE Class] [SEMANTICS query, filter, search]
# @INVARIANT: Time and pagination queries are mutually consistent.
# @BRIEF Query object for server-side report filtering, sorting, and pagination.
#
# @TEST_CONTRACT: ReportQueryModel ->
# {
# optional_fields: {
# page: int, page_size: int, task_types: list[TaskType], statuses: list[ReportStatus],
# time_from: datetime, time_to: datetime, search: str, sort_by: str, sort_order: str
# },
# invariants: [
# "page >= 1", "1 <= page_size <= 100",
# "sort_by in {'updated_at', 'status', 'task_type'}",
# "sort_order in {'asc', 'desc'}",
# "time_from <= time_to if both exist"
# ]
# }
# @TEST_FIXTURE: valid_query -> {"page": 1, "page_size":20, "sort_by": "updated_at", "sort_order": "desc"}
# @TEST_EDGE: invalid_page_size_large -> {"page_size": 150}
# @TEST_EDGE: invalid_sort_by -> {"sort_by": "unknown_field"}
# @TEST_EDGE: invalid_time_range -> {"time_from": "2026-02-26T12:00:00Z", "time_to": "2026-02-25T12:00:00Z"}
# @TEST_INVARIANT: attribute_constraints_enforced -> verifies: [invalid_page_size_large, invalid_sort_by, invalid_time_range]
# @RELATION DEPENDS_ON -> ReportModels
class ReportQuery(BaseModel):
page: int = Field(default=1, ge=1)
page_size: int = Field(default=20, ge=1, le=100)
task_types: list[TaskType] = Field(default_factory=list)
statuses: list[ReportStatus] = Field(default_factory=list)
time_from: datetime | None = None
time_to: datetime | None = None
search: str | None = Field(default=None, max_length=200)
sort_by: str = Field(default="updated_at")
sort_order: str = Field(default="desc")
@field_validator("sort_by")
@classmethod
def _validate_sort_by(cls, value: str) -> str:
allowed = {"updated_at", "status", "task_type"}
if value not in allowed:
raise ValueError(f"sort_by must be one of: {', '.join(sorted(allowed))}")
return value
@field_validator("sort_order")
@classmethod
def _validate_sort_order(cls, value: str) -> str:
if value not in {"asc", "desc"}:
raise ValueError("sort_order must be 'asc' or 'desc'")
return value
@model_validator(mode="after")
def _validate_time_range(self):
if self.time_from and self.time_to and self.time_from > self.time_to:
raise ValueError("time_from must be less than or equal to time_to")
return self
# #endregion ReportQuery
# #region ReportCollection [C:3] [TYPE Class] [SEMANTICS collection, pagination]
# @INVARIANT: Represents paginated data correctly.
# @BRIEF Paginated collection of normalized task reports.
#
# @TEST_CONTRACT: ReportCollectionModel ->
# {
# required_fields: {
# items: list[TaskReport], total: int, page: int, page_size: int, has_next: bool, applied_filters: ReportQuery
# },
# invariants: ["total >= 0", "page >= 1", "page_size >= 1"]
# }
# @TEST_FIXTURE: empty_collection -> {"items": [], "total": 0, "page": 1, "page_size": 20, "has_next": False, "applied_filters": {}}
# @TEST_EDGE: negative_total -> {"items": [], "total": -5, "page": 1, "page_size": 20, "has_next": False, "applied_filters": {}}
# @RELATION DEPENDS_ON -> ReportModels
class ReportCollection(BaseModel):
items: list[TaskReport]
total: int = Field(ge=0)
page: int = Field(ge=1)
page_size: int = Field(ge=1)
has_next: bool
applied_filters: ReportQuery
# #endregion ReportCollection
# #region ReportDetailView [C:3] [TYPE Class] [SEMANTICS view, detail, logs]
# @INVARIANT: Incorporates a report and logs correctly.
# @BRIEF Detailed report representation including diagnostics and recovery actions.
#
# @TEST_CONTRACT: ReportDetailViewModel ->
# {
# required_fields: {report: TaskReport},
# optional_fields: {timeline: list[dict], diagnostics: dict, next_actions: list[str]}
# }
# @TEST_FIXTURE: valid_detail -> {"report": {"report_id": "rep-1", "task_id": "task-1", "task_type": "backup", "status": "success", "updated_at": "2026-02-26T12:00:00Z", "summary": "Done"}}
# @TEST_EDGE: missing_report -> {}
# @RELATION DEPENDS_ON -> ReportModels
class ReportDetailView(BaseModel):
report: TaskReport
timeline: list[dict[str, Any]] = Field(default_factory=list)
diagnostics: dict[str, Any] | None = None
next_actions: list[str] = Field(default_factory=list)
# #endregion ReportDetailView
# #endregion ReportModels