176 lines
6.5 KiB
Python
176 lines
6.5 KiB
Python
# #region ReportsRouter [C:5] [TYPE Module] [SEMANTICS fastapi, report, api, transform, search, task]
|
|
# @BRIEF FastAPI router for unified task report list and detail retrieval endpoints.
|
|
# @LAYER API
|
|
# @RELATION DEPENDS_ON -> [ReportsService]
|
|
# @RELATION DEPENDS_ON -> [get_task_manager]
|
|
# @RELATION DEPENDS_ON -> [get_clean_release_repository]
|
|
# @RELATION DEPENDS_ON -> [has_permission]
|
|
# @INVARIANT Endpoints are read-only and do not trigger long-running tasks.
|
|
# @PRE Reports service and dependencies are initialized.
|
|
# @POST Router is configured and endpoints are ready for registration.
|
|
# @SIDE_EFFECT None
|
|
# @DATA_CONTRACT [ReportQuery] -> [ReportCollection | ReportDetailView]
|
|
|
|
from datetime import datetime
|
|
|
|
from fastapi import APIRouter, Depends, HTTPException, Query, status
|
|
|
|
from ...core.logger import belief_scope
|
|
from ...core.task_manager import TaskManager
|
|
from ...dependencies import (
|
|
get_clean_release_repository,
|
|
get_task_manager,
|
|
has_permission,
|
|
)
|
|
from ...models.report import (
|
|
ReportCollection,
|
|
ReportDetailView,
|
|
ReportQuery,
|
|
ReportStatus,
|
|
TaskType,
|
|
)
|
|
from ...services.clean_release.repository import CleanReleaseRepository
|
|
from ...services.reports.report_service import ReportsService
|
|
|
|
router = APIRouter(prefix="/api/reports", tags=["Reports"])
|
|
|
|
|
|
# #region _parse_csv_enum_list [C:1] [TYPE Function]
|
|
# @BRIEF 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.
|
|
# @RELATION BINDS_TO -> [ReportsRouter]
|
|
def _parse_csv_enum_list(raw: str | None, 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
|
|
|
|
|
|
# #endregion _parse_csv_enum_list
|
|
|
|
|
|
# #region list_reports [C:2] [TYPE Function]
|
|
# @BRIEF 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.
|
|
# @RELATION CALLS -> [_parse_csv_enum_list]
|
|
# @RELATION DEPENDS_ON -> [ReportQuery]
|
|
# @RELATION DEPENDS_ON -> [ReportsService]
|
|
#
|
|
# @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: str | None = Query(None, description="Comma-separated task types"),
|
|
statuses: str | None = Query(None, description="Comma-separated statuses"),
|
|
time_from: datetime | None = Query(None),
|
|
time_to: datetime | None = Query(None),
|
|
search: str | None = Query(None, max_length=200),
|
|
sort_by: str = Query("updated_at"),
|
|
sort_order: str = Query("desc"),
|
|
task_manager: TaskManager = Depends(get_task_manager),
|
|
clean_release_repository: CleanReleaseRepository = Depends(
|
|
get_clean_release_repository
|
|
),
|
|
_=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, clean_release_repository=clean_release_repository
|
|
)
|
|
return service.list_reports(query)
|
|
|
|
|
|
# #endregion list_reports
|
|
|
|
|
|
# #region get_report_detail [C:2] [TYPE Function]
|
|
# @BRIEF 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.
|
|
# @RELATION CALLS -> [ReportsService]
|
|
@router.get("/{report_id}", response_model=ReportDetailView)
|
|
async def get_report_detail(
|
|
report_id: str,
|
|
task_manager: TaskManager = Depends(get_task_manager),
|
|
clean_release_repository: CleanReleaseRepository = Depends(
|
|
get_clean_release_repository
|
|
),
|
|
_=Depends(has_permission("tasks", "READ")),
|
|
):
|
|
with belief_scope("get_report_detail", f"report_id={report_id}"):
|
|
service = ReportsService(
|
|
task_manager, clean_release_repository=clean_release_repository
|
|
)
|
|
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
|
|
|
|
|
|
# #endregion get_report_detail
|
|
|
|
# #endregion ReportsRouter
|