utility-app/app/routes/doc_generator.py

190 lines
5.3 KiB
Python

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",
)