Compare commits

..

16 Commits

Author SHA1 Message Date
6da5919f86 Add a shitload of caching
Some checks are pending
Build Formula10 Docker Image / build-docker (push) Waiting to run
2024-09-24 00:01:41 +02:00
852f0a04ca Add profiler (currently disabled) 2024-09-24 00:00:41 +02:00
aa38d0138f Update FastF1 frontend button description 2024-09-23 21:23:38 +02:00
a9ab892c2a Add functions to obtain specific sessions to EventDataHelper 2024-09-23 21:23:20 +02:00
e6157893ae Remove deprecated import 2024-09-23 17:13:05 +02:00
b2a01ce453 Use column name string when sorting by column 2024-09-23 17:12:55 +02:00
5234e8a06e Add helper classes for FastF1 dataframes 2024-09-23 16:52:23 +02:00
a965c0007b Prefix OpenF1 functions 2024-09-23 16:52:11 +02:00
9acced3899 Add werkzeug to requirements 2024-09-23 16:52:00 +02:00
ed0c5f51b4 Add __repr__ to Race class 2024-09-23 16:51:18 +02:00
ebb862c9d7 Add Pandas to requirements 2024-09-23 16:50:48 +02:00
4a0eb5329c Set FastF1 cache location in dockerfile 2024-09-23 16:50:35 +02:00
30bf475000 Prefix OpenF1 classes + functions with "openf1" 2024-09-22 23:55:54 +02:00
d66087142d Add fastf1 module 2024-09-22 23:34:16 +02:00
8bdb3b5207 Add fastf1 python dependency to requirements 2024-09-22 23:01:22 +02:00
2e685ed646 Add fastf1 python dependency 2024-09-22 22:00:38 +02:00
20 changed files with 784 additions and 249 deletions

View File

@ -1,10 +1,17 @@
# syntax=docker/dockerfile:1 # syntax=docker/dockerfile:1
FROM public.ecr.aws/docker/library/python:3.11.8-slim-bookworm FROM public.ecr.aws/docker/library/python:3.11.8-slim-bookworm
RUN apt-get update -y RUN apt-get update -y
WORKDIR /app WORKDIR /app
COPY requirements.txt requirements.txt COPY requirements.txt requirements.txt
RUN pip3 install -r requirements.txt RUN pip3 install -r requirements.txt
COPY . . COPY . .
EXPOSE 5000 EXPOSE 5000
ENV FASTF1_CACHE="/cache"
CMD ["python3", "-u", "-m", "flask", "--app", "formula10", "run", "--host", "0.0.0.0"] CMD ["python3", "-u", "-m", "flask", "--app", "formula10", "run", "--host", "0.0.0.0"]

165
flake.nix
View File

@ -5,64 +5,141 @@
inputs.flake-utils.url = "github:numtide/flake-utils"; inputs.flake-utils.url = "github:numtide/flake-utils";
inputs.devshell.url = "github:numtide/devshell"; inputs.devshell.url = "github:numtide/devshell";
outputs = { self, nixpkgs, flake-utils, devshell }: outputs = {
flake-utils.lib.eachDefaultSystem (system: self,
let nixpkgs,
pkgs = import nixpkgs { flake-utils,
inherit system; devshell,
config.allowUnfree = true; }:
overlays = [ devshell.overlays.default ]; flake-utils.lib.eachDefaultSystem (system: let
pkgs = import nixpkgs {
inherit system;
config.allowUnfree = true;
overlays = [devshell.overlays.default];
};
timple = pkgs.python311Packages.buildPythonPackage rec {
pname = "timple";
version = "0.1.8";
src = pkgs.python311Packages.fetchPypi {
inherit pname version;
hash = "sha256-u8EgMA8BA6OpPlSg0ASRxLcIcv5psRIEcBpIicagXw8=";
}; };
myPython = pkgs.python311.withPackages (p: with p; [ doCheck = false;
pyproject = false;
# Build time deps
nativeBuildInputs = with pkgs.python311Packages; [
setuptools
];
# Run time deps
dependencies = with pkgs.python311Packages; [
matplotlib
numpy
];
};
fastf1 = pkgs.python311Packages.buildPythonPackage rec {
pname = "fastf1";
version = "3.4.0";
src = pkgs.python311Packages.fetchPypi {
inherit pname version;
hash = "sha256-rCjJaM0W2m9Yk3dhHkMOdIqPiKtVqoXuELBasmA9ybA=";
};
doCheck = false;
pyproject = true;
# Build time deps
nativeBuildInputs = with pkgs.python311Packages; [
hatchling
hatch-vcs
];
# Run time deps
dependencies = with pkgs.python311Packages; [
matplotlib
numpy
pandas
python-dateutil
requests
requests-cache
scipy
rapidfuzz
websockets
timple
];
};
myPython = pkgs.python311.withPackages (p:
with p; [
# Basic # Basic
rich rich
numpy
# Web # Web
flask flask
flask-sqlalchemy flask-sqlalchemy
flask-caching
sqlalchemy sqlalchemy
requests requests
# Test
pytest pytest
fastf1
# TODO: For some reason, listing those under fastf1.dependencies doesn't work???
matplotlib
numpy
pandas
python-dateutil
requests-cache
scipy
rapidfuzz
websockets
timple
]); ]);
in { in {
devShell = pkgs.devshell.mkShell { devShell = pkgs.devshell.mkShell {
name = "Formula10"; name = "Formula10";
packages = with pkgs; [ packages = with pkgs; [
myPython myPython
nodejs_21 nodejs_21
nodePackages.sass nodePackages.sass
nodePackages.postcss-cli nodePackages.postcss-cli
nodePackages.autoprefixer nodePackages.autoprefixer
]; ];
# Use $1 for positional args # Use $1 for positional args
commands = [ commands = [
{ {
name = "vscode"; name = "vscode";
help = "Launch VSCode"; help = "Launch VSCode";
command = "code . &>/dev/null &"; command = "code . &>/dev/null &";
} }
{ {
name = "pycharm"; name = "pycharm";
help = "Launch PyCharm Professional"; help = "Launch PyCharm Professional";
command = "pycharm-professional . &>/dev/null &"; command = "pycharm-professional . &>/dev/null &";
} }
{ {
name = "db"; name = "db";
help = "Launch SQLiteBrowser"; help = "Launch SQLiteBrowser";
command = "sqlitebrowser ./instance/formula10.db &>/dev/null &"; command = "sqlitebrowser ./instance/formula10.db &>/dev/null &";
} }
{ {
name = "api"; name = "api";
help = "Launch Hoppscotch in Google Chrome"; help = "Launch Hoppscotch in Google Chrome";
command = "google-chrome-stable https://hoppscotch.io &>/dev/null &"; command = "google-chrome-stable https://hoppscotch.io &>/dev/null &";
} }
]; ];
}; };
}); });
} }

View File

@ -1,6 +1,8 @@
import os import os
from flask import Flask from flask import Flask
from flask_sqlalchemy import SQLAlchemy from flask_sqlalchemy import SQLAlchemy
from flask_caching import Cache
from werkzeug.middleware.profiler import ProfilerMiddleware
# Load local ENV variables (can be set when calling the executable) # Load local ENV variables (can be set when calling the executable)
ENABLE_TIMING: bool = False if os.getenv("DISABLE_TIMING") == "True" else True ENABLE_TIMING: bool = False if os.getenv("DISABLE_TIMING") == "True" else True
@ -21,6 +23,11 @@ app.url_map.strict_slashes = False
db: SQLAlchemy = SQLAlchemy() db: SQLAlchemy = SQLAlchemy()
db.init_app(app) db.init_app(app)
cache: Cache = Cache(config={"CACHE_TYPE": "SimpleCache"})
cache.init_app(app)
# app.wsgi_app = ProfilerMiddleware(app.wsgi_app, restrictions=("/formula10/*",), sort_by=("cumtime",))
# NOTE: These imports are required to register the routes. They need to be imported after "app" is declared # NOTE: These imports are required to register the routes. They need to be imported after "app" is declared
import formula10.controller.race_controller # type: ignore import formula10.controller.race_controller # type: ignore
import formula10.controller.season_controller import formula10.controller.season_controller

View File

