"""API router — handbook endpoints. Exposes REST endpoints that the PHP application calls over HTTP. """ from __future__ import annotations import logging from typing import Any from fastapi import APIRouter, HTTPException, Query from fastapi.responses import HTMLResponse, Response from app.schemas.handbook import ( ErrorResponse, FontDiagnosticsResponse, GlobalSectionsResponse, HandbookRequest, HealthResponse, SectionItem, UniversitySectionsResponse, UniversityPayload, ) logger = logging.getLogger(__name__) router = APIRouter() # ── Root / HF health probe ── @router.get("/", tags=["system"]) async def root(): """Root endpoint — HF Spaces probes this URL for health checks.""" return {"status": "ok"} # ── Health check ── @router.get("/health", response_model=HealthResponse, tags=["system"]) async def health_check(): """Health check endpoint.""" from app.core.config import get_settings settings = get_settings() return HealthResponse( status="ok", service=settings.app_name, version=settings.app_version, ) # ── Font diagnostics ── @router.get("/diagnostics/fonts", tags=["system"]) async def font_diagnostics(): """Font diagnostics endpoint. Mirrors PHP font_diagnostics.php.""" from app.core.fonts import font_diagnostics as _diag try: result = _diag() return result except Exception as exc: raise HTTPException(status_code=500, detail=str(exc)) # ── Global sections (proxy/fetch) ── @router.get("/api/v1/sections/global", tags=["sections"]) async def get_global_sections(catalog_id: int = Query(0, description="Catalog ID filter")): """Fetch global handbook sections from the upstream API. Returns normalised section data identical to what the PHP code produces. """ from app.services.data_fetcher import fetch_global_sections try: sections = await fetch_global_sections(catalog_id) return { "ok": True, "general_sections": sections, "count": len(sections), } except Exception as exc: logger.exception("Failed to fetch global sections") raise HTTPException(status_code=502, detail=str(exc)) # ── University sections (proxy/fetch) ── @router.get("/api/v1/sections/universities", tags=["sections"]) async def get_university_sections(): """Fetch university handbook sections from the upstream API.""" from app.services.data_fetcher import fetch_university_sections try: by_uni = await fetch_university_sections() return { "ok": True, "universities": by_uni, "count": len(by_uni), } except Exception as exc: logger.exception("Failed to fetch university sections") raise HTTPException(status_code=502, detail=str(exc)) # ── Generate handbook (HTML or PDF) ── @router.get("/api/v1/handbook/pdf", tags=["handbook"]) async def generate_handbook_pdf_get( catalog_id: int = Query(0), include_inactive_programs: bool = Query(False), debug: bool = Query(False), ): """Generate the ISP Handbook as a PDF download (GET for easy PHP integration).""" from app.services.pdf_service import generate_handbook_pdf try: pdf_bytes = await generate_handbook_pdf( catalog_id=catalog_id, include_inactive_programs=include_inactive_programs, debug=debug, ) return Response( content=pdf_bytes, media_type="application/pdf", headers={ "Content-Disposition": 'attachment; filename="ISP_Handbook.pdf"', "Cache-Control": "private, max-age=0, must-revalidate", }, ) except Exception as exc: logger.exception("PDF generation failed") raise HTTPException(status_code=500, detail=str(exc)) @router.post("/api/v1/handbook/pdf", tags=["handbook"]) async def generate_handbook_pdf_post(request: HandbookRequest): """Generate the ISP Handbook as a PDF download (POST with body).""" from app.services.pdf_service import generate_handbook_pdf try: pdf_bytes = await generate_handbook_pdf( catalog_id=request.catalog_id, include_inactive_programs=request.include_inactive_programs, debug=request.debug, ) return Response( content=pdf_bytes, media_type="application/pdf", headers={ "Content-Disposition": 'attachment; filename="ISP_Handbook.pdf"', "Cache-Control": "private, max-age=0, must-revalidate", }, ) except Exception as exc: logger.exception("PDF generation failed") raise HTTPException(status_code=500, detail=str(exc)) @router.get("/api/v1/handbook/html", tags=["handbook"]) async def generate_handbook_html_get( catalog_id: int = Query(0), include_inactive_programs: bool = Query(False), debug: bool = Query(False), ): """Generate the ISP Handbook as raw HTML (useful for preview/debugging).""" from app.services.pdf_service import generate_handbook_html try: html = await generate_handbook_html( catalog_id=catalog_id, include_inactive_programs=include_inactive_programs, debug=debug, ) return HTMLResponse(content=html) except Exception as exc: logger.exception("HTML generation failed") raise HTTPException(status_code=500, detail=str(exc)) @router.post("/api/v1/handbook/render", tags=["handbook"]) async def render_handbook(request: HandbookRequest): """Generate handbook in the requested format (pdf or html).""" if request.output_format == "html": from app.services.pdf_service import generate_handbook_html try: html = await generate_handbook_html( catalog_id=request.catalog_id, include_inactive_programs=request.include_inactive_programs, debug=request.debug, ) return HTMLResponse(content=html) except Exception as exc: logger.exception("HTML generation failed") raise HTTPException(status_code=500, detail=str(exc)) else: from app.services.pdf_service import generate_handbook_pdf try: pdf_bytes = await generate_handbook_pdf( catalog_id=request.catalog_id, include_inactive_programs=request.include_inactive_programs, debug=request.debug, ) return Response( content=pdf_bytes, media_type="application/pdf", headers={ "Content-Disposition": 'attachment; filename="ISP_Handbook.pdf"', "Cache-Control": "private, max-age=0, must-revalidate", }, ) except Exception as exc: logger.exception("PDF generation failed") raise HTTPException(status_code=500, detail=str(exc))