| 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") |
|
|