@ -5,12 +5,13 @@ from werkzeug import Response
from formula10.controller.error_controller import error_redirect from formula10.controller.error_controller import error_redirect
from formula10.database.update_queries import update_race_result, update_user from formula10.database.update_queries import update_race_result, update_user
from formula10.domain.cache_invalidator import cache_invalidate_user_updated, cache_invalidate_race_result_updated
from formula10.domain.domain_model import Model from formula10.domain.domain_model import Model
from formula10.domain.template_model import TemplateModel from formula10.domain.template_model import TemplateModel
from formula10 import app from formula10 import app
from formula10.openf1.model.api_session import ApiSession from formula10.openf1.model.openf1_session import OpenF1Session
from formula10.openf1.openf1_definitions import OPENF1_SESSION_NAME_RACE from formula10.openf1.openf1_definitions import OPENF1_SESSION_NAME_RACE
from formula10.openf1.openf1_fetcher import fetch_openf1_driver, fetch_openf1_position, fetch_openf1_session from formula10.openf1.openf1_fetcher import openf1_fetch_driver, openf1_fetch_position, openf1_fetch_session
@app.route("/result") @app.route("/result")
@ -43,18 +44,20 @@ def result_enter_post(race_name: str) -> Response:
if fastest_lap is None: if fastest_lap is None:
return error_redirect("Data was not saved, because fastest lap was not set.") return error_redirect("Data was not saved, because fastest lap was not set.")
cache_invalidate_race_result_updated()
race_id: int = Model().race_by(race_name=race_name).id race_id: int = Model().race_by(race_name=race_name).id
return update_race_result(race_id, pxxs, first_dnfs, dnfs, excluded, int(fastest_lap), sprint_pxxs, sprint_dnf_drivers) return update_race_result(race_id, pxxs, first_dnfs, dnfs, excluded, int(fastest_lap), sprint_pxxs, sprint_dnf_drivers)
@app.route("/result-fetch/<race_name>", methods=["POST"]) @app.route("/result-fetch/<race_name>", methods=["POST"])
def result_fetch_post(race_name: str) -> Response: def result_fetch_post(race_name: str) -> Response:
session: ApiSession = fetch_openf1_session(OPENF1_SESSION_NAME_RACE, "KSA") session: OpenF1Session = openf1_fetch_session(OPENF1_SESSION_NAME_RACE, "KSA")
fetch_openf1_driver(session.session_key, "VER") openf1_fetch_driver(session.session_key, "VER")
fetch_openf1_position(session.session_key, 1) openf1_fetch_position(session.session_key, 1)
# @todo Fetch stuff and build the race_result using update_race_result(...) # @todo Fetch stuff and build the race_result using update_race_result(...)
cache_invalidate_race_result_updated()
return redirect("/result") return redirect("/result")
@ -68,11 +71,13 @@ def user_root() -> str:
@app.route("/user-add", methods=["POST"]) @app.route("/user-add", methods=["POST"])
def user_add_post() -> Response: def user_add_post() -> Response:
cache_invalidate_user_updated()
username: str | None = request.form.get("select-add-user") username: str | None = request.form.get("select-add-user")
return update_user(username, add=True) return update_user(username, add=True)
@app.route("/user-delete", methods=["POST"]) @app.route("/user-delete", methods=["POST"])
def user_delete_post() -> Response: def user_delete_post() -> Response:
cache_invalidate_user_updated()
username: str | None = request.form.get("select-delete-user") username: str | None = request.form.get("select-delete-user")
return update_user(username, delete=True) return update_user(username, delete=True)

View File

@ -1,8 +1,10 @@
from typing import List
from urllib.parse import unquote from urllib.parse import unquote
from flask import redirect, render_template, request from flask import redirect, render_template, request
from werkzeug import Response from werkzeug import Response
from formula10.database.update_queries import delete_race_guess, update_race_guess from formula10.database.update_queries import delete_race_guess, update_race_guess
from formula10.domain.cache_invalidator import cache_invalidate_race_guess_updated
from formula10.domain.domain_model import Model from formula10.domain.domain_model import Model
from formula10.domain.points_model import PointsModel from formula10.domain.points_model import PointsModel
from formula10.domain.template_model import TemplateModel from formula10.domain.template_model import TemplateModel
@ -37,6 +39,7 @@ def race_guess_post(race_name: str, user_name: str) -> Response:
pxx: str | None = request.form.get("pxxselect") pxx: str | None = request.form.get("pxxselect")
dnf: str | None = request.form.get("dnfselect") dnf: str | None = request.form.get("dnfselect")
cache_invalidate_race_guess_updated()
race_id: int = Model().race_by(race_name=race_name).id race_id: int = Model().race_by(race_name=race_name).id
user_id: int = Model().user_by(user_name=user_name).id user_id: int = Model().user_by(user_name=user_name).id
return update_race_guess(race_id, user_id, return update_race_guess(race_id, user_id,
@ -49,6 +52,7 @@ def race_guess_delete_post(race_name: str, user_name: str) -> Response:
race_name = unquote(race_name) race_name = unquote(race_name)
user_name = unquote(user_name) user_name = unquote(user_name)
cache_invalidate_race_guess_updated()
race_id: int = Model().race_by(race_name=race_name).id race_id: int = Model().race_by(race_name=race_name).id
user_id: int = Model().user_by(user_name=user_name).id user_id: int = Model().user_by(user_name=user_name).id
return delete_race_guess(race_id, user_id) return delete_race_guess(race_id, user_id)

View File

@ -5,6 +5,7 @@ from werkzeug import Response
from formula10.database.model.db_team import DbTeam from formula10.database.model.db_team import DbTeam
from formula10.database.update_queries import update_season_guess from formula10.database.update_queries import update_season_guess
from formula10.domain.cache_invalidator import cache_invalidate_season_guess_updated
from formula10.domain.domain_model import Model from formula10.domain.domain_model import Model
from formula10.domain.model.team import NONE_TEAM from formula10.domain.model.team import NONE_TEAM
from formula10.domain.points_model import PointsModel from formula10.domain.points_model import PointsModel
@ -44,5 +45,6 @@ def season_guess_post(user_name: str) -> Response:
] ]
podium_driver_guesses: List[str] = request.form.getlist("podiumdrivers") podium_driver_guesses: List[str] = request.form.getlist("podiumdrivers")
cache_invalidate_season_guess_updated()
user_id: int = Model().user_by(user_name=user_name).id user_id: int = Model().user_by(user_name=user_name).id
return update_season_guess(user_id, guesses, team_winner_guesses, podium_driver_guesses) return update_season_guess(user_id, guesses, team_winner_guesses, podium_driver_guesses)

View File

@ -0,0 +1,96 @@
from typing import List
from formula10 import cache
def cache_invalidate_user_updated() -> None:
caches: List[str] = [
"domain_all_users",
"domain_all_race_guesses",
"domain_all_season_guesses",
"points_points_per_step",
"points_user_standing",
]
memoized_caches: List[str] = [
"points_by",
"race_guesses_by",
"season_guesses_by",
]
for c in caches:
cache.delete(c)
for c in memoized_caches:
cache.delete_memoized(c)
def cache_invalidate_race_result_updated() -> None:
caches: List[str] = [
"domain_all_race_results",
"points_points_per_step",
"points_team_points_per_step",
"points_dnfs",
"points_driver_points_per_step_cumulative",
"points_wdc_standing_by_position",
"points_wdc_standing_by_driver",
"points_most_dnf_names",
"points_most_gained_names",
"points_most_lost_names",
"points_team_points_per_step_cumulative",
"points_wcc_standing_by_position",
"points_wcc_standing_by_team",
"points_user_standing",
"template_first_race_without_result",
]
memoized_caches: List[str] = [
"driver_points_per_step",
"driver_points_by",
"total_driver_points_by",
"drivers_sorted_by_points",
"total_team_points_by",
"teams_sorted_by_points",
"points_by",
"is_team_winner",
"has_podium",
"picks_with_points_count",
]
for c in caches:
cache.delete(c)
for c in memoized_caches:
cache.delete_memoized(c)
def cache_invalidate_race_guess_updated() -> None:
caches: List[str] = [
"domain_all_race_guesses",
]
memoized_caches: List[str] = [
"race_guesses_by",
]
for c in caches:
cache.delete(c)
for c in memoized_caches:
cache.delete_memoized(c)
def cache_invalidate_season_guess_updated() -> None:
caches: List[str] = [
"domain_all_season_guesses"
]
memoized_caches: List[str] = [
"season_guesses_by"
]
for c in caches:
cache.delete(c)
for c in memoized_caches:
cache.delete_memoized(c)

