| """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() |
|
|
|
|
| |
|
|
| @router.get("/", tags=["system"]) |
| async def root(): |
| """Root endpoint β HF Spaces probes this URL for health checks.""" |
| return {"status": "ok"} |
|
|
|
|
| |
|
|
| @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, |
| ) |
|
|
|
|
| |
|
|
| @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)) |
|
|
|
|
| |
|
|
| @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)) |
|
|
|
|
| |
|
|
| @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)) |
|
|
|
|
| |
|
|
| @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)) |
|
|