| from __future__ import annotations |
|
|
| from datetime import datetime, timedelta, timezone |
| from typing import Any |
|
|
| from sqlalchemy import func, select |
| from sqlalchemy.orm import Session |
|
|
| from .models import CarData, Session as SessionModel |
| from .openf1 import fetch_car_data, fetch_sessions, fetch_sessions_for_date |
|
|
|
|
| def _parse_dt(value: str | None) -> datetime | None: |
| if not value: |
| return None |
| try: |
| if value.endswith("Z"): |
| value = value.replace("Z", "+00:00") |
| return datetime.fromisoformat(value) |
| except ValueError: |
| return None |
|
|
|
|
| def _session_type(value: str | None, fallback: str | None) -> str: |
| return (value or fallback or "").strip() |
|
|
|
|
| def is_practice_session(session_type: str) -> bool: |
| return "practice" in session_type.lower() |
|
|
|
|
| def is_competitive_session(session_type: str) -> bool: |
| lowered = session_type.lower() |
| return any(keyword in lowered for keyword in ("qualifying", "sprint", "race")) |
|
|
|
|
| def ingest_sessions_for_date(db: Session, date_iso: str) -> int: |
| sessions = fetch_sessions_for_date(date_iso) |
| inserted = 0 |
| for item in sessions: |
| session_key = item.get("session_key") |
| if session_key is None: |
| continue |
| exists = db.get(SessionModel, session_key) |
| if exists: |
| continue |
| session_type = _session_type(item.get("session_type"), item.get("session_name")) |
| db.add( |
| SessionModel( |
| session_key=session_key, |
| meeting_key=item.get("meeting_key"), |
| session_name=item.get("session_name"), |
| session_type=session_type, |
| country_name=item.get("country_name"), |
| date_start=_parse_dt(item.get("date_start")), |
| date_end=_parse_dt(item.get("date_end")), |
| ) |
| ) |
| inserted += 1 |
| if inserted: |
| db.commit() |
| return inserted |
|
|
|
|
| def ingest_sessions_with_params(db: Session, params: dict[str, Any]) -> int: |
| sessions = fetch_sessions(params=params) |
| inserted = 0 |
| for item in sessions: |
| session_key = item.get("session_key") |
| if session_key is None: |
| continue |
| exists = db.get(SessionModel, session_key) |
| if exists: |
| continue |
| session_type = _session_type(item.get("session_type"), item.get("session_name")) |
| db.add( |
| SessionModel( |
| session_key=session_key, |
| meeting_key=item.get("meeting_key"), |
| session_name=item.get("session_name"), |
| session_type=session_type, |
| country_name=item.get("country_name"), |
| date_start=_parse_dt(item.get("date_start")), |
| date_end=_parse_dt(item.get("date_end")), |
| ) |
| ) |
| inserted += 1 |
| if inserted: |
| db.commit() |
| return inserted |
|
|
|
|
| def has_car_data(db: Session, session_key: int) -> bool: |
| stmt = select(func.count(CarData.id)).where(CarData.session_key == session_key) |
| return (db.execute(stmt).scalar() or 0) > 0 |
|
|
|
|
| def ingest_car_data_for_session( |
| db: Session, |
| *, |
| session_key: int, |
| driver_number: int | None = None, |
| speed_min: float | None = None, |
| ) -> int: |
| records = fetch_car_data( |
| session_key=session_key, |
| driver_number=driver_number, |
| speed_min=speed_min, |
| ) |
| inserted = 0 |
| for item in records: |
| db.add( |
| CarData( |
| session_key=item.get("session_key"), |
| driver_number=item.get("driver_number"), |
| date=_parse_dt(item.get("date")), |
| speed=item.get("speed"), |
| throttle=item.get("throttle"), |
| brake=item.get("brake"), |
| n_gear=item.get("n_gear"), |
| ) |
| ) |
| inserted += 1 |
| if inserted: |
| db.commit() |
| return inserted |
|
|
|
|
| def ingest_practice_for_date(db: Session, date_iso: str) -> dict[str, int]: |
| inserted_sessions = ingest_sessions_for_date(db, date_iso) |
| try: |
| date_start = datetime.fromisoformat(date_iso).replace(tzinfo=timezone.utc) |
| except ValueError: |
| date_start = datetime.now(timezone.utc).replace(hour=0, minute=0, second=0, microsecond=0) |
| date_end = date_start + timedelta(days=1) |
|
|
| stmt = select(SessionModel).where( |
| SessionModel.date_start.isnot(None), |
| SessionModel.date_start >= date_start, |
| SessionModel.date_start < date_end, |
| ) |
| sessions = db.execute(stmt).scalars().all() |
|
|
| ingested_sessions = 0 |
| ingested_samples = 0 |
| for session in sessions: |
| session_type = session.session_type or session.session_name or "" |
| if not is_practice_session(session_type): |
| continue |
| if has_car_data(db, session.session_key): |
| continue |
| ingested_sessions += 1 |
| ingested_samples += ingest_car_data_for_session(db, session_key=session.session_key) |
| return { |
| "sessions_added": inserted_sessions, |
| "practice_sessions_ingested": ingested_sessions, |
| "samples_ingested": ingested_samples, |
| } |
|
|
|
|
| def telemetry_summary(db: Session, session_key: int) -> list[dict[str, Any]]: |
| stmt = ( |
| select( |
| CarData.driver_number, |
| func.avg(CarData.speed).label("avg_speed"), |
| func.max(CarData.speed).label("max_speed"), |
| func.count(CarData.id).label("samples"), |
| ) |
| .where(CarData.session_key == session_key) |
| .group_by(CarData.driver_number) |
| .order_by(func.avg(CarData.speed).desc()) |
| ) |
| rows = db.execute(stmt).all() |
| return [ |
| { |
| "driver_number": row.driver_number, |
| "avg_speed": float(row.avg_speed or 0), |
| "max_speed": float(row.max_speed or 0), |
| "samples": int(row.samples or 0), |
| } |
| for row in rows |
| ] |
|
|
|
|
| def telemetry_series( |
| db: Session, |
| *, |
| session_key: int, |
| driver_number: int, |
| limit: int = 300, |
| ) -> list[dict[str, Any]]: |
| stmt = ( |
| select( |
| CarData.date, |
| CarData.speed, |
| CarData.throttle, |
| CarData.brake, |
| CarData.n_gear, |
| ) |
| .where(CarData.session_key == session_key, CarData.driver_number == driver_number) |
| .order_by(CarData.date.desc()) |
| .limit(limit) |
| ) |
| rows = db.execute(stmt).all() |
| return [ |
| { |
| "date": row.date.isoformat() if row.date else None, |
| "speed": float(row.speed or 0), |
| "throttle": float(row.throttle or 0), |
| "brake": float(row.brake or 0), |
| "n_gear": int(row.n_gear or 0), |
| } |
| for row in reversed(rows) |
| ] |
|
|