import csv import json import re from pathlib import Path from fastapi import APIRouter, File, HTTPException, UploadFile from fastapi.responses import FileResponse from pydantic import BaseModel from tools.doc_generator.logic.document_types import get_document_type, list_document_types from tools.doc_generator.logic.renderer import generate_docx router = APIRouter() PROJECT_ROOT = Path(__file__).resolve().parent.parent.parent EXPORTS_DIR = PROJECT_ROOT / "exports" INPUTS_DIR = PROJECT_ROOT / "inputs" UPLOADS_DIR = INPUTS_DIR / "uploads" JSON_UPLOADS_DIR = UPLOADS_DIR / "json" DATA_UPLOADS_DIR = UPLOADS_DIR / "data" class GenerateDocRequest(BaseModel): document_type_id: str data: dict def safe_filename(filename: str) -> str: filename = Path(filename or "upload").name filename = re.sub(r"[^A-Za-z0-9._ -]+", "", filename) filename = re.sub(r"\s+", "_", filename).strip("._ ") return filename or "upload" def save_upload(upload: UploadFile, directory: Path) -> Path: directory.mkdir(parents=True, exist_ok=True) filename = safe_filename(upload.filename) path = directory / filename counter = 1 while path.exists(): stem = path.stem suffix = path.suffix path = directory / f"{stem}_{counter}{suffix}" counter += 1 with path.open("wb") as f: f.write(upload.file.read()) return path @router.get("/document-types") def document_types(): return {"document_types": list_document_types()} @router.get("/document-types/{document_type_id}") def document_type(document_type_id: str): try: return get_document_type(document_type_id) except FileNotFoundError as exc: raise HTTPException(status_code=404, detail=str(exc)) @router.post("/upload-json") async def upload_json(file: UploadFile = File(...)): if not file.filename.lower().endswith(".json"): raise HTTPException(status_code=400, detail="Upload must be a JSON file.") path = save_upload(file, JSON_UPLOADS_DIR) try: data = json.loads(path.read_text(encoding="utf-8")) except Exception as exc: raise HTTPException(status_code=400, detail=f"Invalid JSON: {exc}") return { "ok": True, "filename": path.name, "saved_path": str(path.relative_to(PROJECT_ROOT)), "json": data, } @router.get("/uploaded-json") def uploaded_json_files(): JSON_UPLOADS_DIR.mkdir(parents=True, exist_ok=True) files = [] for path in sorted(JSON_UPLOADS_DIR.glob("*.json")): files.append({ "filename": path.name, "saved_path": str(path.relative_to(PROJECT_ROOT)), "modified": path.stat().st_mtime, }) return {"files": files} @router.get("/uploaded-json/{filename}") def get_uploaded_json(filename: str): file_path = JSON_UPLOADS_DIR / safe_filename(filename) if not file_path.exists(): raise HTTPException(status_code=404, detail="Uploaded JSON not found.") try: data = json.loads(file_path.read_text(encoding="utf-8")) except Exception as exc: raise HTTPException(status_code=400, detail=f"Invalid JSON: {exc}") return { "ok": True, "filename": file_path.name, "saved_path": str(file_path.relative_to(PROJECT_ROOT)), "json": data, } @router.delete("/uploaded-json/{filename}") def delete_uploaded_json(filename: str): file_path = JSON_UPLOADS_DIR / safe_filename(filename) if not file_path.exists(): raise HTTPException(status_code=404, detail="Uploaded JSON not found.") file_path.unlink() return { "ok": True, "deleted": file_path.name, } @router.post("/upload-data") async def upload_data(file: UploadFile = File(...)): if not file.filename.lower().endswith(".csv"): raise HTTPException(status_code=400, detail="For now, upload data as CSV.") path = save_upload(file, DATA_UPLOADS_DIR) try: with path.open("r", encoding="utf-8-sig", newline="") as f: reader = csv.DictReader(f) rows = list(reader) except Exception as exc: raise HTTPException(status_code=400, detail=f"Could not read CSV: {exc}") if not rows: raise HTTPException(status_code=400, detail="CSV has no data rows.") return { "ok": True, "filename": path.name, "saved_path": str(path.relative_to(PROJECT_ROOT)), "data": rows[0], "row_count": len(rows), } @router.post("/generate") def generate_document(request: GenerateDocRequest): try: output_path = generate_docx(request.document_type_id, request.data) except FileNotFoundError as exc: raise HTTPException(status_code=404, detail=str(exc)) except Exception as exc: raise HTTPException(status_code=400, detail=str(exc)) return { "ok": True, "filename": output_path.name, "download_url": f"/api/doc-generator/download/{output_path.name}" } @router.get("/download/{filename}") def download(filename: str): file_path = EXPORTS_DIR / safe_filename(filename) if not file_path.exists(): raise HTTPException(status_code=404, detail="File not found") return FileResponse( path=file_path, filename=file_path.name, media_type="application/vnd.openxmlformats-officedocument.wordprocessingml.document", )