View File

@ -18,133 +18,97 @@ from formula10.domain.model.season_guess import SeasonGuess
from formula10.domain.model.season_guess_result import SeasonGuessResult from formula10.domain.model.season_guess_result import SeasonGuessResult
from formula10.domain.model.team import NONE_TEAM, Team from formula10.domain.model.team import NONE_TEAM, Team
from formula10.domain.model.user import User from formula10.domain.model.user import User
from formula10 import db from formula10 import db, cache
class Model: class Model:
_all_users: List[User] | None = None @staticmethod
_all_race_results: List[RaceResult] | None = None @cache.cached(timeout=None, key_prefix="domain_all_users") # Clear when adding/deleting users
_all_race_guesses: List[RaceGuess] | None = None def all_users() -> List[User]:
_all_season_guesses: List[SeasonGuess] | None = None
_all_season_guess_results: List[SeasonGuessResult] | None = None
_all_races: List[Race] | None = None
_all_drivers: List[Driver] | None = None
_all_active_drivers: List[Driver] | None = None
_all_teams: List[Team] | None = None
def all_users(self) -> List[User]:
""" """
Returns a list of all enabled users. Returns a list of all enabled users.
""" """
if self._all_users is None: db_users = db.session.query(DbUser).filter_by(enabled=True).all()
self._all_users = [ return [User.from_db_user(db_user) for db_user in db_users]
User.from_db_user(db_user)
for db_user in db.session.query(DbUser).filter_by(enabled=True).all()
]
return self._all_users @staticmethod
@cache.cached(timeout=None, key_prefix="domain_all_race_results") # Clear when adding/updating results
def all_race_results(self) -> List[RaceResult]: def all_race_results() -> List[RaceResult]:
""" """
Returns a list of all race results, in descending order (most recent first). Returns a list of all race results, in descending order (most recent first).
""" """
if self._all_race_results is None: db_race_results = db.session.query(DbRaceResult).join(DbRaceResult.race).order_by(desc("number")).all()
self._all_race_results = [ return [RaceResult.from_db_race_result(db_race_result) for db_race_result in db_race_results]
RaceResult.from_db_race_result(db_race_result)
for db_race_result in db.session.query(DbRaceResult).join(DbRaceResult.race).order_by(desc(DbRace.number)).all()
]
return self._all_race_results @staticmethod
@cache.cached(timeout=None, key_prefix="domain_all_race_guesses") # Clear when adding/updating race guesses or users
def all_race_guesses(self) -> List[RaceGuess]: def all_race_guesses() -> List[RaceGuess]:
""" """
Returns a list of all race guesses (of enabled users). Returns a list of all race guesses (of enabled users).
""" """
if self._all_race_guesses is None: db_race_guesses = db.session.query(DbRaceGuess).join(DbRaceGuess.user).filter_by(enabled=True).all()
self._all_race_guesses = [ return [RaceGuess.from_db_race_guess(db_race_guess) for db_race_guess in db_race_guesses]
RaceGuess.from_db_race_guess(db_race_guess)
for db_race_guess in db.session.query(DbRaceGuess).join(DbRaceGuess.user).filter_by(enabled=True).all() # Ignore disabled users
]
return self._all_race_guesses @staticmethod
@cache.cached(timeout=None, key_prefix="domain_all_season_guesses") # Clear when adding/updating season guesses or users
def all_season_guesses(self) -> List[SeasonGuess]: def all_season_guesses() -> List[SeasonGuess]:
""" """
Returns a list of all season guesses (of enabled users). Returns a list of all season guesses (of enabled users).
""" """
if self._all_season_guesses is None: db_season_guesses = db.session.query(DbSeasonGuess).join(DbSeasonGuess.user).filter_by(enabled=True).all()
self._all_season_guesses = [ return [SeasonGuess.from_db_season_guess(db_season_guess) for db_season_guess in db_season_guesses]
SeasonGuess.from_db_season_guess(db_season_guess)
for db_season_guess in db.session.query(DbSeasonGuess).join(DbSeasonGuess.user).filter_by(enabled=True).all() # Ignore disabled users
]
return self._all_season_guesses @staticmethod
@cache.cached(timeout=None, key_prefix="domain_all_season_guess_results") # No cleanup, bc entered manually
def all_season_guess_results() -> List[SeasonGuessResult]:
"""
Returns a list of all season guess results (of enabled users).
"""
db_season_guess_results = db.session.query(DbSeasonGuessResult).join(DbSeasonGuessResult.user).filter_by(enabled=True).all()
return [SeasonGuessResult.from_db_season_guess_result(db_season_guess_result) for db_season_guess_result in db_season_guess_results]
def all_season_guess_results(self) -> List[SeasonGuessResult]: @staticmethod
if self._all_season_guess_results is None: @cache.cached(timeout=None, key_prefix="domain_all_races") # No cleanup, bc entered manually
self._all_season_guess_results = [ def all_races() -> List[Race]:
SeasonGuessResult.from_db_season_guess_result(db_season_guess_result)
for db_season_guess_result in db.session.query(DbSeasonGuessResult).join(DbSeasonGuessResult.user).filter_by(enabled=True).all() # Ignore disabled users
]
return self._all_season_guess_results
def all_races(self) -> List[Race]:
""" """
Returns a list of all races, in descending order (last race first). Returns a list of all races, in descending order (last race first).
""" """
if self._all_races is None: db_races = db.session.query(DbRace).order_by(desc("number")).all()
self._all_races = [ return [Race.from_db_race(db_race) for db_race in db_races]
Race.from_db_race(db_race)
for db_race in db.session.query(DbRace).order_by(desc(DbRace.number)).all()
]
return self._all_races @staticmethod
@cache.memoize(timeout=None) # No cleanup, bc entered manually
def all_drivers(self, *, include_none: bool, include_inactive: bool) -> List[Driver]: def all_drivers(*, include_none: bool, include_inactive: bool) -> List[Driver]:
""" """
Returns a list of all active drivers. Returns a list of all active drivers.
""" """
if include_inactive: db_drivers = db.session.query(DbDriver).all()
if self._all_drivers is None: drivers = [Driver.from_db_driver(db_driver) for db_driver in db_drivers]
self._all_drivers = [
Driver.from_db_driver(db_driver)
for db_driver in db.session.query(DbDriver).all()
]
if include_none: if not include_inactive:
return self._all_drivers predicate: Callable[[Driver], bool] = lambda driver: driver.active
else: drivers = find_multiple_strict(predicate, drivers)
predicate: Callable[[Driver], bool] = lambda driver: driver != NONE_DRIVER
return find_multiple_strict(predicate, self._all_drivers)
else:
if self._all_active_drivers is None:
self._all_active_drivers = [
Driver.from_db_driver(db_driver)
for db_driver in db.session.query(DbDriver).filter_by(active=True).all()
]
if include_none: if not include_none:
return self._all_active_drivers predicate: Callable[[Driver], bool] = lambda driver: driver != NONE_DRIVER
else: drivers = find_multiple_strict(predicate, drivers)
predicate: Callable[[Driver], bool] = lambda driver: driver != NONE_DRIVER
return find_multiple_strict(predicate, self._all_active_drivers)
def all_teams(self, *, include_none: bool) -> List[Team]: return drivers
@staticmethod
@cache.memoize(timeout=None) # No cleanup, bc entered manually
def all_teams(*, include_none: bool) -> List[Team]:
""" """
Returns a list of all teams. Returns a list of all teams.
""" """
if self._all_teams is None: db_teams = db.session.query(DbTeam).all()
self._all_teams = [ teams = [Team.from_db_team(db_team) for db_team in db_teams]
Team.from_db_team(db_team)
for db_team in db.session.query(DbTeam).all()
]
if include_none: if not include_none:
return self._all_teams
else:
predicate: Callable[[Team], bool] = lambda team: team != NONE_TEAM predicate: Callable[[Team], bool] = lambda team: team != NONE_TEAM
return find_multiple_strict(predicate, self._all_teams) return find_multiple_strict(predicate, teams)
return teams
# #
# User queries # User queries
@ -217,6 +181,7 @@ class Model:
""" """
return self.race_guesses_by() return self.race_guesses_by()
@cache.memoize(timeout=None, args_to_ignore=["self"]) # Cleanup when adding/updating race guesses or users
def race_guesses_by(self, *, user_name: str | None = None, race_name: str | None = None) -> RaceGuess | List[RaceGuess] | Dict[str, Dict[str, RaceGuess]] | None: def race_guesses_by(self, *, user_name: str | None = None, race_name: str | None = None) -> RaceGuess | List[RaceGuess] | Dict[str, Dict[str, RaceGuess]] | None:
# List of all guesses by a single user # List of all guesses by a single user
if user_name is not None and race_name is None: if user_name is not None and race_name is None:
@ -266,6 +231,7 @@ class Model:
""" """
return self.season_guesses_by() return self.season_guesses_by()
@cache.memoize(timeout=None, args_to_ignore=["self"]) # Cleanup when adding/updating season guesses or users
def season_guesses_by(self, *, user_name: str | None = None) -> SeasonGuess | Dict[str, SeasonGuess] | None: def season_guesses_by(self, *, user_name: str | None = None) -> SeasonGuess | Dict[str, SeasonGuess] | None:
if user_name is not None: if user_name is not None:
predicate: Callable[[SeasonGuess], bool] = lambda guess: guess.user.name == user_name predicate: Callable[[SeasonGuess], bool] = lambda guess: guess.user.name == user_name
@ -320,6 +286,7 @@ class Model:
""" """
return self.drivers_by(include_inactive=include_inactive) return self.drivers_by(include_inactive=include_inactive)
@cache.memoize(timeout=None, args_to_ignore=["self"]) # No Cleanup, data added manually
def drivers_by(self, *, team_name: str | None = None, include_inactive: bool) -> List[Driver] | Dict[str, List[Driver]]: def drivers_by(self, *, team_name: str | None = None, include_inactive: bool) -> List[Driver] | Dict[str, List[Driver]]:
if team_name is not None: if team_name is not None:
predicate: Callable[[Driver], bool] = lambda driver: driver.team.name == team_name predicate: Callable[[Driver], bool] = lambda driver: driver.team.name == team_name

