handbook_engine / app /api /routes.py
internationalscholarsprogram's picture
fix: add root endpoint for HF Spaces health probe
c701395
"""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))