internationalscholarsprogram/handbook-engine
0
1"""API router — handbook endpoints.2 3Exposes REST endpoints that the PHP application calls over HTTP.4"""5 6from __future__ import annotations7 8import logging9from typing import Any10 11from fastapi import APIRouter, HTTPException, Query12from fastapi.responses import HTMLResponse, Response13 14from app.schemas.handbook import (15 ErrorResponse,16 FontDiagnosticsResponse,17 GlobalSectionsResponse,18 HandbookRequest,19 HealthResponse,20 SectionItem,21 UniversitySectionsResponse,22 UniversityPayload,23)24 25logger = logging.getLogger(__name__)26 27router = APIRouter()28 29 30# ── Root / HF health probe ──31 32@router.get("/", tags=["system"])33async def root():34 """Root endpoint — HF Spaces probes this URL for health checks."""35 return {"status": "ok"}36 37 38# ── Health check ──39 40@router.get("/health", response_model=HealthResponse, tags=["system"])41async def health_check():42 """Health check endpoint."""43 from app.core.config import get_settings44 settings = get_settings()45 return HealthResponse(46 status="ok",47 service=settings.app_name,48 version=settings.app_version,49 )50 51 52# ── Font diagnostics ──53 54@router.get("/diagnostics/fonts", tags=["system"])55async def font_diagnostics():56 """Font diagnostics endpoint. Mirrors PHP font_diagnostics.php."""57 from app.core.fonts import font_diagnostics as _diag58 try:59 result = _diag()60 return result61 except Exception as exc:62 raise HTTPException(status_code=500, detail=str(exc))63 64 65# ── Global sections (proxy/fetch) ──66 67@router.get("/api/v1/sections/global", tags=["sections"])68async def get_global_sections(catalog_id: int = Query(0, description="Catalog ID filter")):69 """Fetch global handbook sections from the upstream API.70 71 Returns normalised section data identical to what the PHP code produces.72 """73 from app.services.data_fetcher import fetch_global_sections74 75 try:76 sections = await fetch_global_sections(catalog_id)77 return {78 "ok": True,79 "general_sections": sections,80 "count": len(sections),81 }82 except Exception as exc:83 logger.exception("Failed to fetch global sections")84 raise HTTPException(status_code=502, detail=str(exc))85 86 87# ── University sections (proxy/fetch) ──88 89@router.get("/api/v1/sections/universities", tags=["sections"])90async def get_university_sections():91 """Fetch university handbook sections from the upstream API."""92 from app.services.data_fetcher import fetch_university_sections93 94 try:95 by_uni = await fetch_university_sections()96 return {97 "ok": True,98 "universities": by_uni,99 "count": len(by_uni),100 }101 except Exception as exc:102 logger.exception("Failed to fetch university sections")103 raise HTTPException(status_code=502, detail=str(exc))104 105 106# ── Generate handbook (HTML or PDF) ──107 108@router.get("/api/v1/handbook/pdf", tags=["handbook"])109async def generate_handbook_pdf_get(110 catalog_id: int = Query(0),111 include_inactive_programs: bool = Query(False),112 debug: bool = Query(False),113):114 """Generate the ISP Handbook as a PDF download (GET for easy PHP integration)."""115 from app.services.pdf_service import generate_handbook_pdf116 117 try:118 pdf_bytes = await generate_handbook_pdf(119 catalog_id=catalog_id,120 include_inactive_programs=include_inactive_programs,121 debug=debug,122 )123 return Response(124 content=pdf_bytes,125 media_type="application/pdf",126 headers={127 "Content-Disposition": 'attachment; filename="ISP_Handbook.pdf"',128 "Cache-Control": "private, max-age=0, must-revalidate",129 },130 )131 except Exception as exc:132 logger.exception("PDF generation failed")133 raise HTTPException(status_code=500, detail=str(exc))134 135 136@router.post("/api/v1/handbook/pdf", tags=["handbook"])137async def generate_handbook_pdf_post(request: HandbookRequest):138 """Generate the ISP Handbook as a PDF download (POST with body)."""139 from app.services.pdf_service import generate_handbook_pdf140 141 try:142 pdf_bytes = await generate_handbook_pdf(143 catalog_id=request.catalog_id,144 include_inactive_programs=request.include_inactive_programs,145 debug=request.debug,146 )147 return Response(148 content=pdf_bytes,149 media_type="application/pdf",150 headers={151 "Content-Disposition": 'attachment; filename="ISP_Handbook.pdf"',152 "Cache-Control": "private, max-age=0, must-revalidate",153 },154 )155 except Exception as exc:156 logger.exception("PDF generation failed")157 raise HTTPException(status_code=500, detail=str(exc))158 159 160@router.get("/api/v1/handbook/html", tags=["handbook"])161async def generate_handbook_html_get(162 catalog_id: int = Query(0),163 include_inactive_programs: bool = Query(False),164 debug: bool = Query(False),165):166 """Generate the ISP Handbook as raw HTML (useful for preview/debugging)."""167 from app.services.pdf_service import generate_handbook_html168 169 try:170 html = await generate_handbook_html(171 catalog_id=catalog_id,172 include_inactive_programs=include_inactive_programs,173 debug=debug,174 )175 return HTMLResponse(content=html)176 except Exception as exc:177 logger.exception("HTML generation failed")178 raise HTTPException(status_code=500, detail=str(exc))179 180 181@router.post("/api/v1/handbook/render", tags=["handbook"])182async def render_handbook(request: HandbookRequest):183 """Generate handbook in the requested format (pdf or html)."""184 if request.output_format == "html":185 from app.services.pdf_service import generate_handbook_html186 try:187 html = await generate_handbook_html(188 catalog_id=request.catalog_id,189 include_inactive_programs=request.include_inactive_programs,190 debug=request.debug,191 )192 return HTMLResponse(content=html)193 except Exception as exc:194 logger.exception("HTML generation failed")195 raise HTTPException(status_code=500, detail=str(exc))196 else:197 from app.services.pdf_service import generate_handbook_pdf198 try:199 pdf_bytes = await generate_handbook_pdf(200 catalog_id=request.catalog_id,201 include_inactive_programs=request.include_inactive_programs,202 debug=request.debug,203 )204 return Response(205 content=pdf_bytes,206 media_type="application/pdf",207 headers={208 "Content-Disposition": 'attachment; filename="ISP_Handbook.pdf"',209 "Cache-Control": "private, max-age=0, must-revalidate",210 },211 )212 except Exception as exc:213 logger.exception("PDF generation failed")214 raise HTTPException(status_code=500, detail=str(exc))215 