View File

@ -34,6 +34,11 @@ class Driver:
def __hash__(self) -> int: def __hash__(self) -> int:
return hash(self.id) return hash(self.id)
# This is important to memoize functions getting a Driver as input.
# The repr() will be appended to the cache key.
def __repr__(self) -> str:
return f"Driver(id={self.id}, name={self.name})"
id: int id: int
name: str name: str
abbr: str abbr: str
@ -52,4 +57,4 @@ NONE_DRIVER.name = "None"
NONE_DRIVER.abbr = "None" NONE_DRIVER.abbr = "None"
NONE_DRIVER.country = "NO" NONE_DRIVER.country = "NO"
NONE_DRIVER.team = NONE_TEAM NONE_DRIVER.team = NONE_TEAM
NONE_DRIVER.active = False NONE_DRIVER.active = True

View File

@ -36,6 +36,9 @@ class Race:
def __hash__(self) -> int: def __hash__(self) -> int:
return hash(self.id) return hash(self.id)
def __repr__(self) -> str:
return f"race(\n\tid={self.id}, name={self.name}, number={self.number},\n\tdate={self.date}, quali_date={self.quali_date},\n\thas_sprint={self.has_sprint}, place_to_guess={self.place_to_guess}\n)"
id: int id: int
name: str name: str
number: int number: int
@ -46,4 +49,4 @@ class Race:
@property @property
def name_sanitized(self) -> str: def name_sanitized(self) -> str:
return quote(self.name) return quote(self.name)

View File

