# [DEF:ReportsRouter:Module] # @TIER: CRITICAL # @SEMANTICS: api, reports, list, detail, pagination, filters # @PURPOSE: FastAPI router for unified task report list and detail retrieval endpoints. # @LAYER: UI (API) # @RELATION: DEPENDS_ON -> backend.src.services.reports.report_service.ReportsService # @RELATION: DEPENDS_ON -> backend.src.dependencies # @INVARIANT: Endpoints are read-only and do not trigger long-running tasks. # [SECTION: IMPORTS] from datetime import datetime from typing import List, Optional from fastapi import APIRouter, Depends, HTTPException, Query, status from ...dependencies import get_task_manager, has_permission from ...core.task_manager import TaskManager from ...core.logger import belief_scope from ...models.report import ReportCollection, ReportDetailView, ReportQuery, ReportStatus, TaskType from ...services.reports.report_service import ReportsService # [/SECTION] router = APIRouter(prefix="/api/reports", tags=["Reports"]) # [DEF:_parse_csv_enum_list:Function] # @PURPOSE: Parse comma-separated query value into enum list. # @PRE: raw may be None/empty or comma-separated values. # @POST: Returns enum list or raises HTTP 400 with deterministic machine-readable payload. # @PARAM: raw (Optional[str]) - Comma-separated enum values. # @PARAM: enum_cls (type) - Enum class for validation. # @PARAM: field_name (str) - Query field name for diagnostics. # @RETURN: List - Parsed enum values. def _parse_csv_enum_list(raw: Optional[str], enum_cls, field_name: str) -> List: with belief_scope("_parse_csv_enum_list"): if raw is None or not raw.strip(): return [] values = [item.strip() for item in raw.split(",") if item.strip()] parsed = [] invalid = [] for value in values: try: parsed.append(enum_cls(value)) except ValueError: invalid.append(value) if invalid: raise HTTPException( status_code=status.HTTP_400_BAD_REQUEST, detail={ "message": f"Invalid values for '{field_name}'", "field": field_name, "invalid_values": invalid, "allowed_values": [item.value for item in enum_cls], }, ) return parsed # [/DEF:_parse_csv_enum_list:Function] # [DEF:list_reports:Function] # @PURPOSE: Return paginated unified reports list. # @PRE: authenticated/authorized request and validated query params. # @POST: returns {items,total,page,page_size,has_next,applied_filters}. # @POST: deterministic error payload for invalid filters. # # @TEST_CONTRACT: ListReportsApi -> # { # required_fields: {page: int, page_size: int, sort_by: str, sort_order: str}, # optional_fields: {task_types: str, statuses: str, search: str}, # invariants: [ # "Returns ReportCollection on success", # "Raises HTTPException 400 for invalid query parameters" # ] # } # @TEST_FIXTURE: valid_list_request -> {"page": 1, "page_size": 20} # @TEST_EDGE: invalid_task_type_filter -> raises HTTPException(400) # @TEST_EDGE: malformed_query -> raises HTTPException(400) # @TEST_INVARIANT: consistent_list_payload -> verifies: [valid_list_request] @router.get("", response_model=ReportCollection) async def list_reports( page: int = Query(1, ge=1), page_size: int = Query(20, ge=1, le=100), task_types: Optional[str] = Query(None, description="Comma-separated task types"), statuses: Optional[str] = Query(None, description="Comma-separated statuses"), time_from: Optional[datetime] = Query(None), time_to: Optional[datetime] = Query(None), search: Optional[str] = Query(None, max_length=200), sort_by: str = Query("updated_at"), sort_order: str = Query("desc"), task_manager: TaskManager = Depends(get_task_manager), _=Depends(has_permission("tasks", "READ")), ): with belief_scope("list_reports"): try: parsed_task_types = _parse_csv_enum_list(task_types, TaskType, "task_types") parsed_statuses = _parse_csv_enum_list(statuses, ReportStatus, "statuses") query = ReportQuery( page=page, page_size=page_size, task_types=parsed_task_types, statuses=parsed_statuses, time_from=time_from, time_to=time_to, search=search, sort_by=sort_by, sort_order=sort_order, ) except HTTPException: raise except Exception as exc: raise HTTPException( status_code=status.HTTP_400_BAD_REQUEST, detail={ "message": "Invalid query parameters", "code": "INVALID_REPORT_QUERY", "reason": str(exc), }, ) service = ReportsService(task_manager) return service.list_reports(query) # [/DEF:list_reports:Function] # [DEF:get_report_detail:Function] # @PURPOSE: Return one normalized report detail with diagnostics and next actions. # @PRE: authenticated/authorized request and existing report_id. # @POST: returns normalized detail envelope or 404 when report is not found. @router.get("/{report_id}", response_model=ReportDetailView) async def get_report_detail( report_id: str, task_manager: TaskManager = Depends(get_task_manager), _=Depends(has_permission("tasks", "READ")), ): with belief_scope("get_report_detail", f"report_id={report_id}"): service = ReportsService(task_manager) detail = service.get_report_detail(report_id) if not detail: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail={"message": "Report not found", "code": "REPORT_NOT_FOUND"}, ) return detail # [/DEF:get_report_detail:Function] # [/DEF:ReportsRouter:Module]