Files
scenar-creator/app/api/scenario.py
Daneel e2bdadd0ce
Some checks failed
Build & Push Docker / build (push) Has been cancelled
feat: refactor to FastAPI architecture v2.0
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-20 16:28:21 +01:00

169 lines
5.5 KiB
Python

"""Scenario API endpoints: validate, import-excel, generate-excel, export-json, template."""
import os
from io import BytesIO
from datetime import date, time as dt_time
import pandas as pd
from fastapi import APIRouter, UploadFile, File, Form, HTTPException
from fastapi.responses import StreamingResponse, FileResponse
from app.config import VERSION, MAX_FILE_SIZE_MB
from app.models.event import ScenarioDocument, Block, ProgramType, EventInfo
from app.models.responses import HealthResponse, ValidationResponse, ImportExcelResponse
from app.core.validator import validate_inputs, ValidationError, TemplateError, ScenarsError
from app.core.excel_reader import read_excel, parse_inline_schedule, parse_inline_types
from app.core.timetable import create_timetable
router = APIRouter()
@router.get("/health", response_model=HealthResponse)
async def health():
return HealthResponse(version=VERSION)
@router.post("/validate", response_model=ValidationResponse)
async def validate_scenario(doc: ScenarioDocument):
"""Validate a ScenarioDocument."""
errors = []
if not doc.blocks:
errors.append("No blocks defined")
if not doc.program_types:
errors.append("No program types defined")
type_codes = {pt.code for pt in doc.program_types}
for i, block in enumerate(doc.blocks):
if block.typ not in type_codes:
errors.append(f"Block {i+1}: unknown type '{block.typ}'")
if block.zacatek >= block.konec:
errors.append(f"Block {i+1}: start time must be before end time")
return ValidationResponse(valid=len(errors) == 0, errors=errors)
@router.post("/import-excel")
async def import_excel(
file: UploadFile = File(...),
title: str = Form("Imported Event"),
detail: str = Form("Imported from Excel"),
):
"""Upload Excel file and return ScenarioDocument JSON."""
content = await file.read()
if len(content) > MAX_FILE_SIZE_MB * 1024 * 1024:
raise HTTPException(status_code=413, detail=f"File exceeds {MAX_FILE_SIZE_MB}MB limit")
try:
valid_data, error_rows = read_excel(content)
except TemplateError as e:
raise HTTPException(status_code=422, detail=str(e))
if valid_data.empty:
raise HTTPException(status_code=422, detail="No valid rows found in Excel file")
# Extract unique types
types_in_data = valid_data["Typ"].dropna().unique().tolist()
program_types = [
ProgramType(code=t, description=str(t), color="#0070C0")
for t in types_in_data
]
# Convert rows to blocks
blocks = []
for _, row in valid_data.iterrows():
blocks.append(Block(
datum=row["Datum"],
zacatek=row["Zacatek"],
konec=row["Konec"],
program=str(row["Program"]),
typ=str(row["Typ"]),
garant=str(row["Garant"]) if pd.notna(row.get("Garant")) else None,
poznamka=str(row["Poznamka"]) if pd.notna(row.get("Poznamka")) else None,
))
doc = ScenarioDocument(
event=EventInfo(title=title, detail=detail),
program_types=program_types,
blocks=blocks,
)
warnings = [f"Row {e['index']}: {e.get('error', e.get('Error', 'unknown'))}" for e in error_rows]
return ImportExcelResponse(
success=True,
document=doc.model_dump(mode="json"),
warnings=warnings,
)
@router.post("/generate-excel")
async def generate_excel(doc: ScenarioDocument):
"""Generate Excel timetable from ScenarioDocument."""
try:
validate_inputs(doc.event.title, doc.event.detail, 0)
except ValidationError as e:
raise HTTPException(status_code=422, detail=str(e))
# Convert to DataFrame
rows = []
for block in doc.blocks:
rows.append({
'Datum': block.datum,
'Zacatek': block.zacatek,
'Konec': block.konec,
'Program': block.program,
'Typ': block.typ,
'Garant': block.garant,
'Poznamka': block.poznamka,
})
df = pd.DataFrame(rows)
if df.empty:
raise HTTPException(status_code=422, detail="No blocks provided")
# Build program descriptions and colors
program_descriptions = {pt.code: pt.description for pt in doc.program_types}
program_colors = {pt.code: 'FF' + pt.color.lstrip('#') for pt in doc.program_types}
try:
wb = create_timetable(df, doc.event.title, doc.event.detail,
program_descriptions, program_colors)
except ScenarsError as e:
raise HTTPException(status_code=422, detail=str(e))
output = BytesIO()
wb.save(output)
output.seek(0)
return StreamingResponse(
output,
media_type="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
headers={"Content-Disposition": "attachment; filename=scenar_timetable.xlsx"}
)
@router.post("/export-json")
async def export_json(doc: ScenarioDocument):
"""Export ScenarioDocument as JSON."""
return doc.model_dump(mode="json")
@router.get("/template")
async def download_template():
"""Download the Excel template file."""
template_path = os.path.join(os.path.dirname(__file__), "..", "..", "templates", "scenar_template.xlsx")
template_path = os.path.abspath(template_path)
if not os.path.exists(template_path):
raise HTTPException(status_code=404, detail="Template file not found")
return FileResponse(
template_path,
media_type="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
filename="scenar_template.xlsx"
)