@ -2,6 +2,7 @@ import json
from typing import Any, Callable, Dict, List, overload from typing import Any, Callable, Dict, List, overload
import numpy as np import numpy as np
from formula10 import cache
from formula10.domain.domain_model import Model from formula10.domain.domain_model import Model
from formula10.domain.model.driver import NONE_DRIVER, Driver from formula10.domain.model.driver import NONE_DRIVER, Driver
from formula10.domain.model.race_guess import RaceGuess from formula10.domain.model.race_guess import RaceGuess
@ -119,118 +120,93 @@ class PointsModel(Model):
This class bundles all data + functionality required to do points calculations. This class bundles all data + functionality required to do points calculations.
""" """
_points_per_step: Dict[str, List[int]] | None = None
_driver_points_per_step: Dict[str, List[int]] | None = None
_active_driver_points_per_step: Dict[str, List[int]] | None = None
_team_points_per_step: Dict[str, List[int]] | None = None
_dnfs: Dict[str, int] | None = None
def __init__(self): def __init__(self):
Model.__init__(self) Model.__init__(self)
@cache.cached(timeout=None, key_prefix="points_points_per_step") # Clear when adding/updating race results or users
def points_per_step(self) -> Dict[str, List[int]]: def points_per_step(self) -> Dict[str, List[int]]:
""" """
Returns a dictionary of lists, containing points per race for each user. Returns a dictionary of lists, containing points per race for each user.
""" """
if self._points_per_step is None: points_per_step = dict()
self._points_per_step = dict() for user in self.all_users():
for user in self.all_users(): points_per_step[user.name] = [0] * (len(self.all_races()) + 1) # Start at index 1, like the race numbers
self._points_per_step[user.name] = [0] * (len(self.all_races()) + 1) # Start at index 1, like the race numbers
for race_guess in self.all_race_guesses(): for race_guess in self.all_race_guesses():
user_name: str = race_guess.user.name user_name: str = race_guess.user.name
race_number: int = race_guess.race.number race_number: int = race_guess.race.number
race_result: RaceResult | None = self.race_result_by(race_name=race_guess.race.name) race_result: RaceResult | None = self.race_result_by(race_name=race_guess.race.name)
if race_result is None: if race_result is None:
continue continue
self._points_per_step[user_name][race_number] = standing_points(race_guess, race_result) + dnf_points(race_guess, race_result) points_per_step[user_name][race_number] = standing_points(race_guess, race_result) + dnf_points(race_guess, race_result)
return self._points_per_step return points_per_step
@cache.memoize(timeout=None, args_to_ignore=["self"]) # Clear when adding/updating race results
def driver_points_per_step(self, *, include_inactive: bool) -> Dict[str, List[int]]: def driver_points_per_step(self, *, include_inactive: bool) -> Dict[str, List[int]]:
""" """
Returns a dictionary of lists, containing points per race for each driver. Returns a dictionary of lists, containing points per race for each driver.
""" """
if include_inactive: driver_points_per_step = dict()
if self._driver_points_per_step is None: for driver in self.all_drivers(include_none=False, include_inactive=include_inactive):
self._driver_points_per_step = dict() driver_points_per_step[driver.name] = [0] * (len(self.all_races()) + 1) # Start at index 1, like the race numbers
for driver in self.all_drivers(include_none=False, include_inactive=True):
self._driver_points_per_step[driver.name] = [0] * (len(self.all_races()) + 1) # Start at index 1, like the race numbers
for race_result in self.all_race_results(): for race_result in self.all_race_results():
race_number: int = race_result.race.number race_number: int = race_result.race.number
for position, driver in race_result.standing.items(): for position, driver in race_result.standing.items():
self._driver_points_per_step[driver.name][race_number] = DRIVER_RACE_POINTS[int(position)] if int(position) in DRIVER_RACE_POINTS else 0 driver_points_per_step[driver.name][race_number] = DRIVER_RACE_POINTS[int(position)] if int(position) in DRIVER_RACE_POINTS else 0
self._driver_points_per_step[driver.name][race_number] += DRIVER_FASTEST_LAP_POINTS if race_result.fastest_lap_driver == driver else 0 driver_points_per_step[driver.name][race_number] += DRIVER_FASTEST_LAP_POINTS if race_result.fastest_lap_driver == driver else 0
for position, driver in race_result.sprint_standing.items(): for position, driver in race_result.sprint_standing.items():
driver_name: str = driver.name driver_name: str = driver.name
self._driver_points_per_step[driver_name][race_number] += DRIVER_SPRINT_POINTS[int(position)] if int(position) in DRIVER_SPRINT_POINTS else 0 driver_points_per_step[driver_name][race_number] += DRIVER_SPRINT_POINTS[int(position)] if int(position) in DRIVER_SPRINT_POINTS else 0
return self._driver_points_per_step return driver_points_per_step
else:
if self._active_driver_points_per_step is None:
self._active_driver_points_per_step = dict()
for driver in self.all_drivers(include_none=False, include_inactive=False):
self._active_driver_points_per_step[driver.name] = [0] * (len(self.all_races()) + 1) # Start at index 1, like the race numbers
for race_result in self.all_race_results():
race_number: int = race_result.race.number
for position, driver in race_result.standing.items():
self._active_driver_points_per_step[driver.name][race_number] = DRIVER_RACE_POINTS[int(position)] if int(position) in DRIVER_RACE_POINTS else 0
self._active_driver_points_per_step[driver.name][race_number] += DRIVER_FASTEST_LAP_POINTS if race_result.fastest_lap_driver == driver else 0
for position, driver in race_result.sprint_standing.items():
driver_name: str = driver.name
self._active_driver_points_per_step[driver_name][race_number] += DRIVER_SPRINT_POINTS[int(position)] if int(position) in DRIVER_SPRINT_POINTS else 0
return self._active_driver_points_per_step
@cache.cached(timeout=None, key_prefix="points_team_points_per_step")
def team_points_per_step(self) -> Dict[str, List[int]]: def team_points_per_step(self) -> Dict[str, List[int]]:
""" """
Returns a dictionary of lists, containing points per race for each team. Returns a dictionary of lists, containing points per race for each team.
""" """
if self._team_points_per_step is None: team_points_per_step = dict()
self._team_points_per_step = dict() for team in self.all_teams(include_none=False):
for team in self.all_teams(include_none=False): team_points_per_step[team.name] = [0] * (len(self.all_races()) + 1) # Start at index 1, like the race numbers
self._team_points_per_step[team.name] = [0] * (len(self.all_races()) + 1) # Start at index 1, like the race numbers
for race_result in self.all_race_results(): for race_result in self.all_race_results():
for driver in race_result.standing.values(): for driver in race_result.standing.values():
team_name: str = driver.team.name team_name: str = driver.team.name
race_number: int = race_result.race.number race_number: int = race_result.race.number
self._team_points_per_step[team_name][race_number] += self.driver_points_per_step(include_inactive=True)[driver.name][race_number] team_points_per_step[team_name][race_number] += self.driver_points_per_step(include_inactive=True)[driver.name][race_number]
return self._team_points_per_step return team_points_per_step
@cache.cached(timeout=None, key_prefix="points_dnfs")
def dnfs(self) -> Dict[str, int]: def dnfs(self) -> Dict[str, int]:
if self._dnfs is None: dnfs = dict()
self._dnfs = dict()
for driver in self.all_drivers(include_none=False, include_inactive=True): for driver in self.all_drivers(include_none=False, include_inactive=True):
self._dnfs[driver.name] = 0 dnfs[driver.name] = 0
for race_result in self.all_race_results(): for race_result in self.all_race_results():
for driver in race_result.all_dnfs: for driver in race_result.all_dnfs:
self._dnfs[driver.name] += 1 dnfs[driver.name] += 1
for driver in race_result.sprint_dnfs: for driver in race_result.sprint_dnfs:
self._dnfs[driver.name] += 1 dnfs[driver.name] += 1
return self._dnfs return dnfs
# #
# Driver stats # Driver stats
# #
@cache.cached(timeout=None, key_prefix="points_driver_points_per_step_cumulative") # Cleanup when adding/updating race results
def driver_points_per_step_cumulative(self) -> Dict[str, List[int]]: def driver_points_per_step_cumulative(self) -> Dict[str, List[int]]:
""" """
Returns a dictionary of lists, containing cumulative points per race for each driver. Returns a dictionary of lists, containing cumulative points per race for each driver.
@ -262,6 +238,7 @@ class PointsModel(Model):
""" """
return self.driver_points_by(driver_name=driver_name, race_name=race_name, include_inactive=include_inactive) return self.driver_points_by(driver_name=driver_name, race_name=race_name, include_inactive=include_inactive)
@cache.memoize(timeout=None, args_to_ignore=["self"]) # Cleanup when adding/updating race results
def driver_points_by(self, *, driver_name: str | None = None, race_name: str | None = None, include_inactive: bool) -> List[int] | Dict[str, int] | int: def driver_points_by(self, *, driver_name: str | None = None, race_name: str | None = None, include_inactive: bool) -> List[int] | Dict[str, int] | int:
if driver_name is not None and race_name is None: if driver_name is not None and race_name is None:
return self.driver_points_per_step(include_inactive=include_inactive)[driver_name] return self.driver_points_per_step(include_inactive=include_inactive)[driver_name]
@ -282,13 +259,16 @@ class PointsModel(Model):
raise Exception("driver_points_by received an illegal combination of arguments") raise Exception("driver_points_by received an illegal combination of arguments")
@cache.memoize(timeout=None, args_to_ignore=["self"]) # Cleanup when adding/updating race results
def total_driver_points_by(self, driver_name: str) -> int: def total_driver_points_by(self, driver_name: str) -> int:
return sum(self.driver_points_by(driver_name=driver_name, include_inactive=True)) return sum(self.driver_points_by(driver_name=driver_name, include_inactive=True))
@cache.memoize(timeout=None, args_to_ignore=["self"]) # Cleanup when adding/updating race results
def drivers_sorted_by_points(self, *, include_inactive: bool) -> List[Driver]: def drivers_sorted_by_points(self, *, include_inactive: bool) -> List[Driver]:
comparator: Callable[[Driver], int] = lambda driver: self.total_driver_points_by(driver.name) comparator: Callable[[Driver], int] = lambda driver: self.total_driver_points_by(driver.name)
return sorted(self.all_drivers(include_none=False, include_inactive=include_inactive), key=comparator, reverse=True) return sorted(self.all_drivers(include_none=False, include_inactive=include_inactive), key=comparator, reverse=True)
@cache.cached(timeout=None, key_prefix="points_wdc_standing_by_position") # Cleanup when adding/updating race results
def wdc_standing_by_position(self) -> Dict[int, List[str]]: def wdc_standing_by_position(self) -> Dict[int, List[str]]:
standing: Dict[int, List[str]] = dict() standing: Dict[int, List[str]] = dict()
@ -308,6 +288,7 @@ class PointsModel(Model):
return standing return standing
@cache.cached(timeout=None, key_prefix="points_wdc_standing_by_driver") # Cleanup when adding/updating race results
def wdc_standing_by_driver(self) -> Dict[str, int]: def wdc_standing_by_driver(self) -> Dict[str, int]:
standing: Dict[str, int] = dict() standing: Dict[str, int] = dict()
@ -330,6 +311,7 @@ class PointsModel(Model):
return WDC_STANDING_2023[driver_name] - self.wdc_standing_by_driver()[driver_name] return WDC_STANDING_2023[driver_name] - self.wdc_standing_by_driver()[driver_name]
@cache.cached(timeout=None, key_prefix="points_most_dnf_names") # Cleanup when adding/updating race results
def most_dnf_names(self) -> List[str]: def most_dnf_names(self) -> List[str]:
dnf_names: List[str] = list() dnf_names: List[str] = list()
most_dnfs: int = 0 most_dnfs: int = 0
@ -344,6 +326,7 @@ class PointsModel(Model):
return dnf_names return dnf_names
@cache.cached(timeout=None, key_prefix="points_most_gained_names") # Cleanup when adding/updating race results
def most_gained_names(self) -> List[str]: def most_gained_names(self) -> List[str]:
most_gained_names: List[str] = list() most_gained_names: List[str] = list()
most_gained: int = 0 most_gained: int = 0
@ -362,6 +345,7 @@ class PointsModel(Model):
return most_gained_names return most_gained_names
@cache.cached(timeout=None, key_prefix="points_most_lost_names") # Cleanup when adding/updating race results
def most_lost_names(self) -> List[str]: def most_lost_names(self) -> List[str]:
most_lost_names: List[str] = list() most_lost_names: List[str] = list()
most_lost: int = 100 most_lost: int = 100
@ -384,6 +368,7 @@ class PointsModel(Model):
# Team points # Team points
# #
@cache.cached(timeout=None, key_prefix="points_team_points_per_step_cumulative") # Cleanup when adding/updating race results
def team_points_per_step_cumulative(self) -> Dict[str, List[int]]: def team_points_per_step_cumulative(self) -> Dict[str, List[int]]:
""" """
Returns a dictionary of lists, containing cumulative points per race for each team. Returns a dictionary of lists, containing cumulative points per race for each team.
@ -394,14 +379,17 @@ class PointsModel(Model):
return points_per_step_cumulative return points_per_step_cumulative
@cache.memoize(timeout=None, args_to_ignore=["self"]) # Cleanup when adding/updating race results
def total_team_points_by(self, team_name: str) -> int: def total_team_points_by(self, team_name: str) -> int:
teammates: List[Driver] = self.drivers_by(team_name=team_name, include_inactive=True) teammates: List[Driver] = self.drivers_by(team_name=team_name, include_inactive=True)
return sum(sum(self.driver_points_by(driver_name=teammate.name, include_inactive=True)) for teammate in teammates) return sum(sum(self.driver_points_by(driver_name=teammate.name, include_inactive=True)) for teammate in teammates)
@cache.memoize(timeout=None, args_to_ignore=["self"]) # Cleanup when adding/updating race results
def teams_sorted_by_points(self) -> List[Team]: def teams_sorted_by_points(self) -> List[Team]:
comparator: Callable[[Team], int] = lambda team: self.total_team_points_by(team.name) comparator: Callable[[Team], int] = lambda team: self.total_team_points_by(team.name)
return sorted(self.all_teams(include_none=False), key=comparator, reverse=True) return sorted(self.all_teams(include_none=False), key=comparator, reverse=True)
@cache.cached(timeout=None, key_prefix="points_wcc_standing_by_position") # Cleanup when adding/updating race results
def wcc_standing_by_position(self) -> Dict[int, List[str]]: def wcc_standing_by_position(self) -> Dict[int, List[str]]:
standing: Dict[int, List[str]] = dict() standing: Dict[int, List[str]] = dict()
@ -421,6 +409,7 @@ class PointsModel(Model):
return standing return standing
@cache.cached(timeout=None, key_prefix="points_wcc_standing_by_team") # Cleanup when adding/updating race results
def wcc_standing_by_team(self) -> Dict[str, int]: def wcc_standing_by_team(self) -> Dict[str, int]:
standing: Dict[str, int] = dict() standing: Dict[str, int] = dict()
@ -475,6 +464,7 @@ class PointsModel(Model):
""" """
return self.points_by(user_name=user_name, race_name=race_name) return self.points_by(user_name=user_name, race_name=race_name)
@cache.memoize(timeout=None, args_to_ignore=["self"]) # Cleanup when adding/updating race results or users
def points_by(self, *, user_name: str | None = None, race_name: str | None = None) -> List[int] | Dict[str, int] | int: def points_by(self, *, user_name: str | None = None, race_name: str | None = None) -> List[int] | Dict[str, int] | int:
if user_name is not None and race_name is None: if user_name is not None and race_name is None:
return self.points_per_step()[user_name] return self.points_per_step()[user_name]
@ -508,6 +498,7 @@ class PointsModel(Model):
comparator: Callable[[User], int] = lambda user: self.total_points_by(user.name) comparator: Callable[[User], int] = lambda user: self.total_points_by(user.name)
return sorted(self.all_users(), key=comparator, reverse=True) return sorted(self.all_users(), key=comparator, reverse=True)
@cache.cached(timeout=None, key_prefix="points_user_standing") # Cleanup when adding/updating race results or users
def user_standing(self) -> Dict[str, int]: def user_standing(self) -> Dict[str, int]:
standing: Dict[str, int] = dict() standing: Dict[str, int] = dict()
@ -527,6 +518,7 @@ class PointsModel(Model):
# Treat standing + dnf picks separately # Treat standing + dnf picks separately
return len(self.race_guesses_by(user_name=user_name)) * 2 return len(self.race_guesses_by(user_name=user_name)) * 2
@cache.memoize(timeout=None, args_to_ignore=["self"]) # Cleanup when adding/updating race results
def picks_with_points_count(self, user_name: str) -> int: def picks_with_points_count(self, user_name: str) -> int:
count: int = 0 count: int = 0
@ -594,14 +586,14 @@ class PointsModel(Model):
return season_guess.most_wdc_lost.name in self.most_lost_names() return season_guess.most_wdc_lost.name in self.most_lost_names()
@cache.memoize(timeout=None, args_to_ignore=["self"]) # Cleanup when adding/updating race results
def is_team_winner(self, driver: Driver) -> bool: def is_team_winner(self, driver: Driver) -> bool:
teammates: List[Driver] = self.drivers_by(team_name=driver.team.name, include_inactive=True) teammates: List[Driver] = self.drivers_by(team_name=driver.team.name, include_inactive=True)
teammate: Driver = teammates[0] if teammates[1] == driver else teammates[1] teammate: Driver = teammates[0] if teammates[1] == driver else teammates[1]
print(f"{driver.name} standing: {self.wdc_standing_by_driver()[driver.name]}, {teammate.name} standing: {self.wdc_standing_by_driver()[teammate.name]}")
return self.wdc_standing_by_driver()[driver.name] <= self.wdc_standing_by_driver()[teammate.name] return self.wdc_standing_by_driver()[driver.name] <= self.wdc_standing_by_driver()[teammate.name]
@cache.memoize(timeout=None, args_to_ignore=["self"]) # Cleanup when adding/updating race results
def has_podium(self, driver: Driver) -> bool: def has_podium(self, driver: Driver) -> bool:
for race_result in self.all_race_results(): for race_result in self.all_race_results():
position: int | None = race_result.driver_standing_position(driver) position: int | None = race_result.driver_standing_position(driver)

