From e8a2624049a1aa7edd84525bdbf6cd6e7b9d39db Mon Sep 17 00:00:00 2001 From: Gabriel Date: Wed, 11 Mar 2026 15:25:13 -0300 Subject: [PATCH] Fix silent dropping of stints in legacy list format Historic races (approx. 2018-2019) return Stints from the timing app API as a plain list instead of a dict with string indices. The previous parser assumed dict format unconditionally, causing all stints to be silently dropped when a list was received. Normalize list format to dict before iteration, and switch the loop to use .items() directly, removing the ambiguous isinstance check that was previously inside the loop body. Fixes #863 --- fastf1/_api.py | 8 +++--- fastf1/tests/test_api.py | 62 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 66 insertions(+), 4 deletions(-) diff --git a/fastf1/_api.py b/fastf1/_api.py index 535003a37..7676c53cc 100644 --- a/fastf1/_api.py +++ b/fastf1/_api.py @@ -892,10 +892,10 @@ def timing_app_data(path, response=None, livedata=None): row = entry[1] for driver_number in row['Lines']: if update := recursive_dict_get(row, 'Lines', driver_number, 'Stints'): - for stint_number, stint in enumerate(update): - if isinstance(update, dict): - stint_number = int(stint) - stint = update[stint] + if isinstance(update, list): + update = {str(i): s for i, s in enumerate(update)} + for stint_number, stint in update.items(): + stint_number = int(stint_number) for key in data: if key in stint: val = stint[key] diff --git a/fastf1/tests/test_api.py b/fastf1/tests/test_api.py index 8c3c78728..ee170db11 100644 --- a/fastf1/tests/test_api.py +++ b/fastf1/tests/test_api.py @@ -266,6 +266,68 @@ def test_driver_list_contains_support_race(caplog): _, _, warn_message = caplog.record_tuples[0] assert warn_message.startswith("Skipping delayed declaration of driver") +def test_timing_app_data_legacy_list_format(): + """Stints in list format (legacy API, ~2018-2019) must be parsed correctly. + + Historic races return stints as a plain list instead of a dict with + string indices. Previously, this caused stints to be silently dropped. + See issue #863. + """ + response = [ + [ + "00:05:00:000", + { + "Lines": { + "1": { + "Stints": [ + {"Compound": "SOFT", "New": "true", + "TotalLaps": 0, "StartLaps": 0}, + {"Compound": "HARD", "New": "false", + "TotalLaps": 10, "StartLaps": 10}, + ] + } + } + } + ] + ] + + data = fastf1._api.timing_app_data('api/path', response=response) + + assert isinstance(data, pd.DataFrame) + assert len(data) == 2 # both stints must be present + assert list(data['Stint']) == [0, 1] + assert list(data['Compound']) == ['SOFT', 'HARD'] + assert list(data['Driver']) == ['1', '1'] + + +def test_timing_app_data_modern_dict_format(): + """Stints in dict format (modern API) must continue to work correctly.""" + response = [ + [ + "00:05:00:000", + { + "Lines": { + "1": { + "Stints": { + "0": {"Compound": "MEDIUM", "New": "true", + "TotalLaps": 0, "StartLaps": 0}, + "1": {"Compound": "HARD", "New": "false", + "TotalLaps": 20, "StartLaps": 20}, + } + } + } + } + ] + ] + + data = fastf1._api.timing_app_data('api/path', response=response) + + assert isinstance(data, pd.DataFrame) + assert len(data) == 2 # both stints must be present + assert list(data['Stint']) == [0, 1] + assert list(data['Compound']) == ['MEDIUM', 'HARD'] + assert list(data['Driver']) == ['1', '1'] + @pytest.mark.f1telapi def test_deleted_laps_not_marked_personal_best(): # see issue #165