from __future__ import annotations from datetime import datetime, timedelta, timezone from pathlib import Path from typing import Any from fastapi import FastAPI, HTTPException, Query from fastapi.responses import HTMLResponse from fastapi.staticfiles import StaticFiles from sqlalchemy import func, select from .db import SessionLocal, init_db from .ingest import ( ingest_car_data_for_session, ingest_practice_for_date, ingest_sessions_for_date, is_competitive_session, telemetry_series, telemetry_summary, ) from .models import Session as SessionModel from .openf1 import fetch_sessions_for_date app = FastAPI(title="OpenF1 Telemetry Hub", version="0.1.0") BASE_DIR = Path(__file__).resolve().parents[1] DASHBOARD_DIR = BASE_DIR / "dashboard" @app.on_event("startup") async def startup() -> None: init_db() @app.get("/health") def health() -> dict[str, str]: return {"status": "ok"} @app.post("/ingest/test") def ingest_test( session_key: int = 9159, driver_number: int = 55, speed_min: float = 300, ) -> dict[str, Any]: with SessionLocal() as db: inserted = ingest_car_data_for_session( db, session_key=session_key, driver_number=driver_number, speed_min=speed_min, ) return { "session_key": session_key, "driver_number": driver_number, "speed_min": speed_min, "inserted": inserted, } @app.post("/ingest/sessions") def ingest_sessions(date: str | None = None) -> dict[str, Any]: target_date = date or datetime.now(timezone.utc).date().isoformat() with SessionLocal() as db: inserted = ingest_sessions_for_date(db, target_date) return {"date": target_date, "inserted": inserted} @app.post("/ingest/practice") def ingest_practice(date: str | None = None) -> dict[str, Any]: target_date = date or datetime.now(timezone.utc).date().isoformat() with SessionLocal() as db: result = ingest_practice_for_date(db, target_date) return {"date": target_date, **result} @app.get("/sessions") def list_sessions( date: str | None = None, session_types: str | None = None, refresh: bool = False, ) -> list[dict[str, Any]]: with SessionLocal() as db: if refresh and date: ingest_sessions_for_date(db, date) stmt = select(SessionModel) if date: try: day_start = datetime.fromisoformat(date).replace(tzinfo=timezone.utc) except ValueError: day_start = datetime.now(timezone.utc).replace(hour=0, minute=0, second=0, microsecond=0) day_end = day_start + timedelta(days=1) stmt = stmt.where(SessionModel.date_start >= day_start, SessionModel.date_start < day_end) if session_types: requested = [item.strip().lower() for item in session_types.split(",") if item.strip()] if requested: stmt = stmt.where(func.lower(SessionModel.session_type).in_(requested)) sessions = db.execute(stmt.order_by(SessionModel.date_start.asc())).scalars().all() return [ { "session_key": session.session_key, "meeting_key": session.meeting_key, "session_name": session.session_name, "session_type": session.session_type, "country_name": session.country_name, "date_start": session.date_start.isoformat() if session.date_start else None, "date_end": session.date_end.isoformat() if session.date_end else None, } for session in sessions ] @app.get("/sessions/upcoming") def upcoming_sessions( minutes: int = 120, date: str | None = None, ) -> list[dict[str, Any]]: target_date = date or datetime.now(timezone.utc).date().isoformat() sessions = fetch_sessions_for_date(target_date) now = datetime.now(timezone.utc) window_end = now + timedelta(minutes=minutes) upcoming: list[dict[str, Any]] = [] with SessionLocal() as db: ingest_sessions_for_date(db, target_date) for session in sessions: session_type = session.get("session_type") or session.get("session_name") or "" if not is_competitive_session(session_type): continue start = session.get("date_start") if not start: continue if start.endswith("Z"): start = start.replace("Z", "+00:00") try: start_dt = datetime.fromisoformat(start) except ValueError: continue if start_dt < now or start_dt > window_end: continue delta_minutes = int((start_dt - now).total_seconds() / 60) upcoming.append( { "session_key": session.get("session_key"), "session_name": session.get("session_name"), "session_type": session_type, "country_name": session.get("country_name"), "date_start": start_dt.isoformat(), "minutes_to_start": delta_minutes, } ) return sorted(upcoming, key=lambda item: item["minutes_to_start"]) @app.get("/telemetry/summary") def get_telemetry_summary(session_key: int = Query(..., description="OpenF1 session key")) -> list[dict[str, Any]]: with SessionLocal() as db: rows = telemetry_summary(db, session_key) if not rows: raise HTTPException(status_code=404, detail="No telemetry found for this session") return rows @app.get("/telemetry/series") def get_telemetry_series( session_key: int = Query(..., description="OpenF1 session key"), driver_number: int = Query(..., description="Driver number"), limit: int = Query(300, ge=50, le=2000), ) -> list[dict[str, Any]]: with SessionLocal() as db: rows = telemetry_series(db, session_key=session_key, driver_number=driver_number, limit=limit) if not rows: raise HTTPException(status_code=404, detail="No telemetry series found for this driver") return rows @app.get("/") def dashboard() -> HTMLResponse: index_path = DASHBOARD_DIR / "index.html" if not index_path.exists(): raise HTTPException(status_code=404, detail="Dashboard not found. Build the frontend first.") return HTMLResponse(index_path.read_text(encoding="utf-8")) if DASHBOARD_DIR.exists(): app.mount("/static", StaticFiles(directory=str(DASHBOARD_DIR)), name="static")