View File

@ -1,5 +1,5 @@
from typing import List, Callable from typing import List, Callable
from formula10 import ENABLE_TIMING from formula10 import ENABLE_TIMING, cache
from formula10.domain.domain_model import Model from formula10.domain.domain_model import Model
from formula10.domain.model.driver import Driver from formula10.domain.model.driver import Driver
@ -54,6 +54,7 @@ class TemplateModel(Model):
return self.all_users() return self.all_users()
@cache.cached(timeout=None, key_prefix="template_first_race_without_result") # Cleanup when adding/updating race results
def first_race_without_result(self) -> Race | None: def first_race_without_result(self) -> Race | None:
""" """
Returns the first race-object with no associated race result. Returns the first race-object with no associated race result.

View File

View File

@ -0,0 +1,364 @@
# https://docs.fastf1.dev/events.html#event-formats
from typing import Optional
from fastf1.core import Lap, DriverResult, Session
from fastf1.events import Event
from pandas import Timestamp, Timedelta
FASTF1_SESSIONTYPE_NORMAL: str = "conventional"
FASTF1_SESSIONTYPE_SPRINT: str = "sprint_qualifying"
FASTF1_SPRINT_QUALIFYING_SESSION: int = 2
FASTF1_SPRINT_SESSION: int = 3
FASTF1_QUALIFYING_SESSION: int = 4
FASTF1_RACE_SESSION: int = 5
class EventDataHelper:
"""
Helper class that provides easy access to EventDataFrame columns.
"""
def __init__(self, event: Event):
self.__event = event
__event: Event
@property
def round_number(self) -> int:
return self.__event["RoundNumber"]
@property
def country(self) -> str:
return self.__event["Country"]
@property
def location(self) -> str:
return self.__event["Location"]
@property
def official_event_name(self) -> str:
"""
Get the event name including sponsor names etc.
"""
return self.__event["OfficialEventName"]
@property
def event_name(self) -> str:
return self.__event["EventName"]
@property
def event_date(self) -> Timestamp:
return self.__event["EventDate"]
@property
def event_format(self) -> str:
"""
Get the event format.
@return: Either conventional, sprint, sprint_shootout, sprint_qualifying or testing (sprint_qualifying is the 2024 sprint format).
"""
return self.__event["EventFormat"]
def session(self, session_number: int) -> str:
"""
Get the format of a session belonging to this event.
@param session_number: Either 1, 2, 3, 4 or 5
@return: Either Pactice 1, Practice 2, Practice 3, Qualifying, Sprint, Sprint Shootout or Race
"""
return self.__event[f"Session{session_number}"]
def session_date(self, session_number: int) -> Timestamp:
"""
Get the date of a session belonging to this event.
@param session_number: Either 1, 2, 3, 4 or 5
"""
return self.__event[f"Session{session_number}Date"]
def session_date_utc(self, session_number: int) -> Timestamp:
"""
Get the date a session belonging to this event in coordinated universal time.
@param session_number: Either 1, 2, 3, 4 or 5
"""
return self.__event[f"Session{session_number}DateUtc"]
@property
def f1_api_support(self) -> bool:
return self.__event["F1ApiSupport"]
# The following doesn't correspond to DataFrame columns
@property
def sprint_qualifying(self):
return self.__event.get_sprint_qualifying()
@property
def sprint(self):
return self.__event.get_sprint()
@property
def qualifying(self) -> Session:
return self.__event.get_qualifying()
@property
def race(self) -> Session:
return self.__event.get_race()
class LapDataHelper:
"""
Helper class that provides easy access to Lap DataFrame columns.
"""
def __init__(self, lap: Lap):
self.__lap = lap
__lap: Lap
@property
def time(self) -> Timedelta:
return self.__lap["Time"]
@property
def driver(self) -> str:
return self.__lap["Driver"]
@property
def driver_number(self) -> str:
return self.__lap["DriverNumber"]
@property
def lap_time(self) -> Timedelta:
return self.__lap["LapTime"]
@property
def lap_number(self) -> int:
return self.__lap["LapNumber"]
@property
def stint(self) -> int:
return self.__lap["Stint"]
@property
def pit_out_time(self) -> Timedelta:
"""
Get the session time when the car left the pit.
"""
return self.__lap["PitOutTime"]
@property
def pit_in_time(self) -> Timedelta:
"""
Get the session time when the car entered the pit.
"""
return self.__lap["PitInTime"]
def sector_time(self, sector_number: int) -> Timedelta:
"""
Get the sector times set in this lap.
@param sector_number: Either 1, 2 or 3.
"""
return self.__lap[f"Sector{sector_number}Time"]
def sector_session_time(self, sector_number: int) -> Timedelta:
"""
Get the session time when the sector time was set.
@param sector_number: Either 1, 2 or 3.
"""
return self.__lap[f"Sector{sector_number}SessionTime"]
def speed(self, speedtrap: str) -> float:
"""
Get car speed at measure point in this lap.
@param speedtrap: Either I1, I2, FL or ST (sector 1, sector 2, finish line or longest straight)
"""
return self.__lap[f"Speed{speedtrap}"]
@property
def is_personal_best(self) -> bool:
return self.__lap["IsPersonalBest"]
@property
def compound(self) -> str:
"""
Get compound used in this lap.
@return: Either SOFT, MEDIUM, HARD, INTERMEDIATE or WET
"""
return self.__lap["Compound"]
@property
def tyre_life(self) -> int:
"""
Get laps driven on the current tyre (includes laps from other sessions).
"""
return self.__lap["TyreLife"]
@property
def fresh_tyre(self) -> bool:
return self.__lap["FreshTyre"]
@property
def team(self) -> str:
return self.__lap["Team"]
@property
def lap_start_time(self) -> Timedelta:
return self.__lap["LapStartTime"]
@property
def lap_start_date(self) -> Timestamp:
return self.__lap["LapStartDate"]
@property
def track_status(self) -> str:
"""
Get the track status in this lap.
@return: Either 1, 2, 3, 4, 5, 6 or 7 (clear, yellow flag, ?, SC, red flag, VSC and VSC ending).
"""
return self.__lap["TrackStatus"]
@property
def position(self) -> int:
return self.__lap["Position"]
@property
def deleted(self) -> Optional[bool]:
"""
Determine if the lap was deleted (only available if race control messages are loaded).
"""
return self.__lap["Deleted"]
@property
def deleted_reason(self) -> str:
return self.__lap["DeletedReason"]
@property
def fast_f1_generated(self) -> bool:
"""
Determine if the lap was generated by FastF1 (information is interpolated).
"""
return self.__lap["FastF1Generated"]
@property
def is_accurate(self) -> bool:
"""
Determine if lap start and end match with other laps before and after.
"""
return self.__lap["IsAccurate"]
class DriverResultDataHelper:
"""
Helper class that provides easy access to DriverResult DataFrame columns.
"""
def __init__(self, driver_result: DriverResult):
self.__driver_result = driver_result
__driver_result: DriverResult
@property
def driver_number(self) -> str:
return self.__driver_result["DriverNumber"]
@property
def broadcast_name(self) -> str:
"""
Get this driver's broadcast name.
@return: For example P GASLY
"""
return self.__driver_result["BroadcastName"]
@property
def full_name(self) -> str:
return self.__driver_result["FullName"]
@property
def abbreviation(self) -> str:
return self.__driver_result["Abbreviation"]
# @property
# def driver_id(self) -> str:
# """
# Get the driverId used by the Ergast API.
# """
# return self.__driver_result["DriverID"]
@property
def team_name(self) -> str:
return self.__driver_result["TeamName"]
@property
def team_color(self) -> str:
return self.__driver_result["TeamColor"]
# @property
# def team_id(self) -> str:
# """
# Get the constructorId used by the Ergast API.
# @return:
# """
# return self.__driver_result["TeamID"]
@property
def first_name(self) -> str:
return self.__driver_result["FirstName"]
@property
def last_name(self) -> str:
return self.__driver_result["LastName"]
@property
def headshot_url(self) -> str:
return self.__driver_result["HeadshotUrl"]
@property
def country_code(self) -> str:
"""
Get a driver's three-letter country code.
@return: For example FRA
"""
return self.__driver_result["CountryCode"]
@property
def position(self) -> int:
return self.__driver_result["Position"]
@property
def classified_position(self) -> str:
"""
Get the classification result for this driver.
@return: Either R, D, E, W, F, N (retired, disqualified, excluded, withdrawn, failed to qualify and not classified)
"""
return self.__driver_result["ClassifiedPosition"]
@property
def grid_position(self) -> int:
"""
Get the driver's starting position.
"""
return self.__driver_result["GridPosition"]
def qualifying_time(self, qualifying_number: int) -> Timedelta:
"""
Get the driver's best qualifying time.
@param qualifying_number: Either 1, 2 or 3 (for Q1, Q2 and Q3)
"""
return self.__driver_result[f"Q{qualifying_number}"]
@property
def time(self) -> Timedelta:
"""
Get the driver's total race time.
"""
return self.__driver_result["Time"]
@property
def status(self) -> str:
"""
Get the driver's finishing status.
@return: For example Finished, +1 Lap, Crash, Gearbox...
"""
return self.__driver_result["Status"]
@property
def points(self) -> int:
return self.__driver_result["Points"]

