"""Endpoints d'import : analyse puis exécution.""" import datetime as dt import uuid from fastapi import APIRouter, Depends, File, Form, HTTPException, UploadFile from pydantic import ValidationError from sqlmodel import Session, select from app.db import get_session from app.importing.parser import ImportParseError, parse_upload from app.importing.schemas import ( AnalyzeResponse, DayPartInfo, ImportMapping, ImportReport, PreviewRow, ) from app.importing.service import run_import, suggest_mapping, validate_mapping from app.models import DayPart, Edition router = APIRouter(prefix="/editions/{edition_id}/import", tags=["import"]) _PREVIEW_ROWS = 5 _MAX_BYTES = 5 * 1024 * 1024 def _get_edition(edition_id: uuid.UUID, session: Session) -> Edition: edition = session.get(Edition, edition_id) if edition is None: raise HTTPException(status_code=404, detail={"code": "error.not_found"}) return edition async def _read_upload(file: UploadFile) -> bytes: content = await file.read() if not content: raise HTTPException( status_code=400, detail={"code": "import.error.empty_file"} ) if len(content) > _MAX_BYTES: raise HTTPException( status_code=413, detail={"code": "import.error.file_too_large"} ) return content def _is_xlsx(filename: str, content: bytes) -> bool: return content[:4] == b"PK\x03\x04" or (filename or "").lower().endswith( (".xlsx", ".xlsm") ) def _edition_dates(edition: Edition) -> list[dt.date]: span = (edition.end_date - edition.start_date).days return [edition.start_date + dt.timedelta(days=i) for i in range(span + 1)] def _day_parts(session: Session, edition: Edition) -> list[DayPart]: return list( session.exec( select(DayPart) .where(DayPart.edition_id == edition.id) .order_by(DayPart.order) ) ) @router.post("/analyze", response_model=AnalyzeResponse) async def analyze( edition_id: uuid.UUID, file: UploadFile = File(...), session: Session = Depends(get_session), ) -> AnalyzeResponse: edition = _get_edition(edition_id, session) content = await _read_upload(file) try: table = parse_upload(file.filename or "", content) except ImportParseError as exc: raise HTTPException( status_code=422, detail={"code": exc.code, "params": exc.params}, ) from exc preview = [ PreviewRow(line=i + 2, values=values) for i, values in enumerate(table.as_dicts()[:_PREVIEW_ROWS]) ] return AnalyzeResponse( filename=file.filename or "", format="xlsx" if _is_xlsx(file.filename or "", content) else "csv", headers=table.headers, row_count=len(table.rows), preview=preview, suggested_mapping=suggest_mapping(table.headers), edition_dates=_edition_dates(edition), day_parts=[ DayPartInfo( code=dp.code, label=dp.label, start_time=dp.start_time, end_time=dp.end_time, order=dp.order, ) for dp in _day_parts(session, edition) ], ) @router.post("/execute", response_model=ImportReport) async def execute( edition_id: uuid.UUID, file: UploadFile = File(...), mapping: str = Form(...), session: Session = Depends(get_session), ) -> ImportReport: edition = _get_edition(edition_id, session) content = await _read_upload(file) try: parsed_mapping = ImportMapping.model_validate_json(mapping) except ValidationError as exc: raise HTTPException( status_code=422, detail={"code": "import.mapping.invalid", "errors": exc.errors()}, ) from exc try: table = parse_upload(file.filename or "", content) except ImportParseError as exc: raise HTTPException( status_code=422, detail={"code": exc.code, "params": exc.params}, ) from exc mapping_errors = validate_mapping( table, parsed_mapping, edition, _day_parts(session, edition) ) if mapping_errors: raise HTTPException( status_code=422, detail={ "code": "import.mapping.invalid", "errors": [e.model_dump() for e in mapping_errors], }, ) return run_import( session=session, edition=edition, table=table, mapping=parsed_mapping, )