feat(01-03): API routers (calendar, generate, export, settings) e wiring main.py
- schemas/settings.py: Settings pydantic model con api_key, llm_model, nicchie_attive, tono
- routers/calendar.py: POST /api/calendar/generate, GET /api/calendar/formats
- routers/generate.py: POST /api/generate/bulk (202 + job_id), GET /job/{job_id}/status (polling), GET /job/{job_id}, POST /single
- routers/export.py: GET /api/export/{job_id}/csv (originale), POST /api/export/{job_id}/csv (modifiche inline)
- routers/settings.py: GET /api/settings/status (api_key_configured), GET /api/settings, PUT /api/settings
- main.py: include_router x4 PRIMA di SPAStaticFiles, copia prompt default al primo avvio
This commit is contained in:
150
backend/routers/export.py
Normal file
150
backend/routers/export.py
Normal file
@@ -0,0 +1,150 @@
|
||||
"""Router per l'export del CSV Canva.
|
||||
|
||||
Endpoint:
|
||||
- GET /api/export/{job_id}/csv — scarica CSV originale con Content-Disposition attachment
|
||||
- POST /api/export/{job_id}/csv — accetta dati modificati inline e rigenera il CSV
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
from pathlib import Path
|
||||
|
||||
from fastapi import APIRouter, HTTPException
|
||||
from fastapi.responses import FileResponse, Response
|
||||
from pydantic import BaseModel
|
||||
|
||||
from backend.config import OUTPUTS_PATH
|
||||
from backend.schemas.generate import PostResult
|
||||
from backend.services.csv_builder import CSVBuilder
|
||||
from backend.services.generation_pipeline import GenerationPipeline
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
router = APIRouter(prefix="/api/export", tags=["export"])
|
||||
|
||||
_csv_builder = CSVBuilder()
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Request schema per POST con dati modificati
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
class ExportWithEditsRequest(BaseModel):
|
||||
"""Richiesta POST per rigenerare il CSV con dati modificati dall'utente."""
|
||||
results: list[PostResult]
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Endpoint
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
@router.get("/{job_id}/csv")
|
||||
async def download_csv_original(job_id: str) -> FileResponse:
|
||||
"""Scarica il CSV originale generato per un job.
|
||||
|
||||
Cerca il file CSV in OUTPUTS_PATH/{job_id}.csv e lo serve come attachment.
|
||||
|
||||
Args:
|
||||
job_id: Identificatore del job (da POST /api/generate/bulk).
|
||||
|
||||
Returns:
|
||||
File CSV con Content-Disposition: attachment per il download automatico.
|
||||
|
||||
Raises:
|
||||
HTTPException 404: Se il file CSV non esiste (job non completato o non trovato).
|
||||
"""
|
||||
csv_path = OUTPUTS_PATH / f"{job_id}.csv"
|
||||
|
||||
if not csv_path.exists():
|
||||
raise HTTPException(
|
||||
status_code=404,
|
||||
detail=f"File CSV per job '{job_id}' non trovato. "
|
||||
"Assicurati che la generazione sia completata.",
|
||||
)
|
||||
|
||||
return FileResponse(
|
||||
path=str(csv_path),
|
||||
media_type="text/csv; charset=utf-8",
|
||||
headers={
|
||||
"Content-Disposition": f'attachment; filename="postgenerator_{job_id}.csv"',
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
@router.post("/{job_id}/csv")
|
||||
async def download_csv_with_edits(
|
||||
job_id: str,
|
||||
request: ExportWithEditsRequest,
|
||||
) -> Response:
|
||||
"""Rigenera il CSV con i dati modificati inline dall'utente.
|
||||
|
||||
Accetta i PostResult aggiornati dal frontend (con modifiche al testo
|
||||
delle slide, titoli, caption, etc.) e produce un nuovo CSV aggiornato.
|
||||
|
||||
Il file viene salvato come OUTPUTS_PATH/{job_id}_edited.csv.
|
||||
|
||||
Args:
|
||||
job_id: Identificatore del job originale.
|
||||
request: Lista di PostResult con i dati aggiornati dall'utente.
|
||||
|
||||
Returns:
|
||||
File CSV rigenerato con le modifiche inline.
|
||||
|
||||
Raises:
|
||||
HTTPException 404: Se il job originale (e quindi il calendario) non esiste.
|
||||
"""
|
||||
# Carica il calendario originale dal job per i metadati degli slot
|
||||
job_path = OUTPUTS_PATH / f"{job_id}.json"
|
||||
if not job_path.exists():
|
||||
raise HTTPException(
|
||||
status_code=404,
|
||||
detail=f"Job '{job_id}' non trovato. "
|
||||
"Impossibile rigenerare il CSV senza i metadati originali.",
|
||||
)
|
||||
|
||||
# Carica il calendario dal JSON del job
|
||||
import json
|
||||
from backend.schemas.calendar import CalendarResponse
|
||||
|
||||
try:
|
||||
with open(job_path, "r", encoding="utf-8") as f:
|
||||
job_data = json.load(f)
|
||||
calendar = CalendarResponse.model_validate(job_data["calendar"])
|
||||
except Exception as e:
|
||||
raise HTTPException(
|
||||
status_code=500,
|
||||
detail=f"Errore nel caricamento del job: {str(e)}",
|
||||
)
|
||||
|
||||
# Genera il CSV con i dati modificati
|
||||
csv_content = _csv_builder.build_csv_content(
|
||||
posts=request.results,
|
||||
calendar=calendar,
|
||||
job_id=job_id,
|
||||
)
|
||||
|
||||
# Salva anche su disco come versione edited
|
||||
edited_path = OUTPUTS_PATH / f"{job_id}_edited.csv"
|
||||
edited_path.parent.mkdir(parents=True, exist_ok=True)
|
||||
with open(edited_path, "w", newline="", encoding="utf-8-sig") as f:
|
||||
f.write(csv_content)
|
||||
|
||||
logger.info(
|
||||
"CSV con modifiche generato | job_id=%s | righe=%d | path=%s",
|
||||
job_id,
|
||||
len([r for r in request.results if r.status == "success"]),
|
||||
edited_path,
|
||||
)
|
||||
|
||||
# Ritorna il CSV come response con Content-Disposition attachment
|
||||
# Nota: build_csv_content include già il BOM (\ufeff), usiamo encode("utf-8")
|
||||
# per non raddoppiare il BOM nel body
|
||||
return Response(
|
||||
content=csv_content.encode("utf-8"),
|
||||
media_type="text/csv; charset=utf-8",
|
||||
headers={
|
||||
"Content-Disposition": f'attachment; filename="postgenerator_{job_id}_edited.csv"',
|
||||
},
|
||||
)
|
||||
Reference in New Issue
Block a user