View File

@ -1,7 +1,7 @@
from typing import Any, Callable, Dict from typing import Any, Callable, Dict
class ApiDriver: class OpenF1Driver:
__type_conversion_map__: Dict[str, Callable[[Any], Any]] = { __type_conversion_map__: Dict[str, Callable[[Any], Any]] = {
"session_key": int, "session_key": int,
"meeting_key": int, "meeting_key": int,
@ -30,7 +30,7 @@ class ApiDriver:
setattr(self, key, self.__type_conversion_map__[key](response[key])) setattr(self, key, self.__type_conversion_map__[key](response[key]))
print("ApiDriver:", self.__dict__) print("OpenF1Driver:", self.__dict__)
def to_params(self) -> Dict[str, str]: def to_params(self) -> Dict[str, str]:
params: Dict[str, str] = dict() params: Dict[str, str] = dict()

View File

@ -2,7 +2,7 @@ from datetime import datetime
from typing import Any, Callable, Dict from typing import Any, Callable, Dict
class ApiPosition: class OpenF1Position:
__type_conversion_map__: Dict[str, Callable[[Any], Any]] = { __type_conversion_map__: Dict[str, Callable[[Any], Any]] = {
"session_key": int, "session_key": int,
"meeting_key": int, "meeting_key": int,
@ -24,7 +24,7 @@ class ApiPosition:
setattr(self, key, self.__type_conversion_map__[key](response[key])) setattr(self, key, self.__type_conversion_map__[key](response[key]))
print("ApiPosition:", self.__dict__) print("OpenF1Position:", self.__dict__)
def to_params(self) -> Dict[str, str]: def to_params(self) -> Dict[str, str]:
params: Dict[str, str] = dict() params: Dict[str, str] = dict()

View File

@ -2,7 +2,7 @@ from datetime import datetime, time
from typing import Any, Callable, Dict from typing import Any, Callable, Dict
class ApiSession: class OpenF1Session:
__type_conversion_map__: Dict[str, Callable[[Any], Any]] = { __type_conversion_map__: Dict[str, Callable[[Any], Any]] = {
"location": str, "location": str,
"country_key": int, "country_key": int,
@ -35,7 +35,7 @@ class ApiSession:
setattr(self, key, self.__type_conversion_map__[key](response[key])) setattr(self, key, self.__type_conversion_map__[key](response[key]))
print("ApiSession:", self.__dict__) print("OpenF1Session:", self.__dict__)
def to_params(self) -> Dict[str, str]: def to_params(self) -> Dict[str, str]:
params: Dict[str, str] = dict() params: Dict[str, str] = dict()

View File

@ -3,12 +3,12 @@ import json
from typing import Any, Callable, Dict, List, cast from typing import Any, Callable, Dict, List, cast
from requests import Response, get from requests import Response, get
from formula10.openf1.model.api_driver import ApiDriver from formula10.openf1.model.openf1_driver import OpenF1Driver
from formula10.openf1.model.api_position import ApiPosition from formula10.openf1.model.openf1_position import OpenF1Position
from formula10.openf1.model.api_session import ApiSession from formula10.openf1.model.openf1_session import OpenF1Session
from formula10.openf1.openf1_definitions import OPENF1_DRIVER_ENDPOINT, OPENF1_POSITION_ENDPOINT, OPENF1_SESSION_ENDPOINT, OPENF1_SESSION_NAME_RACE, OPENF1_SESSION_NAME_SPRINT, OPENF1_SESSION_TYPE_RACE from formula10.openf1.openf1_definitions import OPENF1_DRIVER_ENDPOINT, OPENF1_POSITION_ENDPOINT, OPENF1_SESSION_ENDPOINT, OPENF1_SESSION_NAME_RACE, OPENF1_SESSION_NAME_SPRINT, OPENF1_SESSION_TYPE_RACE
def request_helper(endpoint: str, params: Dict[str, str]) -> List[Dict[str, str]]: def openf1_request_helper(endpoint: str, params: Dict[str, str]) -> List[Dict[str, str]]:
response: Response = get(endpoint, params=params) response: Response = get(endpoint, params=params)
if not response.ok: if not response.ok:
raise Exception(f"OpenF1 request to {response.request.url} failed") raise Exception(f"OpenF1 request to {response.request.url} failed")
@ -23,54 +23,54 @@ def request_helper(endpoint: str, params: Dict[str, str]) -> List[Dict[str, str]
raise Exception(f"Unexpected OpenF1 response from {response.request.url}: {obj}") raise Exception(f"Unexpected OpenF1 response from {response.request.url}: {obj}")
def fetch_openf1_latest_session(session_name: str) -> ApiSession: def openf1_fetch_latest_session(session_name: str) -> OpenF1Session:
# ApiSession object only supports integer session_keys # ApiSession object only supports integer session_keys
response: List[Dict[str, str]] = request_helper(OPENF1_SESSION_ENDPOINT, { response: List[Dict[str, str]] = openf1_request_helper(OPENF1_SESSION_ENDPOINT, {
"session_key": "latest", "session_key": "latest",
"session_type": OPENF1_SESSION_TYPE_RACE, "session_type": OPENF1_SESSION_TYPE_RACE,
"session_name": session_name "session_name": session_name
}) })
return ApiSession(response[0]) return OpenF1Session(response[0])
def fetch_openf1_latest_race_session_key() -> int: def openf1_fetch_latest_race_session_key() -> int:
return fetch_openf1_latest_session(OPENF1_SESSION_NAME_RACE).session_key return openf1_fetch_latest_session(OPENF1_SESSION_NAME_RACE).session_key
def fetch_openf1_latest_sprint_session_key() -> int: def openf1_fetch_latest_sprint_session_key() -> int:
return fetch_openf1_latest_session(OPENF1_SESSION_NAME_SPRINT).session_key return openf1_fetch_latest_session(OPENF1_SESSION_NAME_SPRINT).session_key
def fetch_openf1_session(session_name: str, country_code: str) -> ApiSession: def openf1_fetch_session(session_name: str, country_code: str) -> OpenF1Session:
_session: ApiSession = ApiSession(None) _session: OpenF1Session = OpenF1Session(None)
_session.session_type = OPENF1_SESSION_TYPE_RACE # includes races + sprints _session.session_type = OPENF1_SESSION_TYPE_RACE # includes races + sprints
_session.year = 2024 _session.year = 2024
_session.country_code = country_code _session.country_code = country_code
_session.session_name = session_name _session.session_name = session_name
response: List[Dict[str, str]] = request_helper(OPENF1_SESSION_ENDPOINT, _session.to_params()) response: List[Dict[str, str]] = openf1_request_helper(OPENF1_SESSION_ENDPOINT, _session.to_params())
return ApiSession(response[0]) return OpenF1Session(response[0])
def fetch_openf1_driver(session_key: int, name_acronym: str) -> ApiDriver: def openf1_fetch_driver(session_key: int, name_acronym: str) -> OpenF1Driver:
_driver: ApiDriver = ApiDriver(None) _driver: OpenF1Driver = OpenF1Driver(None)
_driver.name_acronym = name_acronym _driver.name_acronym = name_acronym
_driver.session_key = session_key _driver.session_key = session_key
response: List[Dict[str, str]] = request_helper(OPENF1_DRIVER_ENDPOINT, _driver.to_params()) response: List[Dict[str, str]] = openf1_request_helper(OPENF1_DRIVER_ENDPOINT, _driver.to_params())
return ApiDriver(response[0]) return OpenF1Driver(response[0])
def fetch_openf1_position(session_key: int, position: int): def openf1_fetch_position(session_key: int, position: int):
_position: ApiPosition = ApiPosition(None) _position: OpenF1Position = OpenF1Position(None)
_position.session_key = session_key _position.session_key = session_key
_position.position = position _position.position = position
response: List[Dict[str, str]] = request_helper(OPENF1_POSITION_ENDPOINT, _position.to_params()) response: List[Dict[str, str]] = openf1_request_helper(OPENF1_POSITION_ENDPOINT, _position.to_params())
# Find the last driver that was on this position at last # Find the last driver that was on this position at last
predicate: Callable[[Dict[str, str]], datetime] = lambda position: datetime.strptime(position["date"], "%Y-%m-%dT%H:%M:%S.%f") predicate: Callable[[Dict[str, str]], datetime] = lambda position: datetime.strptime(position["date"], "%Y-%m-%dT%H:%M:%S.%f")
return ApiPosition(max(response, key=predicate)) return OpenF1Position(max(response, key=predicate))

View File

@ -54,11 +54,11 @@
<form action="{{ action_fetch_href }}" method="post"> <form action="{{ action_fetch_href }}" method="post">
<div class="card shadow-sm mb-2 w-100"> <div class="card shadow-sm mb-2 w-100">
<div class="card-header"> <div class="card-header">
OpenF1 Autofill
</div> </div>
<div class="card-body"> <div class="card-body">
<input type="submit" class="btn btn-danger mt-2 w-100" value="Fetch from OpenF1" <input type="submit" class="btn btn-danger mt-2 w-100" value="Fetch using FastF1"
{% if race_result_open == false %}disabled="disabled"{% endif %}> {% if race_result_open == false %}disabled="disabled"{% endif %}>
</div> </div>
</div> </div>

View File

@ -3,7 +3,12 @@ numpy
flask flask
flask-sqlalchemy flask-sqlalchemy
flask-caching
sqlalchemy sqlalchemy
requests requests
werkzeug
fastf1
pandas
pytest pytest