From f768b9fde7557ca620b3630def6dc7c7a11810f5 Mon Sep 17 00:00:00 2001 From: theOehrly <23384863+theOehrly@users.noreply.github.com> Date: Tue, 16 Jul 2024 12:29:01 +0200 Subject: [PATCH 1/8] MNT: target py39 as new minimum, enable pyupgrade for ruff, bump min dependencie versions (#614) --- .github/workflows/selective_cache_persist.yml | 2 +- .github/workflows/tests.yml | 3 +- fastf1/_api.py | 9 +++--- fastf1/core.py | 23 ++++++--------- fastf1/ergast/interface.py | 6 ++-- fastf1/events.py | 5 ++-- fastf1/internals/fuzzy.py | 3 +- fastf1/internals/pandas_extensions.py | 8 ++---- fastf1/livetiming/__main__.py | 2 +- fastf1/livetiming/client.py | 6 ++-- fastf1/livetiming/data.py | 2 +- fastf1/plotting/__init__.py | 28 ++++++++----------- fastf1/plotting/_backend.py | 8 ++---- fastf1/plotting/_base.py | 14 ++++------ fastf1/plotting/_constants/__init__.py | 10 +++---- fastf1/plotting/_constants/base.py | 5 ++-- fastf1/plotting/_constants/season2018.py | 5 ++-- fastf1/plotting/_constants/season2019.py | 5 ++-- fastf1/plotting/_constants/season2020.py | 5 ++-- fastf1/plotting/_constants/season2021.py | 5 ++-- fastf1/plotting/_constants/season2022.py | 5 ++-- fastf1/plotting/_constants/season2023.py | 5 ++-- fastf1/plotting/_constants/season2024.py | 5 ++-- fastf1/plotting/_interface.py | 22 +++++++-------- fastf1/plotting/_plotting.py | 15 ++++------ fastf1/req.py | 13 ++++----- fastf1/utils.py | 6 ++-- pyproject.toml | 7 +++-- requirements/minver.txt | 4 +-- 29 files changed, 95 insertions(+), 141 deletions(-) diff --git a/.github/workflows/selective_cache_persist.yml b/.github/workflows/selective_cache_persist.yml index b44e232bd..59e4e690b 100644 --- a/.github/workflows/selective_cache_persist.yml +++ b/.github/workflows/selective_cache_persist.yml @@ -14,7 +14,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - python-version: [ '3.8-minver', '3.8', '3.9', '3.10', '3.11', '3.12'] + python-version: [ '3.9-minver', '3.9', '3.10', '3.11', '3.12'] # name: Persist cache for ${{ matrix.python-version }} steps: diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 10dbe0105..883cbd0dc 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -16,10 +16,9 @@ jobs: matrix: include: - name-suffix: "(Minimum Versions)" - python-version: "3.8" + python-version: "3.9" cache-suffix: "-minver" extra-requirements: "-c requirements/minver.txt" - - python-version: "3.8" - python-version: "3.9" - python-version: "3.10" - python-version: "3.11" diff --git a/fastf1/_api.py b/fastf1/_api.py index e5e3aa5ba..1f7266462 100644 --- a/fastf1/_api.py +++ b/fastf1/_api.py @@ -4,7 +4,6 @@ import json import zlib from typing import ( - Dict, Optional, Union ) @@ -29,7 +28,7 @@ base_url = 'https://livetiming.formula1.com' -headers: Dict[str, str] = { +headers: dict[str, str] = { 'Host': 'livetiming.formula1.com', 'Connection': 'close', 'TE': 'identity', @@ -37,7 +36,7 @@ 'Accept-Encoding': 'gzip, identity', } -pages: Dict[str, str] = { +pages: dict[str, str] = { 'session_data': 'SessionData.json', # track + session status + lap count 'session_info': 'SessionInfo.jsonStream', # more rnd 'archive_status': 'ArchiveStatus.json', # rnd=1880327548 @@ -83,7 +82,7 @@ def make_path(wname, wdate, sname, sdate): # define all empty columns for timing data -EMPTY_LAPS = {'Time': pd.NaT, 'Driver': str(), 'LapTime': pd.NaT, +EMPTY_LAPS = {'Time': pd.NaT, 'Driver': '', 'LapTime': pd.NaT, 'NumberOfLaps': np.nan, 'NumberOfPitStops': np.nan, 'PitOutTime': pd.NaT, 'PitInTime': pd.NaT, 'Sector1Time': pd.NaT, 'Sector2Time': pd.NaT, @@ -92,7 +91,7 @@ def make_path(wname, wdate, sname, sdate): 'SpeedI1': np.nan, 'SpeedI2': np.nan, 'SpeedFL': np.nan, 'SpeedST': np.nan, 'IsPersonalBest': False} -EMPTY_STREAM = {'Time': pd.NaT, 'Driver': str(), 'Position': np.nan, +EMPTY_STREAM = {'Time': pd.NaT, 'Driver': '', 'Position': np.nan, 'GapToLeader': np.nan, 'IntervalToPositionAhead': np.nan} diff --git a/fastf1/core.py b/fastf1/core.py index e25ad54fc..eacd28dab 100644 --- a/fastf1/core.py +++ b/fastf1/core.py @@ -43,15 +43,13 @@ import re import typing import warnings +from collections.abc import Iterable from functools import cached_property from typing import ( Any, Callable, - Iterable, - List, Literal, Optional, - Tuple, Union ) @@ -925,8 +923,8 @@ def add_driver_ahead(self, drop_existing: bool = True) -> "Telemetry": ) if ((d['Date'].shape != dtd['Date'].shape) - or np.any((d['Date'].values - != dtd['Date'].values))): + or np.any(d['Date'].values + != dtd['Date'].values)): dtd = dtd.resample_channels(new_date_ref=d["Date"]) # indices need to match as .join works index-on-index @@ -1510,7 +1508,7 @@ def _load_laps_data(self, livedata=None): elif not len(d2): result = d1.copy() result.reset_index(drop=True, inplace=True) - result['Compound'] = str() + result['Compound'] = '' result['TyreLife'] = np.nan result['Stint'] = 0 result['New'] = False @@ -3295,7 +3293,7 @@ def pick_accurate(self) -> "Laps": """ return self[self['IsAccurate']] - def split_qualifying_sessions(self) -> List[Optional["Laps"]]: + def split_qualifying_sessions(self) -> list[Optional["Laps"]]: """Splits a lap object into individual laps objects for each qualifying session. @@ -3354,7 +3352,7 @@ def split_qualifying_sessions(self) -> List[Optional["Laps"]]: return laps def iterlaps(self, require: Optional[Iterable] = None) \ - -> Iterable[Tuple[int, "Lap"]]: + -> Iterable[tuple[int, "Lap"]]: """Iterator for iterating over all laps in self. This method wraps :meth:`pandas.DataFrame.iterrows`. @@ -3762,9 +3760,8 @@ class NoLapDataError(Exception): after processing the result. """ def __init__(self, *args): - super(NoLapDataError, self).__init__("Failed to load session because " - "the API did not provide any " - "usable data.") + super().__init__("Failed to load session because the API did not " + "provide any usable data.") class InvalidSessionError(Exception): @@ -3772,6 +3769,4 @@ class InvalidSessionError(Exception): can be found.""" def __init__(self, *args): - super(InvalidSessionError, self).__init__( - "No matching session can be found." - ) + super().__init__("No matching session can be found.") diff --git a/fastf1/ergast/interface.py b/fastf1/ergast/interface.py index ff3f00cbc..a157a8f43 100644 --- a/fastf1/ergast/interface.py +++ b/fastf1/ergast/interface.py @@ -1,10 +1,8 @@ import copy import json from typing import ( - List, Literal, Optional, - Type, Union ) @@ -252,7 +250,7 @@ class ErgastSimpleResponse(ErgastResponseMixin, ErgastResultFrame): _internal_names_set = set(_internal_names) @property - def _constructor(self) -> Type["ErgastResultFrame"]: + def _constructor(self) -> type["ErgastResultFrame"]: # drop from ErgastSimpleResponse to ErgastResultFrame, removing the # ErgastResponseMixin because a slice of the data is no longer a full # response and pagination, ... is therefore not supported anymore @@ -363,7 +361,7 @@ def description(self) -> ErgastResultFrame: return self._description @property - def content(self) -> List[ErgastResultFrame]: + def content(self) -> list[ErgastResultFrame]: """A ``list`` of :class:`ErgastResultFrame` that contain the main response data. diff --git a/fastf1/events.py b/fastf1/events.py index a935141c8..0af2aab00 100644 --- a/fastf1/events.py +++ b/fastf1/events.py @@ -194,7 +194,6 @@ from typing import ( Literal, Optional, - Type, Union ) @@ -624,7 +623,7 @@ def _get_schedule_ff1(year): data[f'session{j+1}_date'][i] = pd.Timestamp(date) data[f'session{j+1}_date_Utc'][i] = pd.Timestamp(date_utc) - str().capitalize() + ''.capitalize() df = pd.DataFrame(data) # change column names from snake_case to UpperCamelCase @@ -885,7 +884,7 @@ def __init__(self, *args, year: int = 0, self[col] = self[col].astype(_type) @property - def _constructor_sliced_horizontal(self) -> Type["Event"]: + def _constructor_sliced_horizontal(self) -> type["Event"]: return Event def is_testing(self): diff --git a/fastf1/internals/fuzzy.py b/fastf1/internals/fuzzy.py index 4a5406464..24fd77a72 100644 --- a/fastf1/internals/fuzzy.py +++ b/fastf1/internals/fuzzy.py @@ -1,5 +1,4 @@ import warnings -from typing import List import numpy as np @@ -15,7 +14,7 @@ def fuzzy_matcher( query: str, - reference: List[List[str]], + reference: list[list[str]], abs_confidence: float = 0.0, rel_confidence: float = 0.0 ) -> (int, bool): diff --git a/fastf1/internals/pandas_extensions.py b/fastf1/internals/pandas_extensions.py index 9a5823c81..3188a5f03 100644 --- a/fastf1/internals/pandas_extensions.py +++ b/fastf1/internals/pandas_extensions.py @@ -1,5 +1,3 @@ -from typing import List - import numpy as np from pandas import ( DataFrame, @@ -35,7 +33,7 @@ def create_df_fast( *, - arrays: List[np.ndarray], + arrays: list[np.ndarray], columns: list, fallback: bool = True ) -> DataFrame: @@ -71,7 +69,7 @@ def create_df_fast( def _fallback_create_df( - arrays: List[np.ndarray], + arrays: list[np.ndarray], columns: list ) -> DataFrame: data = {col: arr for col, arr in zip(columns, arrays)} @@ -87,7 +85,7 @@ def _fallback_if_unsupported(func): @_fallback_if_unsupported def _unsafe_create_df_fast( - arrays: List[np.ndarray], + arrays: list[np.ndarray], columns: list ) -> DataFrame: # Implements parts of pandas' internal DataFrame creation mechanics diff --git a/fastf1/livetiming/__main__.py b/fastf1/livetiming/__main__.py index db2741c2d..bfd5e15a5 100644 --- a/fastf1/livetiming/__main__.py +++ b/fastf1/livetiming/__main__.py @@ -15,7 +15,7 @@ def save(args): def convert(args): - with open(args.input, 'r') as infile: + with open(args.input) as infile: messages = infile.readlines() data, ec = messages_from_raw(messages) with open(args.output, 'w') as outfile: diff --git a/fastf1/livetiming/client.py b/fastf1/livetiming/client.py index 4a845f682..6e006f9cd 100644 --- a/fastf1/livetiming/client.py +++ b/fastf1/livetiming/client.py @@ -3,10 +3,8 @@ import json import logging import time -from typing import ( - Iterable, - Optional -) +from collections.abc import Iterable +from typing import Optional import requests diff --git a/fastf1/livetiming/data.py b/fastf1/livetiming/data.py index 220b3e48e..f287674b1 100644 --- a/fastf1/livetiming/data.py +++ b/fastf1/livetiming/data.py @@ -95,7 +95,7 @@ def load(self): next_data = None else: # read a new file as next file - with open(next_file, 'r') as fobj: + with open(next_file) as fobj: next_data = fobj.readlines() if current_data is None: diff --git a/fastf1/plotting/__init__.py b/fastf1/plotting/__init__.py index 691d8ce99..1e6b6c568 100644 --- a/fastf1/plotting/__init__.py +++ b/fastf1/plotting/__init__.py @@ -1,8 +1,4 @@ import warnings -from typing import ( - Dict, - List -) from fastf1.plotting._constants import \ LEGACY_DRIVER_COLORS as _LEGACY_DRIVER_COLORS @@ -91,11 +87,11 @@ def __getattr__(name): raise AttributeError(f"module {__name__!r} has no attribute {name!r}") -_DEPR_COMPOUND_COLORS: Dict[str, str] = { +_DEPR_COMPOUND_COLORS: dict[str, str] = { key: val for key, val in _Constants['2024'].CompoundColors.items() } -COMPOUND_COLORS: Dict[str, str] +COMPOUND_COLORS: dict[str, str] """ Mapping of tyre compound names to compound colors (hex color codes). (current season only) @@ -107,8 +103,8 @@ def __getattr__(name): """ -_DEPR_DRIVER_COLORS: Dict[str, str] = _LEGACY_DRIVER_COLORS.copy() -DRIVER_COLORS: Dict[str, str] +_DEPR_DRIVER_COLORS: dict[str, str] = _LEGACY_DRIVER_COLORS.copy() +DRIVER_COLORS: dict[str, str] """ Mapping of driver names to driver colors (hex color codes). @@ -123,8 +119,8 @@ def __getattr__(name): """ -_DEPR_DRIVER_TRANSLATE: Dict[str, str] = _LEGACY_DRIVER_TRANSLATE.copy() -DRIVER_TRANSLATE: Dict[str, str] +_DEPR_DRIVER_TRANSLATE: dict[str, str] = _LEGACY_DRIVER_TRANSLATE.copy() +DRIVER_TRANSLATE: dict[str, str] """ Mapping of driver names to theirs respective abbreviations. @@ -138,13 +134,13 @@ def __getattr__(name): future version. Use :func:`~fastf1.plotting.get_driver_name` instead. """ -_DEPR_TEAM_COLORS: Dict[str, str] = { +_DEPR_TEAM_COLORS: dict[str, str] = { # str(key.value): val for key, val # in _Constants['2024'].Colormaps[_Colormaps.Default].items() name.replace("kick ", ""): team.TeamColor.FastF1 for name, team in _Constants['2024'].Teams.items() } -TEAM_COLORS: Dict[str, str] +TEAM_COLORS: dict[str, str] """ Mapping of team names to team colors (hex color codes). (current season only) @@ -154,8 +150,8 @@ def __getattr__(name): future version. Use :func:`~fastf1.plotting.get_team_color` instead. """ -_DEPR_TEAM_TRANSLATE: Dict[str, str] = _LEGACY_TEAM_TRANSLATE.copy() -TEAM_TRANSLATE: Dict[str, str] +_DEPR_TEAM_TRANSLATE: dict[str, str] = _LEGACY_TEAM_TRANSLATE.copy() +TEAM_TRANSLATE: dict[str, str] """ Mapping of team names to theirs respective abbreviations. @@ -164,8 +160,8 @@ def __getattr__(name): future version. Use :func:`~fastf1.plotting.get_team_name` instead. """ -_DEPR_COLOR_PALETTE: List[str] = _COLOR_PALETTE.copy() -COLOR_PALETTE: List[str] +_DEPR_COLOR_PALETTE: list[str] = _COLOR_PALETTE.copy() +COLOR_PALETTE: list[str] """ The default color palette for matplotlib plot lines in fastf1's color scheme. diff --git a/fastf1/plotting/_backend.py b/fastf1/plotting/_backend.py index 02c3ac2ea..ab23e635e 100644 --- a/fastf1/plotting/_backend.py +++ b/fastf1/plotting/_backend.py @@ -1,8 +1,4 @@ import dataclasses -from typing import ( - Dict, - List -) import fastf1._api from fastf1.plotting._base import ( @@ -16,12 +12,12 @@ def _load_drivers_from_f1_livetiming( *, api_path: str, year: str -) -> List[_Team]: +) -> list[_Team]: # load the driver information for the determined session driver_info = fastf1._api.driver_info(api_path) # parse the data into the required format - teams: Dict[str, _Team] = dict() + teams: dict[str, _Team] = dict() # Sorting by driver number here will directly guarantee that drivers # are sorted by driver number within each team. This has two advantages: diff --git a/fastf1/plotting/_base.py b/fastf1/plotting/_base.py index e1a4cdfc2..0f7a71703 100644 --- a/fastf1/plotting/_base.py +++ b/fastf1/plotting/_base.py @@ -1,8 +1,4 @@ import unicodedata -from typing import ( - Dict, - List -) from fastf1.logger import get_logger from fastf1.plotting._constants.base import TeamConst @@ -25,21 +21,21 @@ class _Team: def __init__(self): super().__init__() - self.drivers: List["_Driver"] = list() + self.drivers: list[_Driver] = list() class _DriverTeamMapping: def __init__( self, year: str, - teams: List[_Team], + teams: list[_Team], ): self.year = year self.teams = teams - self.drivers_by_normalized: Dict[str, _Driver] = dict() - self.drivers_by_abbreviation: Dict[str, _Driver] = dict() - self.teams_by_normalized: Dict[str, _Team] = dict() + self.drivers_by_normalized: dict[str, _Driver] = dict() + self.drivers_by_abbreviation: dict[str, _Driver] = dict() + self.teams_by_normalized: dict[str, _Team] = dict() for team in teams: for driver in team.drivers: diff --git a/fastf1/plotting/_constants/__init__.py b/fastf1/plotting/_constants/__init__.py index 34fe5fbc4..f79bf1aa2 100644 --- a/fastf1/plotting/_constants/__init__.py +++ b/fastf1/plotting/_constants/__init__.py @@ -1,5 +1,3 @@ -from typing import Dict - from fastf1.plotting._constants import ( # noqa: F401, unused import used through globals() season2018, season2019, @@ -12,7 +10,7 @@ from fastf1.plotting._constants.base import BaseSeasonConst -Constants: Dict[str, BaseSeasonConst] = dict() +Constants: dict[str, BaseSeasonConst] = dict() for year in range(2018, 2025): season = globals()[f"season{year}"] @@ -32,7 +30,7 @@ } -LEGACY_TEAM_TRANSLATE: Dict[str, str] = { +LEGACY_TEAM_TRANSLATE: dict[str, str] = { 'MER': 'mercedes', 'FER': 'ferrari', 'RBR': 'red bull', @@ -46,7 +44,7 @@ } -LEGACY_DRIVER_COLORS: Dict[str, str] = { +LEGACY_DRIVER_COLORS: dict[str, str] = { "valtteri bottas": "#00e701", "zhou guanyu": "#008d01", "theo pourchaire": "#004601", @@ -93,7 +91,7 @@ } -LEGACY_DRIVER_TRANSLATE: Dict[str, str] = { +LEGACY_DRIVER_TRANSLATE: dict[str, str] = { 'LEC': 'charles leclerc', 'SAI': 'carlos sainz', 'SHW': 'robert shwartzman', 'VER': 'max verstappen', 'PER': 'sergio perez', diff --git a/fastf1/plotting/_constants/base.py b/fastf1/plotting/_constants/base.py index b492fbaee..fe74202a0 100644 --- a/fastf1/plotting/_constants/base.py +++ b/fastf1/plotting/_constants/base.py @@ -1,5 +1,4 @@ from dataclasses import dataclass -from typing import Dict class CompoundsConst: @@ -30,5 +29,5 @@ class TeamConst: @dataclass(frozen=True) class BaseSeasonConst: - CompoundColors: Dict[str, str] - Teams: Dict[str, TeamConst] + CompoundColors: dict[str, str] + Teams: dict[str, TeamConst] diff --git a/fastf1/plotting/_constants/season2018.py b/fastf1/plotting/_constants/season2018.py index c8bb81280..782ec33ba 100644 --- a/fastf1/plotting/_constants/season2018.py +++ b/fastf1/plotting/_constants/season2018.py @@ -1,4 +1,3 @@ -from typing import Dict from fastf1.plotting._constants.base import ( CompoundsConst, @@ -11,7 +10,7 @@ # and values may be modified there, it the used API provides different values -Teams: Dict[str, TeamConst] = { +Teams: dict[str, TeamConst] = { 'ferrari': TeamConst( ShortName='Ferrari', TeamColor=TeamColorsConst( @@ -91,7 +90,7 @@ ) } -CompoundColors: Dict[CompoundsConst, str] = { +CompoundColors: dict[CompoundsConst, str] = { CompoundsConst.HyperSoft: "#feb1c1", CompoundsConst.UltraSoft: "#b24ba7", CompoundsConst.SuperSoft: "#fc2b2a", diff --git a/fastf1/plotting/_constants/season2019.py b/fastf1/plotting/_constants/season2019.py index 7c786d602..4b7038cc4 100644 --- a/fastf1/plotting/_constants/season2019.py +++ b/fastf1/plotting/_constants/season2019.py @@ -1,4 +1,3 @@ -from typing import Dict from fastf1.plotting._constants.base import ( CompoundsConst, @@ -11,7 +10,7 @@ # and values may be modified there, it the used API provides different values -Teams: Dict[str, TeamConst] = { +Teams: dict[str, TeamConst] = { 'alfa romeo': TeamConst( ShortName='Alfa Romeo', TeamColor=TeamColorsConst( @@ -84,7 +83,7 @@ ) } -CompoundColors: Dict[CompoundsConst, str] = { +CompoundColors: dict[CompoundsConst, str] = { CompoundsConst.Soft: "#da291c", CompoundsConst.Medium: "#ffd12e", CompoundsConst.Hard: "#f0f0ec", diff --git a/fastf1/plotting/_constants/season2020.py b/fastf1/plotting/_constants/season2020.py index c4f13ddfe..5c87b33f4 100644 --- a/fastf1/plotting/_constants/season2020.py +++ b/fastf1/plotting/_constants/season2020.py @@ -1,4 +1,3 @@ -from typing import Dict from fastf1.plotting._constants.base import ( CompoundsConst, @@ -11,7 +10,7 @@ # and values may be modified there, it the used API provides different values -Teams: Dict[str, TeamConst] = { +Teams: dict[str, TeamConst] = { 'alfa romeo': TeamConst( ShortName='Alfa Romeo', TeamColor=TeamColorsConst( @@ -84,7 +83,7 @@ ) } -CompoundColors: Dict[CompoundsConst, str] = { +CompoundColors: dict[CompoundsConst, str] = { CompoundsConst.Soft: "#da291c", CompoundsConst.Medium: "#ffd12e", CompoundsConst.Hard: "#f0f0ec", diff --git a/fastf1/plotting/_constants/season2021.py b/fastf1/plotting/_constants/season2021.py index 3a03b274e..9273a39ae 100644 --- a/fastf1/plotting/_constants/season2021.py +++ b/fastf1/plotting/_constants/season2021.py @@ -1,4 +1,3 @@ -from typing import Dict from fastf1.plotting._constants.base import ( CompoundsConst, @@ -11,7 +10,7 @@ # and values may be modified there, it the used API provides different values -Teams: Dict[str, TeamConst] = { +Teams: dict[str, TeamConst] = { 'alfa romeo': TeamConst( ShortName='Alfa Romeo', TeamColor=TeamColorsConst( @@ -84,7 +83,7 @@ ) } -CompoundColors: Dict[CompoundsConst, str] = { +CompoundColors: dict[CompoundsConst, str] = { CompoundsConst.Soft: "#da291c", CompoundsConst.Medium: "#ffd12e", CompoundsConst.Hard: "#f0f0ec", diff --git a/fastf1/plotting/_constants/season2022.py b/fastf1/plotting/_constants/season2022.py index 4f09b282c..6983a01df 100644 --- a/fastf1/plotting/_constants/season2022.py +++ b/fastf1/plotting/_constants/season2022.py @@ -1,4 +1,3 @@ -from typing import Dict from fastf1.plotting._constants.base import ( CompoundsConst, @@ -11,7 +10,7 @@ # and values may be modified there, it the used API provides different values -Teams: Dict[str, TeamConst] = { +Teams: dict[str, TeamConst] = { 'alfa romeo': TeamConst( ShortName='Alfa Romeo', TeamColor=TeamColorsConst( @@ -84,7 +83,7 @@ ) } -CompoundColors: Dict[CompoundsConst, str] = { +CompoundColors: dict[CompoundsConst, str] = { CompoundsConst.Soft: "#da291c", CompoundsConst.Medium: "#ffd12e", CompoundsConst.Hard: "#f0f0ec", diff --git a/fastf1/plotting/_constants/season2023.py b/fastf1/plotting/_constants/season2023.py index be88f828e..e58cdfbc0 100644 --- a/fastf1/plotting/_constants/season2023.py +++ b/fastf1/plotting/_constants/season2023.py @@ -1,4 +1,3 @@ -from typing import Dict from fastf1.plotting._constants.base import ( CompoundsConst, @@ -11,7 +10,7 @@ # and values may be modified there, it the used API provides different values -Teams: Dict[str, TeamConst] = { +Teams: dict[str, TeamConst] = { 'alfa romeo': TeamConst( ShortName='Alfa Romeo', TeamColor=TeamColorsConst( @@ -84,7 +83,7 @@ ) } -CompoundColors: Dict[CompoundsConst, str] = { +CompoundColors: dict[CompoundsConst, str] = { CompoundsConst.Soft: "#da291c", CompoundsConst.Medium: "#ffd12e", CompoundsConst.Hard: "#f0f0ec", diff --git a/fastf1/plotting/_constants/season2024.py b/fastf1/plotting/_constants/season2024.py index b067d74b1..4d1b23ed2 100644 --- a/fastf1/plotting/_constants/season2024.py +++ b/fastf1/plotting/_constants/season2024.py @@ -1,4 +1,3 @@ -from typing import Dict from fastf1.plotting._constants.base import ( CompoundsConst, @@ -11,7 +10,7 @@ # and values may be modified there, if the used API provides different values -Teams: Dict[str, TeamConst] = { +Teams: dict[str, TeamConst] = { 'alpine': TeamConst( ShortName='Alpine', TeamColor=TeamColorsConst( @@ -84,7 +83,7 @@ ) } -CompoundColors: Dict[CompoundsConst, str] = { +CompoundColors: dict[CompoundsConst, str] = { CompoundsConst.Soft: "#da291c", CompoundsConst.Medium: "#ffd12e", CompoundsConst.Hard: "#f0f0ec", diff --git a/fastf1/plotting/_interface.py b/fastf1/plotting/_interface.py index d0d085836..ae6ae6236 100644 --- a/fastf1/plotting/_interface.py +++ b/fastf1/plotting/_interface.py @@ -1,11 +1,9 @@ import dataclasses +from collections.abc import Sequence from typing import ( Any, - Dict, - List, Literal, Optional, - Sequence, Union ) @@ -334,7 +332,7 @@ def get_driver_abbreviation( def get_driver_names_by_team( identifier: str, session: Session, *, exact_match: bool = False -) -> List[str]: +) -> list[str]: """ Get a list of full names of all drivers that drove for a team in a given session based on a recognizable and identifiable part of the team name. @@ -351,7 +349,7 @@ def get_driver_names_by_team( def get_driver_abbreviations_by_team( identifier: str, session: Session, *, exact_match: bool = False -) -> List[str]: +) -> list[str]: """ Get a list of abbreviations of all drivers that drove for a team in a given session based on a recognizable and identifiable part of the team name. @@ -411,7 +409,7 @@ def get_driver_style( colormap: str = 'default', additional_color_kws: Union[list, tuple] = (), exact_match: bool = False -) -> Dict[str, Any]: +) -> dict[str, Any]: """ Get a plotting style that is unique for a driver based on the driver's abbreviation or based on a recognizable and identifiable part of the @@ -626,7 +624,7 @@ def get_compound_color(compound: str, session: Session) -> str: return _Constants[year].CompoundColors[compound.upper()] -def get_compound_mapping(session: Session) -> Dict[str, str]: +def get_compound_mapping(session: Session) -> dict[str, str]: """ Returns a dictionary that maps compound names to their associated colors. The colors are given as hexadecimal RGB color codes. @@ -643,7 +641,7 @@ def get_compound_mapping(session: Session) -> Dict[str, str]: def get_driver_color_mapping( session: Session, *, colormap: str = 'default', -) -> Dict[str, str]: +) -> dict[str, str]: """ Returns a dictionary that maps driver abbreviations to their associated colors. The colors are given as hexadecimal RGB color codes. @@ -677,7 +675,7 @@ def get_driver_color_mapping( return colors -def list_team_names(session: Session, *, short: bool = False) -> List[str]: +def list_team_names(session: Session, *, short: bool = False) -> list[str]: """Returns a list of team names of all teams in the ``session``. By default, the full team names are returned. Use the ``short`` argument @@ -699,19 +697,19 @@ def list_team_names(session: Session, *, short: bool = False) -> List[str]: return list(team.value for team in dtm.teams_by_normalized.values()) -def list_driver_abbreviations(session: Session) -> List[str]: +def list_driver_abbreviations(session: Session) -> list[str]: """Returns a list of abbreviations of all drivers in the ``session``.""" dtm = _get_driver_team_mapping(session) return list(dtm.drivers_by_abbreviation.keys()) -def list_driver_names(session: Session) -> List[str]: +def list_driver_names(session: Session) -> list[str]: """Returns a list of full names of all drivers in the ``session``.""" dtm = _get_driver_team_mapping(session) return list(driver.value for driver in dtm.drivers_by_normalized.values()) -def list_compounds(session: Session) -> List[str]: +def list_compounds(session: Session) -> list[str]: """Returns a list of all compound names for this season (not session).""" year = str(session.event['EventDate'].year) return list(_Constants[year].CompoundColors.keys()) diff --git a/fastf1/plotting/_plotting.py b/fastf1/plotting/_plotting.py index a75d4560a..2256a15b5 100644 --- a/fastf1/plotting/_plotting.py +++ b/fastf1/plotting/_plotting.py @@ -1,8 +1,5 @@ import warnings -from typing import ( - List, - Optional -) +from typing import Optional import numpy as np import pandas as pd @@ -37,7 +34,7 @@ _logger = get_logger(__package__) -_COLOR_PALETTE: List[str] = ['#FF79C6', '#50FA7B', '#8BE9FD', '#BD93F9', +_COLOR_PALETTE: list[str] = ['#FF79C6', '#50FA7B', '#8BE9FD', '#BD93F9', '#FFB86C', '#FF5555', '#F1FA8C'] # The default color palette for matplotlib plot lines in fastf1's color scheme @@ -181,9 +178,9 @@ def driver_color(identifier: str) -> str: key_ratios.sort(reverse=True) if key_ratios[0][0] != 100: _logger.warning( - ("Correcting invalid user input " + "Correcting invalid user input " f"'{identifier}' to '{key_ratios[0][1]}'." - ) + ) if ((key_ratios[0][0] < 35) or (key_ratios[0][0] / key_ratios[1][0] < 1.2)): @@ -268,9 +265,9 @@ def team_color(identifier: str) -> str: key_ratios.sort(reverse=True) if key_ratios[0][0] != 100: _logger.warning( - ("Correcting invalid user input " + "Correcting invalid user input " f"'{identifier}' to '{key_ratios[0][1]}'." - ) + ) if ((key_ratios[0][0] < 35) or (key_ratios[0][0] / key_ratios[1][0] < 1.2)): diff --git a/fastf1/req.py b/fastf1/req.py index d23a01f31..f16e0d6ec 100644 --- a/fastf1/req.py +++ b/fastf1/req.py @@ -29,10 +29,7 @@ import re import sys import time -from typing import ( - Optional, - Tuple -) +from typing import Optional import requests from requests_cache import CacheMixin @@ -524,8 +521,8 @@ def _enable_default_cache(cls): try: os.mkdir(cache_dir, mode=0o0700) except Exception as err: - _logger.error("Failed to create cache directory {0}. " - "Error {1}".format(cache_dir, err)) + _logger.error(f"Failed to create cache directory " + f"{cache_dir}. Error {err}") raise # Enable cache with default @@ -632,7 +629,7 @@ def ci_mode(cls, enabled: bool): cls._ci_mode = enabled @classmethod - def get_cache_info(cls) -> Tuple[Optional[str], Optional[int]]: + def get_cache_info(cls) -> tuple[Optional[str], Optional[int]]: """Returns information about the cache directory and its size. If the cache is not configured, None will be returned for both the @@ -658,7 +655,7 @@ def _convert_size(cls, size_bytes): # https://stackoverflow.com/questions/51940 i = int(math.floor(math.log(size_bytes, 1024))) p = math.pow(1024, i) s = round(size_bytes / p, 2) - return "%s %s" % (s, size_name[i]) + return f"{s} {size_name[i]}" @classmethod def _get_size(cls, start_path='.'): # https://stackoverflow.com/questions/1392413/calculating-a-directorys-size-using-python # noqa: E501 diff --git a/fastf1/utils.py b/fastf1/utils.py index 4e35f0314..b3fb36074 100644 --- a/fastf1/utils.py +++ b/fastf1/utils.py @@ -3,9 +3,7 @@ import warnings from functools import reduce from typing import ( - Dict, Optional, - Tuple, Union ) @@ -22,7 +20,7 @@ def delta_time( reference_lap: "fastf1.core.Lap", compare_lap: "fastf1.core.Lap" -) -> Tuple[pd.Series, "fastf1.core.Telemetry", "fastf1.core.Telemetry"]: +) -> tuple[pd.Series, "fastf1.core.Telemetry", "fastf1.core.Telemetry"]: """Calculates the delta time of a given lap, along the 'Distance' axis of the reference lap. @@ -114,7 +112,7 @@ def mini_pro(stream): return delta, ref, comp -def recursive_dict_get(d: Dict, *keys: str, default_none: bool = False): +def recursive_dict_get(d: dict, *keys: str, default_none: bool = False): """Recursive dict get. Can take an arbitrary number of keys and returns an empty dict if any key does not exist. https://stackoverflow.com/a/28225747""" diff --git a/pyproject.toml b/pyproject.toml index 13b9d4c24..c4e5a75af 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -12,16 +12,16 @@ readme = "README.md" license = { file = "LICENSE" } # minimum python version additionally needs to be changed in the test matrix -requires-python = ">=3.8" +requires-python = ">=3.9" # minimum package versions additionally need to be changed in requirements/minver.txt dependencies = [ "matplotlib>=3.5.1,<4.0.0", - "numpy>=1.21.5,<3.0.0", + "numpy>=1.23.1,<3.0.0", "pandas>=1.4.1,<3.0.0", "python-dateutil", "requests>=2.28.1", "requests-cache>=1.0.0", - "scipy>=1.7.3,<2.0.0", + "scipy>=1.8.1,<2.0.0", "rapidfuzz", "timple>=0.1.6", "websockets>=10.3", @@ -85,6 +85,7 @@ select = [ "E", "F", "W", + "UP", "NPY201" ] diff --git a/requirements/minver.txt b/requirements/minver.txt index 1f0f33cc4..cd2f83f8f 100644 --- a/requirements/minver.txt +++ b/requirements/minver.txt @@ -1,8 +1,8 @@ matplotlib==3.5.1 -numpy==1.21.5 +numpy==1.23.1 pandas==1.4.1 requests==2.28.1 requests-cache==1.0.0 -scipy==1.7.3 +scipy==1.8.1 timple==0.1.6 websockets==10.3 \ No newline at end of file From bc96035febfc858bcf3168a82df1a462a0911b32 Mon Sep 17 00:00:00 2001 From: theOehrly <23384863+theOehrly@users.noreply.github.com> Date: Mon, 22 Jul 2024 10:52:20 +0200 Subject: [PATCH 2/8] FIX: properly handle missing driver info on F1 LT Api --- fastf1/core.py | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/fastf1/core.py b/fastf1/core.py index eacd28dab..398ae6f80 100644 --- a/fastf1/core.py +++ b/fastf1/core.py @@ -2243,7 +2243,7 @@ def _drivers_from_f1_api(self, *, livedata=None): except Exception as exc: _logger.warning("Failed to load extended driver information!") _logger.debug("Exception while loading driver list", exc_info=exc) - driver_info = {} + return None else: driver_info = collections.defaultdict(list) @@ -2273,11 +2273,12 @@ def _drivers_from_f1_api(self, *, livedata=None): driver_info['LastName']): driver_info['FullName'].append(f"{first} {last}") - # driver info is required for joining on index (used as index), - # therefore drop rows where driver number is unavailable as they have - # an invalid index - return pd.DataFrame(driver_info, index=driver_info['DriverNumber']) \ - .dropna(subset=['DriverNumber']) + # driver info is required for joining on index (used as index), + # therefore drop rows where driver number is unavailable as they + # have an invalid index + return pd.DataFrame( + driver_info, index=driver_info['DriverNumber'] + ).dropna(subset=['DriverNumber']) def _drivers_results_from_ergast( self, *, load_drivers=False, load_results=False From d26c645f7f077ee35563d25962bbf8330e0a4a72 Mon Sep 17 00:00:00 2001 From: theOehrly <23384863+theOehrly@users.noreply.github.com> Date: Tue, 23 Jul 2024 18:03:31 +0200 Subject: [PATCH 3/8] ENH: implement MVP fallback to livetiming mirror --- fastf1/_api.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/fastf1/_api.py b/fastf1/_api.py index 1f7266462..b6d912124 100644 --- a/fastf1/_api.py +++ b/fastf1/_api.py @@ -27,9 +27,9 @@ _logger = get_logger('api') base_url = 'https://livetiming.formula1.com' +base_url_mirror = 'https://livetiming-mirror.fastf1.dev' headers: dict[str, str] = { - 'Host': 'livetiming.formula1.com', 'Connection': 'close', 'TE': 'identity', 'User-Agent': 'BestHTTP', @@ -1698,7 +1698,14 @@ def fetch_page(path, name): page = pages[name] is_stream = 'jsonStream' in page is_z = '.z.' in page + r = Cache.requests_get(base_url + path + pages[name], headers=headers) + + if r.status_code >= 400: + _logger.debug(f"Falling back to livetiming mirror ({base_url_mirror})") + r = Cache.requests_get(base_url_mirror + path + pages[name], + headers=headers) + if r.status_code == 200: raw = r.content.decode('utf-8-sig') if is_stream: From 01b6890388bf7dcbcc204e7d84c82c9449d89a5d Mon Sep 17 00:00:00 2001 From: theOehrly <23384863+theOehrly@users.noreply.github.com> Date: Wed, 24 Jul 2024 17:22:30 +0200 Subject: [PATCH 4/8] CI/DOC: allow overriding Ergast backend for doc build --- docs/conf.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/docs/conf.py b/docs/conf.py index 389f4a0f8..5bc3dfe6a 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -22,6 +22,14 @@ sys.path.append(os.path.abspath('extensions')) +ERGAST_BACKEND_OVERRIDE = os.environ.get("FASTF1_DOCS_ERGAST_BACKEND_OVERRIDE") + +if ERGAST_BACKEND_OVERRIDE: + import fastf1.ergast + + fastf1.ergast.interface.BASE_URL = ERGAST_BACKEND_OVERRIDE + + # -- FastF1 specific config -------------------------------------------------- # ignore warning on import of fastf1.api warnings.filterwarnings(action='ignore', From 193afd1502c768bd43223109a8824cc84db5c4d2 Mon Sep 17 00:00:00 2001 From: theOehrly <23384863+theOehrly@users.noreply.github.com> Date: Thu, 25 Jul 2024 10:39:30 +0200 Subject: [PATCH 5/8] ENH: reduce Ergast connection timeout to prevent extremley long waiting times --- fastf1/ergast/interface.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/fastf1/ergast/interface.py b/fastf1/ergast/interface.py index a157a8f43..301c58933 100644 --- a/fastf1/ergast/interface.py +++ b/fastf1/ergast/interface.py @@ -16,6 +16,7 @@ BASE_URL = 'https://ergast.com/api/f1' +TIMEOUT = 5.0 HEADERS = {'User-Agent': f'FastF1/{__version_short__}'} @@ -487,7 +488,8 @@ def _build_url( @classmethod def _get(cls, url: str, params: dict) -> Union[dict, list]: # request data from ergast and load the returned json data. - r = Cache.requests_get(url, headers=HEADERS, params=params) + r = Cache.requests_get(url, headers=HEADERS, params=params, + timeout=TIMEOUT) if r.status_code == 200: try: return json.loads(r.content.decode('utf-8')) From fffdf45a765ddb02a44abca2fae7b18433038404 Mon Sep 17 00:00:00 2001 From: theOehrly <23384863+theOehrly@users.noreply.github.com> Date: Thu, 25 Jul 2024 21:11:34 +0200 Subject: [PATCH 6/8] CI: fix ergast backend overrides for tests and docs --- .github/workflows/docs.yml | 5 +++++ .github/workflows/tests.yml | 5 +++++ 2 files changed, 10 insertions(+) diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index a847993a5..c0b7fc7d1 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -11,6 +11,11 @@ on: types: [ released ] +env: + # Set environment variable with value from configuration variable + FASTF1_DOCS_ERGAST_BACKEND_OVERRIDE: ${{ vars.FASTF1_DOCS_ERGAST_BACKEND_OVERRIDE }} + + jobs: build_docs: name: Build Documentation diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 883cbd0dc..d92a5c7e2 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -8,6 +8,11 @@ on: pull_request: +env: + # Set environment variable with value from configuration variable + FASTF1_TEST_ERGAST_BACKEND_OVERRIDE: ${{ vars.FASTF1_TEST_ERGAST_BACKEND_OVERRIDE }} + + jobs: run-code-tests: runs-on: ubuntu-latest From 9763d1abf283e75a48a980784efe4bcd5364007d Mon Sep 17 00:00:00 2001 From: theOehrly <23384863+theOehrly@users.noreply.github.com> Date: Thu, 25 Jul 2024 22:44:26 +0200 Subject: [PATCH 7/8] FIX: bug in rate limiting causing higher than intended rates --- fastf1/req.py | 1 + 1 file changed, 1 insertion(+) diff --git a/fastf1/req.py b/fastf1/req.py index f16e0d6ec..57a198c5b 100644 --- a/fastf1/req.py +++ b/fastf1/req.py @@ -77,6 +77,7 @@ def limit(self): t_now = time.time() if (delta := (t_now - self._t_last)) < self._interval: time.sleep(self._interval - delta) + t_now += self._interval - delta self._t_last = t_now From 93bfddee53608a34b7275151a3f3f60eac5fb3fa Mon Sep 17 00:00:00 2001 From: theOehrly <23384863+theOehrly@users.noreply.github.com> Date: Thu, 25 Jul 2024 22:23:35 +0200 Subject: [PATCH 8/8] TST/CI: fixes and workarounds for various API issues --- docs/ergast.rst | 8 +++++--- fastf1/req.py | 9 ++++++++- fastf1/tests/test_cache.py | 5 +++-- fastf1/tests/test_events.py | 6 ++---- 4 files changed, 18 insertions(+), 10 deletions(-) diff --git a/docs/ergast.rst b/docs/ergast.rst index a2b05eef5..34d096b0f 100644 --- a/docs/ergast.rst +++ b/docs/ergast.rst @@ -206,9 +206,11 @@ response. When 'pandas' is selected as result type, these endpoints return a :class:`~fastf1.ergast.interface.ErgastMultiResponse`. One such endpoint is the constructor standings endpoint. +.. TODO: the following doctests are skipped because of the broken Ergast API + .. doctest:: - >>> standings = ergast.get_constructor_standings() + >>> standings = ergast.get_constructor_standings() # doctest: +SKIP Called without any 'season' specifier, it returns standings for multiple seasons. An overview over the returned data is available as a ``.description`` @@ -216,7 +218,7 @@ of the response: .. doctest:: - >>> standings.description + >>> standings.description # doctest: +SKIP season round 0 1958 11 1 1959 9 @@ -237,7 +239,7 @@ The first element in ``.content`` is associated with the first row of the .. doctest:: - >>> standings.content[0] + >>> standings.content[0] # doctest: +SKIP position positionText ... constructorName constructorNationality 0 1 1 ... Vanwall British 1 2 2 ... Ferrari Italian diff --git a/fastf1/req.py b/fastf1/req.py index 57a198c5b..46cc669b7 100644 --- a/fastf1/req.py +++ b/fastf1/req.py @@ -110,7 +110,14 @@ class _SessionWithRateLimiting(requests.Session): # soft limit 4 calls/sec _CallsPerIntervalLimitRaise(200, 60*60, "ergast.com: 200 calls/h") # hard limit 200 calls/h - ] + ], + # general limits on all other APIs + re.compile(r"^https?://.+\..+"): [ + _MinIntervalLimitDelay(0.25), + # soft limit 4 calls/sec + _CallsPerIntervalLimitRaise(500, 60 * 60, "any API: 500 calls/h") + # hard limit 200 calls/h + ], } def send(self, request, **kwargs): diff --git a/fastf1/tests/test_cache.py b/fastf1/tests/test_cache.py index 5c0e04d0d..53f1905a5 100644 --- a/fastf1/tests/test_cache.py +++ b/fastf1/tests/test_cache.py @@ -2,6 +2,7 @@ import os import fastf1._api +import fastf1.ergast.interface import fastf1.testing from fastf1 import Cache from fastf1.logger import LoggingManager @@ -56,13 +57,13 @@ def _test_cache_used_and_clear(tmpdir): with open('fastf1/testing/reference_data/2020_05_FP2/' 'ergast_race.raw', 'rb') as fobj: content = fobj.read() - mocker.get('https://ergast.com/api/f1/2020/5.json', + mocker.get(f'{fastf1.ergast.interface.BASE_URL}/2020/5.json', content=content, status_code=200) with open('fastf1/testing/reference_data/2020_05_FP2/' 'ergast_race_result.raw', 'rb') as fobj: content = fobj.read() - mocker.get('https://ergast.com/api/f1/2020/5/results.json', + mocker.get(f'{fastf1.ergast.interface.BASE_URL}/2020/5/results.json', content=content, status_code=200) # rainy and short session, good for fast test/quick loading diff --git a/fastf1/tests/test_events.py b/fastf1/tests/test_events.py index 2efc5baf2..0d4d5f50a 100644 --- a/fastf1/tests/test_events.py +++ b/fastf1/tests/test_events.py @@ -237,10 +237,8 @@ def test_event_get_session_name(backend): assert event.get_session_name('Sprint') == 'Sprint' # Sprint Qualifying format introduced for 2024 - if ((backend == 'f1timing') - and (datetime.datetime.now() < datetime.datetime(2024, 4, 21))): - # disables this test until the data should be available - # TODO: remove early exit at any time after 2024/04/21 + if backend == 'f1timing': + # disables this test for the broken livetiming backend -> TODO return event = fastf1.get_event(2024, 5, backend=backend) assert event.year == 2024