"""Фронтенд расписания: статики + прокси к API бэкенда.""" from __future__ import annotations import logging import os from pathlib import Path import requests import uvicorn from dotenv import load_dotenv from fastapi import FastAPI, HTTPException from fastapi.responses import FileResponse, JSONResponse from fastapi.staticfiles import StaticFiles log = logging.getLogger(__name__) logging.basicConfig(level=logging.INFO, format="%(asctime)s %(levelname)s %(name)s: %(message)s") def _find_dotenv() -> Path | None: """Ищем .env вверх от каталога модуля: один общий .env в корне проекта.""" path = Path(__file__).resolve().parent for _ in range(5): env = path / ".env" if env.is_file(): return env if path.parent == path: break path = path.parent return None _dotenv_path = _find_dotenv() if _dotenv_path: load_dotenv(_dotenv_path) # Адрес бэкенда: в docker это http://api:8000 (задаётся в docker-compose), # локально — http://127.0.0.1:8000. SCHEDULE_API = os.getenv("SCHEDULE_API", "http://127.0.0.1:8000").rstrip("/") # Бэкенд на холодном кэше скачивает таблицу (до 2 минут), таймаут выше. PROXY_TIMEOUT = float(os.getenv("SCHEDULE_PROXY_TIMEOUT", "150")) STATIC_DIR = Path(__file__).parent / "static" app = FastAPI(title="Расписание — фронт") @app.get("/healthz") def healthz() -> dict: """Живость процесса; не зависит от бэкенда.""" return {"ok": True} def _proxy(path: str, params: dict) -> object: try: resp = requests.get(f"{SCHEDULE_API}/{path}", params=params, timeout=PROXY_TIMEOUT) except requests.RequestException as exc: raise HTTPException(status_code=502, detail=f"Бэкенд недоступен: {exc}") if resp.status_code != 200: try: detail = resp.json().get("detail", resp.text) except Exception: detail = resp.text raise HTTPException(status_code=resp.status_code, detail=detail) # данные не кэшируем: браузер всегда показывает актуальное расписание return JSONResponse(content=resp.json(), headers={"Cache-Control": "no-store"}) # --- API-маршруты ----------------------------------------------------------- @app.get("/api/groups") def api_groups(): return _proxy("groups", {}) @app.get("/api/schedule/{group}") def api_schedule(group: str): return _proxy(f"schedule/{group}", {}) @app.get("/api/teachers") def api_teachers(): return _proxy("teachers", {}) @app.get("/api/rooms") def api_rooms(): return _proxy("rooms", {}) @app.get("/api/teacher/{name}") def api_teacher(name: str): return _proxy(f"teacher/{name}", {}) @app.get("/api/room/{label}") def api_room(label: str): return _proxy(f"room/{label}", {}) @app.get("/api/rooms-free") def api_rooms_free(day: str, period: int): return _proxy("rooms-free", {"day": day, "period": period}) @app.get("/api/rooms-day") def api_rooms_day(day: str): return _proxy("rooms-day", {"day": day}) @app.get("/api/week") def api_week(): return _proxy("week", {}) @app.get("/api/status") def api_status(): return _proxy("status", {}) # --- статики ----------------------------------------------------------------- @app.get("/") def index(): return FileResponse(STATIC_DIR / "index.html") app.mount("/", StaticFiles(directory=STATIC_DIR, html=True), name="static") if __name__ == "__main__": uvicorn.run("main:app", host="127.0.0.1", port=8080, reload=True)