homelab / services /api /app.py
nj9997
v1.1.2
b10d399
Raw
History Blame Contribute Delete
6.49 kB
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")