from __future__ import annotations from datetime import datetime, timezone from typing import Annotated from fastapi import APIRouter, Depends, HTTPException, Request, Response, status from fastapi.responses import HTMLResponse, StreamingResponse from fastapi.templating import Jinja2Templates from dependencies import get_unit_of_work, require_any_role from schemas.exports import ( ExportFormat, ProjectExportRequest, ScenarioExportRequest, ) from services.export_serializers import ( export_projects_to_excel, export_scenarios_to_excel, stream_projects_to_csv, stream_scenarios_to_csv, ) from services.unit_of_work import UnitOfWork router = APIRouter(prefix="/exports", tags=["exports"]) @router.get( "/modal/{dataset}", response_model=None, response_class=HTMLResponse, include_in_schema=False, name="exports.modal", ) async def export_modal( dataset: str, request: Request, ) -> HTMLResponse: dataset = dataset.lower() if dataset not in {"projects", "scenarios"}: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail="Unknown dataset") submit_url = request.url_for( "export_projects" if dataset == "projects" else "export_scenarios" ) templates = Jinja2Templates(directory="templates") return templates.TemplateResponse( request, "exports/modal.html", { "dataset": dataset, "submit_url": submit_url, }, ) def _timestamp_suffix() -> str: return datetime.now(timezone.utc).strftime("%Y%m%d-%H%M%S") def _ensure_repository(repo, name: str): if repo is None: raise HTTPException(status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail=f"{name} repository unavailable") return repo @router.post( "/projects", status_code=status.HTTP_200_OK, response_class=StreamingResponse, dependencies=[Depends(require_any_role( "admin", "project_manager", "analyst"))], ) async def export_projects( request: ProjectExportRequest, uow: Annotated[UnitOfWork, Depends(get_unit_of_work)], ) -> Response: project_repo = _ensure_repository( getattr(uow, "projects", None), "Project") projects = project_repo.filtered_for_export(request.filters) filename = f"projects-{_timestamp_suffix()}" if request.format == ExportFormat.CSV: stream = stream_projects_to_csv(projects) response = StreamingResponse(stream, media_type="text/csv") response.headers["Content-Disposition"] = f"attachment; filename={filename}.csv" return response data = export_projects_to_excel(projects) return StreamingResponse( iter([data]), media_type="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", headers={ "Content-Disposition": f"attachment; filename={filename}.xlsx", }, ) @router.post( "/scenarios", status_code=status.HTTP_200_OK, response_class=StreamingResponse, dependencies=[Depends(require_any_role( "admin", "project_manager", "analyst"))], ) async def export_scenarios( request: ScenarioExportRequest, uow: Annotated[UnitOfWork, Depends(get_unit_of_work)], ) -> Response: scenario_repo = _ensure_repository( getattr(uow, "scenarios", None), "Scenario") scenarios = scenario_repo.filtered_for_export( request.filters, include_project=True) filename = f"scenarios-{_timestamp_suffix()}" if request.format == ExportFormat.CSV: stream = stream_scenarios_to_csv(scenarios) response = StreamingResponse(stream, media_type="text/csv") response.headers["Content-Disposition"] = f"attachment; filename={filename}.csv" return response data = export_scenarios_to_excel(scenarios) return StreamingResponse( iter([data]), media_type="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", headers={ "Content-Disposition": f"attachment; filename={filename}.xlsx", }, )