├── livef1 ├── utils │ ├── __init__.py │ ├── logger.py │ └── exceptions.py ├── adapters │ ├── signalr_aio │ │ ├── hubs │ │ │ ├── __init__.py │ │ │ └── _hub.py │ │ ├── __init__.py │ │ ├── events │ │ │ ├── __init__.py │ │ │ └── _events.py │ │ ├── transports │ │ │ ├── __init__.py │ │ │ ├── _exceptions.py │ │ │ ├── _queue_events.py │ │ │ ├── _parameters.py │ │ │ └── _transport.py │ │ ├── Readme.md │ │ └── _connection.py │ ├── __init__.py │ ├── functions.py │ ├── other.py │ ├── livetimingf1_adapter.py │ └── realtime_client.py ├── data_processing │ ├── __init__.py │ ├── data_models.py │ └── etl.py ├── models │ ├── __init__.py │ ├── driver.py │ ├── circuit.py │ ├── season.py │ └── meeting.py ├── __init__.py ├── data │ ├── circuits_key.csv │ ├── find_track_start.py │ └── starting_coordinates.json └── api.py ├── test ├── simple_test.py ├── temp_test.py ├── test_realtime_client_basic_test.py └── table_register.py ├── docs ├── source │ ├── livetimingf1 │ │ ├── index.rst │ │ └── f1_data.rst │ ├── .DS_Store │ ├── 404.rst │ ├── _static │ │ ├── .DS_Store │ │ ├── logo.png │ │ ├── favicon.png │ │ ├── LiveF1_red.png │ │ ├── data_lake.png │ │ ├── LiveF1_white.png │ │ ├── LiveF1Overview1.png │ │ ├── robots.txt │ │ └── class.rst │ ├── user_guide │ │ ├── telemetry_data.rst │ │ ├── visualization.rst │ │ ├── informative_data.rst │ │ ├── working_with_realtime_client.rst │ │ ├── accessing_data.rst │ │ ├── logging_config.rst │ │ ├── data_objects.rst │ │ ├── index.rst │ │ ├── data_models.rst │ │ ├── realtime_data.rst │ │ └── historical_data.rst │ ├── features │ │ └── index.rst │ ├── working_with_data │ │ └── index.rst │ ├── developer_notes │ │ └── index.rst │ ├── additional_resources │ │ └── index.rst │ ├── api.rst │ ├── api_reference │ │ ├── index.rst │ │ ├── realtime_client.rst │ │ ├── livetimingf1_adapter.rst │ │ ├── api.rst │ │ ├── medallion_arch.rst │ │ ├── models.rst │ │ ├── utils.rst │ │ └── data_processing.rst │ ├── getting_started │ │ ├── installation.rst │ │ └── index.rst │ ├── examples │ │ ├── index.rst │ │ └── pipelines │ │ │ └── race_analysis.rst │ ├── index.rst │ └── conf.py ├── .DS_Store ├── requirements.txt ├── Makefile └── make.bat ├── .DS_Store ├── requirements.txt ├── setup.cfg ├── test.py ├── .github ├── pip_dependency.yml └── workflows │ ├── publish.yml │ ├── python-publish.yml │ └── build_doc.yml ├── LICENSE ├── setup.py ├── .gitignore ├── CODE_OF_CONDUCT.md ├── README.md └── 2025_schedule.csv /livef1/utils/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /test/simple_test.py: -------------------------------------------------------------------------------- 1 | import livef1 -------------------------------------------------------------------------------- /docs/source/livetimingf1/index.rst: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GoktugOcal/LiveF1/HEAD/.DS_Store -------------------------------------------------------------------------------- /livef1/adapters/signalr_aio/hubs/__init__.py: -------------------------------------------------------------------------------- 1 | from ._hub import Hub 2 | -------------------------------------------------------------------------------- /livef1/adapters/signalr_aio/__init__.py: -------------------------------------------------------------------------------- 1 | from ._connection import Connection -------------------------------------------------------------------------------- /docs/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GoktugOcal/LiveF1/HEAD/docs/.DS_Store -------------------------------------------------------------------------------- /livef1/adapters/signalr_aio/events/__init__.py: -------------------------------------------------------------------------------- 1 | from ._events import EventHook 2 | -------------------------------------------------------------------------------- /livef1/adapters/signalr_aio/transports/__init__.py: -------------------------------------------------------------------------------- 1 | from ._transport import Transport -------------------------------------------------------------------------------- /livef1/data_processing/__init__.py: -------------------------------------------------------------------------------- 1 | from .data_models import ( 2 | BasicResult 3 | ) -------------------------------------------------------------------------------- /docs/source/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GoktugOcal/LiveF1/HEAD/docs/source/.DS_Store -------------------------------------------------------------------------------- /docs/source/404.rst: -------------------------------------------------------------------------------- 1 | :orphan: 2 | :layout: 404 3 | 4 | 404 5 | === 6 | 7 | This page does not exist. -------------------------------------------------------------------------------- /livef1/adapters/signalr_aio/transports/_exceptions.py: -------------------------------------------------------------------------------- 1 | from websockets.exceptions import ConnectionClosed 2 | -------------------------------------------------------------------------------- /docs/source/_static/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GoktugOcal/LiveF1/HEAD/docs/source/_static/.DS_Store -------------------------------------------------------------------------------- /docs/source/_static/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GoktugOcal/LiveF1/HEAD/docs/source/_static/logo.png -------------------------------------------------------------------------------- /docs/source/_static/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GoktugOcal/LiveF1/HEAD/docs/source/_static/favicon.png -------------------------------------------------------------------------------- /docs/source/_static/LiveF1_red.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GoktugOcal/LiveF1/HEAD/docs/source/_static/LiveF1_red.png -------------------------------------------------------------------------------- /docs/source/_static/data_lake.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GoktugOcal/LiveF1/HEAD/docs/source/_static/data_lake.png -------------------------------------------------------------------------------- /docs/source/_static/LiveF1_white.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GoktugOcal/LiveF1/HEAD/docs/source/_static/LiveF1_white.png -------------------------------------------------------------------------------- /docs/source/_static/LiveF1Overview1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GoktugOcal/LiveF1/HEAD/docs/source/_static/LiveF1Overview1.png -------------------------------------------------------------------------------- /docs/source/user_guide/telemetry_data.rst: -------------------------------------------------------------------------------- 1 | ********************************** 2 | Telemetry Data 3 | ********************************** -------------------------------------------------------------------------------- /docs/source/user_guide/visualization.rst: -------------------------------------------------------------------------------- 1 | ********************************** 2 | Visualization 3 | ********************************** -------------------------------------------------------------------------------- /docs/source/user_guide/informative_data.rst: -------------------------------------------------------------------------------- 1 | ********************************** 2 | Informative Data 3 | ********************************** -------------------------------------------------------------------------------- /docs/source/_static/robots.txt: -------------------------------------------------------------------------------- 1 | User-agent: * 2 | Disallow: /_static/ 3 | Disallow: /_build/ 4 | Sitemap: http://livef1.goktugocal.com/sitemap.xml -------------------------------------------------------------------------------- /docs/source/features/index.rst: -------------------------------------------------------------------------------- 1 | features 2 | 3 | ====================== 4 | 5 | .. toctree:: 6 | :maxdepth: 2 7 | :caption: features 8 | 9 | -------------------------------------------------------------------------------- /docs/source/user_guide/working_with_realtime_client.rst: -------------------------------------------------------------------------------- 1 | ********************************** 2 | Working with Real-Time Data 3 | ********************************** -------------------------------------------------------------------------------- /docs/source/working_with_data/index.rst: -------------------------------------------------------------------------------- 1 | Working with Data 2 | =========== 3 | 4 | .. toctree:: 5 | :maxdepth: 1 6 | 7 | historical_data 8 | realtime_data 9 | -------------------------------------------------------------------------------- /docs/source/developer_notes/index.rst: -------------------------------------------------------------------------------- 1 | developer_notes 2 | 3 | ====================== 4 | 5 | .. toctree:: 6 | :maxdepth: 2 7 | :caption: developer_notes 8 | 9 | -------------------------------------------------------------------------------- /livef1/adapters/__init__.py: -------------------------------------------------------------------------------- 1 | from .livetimingf1_adapter import * 2 | from .realtime_client import ( 3 | RealF1Client 4 | ) 5 | from .functions import ( 6 | download_data 7 | ) -------------------------------------------------------------------------------- /docs/source/additional_resources/index.rst: -------------------------------------------------------------------------------- 1 | additional_resources 2 | 3 | ====================== 4 | 5 | .. toctree:: 6 | :maxdepth: 2 7 | :caption: additional_resources 8 | 9 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | jellyfish==1.1.3 2 | numpy==1.26.4 3 | pandas==2.2.3 4 | python_dateutil==2.9.0.post0 5 | Requests==2.32.3 6 | setuptools==59.6.0 7 | ujson==5.10.0 8 | websockets==13.0.1 9 | scipy==1.11.4 -------------------------------------------------------------------------------- /docs/requirements.txt: -------------------------------------------------------------------------------- 1 | sphinx-copybutton 2 | sphinx-rtd-theme 3 | numpydoc 4 | websockets 5 | jellyfish 6 | shibuya 7 | sphinx-tabs 8 | sphinx_design 9 | sphinx_sitemap 10 | scipy 11 | ujson 12 | numpy 13 | pandas 14 | beautifulsoup4 -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [metadata] 2 | name = livef1 3 | version = livef1.__version__ 4 | author = Göktuğ Öcal 5 | description = A Python toolkit for seamless access to live and historical Formula 1 data 6 | license = MIT 7 | home-page = https://github.com/GoktugOcal/LiveF1 -------------------------------------------------------------------------------- /docs/source/api.rst: -------------------------------------------------------------------------------- 1 | F1 API - :mod:`livef1.api` 2 | ================================= 3 | 4 | Functions 5 | --------- 6 | 7 | .. automodule:: livef1.api 8 | :members: download_data 9 | 10 | Exceptions 11 | ---------- 12 | 13 | .. .. autoclass:: SessionNotAvailableError -------------------------------------------------------------------------------- /livef1/models/__init__.py: -------------------------------------------------------------------------------- 1 | from .season import Season 2 | from .meeting import Meeting 3 | from .session import Session 4 | from .driver import Driver 5 | from .circuit import Circuit 6 | 7 | __all__ = [ 8 | "Season", 9 | "Meeting", 10 | "Session", 11 | "Driver", 12 | "Circuit" 13 | ] -------------------------------------------------------------------------------- /docs/source/api_reference/index.rst: -------------------------------------------------------------------------------- 1 | .. _api_reference: 2 | 3 | API Reference 4 | ========================================================= 5 | 6 | .. toctree:: 7 | :maxdepth: 2 8 | :caption: api_reference 9 | 10 | api 11 | models 12 | data_processing 13 | livetimingf1_adapter 14 | medallion_arch 15 | realtime_client 16 | utils -------------------------------------------------------------------------------- /docs/source/api_reference/realtime_client.rst: -------------------------------------------------------------------------------- 1 | Real-Time Client - :mod:`livef1.adapters.realtime_client` 2 | ================================= 3 | 4 | .. automodule:: livef1.adapters.realtime_client 5 | :undoc-members: 6 | :inherited-members: 7 | 8 | .. rubric:: Classes 9 | .. autosummary:: 10 | RealF1Client 11 | MessageHandlerTemplate -------------------------------------------------------------------------------- /livef1/adapters/signalr_aio/Readme.md: -------------------------------------------------------------------------------- 1 | # python-signalr-client 2 | **Python** signalR client using asyncio. 3 | 4 | That module is based on the [repository](https://github.com/slazarov/python-signalr-client) of [Stanislav Lazarov](https://github.com/slazarov), thanks for the work! 5 | 6 | That repository has been archived on Oct 13, 2023. So, we have duplicated it. 7 | -------------------------------------------------------------------------------- /docs/source/api_reference/livetimingf1_adapter.rst: -------------------------------------------------------------------------------- 1 | LiveTimingF1 Adapter - :mod:`livef1.adapters.livetimingf1_adapter` 2 | ================================= 3 | 4 | .. automodule:: livef1.adapters.livetimingf1_adapter 5 | :undoc-members: 6 | :inherited-members: 7 | 8 | .. rubric:: Functions 9 | .. autosummary:: 10 | livetimingF1_getdata 11 | livetimingF1_request 12 | 13 | .. rubric:: Classes 14 | .. autosummary:: 15 | LivetimingF1adapters -------------------------------------------------------------------------------- /livef1/adapters/signalr_aio/transports/_queue_events.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | # -*- coding: utf-8 -*- 3 | 4 | # signalr_aio/transports/_queue_events.py 5 | # Stanislav Lazarov 6 | 7 | class Event(object): 8 | """ 9 | Event is base class providing an interface 10 | for all subsequent(inherited) events. 11 | """ 12 | 13 | 14 | class InvokeEvent(Event): 15 | def __init__(self, message): 16 | self.type = 'INVOKE' 17 | self.message = message 18 | 19 | 20 | class CloseEvent(Event): 21 | def __init__(self): 22 | self.type = 'CLOSE' 23 | -------------------------------------------------------------------------------- /docs/source/api_reference/api.rst: -------------------------------------------------------------------------------- 1 | LiveF1 API - :mod:`livef1` 2 | ================================= 3 | 4 | .. automodule:: livef1.api 5 | :undoc-members: 6 | :inherited-members: 7 | 8 | .. rubric:: Functions 9 | .. autosummary:: 10 | get_season 11 | get_meeting 12 | get_session 13 | 14 | .. .. automodule:: livef1.api 15 | .. :members: download_data 16 | 17 | .. .. automodule:: livef1.api 18 | .. :members: get_season 19 | 20 | .. .. automodule:: livef1.api 21 | .. :members: get_meeting 22 | 23 | .. .. automodule:: livef1.api 24 | .. :members: get_session -------------------------------------------------------------------------------- /test.py: -------------------------------------------------------------------------------- 1 | import livef1 2 | from livef1.utils.constants import TABLE_REQUIREMENTS, TABLE_GENERATION_FUNCTIONS 3 | from livef1.data_processing.silver_functions import * 4 | 5 | # meeting = livef1.get_meeting(2024, "Lusail") 6 | # print(meeting) 7 | 8 | session = livef1.get_session( 9 | 2024, 10 | meeting_identifier = "Spa", 11 | session_identifier = "Race" 12 | ) 13 | 14 | # session.create_silver_table("laps", TABLE_REQUIREMENTS["laps"], include_session=True)(globals()[TABLE_GENERATION_FUNCTIONS["laps"]]) 15 | 16 | print(session.meeting.circuit) 17 | 18 | # session = livef1.get_session(2024, "Sao Paulo", "Qualifying") 19 | # session.generate() 20 | # print(session.name) -------------------------------------------------------------------------------- /livef1/adapters/signalr_aio/events/_events.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | # -*- coding: utf-8 -*- 3 | 4 | # signalr_aio/events/_events.py 5 | # Stanislav Lazarov 6 | 7 | # Structure inspired by https://github.com/TargetProcess/signalr-client-py 8 | 9 | 10 | class EventHook(object): 11 | def __init__(self): 12 | self._handlers = [] 13 | 14 | def __iadd__(self, handler): 15 | self._handlers.append(handler) 16 | return self 17 | 18 | def __isub__(self, handler): 19 | self._handlers.remove(handler) 20 | return self 21 | 22 | async def fire(self, *args, **kwargs): 23 | for handler in self._handlers: 24 | await handler(*args, **kwargs) 25 | -------------------------------------------------------------------------------- /docs/Makefile: -------------------------------------------------------------------------------- 1 | # Minimal makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line, and also 5 | # from the environment for the first two. 6 | SPHINXOPTS ?= 7 | SPHINXBUILD ?= sphinx-build 8 | SOURCEDIR = source 9 | BUILDDIR = _build 10 | 11 | # Put it first so that "make" without argument is like "make help". 12 | help: 13 | @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 14 | 15 | .PHONY: help Makefile 16 | 17 | # Catch-all target: route all unknown targets to Sphinx using the new 18 | # "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). 19 | %: Makefile 20 | @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 21 | -------------------------------------------------------------------------------- /docs/source/api_reference/medallion_arch.rst: -------------------------------------------------------------------------------- 1 | Medallion Architecture - :mod:`livef1.data_processing.lake` 2 | ================================= 3 | 4 | Data Lakes 5 | ------------------- 6 | .. automodule:: livef1.data_processing.lakes 7 | :members: BasicResult 8 | 9 | .. rubric:: Classes 10 | .. autosummary:: 11 | DataLake 12 | BronzeLake 13 | SilverLake 14 | GoldLake 15 | 16 | 17 | Silver Table Generation Functions 18 | -------------------- 19 | .. automodule:: livef1.data_processing.silver_functions 20 | :undoc-members: 21 | :inherited-members: 22 | 23 | .. rubric:: Functions 24 | .. autosummary:: 25 | generate_laps_table 26 | generate_car_telemetry_table -------------------------------------------------------------------------------- /docs/source/api_reference/models.rst: -------------------------------------------------------------------------------- 1 | Models - :mod:`livef1.models` 2 | ================================= 3 | 4 | .. automodule:: livef1.models 5 | :undoc-members: 6 | :inherited-members: 7 | 8 | .. rubric:: Classes 9 | .. autosummary:: 10 | Season 11 | Meeting 12 | Session 13 | Circuit 14 | Driver 15 | 16 | .. .. Season 17 | .. .. ------- 18 | .. .. automodule:: livef1.models.season 19 | .. :undoc-members: 20 | .. :inherited-members: 21 | 22 | .. .. Meeting 23 | .. .. ------- 24 | .. .. automodule:: livef1.models.meeting 25 | .. :undoc-members: 26 | .. :inherited-members: 27 | 28 | .. .. Session 29 | .. .. ------- 30 | .. .. automodule:: livef1.models.session 31 | .. :undoc-members: 32 | .. :inherited-members: -------------------------------------------------------------------------------- /livef1/__init__.py: -------------------------------------------------------------------------------- 1 | from .models import ( 2 | Session, 3 | Season, 4 | Meeting 5 | ) 6 | 7 | from .api import ( 8 | get_season, 9 | get_meeting, 10 | get_session 11 | ) 12 | 13 | from .data_processing import ( 14 | BasicResult 15 | ) 16 | 17 | from .utils.helper import * 18 | from .adapters.livetimingf1_adapter import LivetimingF1adapters 19 | from .utils.logger import set_log_level 20 | 21 | import warnings 22 | from pandas.errors import SettingWithCopyWarning 23 | warnings.simplefilter(action="ignore", category=SettingWithCopyWarning) 24 | warnings.simplefilter(action='ignore', category=FutureWarning) 25 | 26 | __version__ = "1.0.970" 27 | 28 | __all__ = [ 29 | 'set_log_level', 30 | 'get_season', 31 | 'get_meeting', 32 | 'get_session' 33 | ] -------------------------------------------------------------------------------- /.github/pip_dependency.yml: -------------------------------------------------------------------------------- 1 | name: Generate Requirements with pipreqs 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | 8 | jobs: 9 | generate-requirements: 10 | runs-on: ubuntu-latest 11 | 12 | steps: 13 | - name: Checkout Code 14 | uses: actions/checkout@v3 15 | 16 | - name: Set up Python 17 | uses: actions/setup-python@v4 18 | with: 19 | python-version: "3.x" 20 | 21 | - name: Install pipreqs 22 | run: pip install pipreqs 23 | 24 | - name: Generate Requirements File with pipreqs 25 | run: | 26 | pipreqs . --force 27 | 28 | - name: Commit Changes 29 | uses: stefanzweifel/git-auto-commit-action@v4 30 | with: 31 | commit_message: "Update requirements.txt generated by pipreqs" 32 | file_pattern: "requirements.txt" 33 | -------------------------------------------------------------------------------- /livef1/data/circuits_key.csv: -------------------------------------------------------------------------------- 1 | gp_name,key 2 | Australian Grand Prix,australia 3 | Chinese Grand Prix,china 4 | Japanese Grand Prix,japan 5 | Bahrain Grand Prix,bahrain 6 | Saudi Arabian Grand Prix,saudi-arabia 7 | Miami Grand Prix,miami 8 | Emilia Romagna Grand Prix,emiliaromagna 9 | Monaco Grand Prix,monaco 10 | Spanish Grand Prix,spain 11 | Canadian Grand Prix,canada 12 | Austrian Grand Prix,austria 13 | British Grand Prix,great-britain 14 | Belgian Grand Prix,belgium 15 | Hungarian Grand Prix,hungary 16 | Dutch Grand Prix,netherlands 17 | Italian Grand Prix,italy 18 | Azerbaijan Grand Prix,azerbaijan 19 | Singapore Grand Prix,singapore 20 | United States Grand Prix,united-states 21 | Mexico City Grand Prix,mexico 22 | São Paulo Grand Prix,brazil 23 | Las Vegas Grand Prix,las-vegas 24 | Qatar Grand Prix,qatar 25 | Abu Dhabi Grand Prix,united-arab-emirates -------------------------------------------------------------------------------- /docs/source/getting_started/installation.rst: -------------------------------------------------------------------------------- 1 | ******************************** 2 | Installation 3 | ******************************** 4 | 5 | Install via pip 6 | ------------------- 7 | 8 | The easiest way to install LiveF1 is through pip. Run the following command in your terminal: 9 | 10 | .. code-block:: bash 11 | 12 | pip install livef1 13 | 14 | Install from source 15 | ------------------- 16 | 17 | If you want to install the latest development version or contribute to LiveF1, follow these steps: 18 | 19 | 1. Clone the repository: 20 | 21 | .. code-block:: bash 22 | 23 | git clone https://github.com/goktugocal/LiveF1.git 24 | 25 | 2. Navigate into the project directory: 26 | 27 | .. code-block:: bash 28 | 29 | cd LiveF1 30 | 31 | 3. Install dependencies and the package: 32 | 33 | .. code-block:: bash 34 | 35 | pip install -r requirements.txt 36 | pip install . -------------------------------------------------------------------------------- /docs/make.bat: -------------------------------------------------------------------------------- 1 | @ECHO OFF 2 | 3 | pushd %~dp0 4 | 5 | REM Command file for Sphinx documentation 6 | 7 | if "%SPHINXBUILD%" == "" ( 8 | set SPHINXBUILD=sphinx-build 9 | ) 10 | set SOURCEDIR=source 11 | set BUILDDIR=_build 12 | 13 | %SPHINXBUILD% >NUL 2>NUL 14 | if errorlevel 9009 ( 15 | echo. 16 | echo.The 'sphinx-build' command was not found. Make sure you have Sphinx 17 | echo.installed, then set the SPHINXBUILD environment variable to point 18 | echo.to the full path of the 'sphinx-build' executable. Alternatively you 19 | echo.may add the Sphinx directory to PATH. 20 | echo. 21 | echo.If you don't have Sphinx installed, grab it from 22 | echo.https://www.sphinx-doc.org/ 23 | exit /b 1 24 | ) 25 | 26 | if "%1" == "" goto help 27 | 28 | %SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% 29 | goto end 30 | 31 | :help 32 | %SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% 33 | 34 | :end 35 | popd 36 | -------------------------------------------------------------------------------- /test/temp_test.py: -------------------------------------------------------------------------------- 1 | from livef1.adapters.realtime_client import RealF1Client 2 | import datetime 3 | import json 4 | 5 | import livef1 6 | 7 | # Initialize client 8 | client = RealF1Client( 9 | topics=["DriverList"], 10 | log_file_name="session_data.json" 11 | ) 12 | 13 | # Define multiple handlers 14 | @client.callback("process_telemetry") 15 | async def handle_telemetry(records): 16 | # Process car telemetry data 17 | telemetry_data = records.get("DriverList") 18 | if telemetry_data: 19 | for record in telemetry_data: 20 | print("Driver >", record) # this is a placeholder for your code 21 | 22 | # @client.callback("track_status") 23 | # async def handle_track_status(records): 24 | # # Monitor track conditions 25 | # track_data = records.get("TrackStatus") 26 | # if track_data: 27 | # for record in track_data: 28 | # print("Track -", record) # this is a placeholder for your code 29 | 30 | 31 | # Start the client 32 | client.run() -------------------------------------------------------------------------------- /docs/source/_static/class.rst: -------------------------------------------------------------------------------- 1 | {{ fullname }} 2 | {{ underline }} 3 | 4 | {{ docstring }} 5 | 6 | {% if methods %} 7 | Functions Summary 8 | ----------------- 9 | 10 | .. list-table:: 11 | :widths: 30 70 12 | :header-rows: 1 13 | 14 | * - Function Name 15 | - Description 16 | {% for method in methods %} 17 | * - :meth:`{{ method.fullname }}` 18 | - {{ method.docstring | escape | eol_escape }} 19 | {% endfor %} 20 | {% endif %} 21 | 22 | Attributes Summary 23 | ------------------ 24 | 25 | {% if attributes %} 26 | .. list-table:: 27 | :widths: 30 70 28 | :header-rows: 1 29 | 30 | * - Attribute Name 31 | - Description 32 | {% for attr in attributes %} 33 | * - ``{{ attr.name }}`` 34 | - {{ attr.docstring | escape | eol_escape }} 35 | {% endfor %} 36 | {% else %} 37 | No attributes documented. 38 | {% endif %} 39 | 40 | Details 41 | ------- 42 | 43 | .. automethod:: {{ fullname }} 44 | :noindex: 45 | :undoc-members: 46 | :inherited-members: 47 | -------------------------------------------------------------------------------- /test/test_realtime_client_basic_test.py: -------------------------------------------------------------------------------- 1 | from livef1.adapters.realtime_client import RealF1Client 2 | import datetime 3 | import json 4 | 5 | # Initialize client 6 | client = RealF1Client( 7 | topics=["CarData.z", "SessionInfo", "TrackStatus"], 8 | log_file_name="session_data.json" 9 | ) 10 | 11 | # Define multiple handlers 12 | @client.callback("process_telemetry") 13 | async def handle_telemetry(records): 14 | # Process car telemetry data 15 | telemetry_data = records.get("CarData.z") 16 | if telemetry_data: 17 | for record in telemetry_data: 18 | print("CarData >", record) # this is a placeholder for your code 19 | 20 | @client.callback("track_status") 21 | async def handle_track_status(records): 22 | # Monitor track conditions 23 | track_data = records.get("TrackStatus") 24 | if track_data: 25 | for record in track_data: 26 | print("Track -", record) # this is a placeholder for your code 27 | 28 | 29 | # Start the client 30 | client.run() -------------------------------------------------------------------------------- /.github/workflows/publish.yml: -------------------------------------------------------------------------------- 1 | name: Upload Python Package to PyPI when a Release is Created 2 | 3 | on: 4 | release: 5 | types: [created] 6 | 7 | jobs: 8 | pypi-publish: 9 | name: Publish release to PyPI 10 | runs-on: ubuntu-latest 11 | environment: 12 | name: pypi 13 | url: https://pypi.org/p/livef1/ 14 | permissions: 15 | id-token: write 16 | steps: 17 | - uses: actions/checkout@v4 18 | - name: Set up Python 19 | uses: actions/setup-python@v4 20 | with: 21 | python-version: "3.x" 22 | - name: Install dependencies 23 | run: | 24 | python -m pip install --upgrade pip 25 | pip install setuptools wheel 26 | - name: Build package 27 | run: | 28 | python setup.py sdist bdist_wheel # Could also be python -m build 29 | - name: Publish package distributions to PyPI 30 | uses: pypa/gh-action-pypi-publish@release/v1 31 | with: 32 | password: ${{ secrets.PYPI_API_TOKEN }} -------------------------------------------------------------------------------- /docs/source/api_reference/utils.rst: -------------------------------------------------------------------------------- 1 | Utils - :mod:`livef1.utils` 2 | ================================= 3 | 4 | 5 | .. Constants 6 | .. --------- 7 | .. .. automodule:: livef1.utils 8 | .. :undoc-members: 9 | .. :inherited-members: 10 | 11 | .. .. rubric:: Classes 12 | .. .. autosummary:: 13 | .. constants 14 | 15 | 16 | Helper 17 | --------- 18 | .. automodule:: livef1.utils.helper 19 | :undoc-members: 20 | :inherited-members: 21 | 22 | .. rubric:: Functions 23 | .. autosummary:: 24 | build_session_endpoint 25 | json_parser_for_objects 26 | get_data 27 | get_car_data_stream 28 | parse 29 | parse_hash 30 | parse_helper_for_nested_dict 31 | find_most_similar_vectorized 32 | 33 | Exceptions 34 | --------- 35 | .. automodule:: livef1.utils.exceptions 36 | :undoc-members: 37 | :inherited-members: 38 | 39 | .. rubric:: Functions 40 | .. autosummary:: 41 | 42 | Logger 43 | --------- 44 | .. automodule:: livef1.utils.logger 45 | :undoc-members: 46 | :inherited-members: 47 | 48 | .. rubric:: Functions 49 | .. autosummary:: -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 Göktuğ Öcal 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /docs/source/examples/index.rst: -------------------------------------------------------------------------------- 1 | ******************************** 2 | Examples 3 | ******************************** 4 | 5 | This section provides comprehensive examples of using LiveF1 for various Formula 1 data analysis scenarios. Each example demonstrates different aspects of the package, from basic data access to complex analysis pipelines. 6 | 7 | .. Basic Examples 8 | .. ------------- 9 | 10 | .. .. toctree:: 11 | .. :maxdepth: 1 12 | 13 | .. basic/accessing_data 14 | .. basic/data_processing 15 | .. basic/visualization 16 | 17 | Analysis Pipelines 18 | ----------------- 19 | 20 | .. toctree:: 21 | :maxdepth: 1 22 | 23 | pipelines/race_analysis 24 | pipelines/qualifying_analysis 25 | pipelines/practice_analysis 26 | pipelines/season_analysis 27 | 28 | .. Real-Time Applications 29 | .. -------------------- 30 | 31 | .. .. toctree:: 32 | .. :maxdepth: 1 33 | 34 | .. realtime/live_telemetry 35 | .. realtime/race_dashboard 36 | .. realtime/strategy_monitor 37 | 38 | .. Advanced Topics 39 | .. ------------- 40 | 41 | .. .. toctree:: 42 | .. :maxdepth: 1 43 | 44 | .. advanced/custom_analysis 45 | .. advanced/data_export 46 | .. advanced/performance_optimization -------------------------------------------------------------------------------- /docs/source/api_reference/data_processing.rst: -------------------------------------------------------------------------------- 1 | Data Processing - :mod:`livef1.data_processing` 2 | ================================= 3 | 4 | Data Models 5 | ------------------- 6 | .. automodule:: livef1.data_processing.data_models 7 | :members: BasicResult 8 | 9 | .. rubric:: Classes 10 | .. autosummary:: 11 | BasicResult 12 | 13 | 14 | Data Preprocessing 15 | -------------------- 16 | .. automodule:: livef1.data_processing.etl 17 | :undoc-members: 18 | :inherited-members: 19 | 20 | .. rubric:: Classes 21 | .. autosummary:: 22 | livef1SessionETL 23 | 24 | 25 | .. automodule:: livef1.data_processing.parse_functions 26 | :undoc-members: 27 | :inherited-members: 28 | 29 | .. rubric:: Functions 30 | .. autosummary:: 31 | parse_car_data_z 32 | parse_current_tyres 33 | parse_driver_list 34 | parse_driver_race_info 35 | parse_extrapolated_clock 36 | parse_hearthbeat 37 | parse_lap_series 38 | parse_position_z 39 | parse_race_control_messages 40 | parse_session_data 41 | parse_session_info 42 | parse_session_status 43 | parse_team_radio 44 | parse_timing_data 45 | parse_tlarcm 46 | parse_top_three 47 | parse_tyre_stint_series 48 | parse_weather_data 49 | 50 | -------------------------------------------------------------------------------- /docs/source/getting_started/index.rst: -------------------------------------------------------------------------------- 1 | ******************* 2 | LiveF1 3 | ******************* 4 | 5 | Welcome to LiveF1! This guide will walk you through the installation process and help you get up and running with the toolkit. 6 | 7 | What it is provided in LiveF1? 8 | ------------------- 9 | 10 | - **Real-Time Race Data**: Provides live telemetry, timing, and position updates, ideal for powering dashboards and live analytics. 11 | - **Historical Data Access**: Includes comprehensive race data from past seasons, perfect for performance analysis and comparisons. 12 | - **Data Processing Modules**: Built-in ETL tools make raw data immediately usable, supporting analysis and seamless data storage. 13 | 14 | Using LiveF1, you can seamlessly access real-time and historical racing data, enabling powerful analytics and visualizations. LiveF1 is your gateway to unlocking the power of Formula 1 data. 15 | 16 | 17 | What is next? 18 | ------------------- 19 | 20 | .. grid:: 3 21 | :gutter: 2 22 | :padding: 0 23 | :class-row: surface 24 | 25 | .. grid-item-card:: :octicon:`desktop-download` Installation 26 | :link: installation.html 27 | 28 | .. grid-item-card:: :octicon:`zap` Quick Start 29 | :link: quick_start.html 30 | 31 | .. grid-item-card:: :octicon:`light-bulb` Work with Data 32 | :link: ../working_with_data/index.html 33 | 34 | 35 | **Happy Racing!** -------------------------------------------------------------------------------- /test/table_register.py: -------------------------------------------------------------------------------- 1 | import livef1 2 | 3 | session = livef1.get_session( 4 | 2025, 5 | "British", 6 | "Qualifying" 7 | ) 8 | 9 | @session.create_silver_table( 10 | table_name = "SectorDiff", 11 | source_tables = ["laps"], # This time we source from a default silver table to create a silver table. 12 | include_session = True 13 | ) 14 | def sector_diff(session, laps): 15 | df = laps.groupby("DriverNo")[["Sector1_Time","Sector2_Time","Sector3_Time"]].min().reset_index() 16 | df["sector1_diff"] = (df["Sector1_Time"] - df["Sector1_Time"].min()).dt.total_seconds() 17 | df["sector2_diff"] = (df["Sector2_Time"] - df["Sector2_Time"].min()).dt.total_seconds() 18 | df["sector3_diff"] = (df["Sector3_Time"] - df["Sector3_Time"].min()).dt.total_seconds() 19 | df["DriverName"] = df["DriverNo"].map(lambda x: session.drivers[x].FullName) 20 | return df 21 | 22 | 23 | @session.create_gold_table( 24 | table_name = "SectorLeaders", 25 | source_tables = ["SectorDiff"], # This time we source from a default silver table to create a silver table. 26 | include_session = True 27 | ) 28 | def sector_diff(session, SectorDiff): 29 | return SectorDiff.iloc[SectorDiff[["sector1_diff","sector2_diff","sector3_diff"]].idxmin().values] 30 | 31 | 32 | session.generate(silver=True, gold=True) 33 | 34 | print(session.SectorDiff.head().to_markdown()) 35 | print(session.SectorLeaders.head()) -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import setup, find_packages 2 | import os 3 | import re 4 | 5 | with open("README.md", "r") as fh: 6 | long_description = fh.read() 7 | 8 | with open("requirements.txt") as f: 9 | required = f.read().splitlines() 10 | 11 | def get_version(): 12 | version_file = os.path.join("livef1", "__init__.py") 13 | with open(version_file) as f: 14 | content = f.read() 15 | return re.search(r'__version__ = ["\'](.+?)["\']', content).group(1) 16 | 17 | 18 | setup( 19 | name="livef1", 20 | version=get_version(), 21 | description="A Python toolkit for seamless access to live and historical Formula 1 data.", 22 | long_description=long_description, 23 | long_description_content_type="text/markdown", 24 | author="Göktuğ Öcal", 25 | url="https://github.com/GoktugOcal/LiveF1", 26 | packages=find_packages(), 27 | classifiers=[ 28 | "Programming Language :: Python :: 3", 29 | "License :: OSI Approved :: MIT License", 30 | "Operating System :: OS Independent", 31 | ], 32 | python_requires=">=3.7", # Minimum Python version requirement 33 | install_requires=required, 34 | license="MIT", 35 | project_urls={ 36 | "Bug Tracker": "https://github.com/GoktugOcal/LiveF1/issues", 37 | "Documentation": "https://github.com/GoktugOcal/LiveF1#readme", 38 | "Source Code": "https://github.com/GoktugOcal/LiveF1", 39 | }, 40 | include_package_data=True 41 | ) -------------------------------------------------------------------------------- /livef1/utils/logger.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import os 3 | 4 | # Logger setup 5 | # LOG_LEVEL = logging.DEBUG if os.getenv("DEBUG", "False").lower() == "true" else logging.INFO 6 | LOG_LEVEL = logging.INFO 7 | STREAM_LOG_FORMAT = "%(asctime)s - %(message)s" 8 | FILE_LOG_FORMAT = "%(asctime)s - %(name)s - %(levelname)s - %(message)s" 9 | 10 | # Configure logger 11 | logger = logging.getLogger("livef1") 12 | logger.setLevel(LOG_LEVEL) 13 | 14 | # Handlers 15 | console_handler = logging.StreamHandler() 16 | console_handler.setFormatter(logging.Formatter(STREAM_LOG_FORMAT,"%H:%M:%S")) 17 | 18 | file_handler = logging.FileHandler("livef1.log") 19 | file_handler.setFormatter(logging.Formatter(FILE_LOG_FORMAT,"%Y-%m-%d %H:%M:%S")) 20 | 21 | # Add handlers to logger 22 | logger.addHandler(console_handler) 23 | logger.addHandler(file_handler) 24 | 25 | def set_log_level(level): 26 | """ 27 | Set the logging level for the livef1 logger. 28 | 29 | Parameters 30 | ---------- 31 | level : Union[str, int] 32 | The logging level to set. Can be either a string ('DEBUG', 'INFO', 'WARNING', 'ERROR', 'CRITICAL') 33 | or the corresponding integer value. 34 | 35 | Examples 36 | -------- 37 | >>> set_log_level('DEBUG') # Set to debug level 38 | >>> set_log_level(logging.INFO) # Set to info level 39 | """ 40 | if isinstance(level, str): 41 | level = level.upper() 42 | numeric_level = getattr(logging, level, None) 43 | if not isinstance(numeric_level, int): 44 | raise ValueError(f'Invalid log level: {level}') 45 | level = numeric_level 46 | 47 | logger.setLevel(level) -------------------------------------------------------------------------------- /livef1/adapters/signalr_aio/_connection.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | # -*- coding: utf-8 -*- 3 | 4 | # signalr_aio/_connection.py 5 | # Stanislav Lazarov 6 | 7 | 8 | from .events import EventHook 9 | from .hubs import Hub 10 | from .transports import Transport 11 | 12 | 13 | class Connection(object): 14 | protocol_version = '1.5' 15 | 16 | def __init__(self, url, session=None): 17 | self.url = url 18 | self.__hubs = {} 19 | self.__send_counter = -1 20 | self.hub = None 21 | self.session = session 22 | self.received = EventHook() 23 | self.error = EventHook() 24 | self.__transport = Transport(self) 25 | self.started = False 26 | 27 | async def handle_error(**data): 28 | error = data["E"] if "E" in data else None 29 | if error is not None: 30 | await self.error.fire(error) 31 | 32 | self.received += handle_error 33 | 34 | def start(self): 35 | self.hub = [hub_name for hub_name in self.__hubs][0] 36 | self.__transport.start() 37 | 38 | def register_hub(self, name): 39 | if name not in self.__hubs: 40 | if self.started: 41 | raise RuntimeError( 42 | 'Cannot create new hub because connection is already started.') 43 | self.__hubs[name] = Hub(name, self) 44 | return self.__hubs[name] 45 | 46 | def increment_send_counter(self): 47 | self.__send_counter += 1 48 | return self.__send_counter 49 | 50 | def send(self, message): 51 | self.__transport.send(message) 52 | 53 | def close(self): 54 | self.__transport.close() 55 | -------------------------------------------------------------------------------- /livef1/adapters/signalr_aio/hubs/_hub.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | # -*- coding: utf-8 -*- 3 | 4 | # signalr_aio/hubs/_hub.py 5 | # Stanislav Lazarov 6 | 7 | import asyncio 8 | import time 9 | 10 | class Hub: 11 | def __init__(self, name, connection): 12 | self.name = name 13 | self.server = HubServer(name, connection, self) 14 | self.client = HubClient(name, connection) 15 | 16 | 17 | class HubServer: 18 | def __init__(self, name, connection, hub): 19 | self.name = name 20 | self.__connection = connection 21 | self.__hub = hub 22 | 23 | def invoke(self, method, *data): 24 | message = { 25 | 'H': self.name, 26 | 'M': method, 27 | 'A': data, 28 | 'I': self.__connection.increment_send_counter() 29 | } 30 | self.__connection.send(message) 31 | 32 | 33 | class HubClient(object): 34 | def __init__(self, name, connection): 35 | self.name = name 36 | self.__handlers = {} 37 | self.__handler = lambda x: x 38 | 39 | async def handle(**data): 40 | try: 41 | asyncio.gather(*[handler(data) for handler in list(self.__handlers.values())]) 42 | except Exception as e: 43 | print(e) 44 | 45 | connection.received += handle 46 | 47 | def on(self, method, handler): 48 | if method not in self.__handlers: 49 | self.__handlers[method] = handler 50 | 51 | # def on(self, handler): 52 | # self.__handler = handler 53 | 54 | def off(self, method, handler): 55 | if method in self.__handlers: 56 | self.__handlers[method] -= handler 57 | -------------------------------------------------------------------------------- /docs/source/user_guide/accessing_data.rst: -------------------------------------------------------------------------------- 1 | ********************************** 2 | Accessing Data 3 | ********************************** 4 | 5 | Accessing Data and Generating Lake Levels 6 | ========================================== 7 | 8 | This section explains how to access data and generate different lake levels (bronze, silver, and gold) using the `Session` class. 9 | 10 | Accessing Data 11 | -------------- 12 | 13 | To access data for a specific session, you can use the `get_data` method. This method retrieves data from the data lake or loads it if not present. 14 | 15 | Example: 16 | 17 | .. code-block:: python 18 | 19 | session = Session(season=season, year=2023, meeting=meeting, key=1, name="Race", type="race") 20 | timing_data = session.get_data("Timing_Data") 21 | print(timing_data) 22 | 23 | Generating Silver and Gold Tables 24 | --------------------------------- 25 | 26 | To generate silver and gold tables, use the `generate` method. This method processes raw data and creates higher-level tables. 27 | 28 | Example: 29 | 30 | .. code-block:: python 31 | 32 | session.generate(silver=True, gold=False) # Generate silver tables only 33 | session.generate(silver=True, gold=True) # Generate both silver and gold tables (not implemented yet) 34 | 35 | Accessing Generated Tables 36 | -------------------------- 37 | 38 | After generating the tables, you can access them using the corresponding methods. 39 | 40 | Example: 41 | 42 | .. code-block:: python 43 | 44 | laps_data = session.laps 45 | car_telemetry_data = session.carTelemetry 46 | weather_data = session.get_weather() 47 | timing_data = session.get_timing() 48 | 49 | print(laps_data) 50 | print(car_telemetry_data) 51 | print(weather_data) 52 | print(timing_data) -------------------------------------------------------------------------------- /.github/workflows/python-publish.yml: -------------------------------------------------------------------------------- 1 | # This workflow will upload a Python Package to PyPI when a release is created 2 | # For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-python#publishing-to-package-registries 3 | 4 | # This workflow uses actions that are not certified by GitHub. 5 | # They are provided by a third-party and are governed by 6 | # separate terms of service, privacy policy, and support 7 | # documentation. 8 | 9 | name: Upload Python Package 10 | 11 | on: 12 | release: 13 | types: [created] 14 | 15 | permissions: 16 | contents: read 17 | 18 | jobs: 19 | release-build: 20 | runs-on: ubuntu-latest 21 | 22 | steps: 23 | - uses: actions/checkout@v4 24 | 25 | - uses: actions/setup-python@v5 26 | with: 27 | python-version: "3.x" 28 | 29 | - name: Build release distributions 30 | run: | 31 | # NOTE: put your own distribution build steps here. 32 | python -m pip install build 33 | python -m build 34 | 35 | - name: Upload distributions 36 | uses: actions/upload-artifact@v4 37 | with: 38 | name: release-dists 39 | path: dist/ 40 | 41 | pypi-publish: 42 | runs-on: ubuntu-latest 43 | needs: 44 | - release-build 45 | permissions: 46 | # IMPORTANT: this permission is mandatory for trusted publishing 47 | id-token: write 48 | 49 | # Dedicated environments with protections for publishing are strongly recommended. 50 | # For more information, see: https://docs.github.com/en/actions/deployment/targeting-different-environments/using-environments-for-deployment#deployment-protection-rules 51 | environment: 52 | name: pypi 53 | url: https://pypi.org/p/livef1 54 | # OPTIONAL: uncomment and update to include your PyPI project URL in the deployment status: 55 | # url: https://pypi.org/p/YOURPROJECT 56 | # 57 | # ALTERNATIVE: if your GitHub Release name is the PyPI project version string 58 | # ALTERNATIVE: exactly, uncomment the following line instead: 59 | # url: https://pypi.org/project/YOURPROJECT/${{ github.event.release.name }} 60 | 61 | steps: 62 | - name: Retrieve release distributions 63 | uses: actions/download-artifact@v4 64 | with: 65 | name: release-dists 66 | path: dist/ 67 | 68 | - name: Publish release distributions to PyPI 69 | uses: pypa/gh-action-pypi-publish@release/v1 70 | with: 71 | packages-dir: dist/ 72 | -------------------------------------------------------------------------------- /livef1/utils/exceptions.py: -------------------------------------------------------------------------------- 1 | from .logger import logger 2 | 3 | class livef1Exception(Exception): 4 | pass 5 | 6 | class LiveF1Error(Exception): 7 | """Base class for all LiveF1 module exceptions.""" 8 | def __init__(self, message): 9 | """ 10 | Initializes the exception and logs the message. 11 | 12 | Parameters 13 | ---------- 14 | message : str 15 | The error message for the exception. 16 | """ 17 | super().__init__(message) 18 | logger.error(str(self.__class__.__name__) + " - " + message) 19 | pass 20 | 21 | class RealF1Error(LiveF1Error): 22 | """Exception for RealF1Client related errors""" 23 | pass 24 | 25 | class ArgumentError(LiveF1Error): 26 | """Exception for arguments of methods related errors""" 27 | pass 28 | 29 | class MissingFunctionError(LiveF1Error): 30 | """Raised when ETL functions does not include a function""" 31 | pass 32 | 33 | class TopicNotFoundError(LiveF1Error): 34 | """Raised when topic name is not correct""" 35 | pass 36 | 37 | 38 | class AdapterError(LiveF1Error): 39 | """Base exception for adapter-related issues.""" 40 | pass 41 | 42 | class InvalidResponseError(AdapterError): 43 | """Exception for invalid API responses.""" 44 | pass 45 | 46 | class InvalidEndpointError(AdapterError): 47 | """Raised when an invalid endpoint is accessed.""" 48 | pass 49 | 50 | # class AuthenticationError(AdapterError): 51 | # """Raised when authentication fails.""" 52 | # pass 53 | 54 | # class TimeoutError(AdapterError): 55 | # """Raised when a request times out.""" 56 | # pass 57 | 58 | # class ConnectionError(AdapterError): 59 | # """Raised when there is a connection issue.""" 60 | # pass 61 | 62 | class DataDecodingError(AdapterError): 63 | """Raised when decoding the response fails.""" 64 | pass 65 | 66 | 67 | 68 | 69 | class DataProcessingError(LiveF1Error): 70 | """Raised when data processing / parsing related error occurs.""" 71 | pass 72 | 73 | class ParsingError(DataProcessingError): 74 | """Raised when parsing the data fails.""" 75 | pass 76 | 77 | class ETLError(DataProcessingError): 78 | """Exception for ETL-specific issues.""" 79 | pass 80 | 81 | # class DataValidationError(DataProcessingError): 82 | # """Exception for invalid data during processing.""" 83 | # pass 84 | 85 | 86 | 87 | 88 | class SubscriptionError(AdapterError): 89 | """Exception for subscription errors.""" 90 | pass 91 | 92 | class DataFormatError(AdapterError): 93 | """Exception for unexpected data formats.""" 94 | pass -------------------------------------------------------------------------------- /livef1/adapters/signalr_aio/transports/_parameters.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | # -*- coding: utf-8 -*- 3 | 4 | # signalr_aio/transports/_parameters.py 5 | # Stanislav Lazarov 6 | 7 | 8 | from json import dumps 9 | from urllib.parse import urlparse, urlunparse, urlencode 10 | import requests 11 | 12 | 13 | class WebSocketParameters: 14 | def __init__(self, connection): 15 | self.protocol_version = '1.5' 16 | self.raw_url = self._clean_url(connection.url) 17 | self.conn_data = self._get_conn_data(connection.hub) 18 | self.session = connection.session 19 | self.headers = None 20 | self.socket_conf = None 21 | self._negotiate() 22 | self.socket_url = self._get_socket_url() 23 | 24 | @staticmethod 25 | def _clean_url(url): 26 | if url[-1] == '/': 27 | return url[:-1] 28 | else: 29 | return url 30 | 31 | @staticmethod 32 | def _get_conn_data(hub): 33 | conn_data = dumps([{'name': hub}]) 34 | return conn_data 35 | 36 | @staticmethod 37 | def _format_url(url, action, query): 38 | return '{url}/{action}?{query}'.format(url=url, action=action, query=query) 39 | 40 | def _negotiate(self): 41 | if self.session is None: 42 | self.session = requests.Session() 43 | query = urlencode({ 44 | 'connectionData': self.conn_data, 45 | 'clientProtocol': self.protocol_version, 46 | }) 47 | url = self._format_url(self.raw_url, 'negotiate', query) 48 | self.headers = dict(self.session.headers) 49 | request = self.session.get(url) 50 | self.headers['Cookie'] = self._get_cookie_str(request.cookies) 51 | self.socket_conf = request.json() 52 | 53 | @staticmethod 54 | def _get_cookie_str(request): 55 | return '; '.join([ 56 | '%s=%s' % (name, value) 57 | for name, value in request.items() 58 | ]) 59 | 60 | def _get_socket_url(self): 61 | ws_url = self._get_ws_url_from() 62 | query = urlencode({ 63 | 'transport': 'webSockets', 64 | 'connectionToken': self.socket_conf['ConnectionToken'], 65 | 'connectionData': self.conn_data, 66 | 'clientProtocol': self.socket_conf['ProtocolVersion'], 67 | }) 68 | 69 | return self._format_url(ws_url, 'connect', query) 70 | 71 | def _get_ws_url_from(self): 72 | parsed = urlparse(self.raw_url) 73 | scheme = 'wss' if parsed.scheme == 'https' else 'ws' 74 | url_data = (scheme, parsed.netloc, parsed.path, parsed.params, parsed.query, parsed.fragment) 75 | 76 | return urlunparse(url_data) 77 | -------------------------------------------------------------------------------- /docs/source/user_guide/logging_config.rst: -------------------------------------------------------------------------------- 1 | Logging Configuration 2 | =================== 3 | 4 | LiveF1 provides built-in logging capabilities through its ``logger`` module. The package uses Python's standard logging module with pre-configured formatters and handlers. 5 | 6 | Basic Setup 7 | ---------- 8 | 9 | The logging system is automatically configured when you import the package. By default, it: 10 | 11 | - Logs to both console and file (``livef1.log``) 12 | - Sets INFO as the default log level 13 | - Uses different formats for console and file output 14 | 15 | Configuration Options 16 | ------------------ 17 | 18 | Log Levels 19 | ^^^^^^^^^ 20 | 21 | You can adjust the logging level using the ``set_log_level`` function: 22 | 23 | .. code-block:: python 24 | 25 | from livef1.utils.logger import set_log_level 26 | import logging 27 | 28 | # Set using string 29 | set_log_level('DEBUG') 30 | 31 | # Or using logging constants 32 | set_log_level(logging.DEBUG) 33 | 34 | Available log levels: 35 | - DEBUG: Detailed information for diagnosing problems 36 | - INFO: Confirmation that things are working (default) 37 | - WARNING: Indication that something unexpected happened 38 | - ERROR: More serious problem 39 | - CRITICAL: Program may not be able to continue 40 | 41 | Output Formats 42 | ------------ 43 | 44 | Console output format: 45 | ``HH:MM:SS - message`` 46 | 47 | File output format: 48 | ``YYYY-MM-DD HH:MM:SS - logger_name - level - message`` 49 | 50 | Custom Handlers 51 | ------------- 52 | 53 | You can add your own handlers to the logger: 54 | 55 | .. code-block:: python 56 | 57 | from livef1.utils.logger import logger 58 | import logging 59 | 60 | # Create custom handler 61 | custom_handler = logging.StreamHandler() 62 | custom_handler.setFormatter(logging.Formatter('%(asctime)s - %(message)s')) 63 | logger.addHandler(custom_handler) 64 | 65 | Example Usage 66 | ----------- 67 | 68 | .. code-block:: python 69 | 70 | from livef1.adapters.realtime_client import RealF1Client 71 | from livef1.utils.logger import logger, set_log_level 72 | import logging 73 | 74 | # Enable debug logging 75 | set_log_level('DEBUG') 76 | 77 | # Initialize client 78 | client = RealF1Client(topics=["CarData.z"]) 79 | 80 | @client.callback("logging_example") 81 | async def handle_data(records): 82 | logger.debug(f"Received {len(records)} records") 83 | logger.info("Processing new data batch") 84 | 85 | try: 86 | # Process records 87 | process_records(records) 88 | except Exception as e: 89 | logger.error(f"Error processing records: {e}") 90 | -------------------------------------------------------------------------------- /docs/source/index.rst: -------------------------------------------------------------------------------- 1 | :layout: landing 2 | :description: LiveF1 is a powerful toolkit for accessing and analyzing Formula 1 data 3 | :og:author: Goktug Ocal 4 | 5 | LiveF1 6 | ========================================================= 7 | 8 | .. rst-class:: lead 9 | 10 | LiveF1 is a powerful toolkit for accessing and analyzing Formula 1 data in real time or from historical archives, designed for developers, analysts, and fans building applications around F1 insights. 11 | 12 | .. container:: buttons 13 | 14 | `Docs `_ 15 | `GitHub `_ 16 | 17 | .. grid:: 2 18 | :gutter: 2 19 | :padding: 0 20 | :class-row: surface 21 | 22 | .. grid-item-card:: :octicon:`desktop-download` How to install? 23 | :link: getting_started/installation.html 24 | 25 | Before starting analysis, follow our simple guide to install the library in Python. 26 | 27 | .. grid-item-card:: :octicon:`zap` What can I do with LiveF1? 28 | :link: user_guide/index.html 29 | 30 | Follow the user guide step by step to understand the origins of the F1's Livetiming data and start using LiveF1, with various ways that fits your needs. 31 | 32 | .. grid-item-card:: :octicon:`light-bulb` Beautiful Examples 33 | :link: examples/index.html 34 | 35 | Look at the examples to see the capabilities of LiveF1 and get insights. Also you can prepare and send your examples to us. 36 | 37 | .. grid-item-card:: :octicon:`broadcast` F1 Livetiming API 38 | :link: livetimingf1/f1_data.html 39 | 40 | Learn about the official F1 Livetiming API, available data topics, and how LiveF1 interacts with the service. 41 | 42 | .. How to Contribute 43 | .. ----------------- 44 | .. LiveF1 is an open-source project! Contributions are welcome to enhance its functionality. Please feel free to submit a Pull Request. For major changes, please open an issue first to discuss what you would like to change. 45 | 46 | .. toctree:: 47 | :maxdepth: 1 48 | :caption: Power of F1 data 49 | :hidden: 50 | 51 | getting_started/index 52 | getting_started/installation 53 | getting_started/quick_start 54 | getting_start/features 55 | 56 | .. toctree:: 57 | :maxdepth: 1 58 | :caption: User Guide 59 | :hidden: 60 | 61 | user_guide/index 62 | user_guide/medallion_architecture 63 | user_guide/data_models 64 | user_guide/historical_data 65 | user_guide/realtime_data 66 | 67 | .. toctree:: 68 | :maxdepth: 1 69 | :caption: Official F1 Livetiming API 70 | :hidden: 71 | 72 | livetimingf1/f1_data 73 | livetimingf1/data_topics 74 | 75 | .. toctree:: 76 | :maxdepth: 1 77 | :caption: Reference 78 | :hidden: 79 | 80 | api_reference/index 81 | user_guide/logging_config -------------------------------------------------------------------------------- /docs/source/user_guide/data_objects.rst: -------------------------------------------------------------------------------- 1 | ******************* 2 | Data Objects 3 | ******************* 4 | 5 | .. _get_season: 6 | Season 7 | ====== 8 | 9 | The `Season` class represents a Formula 1 season, containing methods to load and manage the season's meetings and sessions. 10 | 11 | Example: 12 | 13 | .. code-block:: python 14 | 15 | from livef1.models.season import Season 16 | 17 | # Initialize a season for the year 2023 18 | season_2023 = Season(year=2023, meetings=[]) 19 | 20 | # Load the season data 21 | season_2023.load() 22 | 23 | # Access the meetings 24 | meetings = season_2023.meetings 25 | 26 | .. _get_meeting: 27 | Meeting 28 | ======= 29 | 30 | The `Meeting` class represents a meeting in a specific season with relevant details and associated sessions. 31 | 32 | Example: 33 | 34 | .. code-block:: python 35 | 36 | from livef1.models.meeting import Meeting 37 | 38 | # Initialize a meeting 39 | meeting = Meeting(season=season_2023, year=2023, location="Monaco") 40 | 41 | # Load the meeting data 42 | meeting.load() 43 | 44 | # Access the sessions 45 | sessions = meeting.sessions 46 | 47 | .. _get_session: 48 | Session 49 | ======= 50 | 51 | The `Session` class represents a Formula 1 session, containing methods to retrieve live timing data and process it. 52 | 53 | Example: 54 | 55 | .. code-block:: python 56 | 57 | from livef1.models.session import Session 58 | 59 | # Initialize a session 60 | session = Session(season=season_2023, meeting=meeting, name="Practice 1") 61 | 62 | # Get topic names 63 | topic_names = session.get_topic_names() 64 | 65 | # Load data for a specific topic 66 | data = session.load_data(dataName="Car_Data") 67 | 68 | Generating Data 69 | =============== 70 | 71 | The `generate` method in the `Session` class is used to generate and save processed data tables (silver and gold tables) for the session. This is useful for organizing and accessing detailed session data efficiently. 72 | 73 | Example: 74 | 75 | .. code-block:: python 76 | 77 | # Generate silver tables for the session 78 | session.generate(silver=True, gold=False) 79 | 80 | # Access the generated data 81 | laps = session.laps 82 | telemetry = session.carTelemetry 83 | weather = session.get_weather() 84 | timing = session.get_timing() 85 | 86 | Why Run `.generate` 87 | =================== 88 | 89 | Running the `.generate` method is important for the following reasons: 90 | 91 | 1. **Data Organization**: It organizes raw data into structured tables, making it easier to analyze and visualize. 92 | 2. **Efficiency**: Preprocessing and storing data in tables reduces the need for repeated data parsing and processing. 93 | 3. **Accessibility**: Generated tables can be accessed directly through the session object, simplifying data retrieval. 94 | 95 | 96 | .. automodule:: livef1 97 | :undoc-members: 98 | :inherited-members: 99 | 100 | .. autosummary:: 101 | models.Season 102 | models.Meeting 103 | models.Session -------------------------------------------------------------------------------- /livef1/models/driver.py: -------------------------------------------------------------------------------- 1 | class Driver: 2 | """ 3 | Represents a Formula 1 driver with their associated information. 4 | 5 | Attributes 6 | ---------- 7 | RacingNumber : str 8 | Driver's racing number (e.g., "1") 9 | BroadcastName : str 10 | Name used in broadcasts (e.g., "M VERSTAPPEN") 11 | FullName : str 12 | Driver's complete name (e.g., "Max VERSTAPPEN") 13 | Tla : str 14 | Three letter abbreviation (e.g., "VER") 15 | Line : int 16 | Driver's line number in timing screens 17 | TeamName : str 18 | Current team name (e.g., "Red Bull Racing") 19 | TeamColour : str 20 | Team's primary color hex code without # (e.g., "3671C6") 21 | FirstName : str 22 | Driver's first name (e.g., "Max") 23 | LastName : str 24 | Driver's last name (e.g., "Verstappen") 25 | Reference : str 26 | Driver's unique reference code (e.g., "MAXVER01") 27 | HeadshotUrl : str 28 | URL to driver's headshot image 29 | session : Session 30 | Reference to the parent session object 31 | """ 32 | 33 | def __init__( 34 | self, 35 | RacingNumber: str = None, 36 | BroadcastName: str = None, 37 | FullName: str = None, 38 | Tla: str = None, 39 | Line: int = None, 40 | TeamName: str = None, 41 | TeamColour: str = None, 42 | FirstName: str = None, 43 | LastName: str = None, 44 | Reference: str = None, 45 | HeadshotUrl: str = None, 46 | session: "Session" = None, 47 | **kwargs 48 | ): 49 | self.RacingNumber = RacingNumber 50 | self.BroadcastName = BroadcastName 51 | self.FullName = FullName 52 | self.Tla = Tla 53 | self.Line = Line 54 | self.TeamName = TeamName 55 | self.TeamColour = TeamColour 56 | self.FirstName = FirstName 57 | self.LastName = LastName 58 | self.Reference = Reference 59 | self.HeadshotUrl = HeadshotUrl 60 | self.session = session 61 | 62 | # Set any additional attributes from kwargs 63 | for key, value in kwargs.items(): 64 | setattr(self, key, value) # Removed .lower() to preserve exact key names 65 | 66 | def get_telemetry(self): 67 | """ 68 | Get telemetry data for this driver from the session. 69 | 70 | Returns 71 | ------- 72 | DataFrame 73 | Telemetry data filtered for this driver 74 | """ 75 | if self.session and self.session.carTelemetry is not None: 76 | return self.session.carTelemetry[ 77 | self.session.carTelemetry["DriverNo"] == self.RacingNumber 78 | ] 79 | return None 80 | 81 | def get_laps(self): 82 | """ 83 | Get lap data for this driver from the session. 84 | 85 | Returns 86 | ------- 87 | DataFrame 88 | Lap data filtered for this driver 89 | """ 90 | if self.session and self.session.laps is not None: 91 | return self.session.laps[ 92 | self.session.laps["DriverNo"] == self.RacingNumber 93 | ] 94 | return None 95 | 96 | def __repr__(self): 97 | return f"[{self.RacingNumber}] {self.Tla} - {self.TeamName}" -------------------------------------------------------------------------------- /docs/source/user_guide/index.rst: -------------------------------------------------------------------------------- 1 | User Guide 2 | =========== 3 | 4 | Welcome to the **LiveF1 User Guide**! This guide will help you understand how to effectively use the LiveF1 package for accessing and analyzing Formula 1 timing data. Whether you're interested in real-time race analytics or historical data analysis, this guide will walk you through the essential concepts and features. 5 | 6 | Understanding Formula 1 Data 7 | --------------------------- 8 | 9 | Formula 1 live timing data provides comprehensive insights into the performance of drivers, teams, and cars during races. This data is critical for real-time analytics, historical analysis, and developing strategies for races. 10 | 11 | The live timing API follows a structured format to ensure efficient data access. Each API address corresponds to a specific endpoint, which streams or delivers data for various aspects of Formula 1 sessions. These endpoints can be accessed via HTTP requests. 12 | 13 | .. seealso:: 14 | For detailed information about the F1 timing data structure and available endpoints, see :ref:`f1-livetiming-data`. 15 | 16 | The LiveF1 package organizes this data using the :ref:`medallion_architecture`, which structures data into Bronze (raw), Silver (cleaned), and Gold (analytics-ready) layers. 17 | 18 | Core Concepts 19 | ------------ 20 | 21 | Before diving into specific features, it's important to understand these key concepts: 22 | 23 | 1. **Data Organization** 24 | 25 | - Seasons contain multiple race meetings 26 | - Meetings contain multiple sessions (Practice, Qualifying, Race) 27 | - Sessions contain various data feeds (telemetry, timing, weather, etc.) 28 | 29 | 2. **Data Access Patterns** 30 | 31 | - Historical data access for past races 32 | - Real-time data streaming for live sessions 33 | - Data transformation through the medallion architecture 34 | 35 | 3. **Data Types** 36 | 37 | - Each session provides multiple data feeds, organized by topics like car telemetry, position data, and timing information. 38 | 39 | .. tip:: 40 | Browse available data topics and their descriptions in :ref:`data_topics`. 41 | 42 | Getting Started with Data 43 | ------------------------ 44 | 45 | The package provides three main functions for accessing F1 data: 46 | 47 | 1. :ref:`get_season`: Access an entire F1 season 48 | 2. :ref:`get_meeting`: Access a specific race meeting 49 | 3. :ref:`get_session`: Access a specific session 50 | 51 | Once you have a session object, you can: 52 | 53 | - Load raw data using ``get_data()`` 54 | - Generate processed tables using ``generate()`` 55 | - Access specific data types through attributes like ``laps``, ``carTelemetry``, etc. The attribute names are defined while registering tables; have a look at :ref:`registering_custom_tables`. 56 | 57 | Examples and Use Cases 58 | --------------------- 59 | 60 | - :ref:`historical_data`: Learn how to analyze past race data 61 | - :ref:`realtime_data`: Learn how to work with live session data 62 | - :ref:`quick_start`: Quick examples to get started 63 | 64 | Where to Go Next 65 | --------------- 66 | 67 | * For detailed API documentation, see the :ref:`api_reference` 68 | * For examples and tutorials, check out the :ref:`examples` section 69 | * For understanding data organization, read about the :ref:`medallion_architecture` 70 | * For available data feeds, browse the :ref:`data_topics` 71 | 72 | .. .. toctree:: 73 | .. :maxdepth: 2 74 | .. :hidden: 75 | 76 | .. medallion_architecture 77 | .. data_objects 78 | .. data_models 79 | .. historical_data 80 | .. realtime_data -------------------------------------------------------------------------------- /docs/source/conf.py: -------------------------------------------------------------------------------- 1 | import sys 2 | from pathlib import Path 3 | 4 | sys.path.insert(0, str(Path('..','..').resolve())) 5 | 6 | import livef1 7 | 8 | # Configuration file for the Sphinx documentation builder. 9 | # 10 | # For the full list of built-in configuration values, see the documentation: 11 | # https://www.sphinx-doc.org/en/master/usage/configuration.html 12 | 13 | # -- Project information ----------------------------------------------------- 14 | # https://www.sphinx-doc.org/en/master/usage/configuration.html#project-information 15 | 16 | project = 'LiveF1' 17 | copyright = '2024, Göktuğ Öcal' 18 | author = 'Göktuğ Öcal' 19 | release = '0.0.1' 20 | 21 | # -- General configuration --------------------------------------------------- 22 | # https://www.sphinx-doc.org/en/master/usage/configuration.html#general-configuration 23 | 24 | extensions = [ 25 | 'sphinx.ext.duration', 26 | 'sphinx.ext.doctest', 27 | 'sphinx.ext.autodoc', 28 | 'sphinx.ext.coverage', 29 | 'sphinx.ext.viewcode', 30 | # 'sphinx.ext.napoleon', 31 | "sphinx_copybutton", 32 | 'numpydoc', 33 | "sphinx.ext.autosummary", 34 | 'sphinx.ext.autosectionlabel', 35 | 'sphinx_tabs.tabs', 36 | 'sphinx_design', 37 | # 'jupyter_sphinx' 38 | 'sphinx_sitemap' 39 | ] 40 | # Automatically generate summary tables 41 | autosummary_generate = True # Enable autosummary generation 42 | autosummary_imported_members = False # Skip imported members 43 | autodoc_inherit_docstrings = True 44 | autodoc_class_signature = "mixed" 45 | 46 | 47 | templates_path = ['_templates'] 48 | exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store'] 49 | 50 | 51 | # -- Options for HTML output ------------------------------------------------- 52 | # https://www.sphinx-doc.org/en/master/usage/configuration.html#options-for-html-output 53 | 54 | html_theme = "shibuya" 55 | 56 | html_favicon = "_static/favicon.png" 57 | 58 | # Select a color scheme for light mode 59 | pygments_style = "xcode" 60 | # Select a different color scheme for dark mode 61 | pygments_style_dark = "monokai" 62 | 63 | html_baseurl = "http://livef1.goktugocal.com" 64 | sitemap_excluded_pages = ["404.html"] 65 | sitemap_url_scheme = "{link}" 66 | html_extra_path = ['_static/robots.txt'] 67 | 68 | html_favicon = "_static/favicon.png" 69 | html_static_path = ['_static'] 70 | html_theme_options = { 71 | "extrahead": "", 72 | "show_nav_level": 2, # Adjust depth of sidebar navigation (optional) 73 | "rightsidebar": "true", 74 | "relbarbgcolor": "black", 75 | # Theming 76 | "accent_color": "red", 77 | "light_logo": "_static/LiveF1_red.png", 78 | "dark_logo": "_static/LiveF1_white.png", 79 | 'logo_only': True, 80 | 'display_version': False, 81 | # Navbar things 82 | "github_url": "https://github.com/goktugocal/livef1", 83 | "nav_links": [ 84 | { 85 | "title": "Getting Started", 86 | "url": "getting_started/index" 87 | }, 88 | { 89 | "title": "User Guide", 90 | "url": "user_guide/index" 91 | }, 92 | { 93 | "title": "API Reference", 94 | "url": "api_reference/index" 95 | }, 96 | # { 97 | # "title": "Sponsor me", 98 | # "url": "https://github.com/sponsors/goktugocal" 99 | # }, 100 | ] 101 | } 102 | 103 | html_sidebars = { 104 | "**": [ 105 | "sidebars/localtoc.html", 106 | "sidebars/repo-stats.html", 107 | "sidebars/edit-this-page.html" 108 | ] 109 | } 110 | 111 | html_context = { 112 | "source_type": "github", 113 | "source_user": "goktugocal", 114 | "source_repo": "livef1", 115 | "source_docs_path": "/docs/source/" 116 | } -------------------------------------------------------------------------------- /.github/workflows/build_doc.yml: -------------------------------------------------------------------------------- 1 | name: Build Documentation 2 | 3 | 4 | on: 5 | push: 6 | branches: 7 | - main 8 | 9 | jobs: 10 | build: 11 | name: Build Documentation 12 | runs-on: ubuntu-latest 13 | steps: 14 | - name: Setup python 15 | uses: actions/setup-python@v5 16 | with: 17 | python-version: '3.12' 18 | 19 | - name: Checkout repo 20 | uses: actions/checkout@v4 21 | with: 22 | fetch-depth: 0 # fetch the complete repo history (for setuptools-scm) 23 | 24 | - name: Cache pip 25 | uses: actions/cache@v4 26 | with: 27 | path: ~/.cache/pip 28 | key: pip-cache-${{ hashFiles('requirements.txt') }} 29 | restore-keys: | 30 | pip-cache 31 | 32 | - name: Install python requirements 33 | run: | 34 | python -m pip install --upgrade pip 35 | python -m pip install --upgrade build twine 36 | python -m pip install -r requirements.txt 37 | python -m pip install -r docs/requirements.txt 38 | 39 | - name: Create cache directory 40 | run: | 41 | mkdir doc_cache # make sure cache dir exists 42 | 43 | - name: Restore cache for livef1 44 | id: cache-doc-restore 45 | uses: actions/cache/restore@v4 46 | with: 47 | path: ./doc_cache 48 | # Restore any matching cache independent of hash suffix. We cannot 49 | # know if anything has changed at this stage. 50 | key: livef1-doc-cache-${{ hashFiles('./doc_cache/**/*.*') }} 51 | restore-keys: | 52 | livef1-doc-cache 53 | 54 | - name: Install at pinned tag 55 | if: inputs.pin-tag != '' 56 | run: | 57 | git checkout ${{ inputs.pin-tag }} 58 | 59 | - name: Install Fast-F1 from sources 60 | run: | 61 | python -m pip install -e . 62 | 63 | - name: Re-checkout current commit after install at pinned tag 64 | if: inputs.pin-tag != '' 65 | run: | 66 | git checkout ${{ github.sha }} 67 | 68 | - name: Build docs 69 | run: | 70 | mkdir test_cache # not really need but pytest setup relies on it 71 | cd docs 72 | make html 73 | 74 | - name: Upload docs as artifact 75 | uses: actions/upload-pages-artifact@v3 76 | with: 77 | path: docs/_build/html/ 78 | 79 | - name: Save cache for livef1 80 | id: cache-doc-save 81 | # Save the cache even in case of a failure but only if the content of 82 | # the cached directory has changed. 83 | if: always() && steps.cache-doc-restore.outputs.cache-matched-key != hashFiles('./doc_cache/**/*.*') 84 | uses: actions/cache/save@v4 85 | with: 86 | key: livef1-doc-cache-${{ hashFiles('./doc_cache/**/*.*') }} 87 | path: ./doc_cache 88 | 89 | deploy: 90 | name: Deploy Documentation 91 | 92 | # Add a dependency to the build job 93 | needs: build 94 | 95 | # Grant GITHUB_TOKEN the permissions required to make a Pages deployment 96 | permissions: 97 | pages: write # to deploy to Pages 98 | id-token: write # to verify the deployment originates from an appropriate source 99 | 100 | # Deploy to the github-pages environment 101 | environment: 102 | name: github-pages 103 | url: ${{ steps.deployment.outputs.page_url }} 104 | 105 | # Specify runner + deployment step 106 | runs-on: ubuntu-latest 107 | steps: 108 | - name: Deploy to GitHub Pages 109 | id: deployment 110 | uses: actions/deploy-pages@v4 # or specific "vX.X.X" version tag for this action -------------------------------------------------------------------------------- /livef1/adapters/functions.py: -------------------------------------------------------------------------------- 1 | # Standard Library Imports 2 | from urllib.parse import urljoin 3 | 4 | # Internal Project Imports 5 | from .livetimingf1_adapter import livetimingF1_request 6 | from ..utils.exceptions import livef1Exception 7 | 8 | def download_data( 9 | season_identifier: int = None, 10 | location_identifier: str = None, 11 | session_identifier: str | int = None 12 | ): 13 | """ 14 | Downloads and filters F1 data based on the provided season, location, and session identifiers. 15 | 16 | Parameters 17 | ---------- 18 | 19 | season_identifier : :class:`int` 20 | The unique identifier for the F1 season. This is a required parameter. 21 | location_identifier : :class:`str` 22 | The location (circuit or country name) for filtering meetings (races). 23 | session_identifier : :class:`str` 24 | The session name (e.g., 'FP1', 'Qualifying') or key (integer) to filter a specific session within a meeting. 25 | 26 | Returns 27 | ---------- 28 | dict 29 | The filtered dataset containing the requested season, meeting, or session data. 30 | 31 | Raises 32 | ---------- 33 | livef1Exception 34 | Raised if any of the required parameters are missing or if no matching data is found. 35 | 36 | Examples 37 | ------------- 38 | .. code-block:: python 39 | 40 | print("Hello World") 41 | 42 | """ 43 | 44 | # Initialize a variable to store the final filtered data 45 | last_data = None 46 | 47 | # Ensure a season identifier is provided (mandatory) 48 | if season_identifier is None: 49 | raise livef1Exception("Please provide at least a `season_identifier`.") 50 | 51 | try: 52 | # Download full season data using the F1 API 53 | season_data = livetimingF1_request(urljoin(str(season_identifier) + "/", "Index.json")) 54 | last_data = season_data # Default to entire season data initially 55 | 56 | # If a location (race circuit) is provided, filter the season data to find the specific meeting (race) 57 | if location_identifier: 58 | meeting_data = next( 59 | (meeting for meeting in season_data["Meetings"] if meeting["Location"] == location_identifier), 60 | None 61 | ) 62 | if meeting_data: 63 | last_data = meeting_data # Update with filtered meeting data 64 | else: 65 | raise livef1Exception(f"Meeting at location '{location_identifier}' not found.") 66 | else: 67 | meeting_data = season_data["Meetings"] 68 | 69 | # If a session (e.g., FP1, Qualifying) is provided, further filter the meeting data 70 | if session_identifier: 71 | if isinstance(session_identifier, str): 72 | # Filter by session name (string match) 73 | session_data = next( 74 | (session for session in meeting_data['Sessions'] if session['Name'] == session_identifier), 75 | None 76 | ) 77 | elif isinstance(session_identifier, int): 78 | # Filter by session key (integer match) 79 | session_data = next( 80 | (session for session in meeting_data['Sessions'] if session['Key'] == session_identifier), 81 | None 82 | ) 83 | 84 | if session_data: 85 | last_data = session_data # Update with filtered session data 86 | else: 87 | raise livef1Exception(f"Session with identifier '{session_identifier}' not found.") 88 | 89 | except Exception as e: 90 | # Catch any exception and wrap it in a custom livef1Exception 91 | raise livef1Exception(e) from e 92 | 93 | # Return the final filtered data (season, meeting, or session) 94 | return last_data 95 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | share/python-wheels/ 24 | *.egg-info/ 25 | .installed.cfg 26 | *.egg 27 | MANIFEST 28 | 29 | # PyInstaller 30 | # Usually these files are written by a python script from a template 31 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 32 | *.manifest 33 | *.spec 34 | 35 | # Installer logs 36 | pip-log.txt 37 | pip-delete-this-directory.txt 38 | 39 | # Unit test / coverage reports 40 | htmlcov/ 41 | .tox/ 42 | .nox/ 43 | .coverage 44 | .coverage.* 45 | .cache 46 | nosetests.xml 47 | coverage.xml 48 | *.cover 49 | *.py,cover 50 | .hypothesis/ 51 | .pytest_cache/ 52 | cover/ 53 | 54 | # Translations 55 | *.mo 56 | *.pot 57 | 58 | # Django stuff: 59 | *.log 60 | local_settings.py 61 | db.sqlite3 62 | db.sqlite3-journal 63 | 64 | # Flask stuff: 65 | instance/ 66 | .webassets-cache 67 | 68 | # Scrapy stuff: 69 | .scrapy 70 | 71 | # Sphinx documentation 72 | doc/_build/ 73 | 74 | # PyBuilder 75 | .pybuilder/ 76 | target/ 77 | 78 | # Jupyter Notebook 79 | .ipynb_checkpoints 80 | 81 | # IPython 82 | profile_default/ 83 | ipython_config.py 84 | 85 | # pyenv 86 | # For a library or package, you might want to ignore these files since the code is 87 | # intended to run in multiple environments; otherwise, check them in: 88 | # .python-version 89 | 90 | # pipenv 91 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 92 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 93 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 94 | # install all needed dependencies. 95 | #Pipfile.lock 96 | 97 | # poetry 98 | # Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. 99 | # This is especially recommended for binary packages to ensure reproducibility, and is more 100 | # commonly ignored for libraries. 101 | # https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control 102 | #poetry.lock 103 | 104 | # pdm 105 | # Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. 106 | #pdm.lock 107 | # pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it 108 | # in version control. 109 | # https://pdm.fming.dev/latest/usage/project/#working-with-version-control 110 | .pdm.toml 111 | .pdm-python 112 | .pdm-build/ 113 | 114 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm 115 | __pypackages__/ 116 | 117 | # Celery stuff 118 | celerybeat-schedule 119 | celerybeat.pid 120 | 121 | # SageMath parsed files 122 | *.sage.py 123 | 124 | # Environments 125 | .env 126 | .venv 127 | env/ 128 | venv/ 129 | ENV/ 130 | env.bak/ 131 | venv.bak/ 132 | 133 | # Spyder project settings 134 | .spyderproject 135 | .spyproject 136 | 137 | # Rope project settings 138 | .ropeproject 139 | 140 | # mkdocs documentation 141 | /site 142 | 143 | # mypy 144 | .mypy_cache/ 145 | .dmypy.json 146 | dmypy.json 147 | 148 | # Pyre type checker 149 | .pyre/ 150 | 151 | # pytype static type analyzer 152 | .pytype/ 153 | 154 | # Cython debug symbols 155 | cython_debug/ 156 | 157 | # PyCharm 158 | # JetBrains specific template is maintained in a separate JetBrains.gitignore that can 159 | # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore 160 | # and can be added to the global gitignore or merged into this file. For a more nuclear 161 | # option (not recommended) you can uncomment the following to ignore the entire idea folder. 162 | #.idea/ 163 | -------------------------------------------------------------------------------- /docs/source/examples/pipelines/race_analysis.rst: -------------------------------------------------------------------------------- 1 | Race Analysis Pipeline 2 | ==================== 3 | 4 | This example demonstrates a complete end-to-end pipeline for analyzing Formula 1 race data, including lap times, tire strategies, and performance metrics. 5 | 6 | Setup 7 | ----- 8 | 9 | First, let's import the required libraries: 10 | 11 | .. code-block:: python 12 | 13 | import livef1 14 | import pandas as pd 15 | import matplotlib.pyplot as plt 16 | import seaborn as sns 17 | from datetime import timedelta 18 | 19 | Loading Race Data 20 | --------------- 21 | 22 | .. code-block:: python 23 | 24 | # Get the race session 25 | race = livef1.get_session( 26 | season=2024, 27 | meeting_identifier="Spa", 28 | session_identifier="Race" 29 | ) 30 | 31 | # Generate silver tables 32 | race.generate(silver=True) 33 | 34 | # Load different data types 35 | laps = race.laps 36 | telemetry = race.carTelemetry 37 | weather = race.get_weather() 38 | timing = race.get_timing() 39 | 40 | Lap Time Analysis 41 | --------------- 42 | 43 | .. code-block:: python 44 | 45 | def analyze_lap_times(laps_df): 46 | # Calculate average lap times per driver 47 | avg_lap_times = laps_df.groupby('DriverNo')['lap_time'].mean() 48 | 49 | # Create lap time evolution plot 50 | plt.figure(figsize=(15, 8)) 51 | for driver in laps_df['DriverNo'].unique(): 52 | driver_laps = laps_df[(laps_df['DriverNo'] == driver) & (laps_df['lap_time'] > timedelta(seconds=10))] 53 | plt.plot( 54 | driver_laps['lap_number'], 55 | driver_laps['lap_time'], 56 | label=f'Driver {driver}' 57 | ) 58 | 59 | plt.title('Lap Time Evolution') 60 | plt.xlabel('Lap Number') 61 | plt.ylabel('Lap Time (seconds)') 62 | plt.legend(bbox_to_anchor=(1.05, 1), loc='upper left') 63 | plt.grid(True) 64 | plt.tight_layout() 65 | plt.show() 66 | 67 | return avg_lap_times 68 | 69 | avg_lap_times = analyze_lap_times(laps) 70 | print("\nAverage Lap Times:") 71 | print(avg_lap_times) 72 | 73 | 74 | Performance Analysis 75 | ----------------- 76 | 77 | .. code-block:: python 78 | 79 | def analyze_performance(telemetry_df): 80 | # Calculate speed statistics per driver 81 | speed_stats = telemetry_df.groupby('DriverNo').agg({ 82 | 'speed': ['mean', 'max', 'std'] 83 | }) 84 | 85 | # Create speed distribution plot 86 | plt.figure(figsize=(15, 8)) 87 | sns.boxplot(data=telemetry_df, x='DriverNo', y='speed') 88 | plt.title('Speed Distribution by Driver') 89 | plt.xlabel('Driver Number') 90 | plt.ylabel('Speed (km/h)') 91 | plt.grid(True) 92 | plt.show() 93 | 94 | return speed_stats 95 | 96 | performance_stats = analyze_performance(telemetry) 97 | print("\nPerformance Statistics:") 98 | print(performance_stats) 99 | 100 | Complete Pipeline 101 | --------------- 102 | 103 | Here's how to combine all analyses into a complete pipeline: 104 | 105 | .. code-block:: python 106 | 107 | def race_analysis_pipeline(session): 108 | # Generate required data 109 | session.generate(silver=True) 110 | 111 | # Load data 112 | laps = session.laps 113 | telemetry = session.carTelemetry 114 | 115 | # Run analyses 116 | lap_analysis = analyze_lap_times(laps) 117 | performance_analysis = analyze_performance(telemetry) 118 | 119 | # Combine results 120 | results = { 121 | 'lap_times': lap_analysis, 122 | 'performance': performance_analysis 123 | } 124 | 125 | return results 126 | 127 | # Run the complete pipeline 128 | race_results = race_analysis_pipeline(race) 129 | 130 | # Export results 131 | for analysis_name, data in race_results.items(): 132 | data.to_csv(f'{analysis_name}.csv') -------------------------------------------------------------------------------- /livef1/adapters/signalr_aio/transports/_transport.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | # -*- coding: utf-8 -*- 3 | 4 | # signalr_aio/transports/_transport.py 5 | # Stanislav Lazarov 6 | 7 | # python compatiblity for <3.6 8 | try: 9 | ModuleNotFoundError 10 | except NameError: 11 | ModuleNotFoundError = ImportError 12 | 13 | # ----------------------------------- 14 | # Internal Imports 15 | from ._exceptions import ConnectionClosed 16 | from ._parameters import WebSocketParameters 17 | from ._queue_events import InvokeEvent, CloseEvent 18 | 19 | # ----------------------------------- 20 | # External Imports 21 | try: 22 | from ujson import dumps, loads 23 | except ModuleNotFoundError: 24 | from json import dumps, loads 25 | import websockets 26 | import asyncio 27 | import time 28 | 29 | try: 30 | import uvloop 31 | 32 | asyncio.set_event_loop_policy(uvloop.EventLoopPolicy()) 33 | except ModuleNotFoundError: 34 | pass 35 | 36 | 37 | class Transport: 38 | def __init__(self, connection): 39 | self._connection = connection 40 | self._ws_params = None 41 | self._conn_handler = None 42 | self.ws_loop = None 43 | self.invoke_queue = None 44 | self.ws = None 45 | self._set_loop_and_queue() 46 | 47 | # =================================== 48 | # Public Methods 49 | 50 | def start(self): 51 | self._ws_params = WebSocketParameters(self._connection) 52 | self._connect() 53 | if not self.ws_loop.is_running(): 54 | self.ws_loop.run_forever() 55 | 56 | def send(self, message): 57 | asyncio.Task(self.invoke_queue.put(InvokeEvent(message)), loop=self.ws_loop) 58 | 59 | def close(self): 60 | asyncio.Task(self.invoke_queue.put(CloseEvent()), loop=self.ws_loop) 61 | 62 | # ----------------------------------- 63 | # Private Methods 64 | 65 | def _set_loop_and_queue(self): 66 | try: 67 | self.ws_loop = asyncio.get_event_loop() 68 | except RuntimeError: 69 | self.ws_loop = asyncio.new_event_loop() 70 | asyncio.set_event_loop(self.ws_loop) 71 | # self.invoke_queue = asyncio.Queue(loop=self.ws_loop) 72 | self.invoke_queue = asyncio.Queue() 73 | 74 | def _connect(self): 75 | self._conn_handler = asyncio.ensure_future(self._socket(self.ws_loop), loop=self.ws_loop) 76 | 77 | async def _socket(self, loop): 78 | async with websockets.connect(self._ws_params.socket_url, extra_headers=self._ws_params.headers, 79 | loop=loop) as self.ws: 80 | self._connection.started = True 81 | await self._master_handler(self.ws) 82 | 83 | async def _master_handler(self, ws): 84 | consumer_task = asyncio.ensure_future(self._consumer_handler(ws), loop=self.ws_loop) 85 | producer_task = asyncio.ensure_future(self._producer_handler(ws), loop=self.ws_loop) 86 | # done, pending = await asyncio.wait([consumer_task, producer_task], 87 | # loop=self.ws_loop, return_when=asyncio.FIRST_EXCEPTION) 88 | done, pending = await asyncio.wait([consumer_task, producer_task], 89 | return_when=asyncio.FIRST_EXCEPTION) 90 | 91 | for task in pending: 92 | task.cancel() 93 | 94 | async def _consumer_handler(self, ws): 95 | while True: 96 | message = await ws.recv() 97 | if len(message) > 0: 98 | data = loads(message) 99 | await self._connection.received.fire(**data) 100 | 101 | async def _producer_handler(self, ws): 102 | while True: 103 | try: 104 | event = await self.invoke_queue.get() 105 | if event is not None: 106 | if event.type == 'INVOKE': 107 | await ws.send(dumps(event.message)) 108 | elif event.type == 'CLOSE': 109 | await ws.close() 110 | while ws.open is True: 111 | await asyncio.sleep(0.1) 112 | else: 113 | self._connection.started = False 114 | break 115 | else: 116 | break 117 | self.invoke_queue.task_done() 118 | except Exception as e: 119 | raise e -------------------------------------------------------------------------------- /livef1/models/circuit.py: -------------------------------------------------------------------------------- 1 | from typing import Dict, Optional 2 | import requests 3 | import pandas as pd 4 | 5 | from livef1.utils.constants import START_COORDINATES_URL 6 | from livef1.utils.exceptions import livef1Exception 7 | from livef1.utils.helper import string_match_ratio 8 | 9 | 10 | class Circuit: 11 | """ 12 | Represents a Formula 1 circuit with its characteristics and metadata. 13 | 14 | Attributes 15 | ---------- 16 | key : str 17 | The unique identifier for the circuit. 18 | short_name : str 19 | Short name/abbreviation of the circuit. 20 | name : str, optional 21 | Full name of the circuit. 22 | length : float, optional 23 | Length of the circuit in kilometers. 24 | laps : int, optional 25 | Standard number of race laps. 26 | country : Dict, optional 27 | Dictionary containing country information. 28 | location : str, optional 29 | Geographic location of the circuit. 30 | coordinates : Dict[str, float], optional 31 | Latitude and longitude of the circuit. 32 | """ 33 | 34 | def __init__( 35 | self, 36 | key: str, 37 | short_name: str 38 | ): 39 | self.key = key 40 | self.short_name = short_name 41 | 42 | def _load_start_coordinates(self): 43 | """ 44 | Load the start coordinates of the circuit from an external API. 45 | """ 46 | response = requests.get(START_COORDINATES_URL) 47 | 48 | if response.status_code == 200: 49 | data = response.json() 50 | try: 51 | self.start_coordinates = data[self.short_name]["start_coordinates"] 52 | self.start_direction = data[self.short_name]["start_direction"] 53 | except: 54 | pass 55 | else: 56 | raise Exception(f"Failed to load start coordinates: {response.status_code}") 57 | 58 | def _load_circuit_data(self): 59 | HEADERS = {'User-Agent': 'LiveF1/user'} 60 | response = requests.get("https://api.multiviewer.app/api/v1/circuits", headers=HEADERS) 61 | circuits = response.json() 62 | 63 | circuit_ref = next( 64 | ( 65 | v for v in circuits.values() 66 | if 67 | (v.get('name').lower() == self.short_name.lower()) 68 | | (string_match_ratio(v.get('name').lower(), self.short_name.lower()) > 0.8) 69 | ), 70 | None 71 | ) 72 | if circuit_ref == None: 73 | raise livef1Exception(f"Circut {self.short_name} couldn't be found in circuit api.") 74 | 75 | circuit_key = circuit_ref["circuitKey"] 76 | circuit_ref_years = circuit_ref["years"] 77 | 78 | response = requests.get(f"https://api.multiviewer.app/api/v1/circuits/{circuit_key}/{circuit_ref_years[0]}/", headers=HEADERS) 79 | self._raw_circuit_data = response.json() 80 | 81 | for corner in self._raw_circuit_data["corners"]: 82 | corner.update(corner["trackPosition"]) 83 | df_corners = pd.DataFrame(self._raw_circuit_data["corners"]).rename( 84 | columns = { 85 | "length": "Distance", 86 | "x": "X", 87 | "y": "Y" 88 | } 89 | ) 90 | df_corners.Distance = df_corners.Distance / 10 91 | df_corners["corner_start"] = df_corners.Distance - 50 92 | df_corners["corner_end"] = df_corners.Distance + 50 93 | 94 | df_corners["corner_end"] = (df_corners["corner_end"] > df_corners["corner_start"].shift(-1).fillna(1000000)) * df_corners["corner_start"].shift(-1).fillna(0) + (df_corners["corner_end"] < df_corners["corner_start"].shift(-1).fillna(1000000)) * df_corners["corner_end"] 95 | 96 | df_corners["name"] = "T" + df_corners["number"].astype(int).astype(str) 97 | df_corners["type"] = "Corner" 98 | df_corners = df_corners[["type","name","number","X","Y","corner_start","corner_end","angle","Distance"]] 99 | 100 | 101 | df_straights = pd.DataFrame( 102 | { 103 | "corner_start" : df_corners.corner_end.shift(1).fillna(df_corners.corner_end.max()), 104 | "corner_end" : df_corners.corner_start, 105 | "number" : df_corners.number.shift(1).fillna(0).astype(int), 106 | } 107 | ) 108 | df_straights["type"] = "Straight" 109 | df_straights["name"] = "S" + df_straights["number"].astype(str) 110 | 111 | self.track_regions = pd.concat([df_corners, df_straights]) -------------------------------------------------------------------------------- /livef1/adapters/other.py: -------------------------------------------------------------------------------- 1 | import pandas as pd 2 | import requests 3 | from bs4 import BeautifulSoup 4 | from datetime import datetime 5 | from string import digits 6 | 7 | 8 | def _parse_tables_from_wikipedia(url): 9 | 10 | response = requests.get(url) 11 | soup = BeautifulSoup(response.content, 'html.parser') 12 | 13 | # Store parsed tables with associated titles 14 | tables_dict = {} 15 | all_tables = soup.find_all('table', class_='wikitable') 16 | 17 | for i, table in enumerate(all_tables): 18 | # Try caption first 19 | caption = table.find('caption') 20 | if caption: 21 | title = caption.get_text(strip=True) 22 | else: 23 | # No caption: find closest previous header (h2-h6) 24 | prev = table.find_previous(['h2', 'h3', 'h4', 'h5', 'h6']) 25 | if prev: 26 | title = prev.get_text(strip=True).replace('[edit]', '') 27 | else: 28 | title = f'Table {i+1}' # fallback generic title 29 | 30 | # Read the table with pandas 31 | df = pd.read_html(str(table))[0] 32 | tables_dict[title] = df 33 | 34 | return tables_dict 35 | 36 | def get_table_from_wikipedia(url, table_name): 37 | tables_dict = _parse_tables_from_wikipedia(url) 38 | 39 | try: 40 | return tables_dict.get(table_name) 41 | except: 42 | return None 43 | 44 | def parse_schedule_from_f1com(season: int) -> pd.DataFrame: 45 | """Parse the schedule for the given season from the official Formula 1 website. 46 | Args: 47 | season (int): The year of the season to parse. 48 | Returns: 49 | pd.DataFrame: A DataFrame containing the schedule with columns: 50 | - "Meeting Circuit Shortname" 51 | - "Meeting Offname" 52 | - "Session Name" 53 | - "Start Date" 54 | - "End Date" 55 | """ 56 | 57 | BASE_URL = 'https://www.formula1.com' 58 | SEASON_URL = BASE_URL + f'/en/racing/{season}' 59 | 60 | """Given a race weekend URL, return a dict of session names → datetime.""" 61 | resp = requests.get(SEASON_URL) 62 | soup = BeautifulSoup(resp.content, 'html.parser') 63 | 64 | meetings = [] 65 | for meeting_obj in soup.find_all('a', class_='group'): 66 | 67 | # short_name = meeting_obj.find(class_="typography-module_display-xl-bold__Gyl5W").text 68 | offname = meeting_obj.find(class_="typography-module_body-xs-semibold__Fyfwn").text 69 | meeting_url = BASE_URL + meeting_obj["href"] 70 | 71 | sub_resp = requests.get(meeting_url) 72 | sub_soup = BeautifulSoup(sub_resp.content, 'html.parser') 73 | short_name = sub_soup.title.text.split(" - ")[0].split("Grand Prix")[0].strip() 74 | 75 | 76 | ul = sub_soup.find('ul', class_="contents") 77 | items = [li for li in ul.find_all('li')] 78 | sessions = {} 79 | 80 | for item in items: 81 | session_name = item.find(class_="typography-module_display-m-bold__qgZFB").text 82 | times = item.find_all("time") 83 | 84 | day = item.find(class_="typography-module_technical-l-bold__AKrZb").text 85 | month = item.find(class_="typography-module_technical-s-regular__6LvKq").text 86 | season = 2025 87 | 88 | if len(times) > 1: 89 | start = times[0].text 90 | end = times[1].text 91 | start_date = datetime.strptime(f"{day} {month} {season} - {start}", "%d %b %Y - %H:%M") 92 | end_date = datetime.strptime(f"{day} {month} {season} - {end}", "%d %b %Y - %H:%M") 93 | 94 | else: 95 | start = times[0].text 96 | end = None 97 | start_date = datetime.strptime(f"{day} {month} {season} - {start}", "%d %b %Y - %H:%M") 98 | end_date = None 99 | 100 | sessions[session_name] = { 101 | "start_date" : start_date, 102 | "end_date" : end_date 103 | } 104 | 105 | 106 | meetings.append( 107 | { 108 | "short_name" : short_name, 109 | "offname" : offname, 110 | "sessions" : sessions 111 | } 112 | ) 113 | 114 | 115 | row = [] 116 | for meeting in meetings: 117 | 118 | meeting_shortname = meeting.get("short_name") 119 | meeting_offname = meeting.get("offname") 120 | sessions = meeting.get("sessions") 121 | 122 | for session_name, dates in sessions.items(): 123 | row.append( 124 | [ 125 | meeting_shortname, 126 | meeting_offname, 127 | session_name, 128 | dates["start_date"], 129 | dates["end_date"] 130 | ] 131 | ) 132 | 133 | schedule_df = pd.DataFrame(row, columns=["Meeting Shortname", "Meeting Offname", "Session Name", "Start Date", "End Date"]) 134 | return schedule_df -------------------------------------------------------------------------------- /docs/source/user_guide/data_models.rst: -------------------------------------------------------------------------------- 1 | Data Models 2 | ========== 3 | 4 | .. automodule:: livef1.models 5 | 6 | .. rubric:: Classes 7 | .. autosummary:: 8 | Season 9 | Meeting 10 | Session 11 | Circuit 12 | Driver 13 | 14 | Season 15 | ------ 16 | The Season model is the top-level container for Formula 1 data. It manages all meetings and sessions for a specific year. 17 | 18 | **Key Attributes:** 19 | 20 | - ``year``: The F1 season year 21 | 22 | - ``meetings``: List of Meeting objects 23 | 24 | - ``meetings_json``: Raw meeting data 25 | 26 | - ``season_table``: Pandas DataFrame with season data 27 | 28 | - ``meetings_table``: Aggregated meetings data 29 | 30 | **Example Usage:** 31 | 32 | .. code-block:: python 33 | 34 | import livef1 35 | 36 | # Get a season 37 | season = livef1.get_season(2024) 38 | 39 | # Access meetings 40 | print(season.meetings_table) # View all meetings 41 | print(season.meetings) # Access Meeting objects 42 | 43 | # Get specific meeting 44 | monaco_gp = [m for m in season.meetings if m.location == "Monaco"][0] 45 | 46 | Meeting 47 | ------- 48 | The Meeting model represents a specific Grand Prix event within a season. It manages session data and meeting metadata. 49 | 50 | **Key Attributes:** 51 | 52 | - ``season``: Reference to parent Season object 53 | 54 | - ``code``: Meeting code (e.g., "MON") 55 | 56 | - ``key``: Unique identifier 57 | 58 | - ``location``: Circuit location 59 | 60 | - ``name``: Grand Prix name 61 | 62 | - ``sessions``: List of Session objects 63 | 64 | - ``sessions_table``: DataFrame of session data 65 | 66 | **Example Usage:** 67 | 68 | .. code-block:: python 69 | 70 | # Get specific meeting 71 | meeting = livef1.get_meeting( 72 | season=2024, 73 | meeting_identifier="Monaco" 74 | ) 75 | 76 | # Access sessions 77 | print(meeting.sessions_table) # View all sessions 78 | 79 | # Get specific session 80 | race = [s for s in meeting.sessions if s.type == "Race"][0] 81 | 82 | Session 83 | ------- 84 | The Session model represents individual F1 sessions (Practice, Qualifying, Race) and implements the medallion architecture for data processing. 85 | 86 | **Key Attributes:** 87 | 88 | - ``meeting``: Reference to parent Meeting object 89 | 90 | - ``type``: Session type (e.g., "Race", "Practice 1") 91 | 92 | - ``name``: Session name 93 | 94 | - ``key``: Unique identifier 95 | 96 | - ``startdate``: Session start time 97 | 98 | - ``enddate``: Session end time 99 | 100 | - ``data_lake``: DataLake object for data storage 101 | 102 | - ``topic_names_info``: Available data topics 103 | 104 | **Data Access Methods:** 105 | 106 | - ``get_data()``: Retrieve raw data 107 | 108 | - ``generate()``: Create processed tables 109 | 110 | - ``laps``: Access lap data 111 | 112 | - ``carTelemetry``: Access telemetry data 113 | 114 | **Example Usage:** 115 | 116 | .. code-block:: python 117 | 118 | # Get specific session 119 | session = livef1.get_session( 120 | season=2024, 121 | meeting_identifier="Monaco", 122 | session_identifier="Race" 123 | ) 124 | 125 | # Load raw data 126 | telemetry = session.get_data("CarData.z") 127 | 128 | # Generate processed tables 129 | session.generate(silver=True) 130 | 131 | # Access processed data 132 | laps = session.laps 133 | telemetry = session.carTelemetry 134 | 135 | Data Flow 136 | --------- 137 | The models work together in a hierarchical structure: 138 | 139 | .. code-block:: text 140 | 141 | Season 142 | ├── Meeting 1 143 | │ ├── Practice 1 144 | │ ├── Practice 2 145 | │ ├── Practice 3 146 | │ ├── Qualifying 147 | │ └── Race 148 | ├── Meeting 2 149 | │ └── ... 150 | └── Meeting N 151 | └── ... 152 | 153 | Each level provides specific functionality: 154 | 155 | 1. **Season Level** 156 | 157 | - Season-wide data access 158 | - Meeting management 159 | 160 | 2. **Meeting Level** 161 | 162 | - Session management 163 | - Meeting-specific data 164 | - Circuit information 165 | 166 | 3. **Session Level** 167 | 168 | - Raw data access 169 | - Data processing 170 | - DataLake architecture implementation 171 | 172 | Best Practices 173 | ------------- 174 | 1. **Data Access** 175 | 176 | - Use high-level functions (``get_season()``, ``get_meeting()``, ``get_session()``) 177 | - Access objects through their parent when possible 178 | - Use data frames for bulk data analysis 179 | 180 | 2. **Data Processing** 181 | 182 | - Generate silver tables before accessing processed data 183 | - Use parallel processing for multiple data topics 184 | - Cache frequently accessed data 185 | 186 | 3. **Memory Management** 187 | 188 | - Load data only when needed 189 | - Use data lake for persistent storage 190 | - Clear unused data from memory 191 | 192 | .. seealso:: 193 | - For more details on data processing, see :ref:`medallion_architecture` 194 | - For API documentation, see :ref:`api_reference` 195 | 196 | 197 | -------------------------------------------------------------------------------- /livef1/data/find_track_start.py: -------------------------------------------------------------------------------- 1 | import livef1 2 | import requests 3 | import pandas as pd 4 | import numpy as np 5 | import json 6 | from livef1.utils.constants import TABLE_REQUIREMENTS, TABLE_GENERATION_FUNCTIONS 7 | from livef1.data_processing.silver_functions import * 8 | 9 | def get_circuit_data(): 10 | HEADERS = {'User-Agent': 'LiveF1/trial'} 11 | 12 | headers=HEADERS 13 | 14 | response = requests.get("https://api.multiviewer.app/api/v1/circuits", headers=HEADERS) 15 | if response.status_code == 200: 16 | circuits_data = response.json() 17 | return circuits_data 18 | else: 19 | print(f"Failed to fetch data. Status code: {response.status_code}") 20 | 21 | def collect_sessions_by_circuit(circuits_data): 22 | # sessions = [] 23 | for key, circ in circuits_data.items(): 24 | circ_name = circ["name"] 25 | circ_years = circ["years"] 26 | circ_years.append(2023) 27 | circ_years.append(2025) 28 | 29 | for year in circ_years: 30 | try: 31 | session = livef1.get_session( 32 | year, 33 | meeting_identifier = circ_name, 34 | session_identifier = "Race" 35 | ) 36 | session.generate() 37 | # sessions.append(session) 38 | break 39 | except: 40 | pass 41 | 42 | return sessions 43 | 44 | def find_starting_coordinates(session): 45 | 46 | car_tel = session.carTelemetry.copy() 47 | sample = car_tel[car_tel["LapNo"] > 1].dropna(subset=["X", "Y"]).head(2) 48 | 49 | x_chng = sample.X.values[-1] - sample.X.values[0] 50 | y_chng = sample.Y.values[-1] - sample.Y.values[0] 51 | x_coeff = x_chng / abs(x_chng) 52 | y_coeff = y_chng / abs(y_chng) 53 | 54 | laps = session.laps 55 | 56 | filtered_laps = laps[laps["PitIn"].isnull() & laps["PitOut"].isnull() & ~laps["IsDeleted"]] 57 | filter_tuples = list(zip(filtered_laps["DriverNo"], filtered_laps["LapNo"])) 58 | session.carTelemetry["driver_lap_tuple"] = list(zip(session.carTelemetry["DriverNo"], session.carTelemetry["LapNo"])) 59 | filtered_df = session.carTelemetry[session.carTelemetry["driver_lap_tuple"].isin(filter_tuples)] 60 | 61 | last_points = filtered_df.groupby(["DriverNo", "LapNo"]).last().reset_index() 62 | 63 | # Calculate IQR for X and Y 64 | Q1_X = last_points['X'].quantile(0.25) 65 | Q3_X = last_points['X'].quantile(0.75) 66 | IQR_X = Q3_X - Q1_X 67 | 68 | Q1_Y = last_points['Y'].quantile(0.25) 69 | Q3_Y = last_points['Y'].quantile(0.75) 70 | IQR_Y = Q3_Y - Q1_Y 71 | 72 | # Define bounds for X and Y 73 | lower_bound_X = Q1_X - 1.5 * IQR_X 74 | upper_bound_X = Q3_X + 1.5 * IQR_X 75 | 76 | lower_bound_Y = Q1_Y - 1.5 * IQR_Y 77 | upper_bound_Y = Q3_Y + 1.5 * IQR_Y 78 | 79 | # Filter out outliers 80 | last_points_filtered = last_points[ 81 | (last_points['X'] >= lower_bound_X) & (last_points['X'] <= upper_bound_X) & 82 | (last_points['Y'] >= lower_bound_Y) & (last_points['Y'] <= upper_bound_Y) 83 | ] 84 | 85 | last_points = filtered_df.groupby(["DriverNo", "LapNo"]).last().reset_index() 86 | 87 | limit = 95 88 | limit_x = 100 - limit if x_coeff < 0 else limit 89 | limit_y = 100 - limit if y_coeff < 0 else limit 90 | 91 | start_x = np.percentile(last_points_filtered["X"], limit_x) 92 | start_y = np.percentile(last_points_filtered["Y"], limit_y) 93 | 94 | return start_x, start_y, x_coeff, y_coeff 95 | 96 | if __name__ == "__main__": 97 | 98 | circuit_data = get_circuit_data() 99 | # sessions = collect_sessions_by_circuit(circuit_data) 100 | 101 | coordinates = {} 102 | for key, circ in circuit_data.items(): 103 | circ_name = circ["name"] 104 | circ_years = circ["years"] 105 | circ_years.append(2023) 106 | circ_years.append(2025) 107 | 108 | for year in circ_years: 109 | try: 110 | session = livef1.get_session( 111 | year, 112 | meeting_identifier = circ_name, 113 | session_identifier = "Race" 114 | ) 115 | session.generate() 116 | # session.create_silver_table("laps", TABLE_REQUIREMENTS["laps"], include_session=True)(globals()[TABLE_GENERATION_FUNCTIONS["laps"]]) 117 | # session.carTelemetry = session.get_data("CarData.z") 118 | 119 | start_x, start_y, x_coeff, y_coeff = find_starting_coordinates(session) 120 | 121 | coordinates[session.meeting.circuit.short_name] = { 122 | "start_coordinates": (start_x, start_y), 123 | "start_direction": (x_coeff, y_coeff), 124 | } 125 | 126 | del session 127 | 128 | break 129 | except Exception as e: 130 | print(e) 131 | import traceback; traceback.print_exc() 132 | pass 133 | 134 | 135 | with open("starting_coordinates.json", "w") as f: 136 | json.dump(coordinates, f, indent=4) 137 | print("Starting coordinates and directions saved to starting_coordinates.json") 138 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | We as members, contributors, and leaders pledge to make participation in our 6 | community a harassment-free experience for everyone, regardless of age, body 7 | size, visible or invisible disability, ethnicity, sex characteristics, gender 8 | identity and expression, level of experience, education, socio-economic status, 9 | nationality, personal appearance, race, religion, or sexual identity 10 | and orientation. 11 | 12 | We pledge to act and interact in ways that contribute to an open, welcoming, 13 | diverse, inclusive, and healthy community. 14 | 15 | ## Our Standards 16 | 17 | Examples of behavior that contributes to a positive environment for our 18 | community include: 19 | 20 | * Demonstrating empathy and kindness toward other people 21 | * Being respectful of differing opinions, viewpoints, and experiences 22 | * Giving and gracefully accepting constructive feedback 23 | * Accepting responsibility and apologizing to those affected by our mistakes, 24 | and learning from the experience 25 | * Focusing on what is best not just for us as individuals, but for the 26 | overall community 27 | 28 | Examples of unacceptable behavior include: 29 | 30 | * The use of sexualized language or imagery, and sexual attention or 31 | advances of any kind 32 | * Trolling, insulting or derogatory comments, and personal or political attacks 33 | * Public or private harassment 34 | * Publishing others' private information, such as a physical or email 35 | address, without their explicit permission 36 | * Other conduct which could reasonably be considered inappropriate in a 37 | professional setting 38 | 39 | ## Enforcement Responsibilities 40 | 41 | Community leaders are responsible for clarifying and enforcing our standards of 42 | acceptable behavior and will take appropriate and fair corrective action in 43 | response to any behavior that they deem inappropriate, threatening, offensive, 44 | or harmful. 45 | 46 | Community leaders have the right and responsibility to remove, edit, or reject 47 | comments, commits, code, wiki edits, issues, and other contributions that are 48 | not aligned to this Code of Conduct, and will communicate reasons for moderation 49 | decisions when appropriate. 50 | 51 | ## Scope 52 | 53 | This Code of Conduct applies within all community spaces, and also applies when 54 | an individual is officially representing the community in public spaces. 55 | Examples of representing our community include using an official e-mail address, 56 | posting via an official social media account, or acting as an appointed 57 | representative at an online or offline event. 58 | 59 | ## Enforcement 60 | 61 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 62 | reported to the community leaders responsible for enforcement at 63 | goktugocal41@gmail.com. 64 | All complaints will be reviewed and investigated promptly and fairly. 65 | 66 | All community leaders are obligated to respect the privacy and security of the 67 | reporter of any incident. 68 | 69 | ## Enforcement Guidelines 70 | 71 | Community leaders will follow these Community Impact Guidelines in determining 72 | the consequences for any action they deem in violation of this Code of Conduct: 73 | 74 | ### 1. Correction 75 | 76 | **Community Impact**: Use of inappropriate language or other behavior deemed 77 | unprofessional or unwelcome in the community. 78 | 79 | **Consequence**: A private, written warning from community leaders, providing 80 | clarity around the nature of the violation and an explanation of why the 81 | behavior was inappropriate. A public apology may be requested. 82 | 83 | ### 2. Warning 84 | 85 | **Community Impact**: A violation through a single incident or series 86 | of actions. 87 | 88 | **Consequence**: A warning with consequences for continued behavior. No 89 | interaction with the people involved, including unsolicited interaction with 90 | those enforcing the Code of Conduct, for a specified period of time. This 91 | includes avoiding interactions in community spaces as well as external channels 92 | like social media. Violating these terms may lead to a temporary or 93 | permanent ban. 94 | 95 | ### 3. Temporary Ban 96 | 97 | **Community Impact**: A serious violation of community standards, including 98 | sustained inappropriate behavior. 99 | 100 | **Consequence**: A temporary ban from any sort of interaction or public 101 | communication with the community for a specified period of time. No public or 102 | private interaction with the people involved, including unsolicited interaction 103 | with those enforcing the Code of Conduct, is allowed during this period. 104 | Violating these terms may lead to a permanent ban. 105 | 106 | ### 4. Permanent Ban 107 | 108 | **Community Impact**: Demonstrating a pattern of violation of community 109 | standards, including sustained inappropriate behavior, harassment of an 110 | individual, or aggression toward or disparagement of classes of individuals. 111 | 112 | **Consequence**: A permanent ban from any sort of public interaction within 113 | the community. 114 | 115 | ## Attribution 116 | 117 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], 118 | version 2.0, available at 119 | https://www.contributor-covenant.org/version/2/0/code_of_conduct.html. 120 | 121 | Community Impact Guidelines were inspired by [Mozilla's code of conduct 122 | enforcement ladder](https://github.com/mozilla/diversity). 123 | 124 | [homepage]: https://www.contributor-covenant.org 125 | 126 | For answers to common questions about this code of conduct, see the FAQ at 127 | https://www.contributor-covenant.org/faq. Translations are available at 128 | https://www.contributor-covenant.org/translations. 129 | -------------------------------------------------------------------------------- /livef1/data_processing/data_models.py: -------------------------------------------------------------------------------- 1 | # Standard Library Imports 2 | import json 3 | from datetime import datetime 4 | 5 | # Third-Party Library Imports 6 | import pandas as pd 7 | from ..utils.constants import column_mapping 8 | 9 | 10 | class BasicResult: 11 | """ 12 | Encapsulates a basic result dataset, typically in JSON format. 13 | 14 | Parameters 15 | ---------- 16 | data : :class:`dict` 17 | The JSON-like data to be encapsulated within the result. 18 | 19 | Attributes 20 | ---------- 21 | value : :class:`dict` 22 | The data associated with the result, stored as a dictionary (JSON-like structure). 23 | """ 24 | 25 | def __init__(self, data: dict): 26 | """ 27 | Initializes the BasicResult instance with the provided data. 28 | """ 29 | self.value = data 30 | self.df = pd.DataFrame(data).rename( 31 | columns = column_mapping 32 | ) 33 | 34 | def __get__(self): 35 | """ 36 | Retrieves the stored value. 37 | 38 | Returns 39 | ------- 40 | dict 41 | The JSON-like data encapsulated within the instance. 42 | """ 43 | return self.value 44 | 45 | def __str__(self): 46 | """ 47 | Returns a string representation of the stored data as a DataFrame. 48 | 49 | Returns 50 | ------- 51 | str 52 | A string representation of the data in tabular format (Pandas DataFrame). 53 | """ 54 | return self.df.__str__() 55 | 56 | 57 | # class BronzeResult(BasicResult): 58 | # """ 59 | # Encapsulates bronze level data, typically raw data. 60 | 61 | # Parameters 62 | # ---------- 63 | # data : :class:`dict` 64 | # The raw data to be encapsulated within the result. 65 | # """ 66 | 67 | # def __init__(self, data: dict): 68 | # """ 69 | # Initializes the BronzeResult instance with the provided data. 70 | # """ 71 | # super().__init__(data) 72 | 73 | 74 | # class SilverResult(BasicResult): 75 | # """ 76 | # Encapsulates silver level data, typically cleaned data. 77 | 78 | # Parameters 79 | # ---------- 80 | # data : :class:`dict` 81 | # The cleaned data to be encapsulated within the result. 82 | # """ 83 | 84 | # def __init__(self, data: dict): 85 | # """ 86 | # Initializes the SilverResult instance with the provided data. 87 | # """ 88 | # super().__init__(data) 89 | 90 | 91 | # class GoldResult(BasicResult): 92 | # """ 93 | # Encapsulates gold level data, typically aggregated data. 94 | 95 | # Parameters 96 | # ---------- 97 | # data : :class:`dict` 98 | # The aggregated data to be encapsulated within the result. 99 | # """ 100 | 101 | # def __init__(self, data: dict): 102 | # """ 103 | # Initializes the GoldResult instance with the provided data. 104 | # """ 105 | # super().__init__(data) 106 | 107 | 108 | 109 | 110 | 111 | class Table: 112 | def __init__(self, table_name, data_lake = None): 113 | self.data_lake = data_lake 114 | self.table_name = table_name 115 | self.table = None 116 | self.callback = None 117 | self.df = None 118 | 119 | def generate_table(self): 120 | if self.callback: 121 | self.df = self.callback(self) 122 | self.data_lake.update_metadata( 123 | table_name = self.table_name, 124 | level = None, 125 | created_at = datetime.now(), 126 | generated = True 127 | ) 128 | return self.df 129 | 130 | class BronzeTable(Table): 131 | def __init__(self, table_name, data, parsed_data, data_lake = None): 132 | super().__init__(table_name, data_lake) 133 | self.raw = data 134 | self.df = pd.DataFrame(parsed_data).rename( 135 | columns = column_mapping 136 | ) 137 | if "timestamp" in self.df.columns: 138 | self.df.timestamp = pd.to_timedelta(self.df.timestamp) 139 | 140 | class SilverTable(Table): 141 | def __init__(self, table_name, sources, source_tables = {"bronze": [], "silver": [], "gold": []}, data_lake = None): 142 | super().__init__(table_name, data_lake) 143 | self.sources = sources 144 | self.source_tables = source_tables 145 | self.df = None 146 | self.dependency_tables = [] 147 | 148 | def refine_sources(self): 149 | for source in self.sources: 150 | level = self.data_lake._identify_table_level(source) 151 | if level: 152 | self.source_tables[level].append(source) 153 | if level in ["silver", "gold"]: 154 | dependency_table = self.data_lake.get(level = level, table_name = source) 155 | if dependency_table: 156 | self.dependency_tables.append(dependency_table) 157 | else: 158 | raise ValueError(f"Source table '{source}' not found in data lake.") 159 | else: 160 | raise ValueError(f"Source table '{source}' not found in data lake.") 161 | 162 | class GoldTable(Table): 163 | def __init__(self, table_name, sources, source_tables = {"bronze": [], "silver": [], "gold": []}, data_lake = None): 164 | super().__init__(table_name, data_lake) 165 | self.sources = sources 166 | self.source_tables = source_tables 167 | self.df = None 168 | self.dependency_tables = [] 169 | 170 | 171 | def refine_sources(self): 172 | for source in self.sources: 173 | level = self.data_lake._identify_table_level(source) 174 | if level: 175 | self.source_tables[level].append(source) 176 | if level in ["silver", "gold"]: 177 | dependency_table = self.data_lake.get(level = level, table_name = source) 178 | if dependency_table: 179 | self.dependency_tables.append(dependency_table) 180 | else: 181 | raise ValueError(f"Source table '{source}' not found in data lake.") 182 | else: 183 | raise ValueError(f"Source table '{source}' not found in data lake.") -------------------------------------------------------------------------------- /livef1/adapters/livetimingf1_adapter.py: -------------------------------------------------------------------------------- 1 | # Standard Library Imports 2 | import json 3 | import urllib 4 | 5 | # Third-Party Library Imports 6 | import requests 7 | from typing import List, Dict 8 | 9 | # Internal Project Imports 10 | from ..utils.constants import * 11 | from ..utils.logger import logger 12 | from ..utils.exceptions import ( 13 | AdapterError, 14 | InvalidEndpointError, 15 | ParsingError 16 | ) 17 | 18 | __all__ = [ 19 | "LivetimingF1adapters", 20 | "livetimingF1_request", 21 | "livetimingF1_getdata" 22 | ] 23 | 24 | 25 | class LivetimingF1adapters: 26 | """ 27 | Adapter class for interacting with the F1 Livetiming API. 28 | 29 | This class builds and sends HTTP requests to retrieve data from the static 30 | Livetiming API, using a base URL and various endpoints. 31 | """ 32 | 33 | def __init__(self): 34 | """ 35 | Initializes the LivetimingF1adapters class with the base URL for the Livetiming API. 36 | 37 | The base URL is constructed using the BASE_URL and STATIC_ENDPOINT constants. 38 | """ 39 | self.url = urllib.parse.urljoin(BASE_URL, STATIC_ENDPOINT) # Base URL for F1 Livetiming API 40 | 41 | def get(self, endpoint: str, header: Dict = None): 42 | """ 43 | Sends a GET request to the specified endpoint. 44 | 45 | Parameters 46 | ---------- 47 | endpoint : :class:`str` 48 | The specific API endpoint to append to the base URL. 49 | header : :class:`dict` 50 | HTTP headers to send with the request (default is None). 51 | 52 | Returns 53 | ---------- 54 | - str: The response content decoded as a UTF-8 string. 55 | """ 56 | 57 | try: 58 | req_url = urllib.parse.urljoin(self.url, endpoint) # Build the full request URL 59 | logger.debug(f"Sending GET request to URL: {req_url}") 60 | response = requests.get( 61 | url=req_url, 62 | headers=header, 63 | timeout=300 # Timeout added for robustness 64 | ) 65 | response.raise_for_status() 66 | 67 | # Decode response 68 | try: 69 | res_text = response.content.decode('utf-8-sig') 70 | except UnicodeDecodeError as decode_err: 71 | logger.error(f"Failed to decode response: {decode_err}", exc_info=True) 72 | raise DataDecodingError(f"Failed to decode response: {decode_err}") from decode_err 73 | 74 | return res_text 75 | except requests.exceptions.Timeout as timeout_err: 76 | logger.error(f"Request timed out for URL: {req_url}", exc_info=True) 77 | raise TimeoutError(f"Request timed out: {timeout_err}") from timeout_err 78 | except requests.exceptions.ConnectionError as conn_err: 79 | logger.error(f"Connection failed for URL: {req_url}", exc_info=True) 80 | raise ConnectionError(f"Connection failed: {conn_err}") from conn_err 81 | except requests.exceptions.HTTPError as http_err: 82 | if response.status_code in [403,404]: 83 | logger.error(f"Endpoint not found: {req_url}", exc_info=True) 84 | raise InvalidEndpointError(f"Endpoint not found: {endpoint}") from http_err 85 | else: 86 | logger.error(f"HTTP error occurred for URL {req_url}: {http_err}", exc_info=True) 87 | raise AdapterError(f"HTTP error occurred: {http_err}") from http_err 88 | except Exception as e: 89 | logger.critical(f"Unexpected error for URL {req_url}: {e}", exc_info=True) 90 | raise AdapterError(f"An unexpected error occurred: {e}") from e 91 | 92 | def livetimingF1_request(url): 93 | """ 94 | Wrapper function to perform a GET request to the Livetiming F1 API. 95 | 96 | Parameters 97 | ---------- 98 | url : :class:`str` 99 | The full URL to request. 100 | 101 | Returns 102 | ---------- 103 | dict 104 | Parsed JSON response from the API. 105 | """ 106 | adapters = LivetimingF1adapters() # Initialize the adapter class 107 | response = adapters.get(url) # Perform the GET request 108 | try: 109 | data = json.loads(response) # Parse the JSON response 110 | except: 111 | logger.error("Error parsing request .", exc_info=True) 112 | raise ParsingError(f"Error parsing request: {parse_err}") from parse_err 113 | return data 114 | 115 | def livetimingF1_getdata(url, stream): 116 | """ 117 | Retrieves data from the Livetiming F1 API, either as a stream of records or a static response. 118 | 119 | Parameters 120 | ---------- 121 | url : :class:`str` 122 | The full URL to request. 123 | stream : :class:`bool` 124 | If True, treats the response as a stream of newline-separated records. 125 | If False, treats it as a static JSON response. 126 | 127 | Returns 128 | ---------- 129 | dict 130 | A dictionary containing parsed data. If streaming, each line is parsed and split. 131 | """ 132 | adapters = LivetimingF1adapters() # Initialize the adapter class 133 | res_text = adapters.get(endpoint=url) # Perform the GET request 134 | 135 | if stream: 136 | try: 137 | # Streamed data is split by newline and each record is processed 138 | records = res_text.split('\r\n')[:-1] # Remove the last empty line 139 | tl = 12 # Record key length (first 12 characters are the key) 140 | # Return a dictionary of keys and their parsed JSON values 141 | parsed_data = list((r[:tl], json.loads(r[tl:])) for r in records) 142 | logger.debug("Successfully parsed streamed data.") 143 | return parsed_data 144 | except (json.JSONDecodeError, IndexError) as parse_err: 145 | logger.error("Error parsing streamed data.", exc_info=True) 146 | raise ParsingError(f"Error parsing streamed data: {parse_err}") from parse_err 147 | else: 148 | try: 149 | # If not streaming, parse the entire response as JSON 150 | records = json.loads(res_text) 151 | logger.debug("Successfully parsed static JSON data.") 152 | return records 153 | except json.JSONDecodeError as parse_err: 154 | logger.error("Error parsing static JSON data.", exc_info=True) 155 | raise ParsingError(f"Error parsing static JSON data: {parse_err}") from parse_err 156 | -------------------------------------------------------------------------------- /docs/source/user_guide/realtime_data.rst: -------------------------------------------------------------------------------- 1 | .. _realtime_data: 2 | 3 | Real-Time Data 4 | ============= 5 | 6 | LiveF1 provides real-time data access through the `RealF1Client` class, allowing you to stream live Formula 1 data during race weekends. 7 | 8 | Creating a Real-Time Client 9 | -------------------------- 10 | 11 | The RealF1Client automatically connects to the currently active F1 session and streams its data in real-time, with no need to specify session details: 12 | 13 | .. code-block:: python 14 | 15 | from livef1.adapters.realtime_client import RealF1Client 16 | 17 | client = RealF1Client( 18 | topics=["CarData.z", "SessionInfo"], 19 | log_file_name="./output.json" # Optional: log incoming data 20 | ) 21 | 22 | .. seealso:: 23 | For a complete list of available topics, see :ref:`data_topics` 24 | 25 | Handling Real-Time Data 26 | ---------------------- 27 | 28 | 1. Basic Handler 29 | ^^^^^^^^^^^^^^^ 30 | 31 | Create a basic handler for incoming data: 32 | 33 | .. code-block:: python 34 | 35 | @client.callback("basic_handler") 36 | async def handle_data(records): 37 | print(records) # Process incoming records 38 | 39 | 2. Logging Handler 40 | ^^^^^^^^^^^^^^^ 41 | 42 | Log data with timestamps: 43 | 44 | .. code-block:: python 45 | 46 | import datetime 47 | 48 | @client.callback("log_handler") 49 | async def log_with_timestamp(records): 50 | with open("data_with_timestamp.log", "a") as f: 51 | for record in records: 52 | timestamp = datetime.datetime.now().isoformat() 53 | f.write(f"{timestamp} - {record}\n") 54 | 55 | Running the Client 56 | ---------------- 57 | 58 | Start receiving real-time data: 59 | 60 | .. code-block:: python 61 | 62 | client.run() # Starts the client and begins receiving data 63 | 64 | 65 | How Callbacks Work 66 | ---------------- 67 | 68 | 1. **Registration**: The callback decorator registers your handler with the client: 69 | 70 | .. code-block:: python 71 | 72 | @client.callback("handler_name") 73 | 74 | 75 | 2. **Activation**: When you call `client.run()`, the client: 76 | 77 | - Establishes a connection to the F1 live timing service 78 | 79 | - Subscribes to your specified topics 80 | 81 | - Activates all registered callbacks 82 | 83 | 3. **Data Flow**: 84 | 85 | - New data arrives for subscribed topics 86 | 87 | - Each callback receives the data as `records` 88 | 89 | - Callbacks process the data asynchronously 90 | 91 | - Multiple callbacks can handle the same data 92 | 93 | Best Practices 94 | ------------- 95 | 96 | 1. **Connection Management** 97 | 98 | - Handle connection interruptions gracefully 99 | 100 | - Implement reconnection logic 101 | 102 | - Monitor connection health 103 | 104 | 2. **Data Processing** 105 | 106 | - Process data asynchronously to avoid blocking 107 | 108 | - Implement proper error handling 109 | 110 | - Consider data buffering for high-frequency updates 111 | 112 | 3. **Resource Management** 113 | 114 | - Close connections properly when done 115 | 116 | - Monitor memory usage 117 | 118 | - Implement proper logging 119 | 120 | Example: Complete Real-Time Client 121 | -------------------------------- 122 | 123 | Here's a complete example combining multiple features: 124 | 125 | .. code-block:: python 126 | 127 | from livef1.adapters.realtime_client import RealF1Client 128 | import datetime 129 | import json 130 | 131 | # Initialize client 132 | client = RealF1Client( 133 | topics=["CarData.z", "SessionInfo", "TrackStatus"], 134 | log_file_name="session_data.json" 135 | ) 136 | 137 | # Define multiple handlers 138 | @client.callback("process_telemetry") 139 | async def handle_telemetry(records): 140 | # Process car telemetry data 141 | telemetry_data = records.get("CarData.z") 142 | if telemetry_data: 143 | for record in telemetry_data: 144 | process_telemetry_data(record) # this is a placeholder for your code 145 | 146 | @client.callback("track_status") 147 | async def handle_track_status(records): 148 | # Monitor track conditions 149 | track_data = records.get("TrackStatus") 150 | if track_data: 151 | for record in track_data: 152 | update_track_status(record) # this is a placeholder for your code 153 | 154 | # Start the client 155 | client.run() 156 | 157 | .. seealso:: 158 | For information about logging configuration, see :doc:`logging_config` 159 | 160 | Example:Database Handler 161 | -------------------------------- 162 | Store incoming data in a database. 163 | 164 | .. code-block:: python 165 | 166 | import sqlite3 167 | from datetime import datetime 168 | import json 169 | 170 | class F1Database: 171 | def __init__(self, db_name="f1_data.db"): 172 | self.conn = sqlite3.connect(db_name) 173 | self.create_tables() 174 | 175 | def create_tables(self): 176 | cursor = self.conn.cursor() 177 | cursor.execute(''' 178 | CREATE TABLE IF NOT EXISTS telemetry_data ( 179 | id INTEGER PRIMARY KEY AUTOINCREMENT, 180 | timestamp TEXT, 181 | topic TEXT, 182 | driver_number INTEGER, 183 | data JSON 184 | ) 185 | ''') 186 | self.conn.commit() 187 | 188 | def insert_data(self, timestamp, topic, driver_number, data): 189 | cursor = self.conn.cursor() 190 | cursor.execute(''' 191 | INSERT INTO telemetry_data (timestamp, topic, driver_number, data) 192 | VALUES (?, ?, ?, ?) 193 | ''', (timestamp, topic, driver_number, json.dumps(data))) 194 | self.conn.commit() 195 | 196 | # Initialize database 197 | db = F1Database() 198 | 199 | @client.callback("database_handler") 200 | async def handle_database_storage(records): 201 | for record in records: 202 | timestamp = datetime.now().isoformat() 203 | 204 | # Extract topic and driver number if available 205 | topic = next(iter(record.keys())) # Get the first key as topic 206 | driver_number = record.get(topic, {}).get('DriverNumber', 0) 207 | 208 | # Store in database 209 | db.insert_data( 210 | timestamp=timestamp, 211 | topic=topic, 212 | driver_number=driver_number, 213 | data=record 214 | ) 215 | -------------------------------------------------------------------------------- /docs/source/livetimingf1/f1_data.rst: -------------------------------------------------------------------------------- 1 | .. _f1-livetiming-data: 2 | **************************** 3 | Formula 1 LiveTiming Data 4 | **************************** 5 | 6 | Formula 1 live timing data provides comprehensive insights into the performance of drivers, teams, and cars during races. This data is critical for real-time analytics, historical analysis, and developing strategies for races. 7 | 8 | Source of Data 9 | ====================================== 10 | Formula 1 live timing data delivers real-time and historical telemetry, timing, and contextual data. This data is critical for real-time analytics, historical analysis, and developing strategies for races. 11 | 12 | API Structure 13 | -------------------------------------------- 14 | 15 | .. Document Subsubsection 16 | .. ^^^^^^^^^^^^^^^^^^^^^^ 17 | 18 | .. Document Paragraph 19 | .. """""""""""""""""" 20 | 21 | The live timing API follows a structured format to ensure efficient data access. Each API address corresponds to a specific endpoint, which streams or delivers data for various aspects of Formula 1 sessions. These endpoints can be accessed via HTTP requests. 22 | 23 | **Key Points:** 24 | 25 | - Base URL: :code:`https://livetiming.formula1.com/` 26 | - Endpoints: Each topic is mapped to an endpoint path. 27 | - Response Format: Data is returned in JSON format. 28 | 29 | **Accessing the Data:** 30 | 31 | 1. Use the base URL to connect to the API. 32 | 2. Append specific paths for desired data. 33 | 3. Process the JSON responses for your use case. 34 | 35 | API Addresses and Endpoints 36 | -------------------------------------------- 37 | 38 | Here is a list of key API addresses and their respective endpoints: 39 | 40 | 1. Base Information 41 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 42 | 43 | **Endpoint:** :code:`https://livetiming.formula1.com/static/Index.json` 44 | 45 | **Description:** Provides a list of available years and their corresponding paths. 46 | 47 | **Response Example:** 48 | 49 | .. code-block:: json 50 | 51 | { 52 | "Years": [ 53 | { 54 | "Year": 2024, 55 | "Path": "2024/" 56 | } 57 | ] 58 | } 59 | 60 | .. list-table:: 61 | :header-rows: 1 62 | 63 | * - Key 64 | - Type 65 | - Description 66 | * - Years 67 | - Array 68 | - List of available years and paths 69 | * - Year 70 | - Integer 71 | - The year of the data 72 | * - Path 73 | - String 74 | - Path to access the year-specific data 75 | 76 | 2. Yearly Sessions 77 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 78 | 79 | **Endpoint:** :code:`https://livetiming.formula1.com/static/2024/Index.json` 80 | 81 | **Description:** Lists races and sessions for the year 2024. 82 | 83 | **Response Example:** 84 | 85 | .. code-block:: json 86 | 87 | { 88 | "Year": 2024, 89 | "Meetings": [ 90 | { 91 | "Key": 1238, 92 | "Name": "Spanish Grand Prix", 93 | "Sessions": [ 94 | { 95 | "Key": 9539, 96 | "Type": "Race", 97 | "Path": "2024/2024-06-23_Spanish_Grand_Prix/2024-06-23_Race/" 98 | }, 99 | // ...other sessions 100 | ] 101 | }, 102 | // ...other meetings 103 | ] 104 | } 105 | 106 | .. list-table:: 107 | :header-rows: 1 108 | 109 | * - Key 110 | - Type 111 | - Description 112 | * - Year 113 | - Integer 114 | - The year of the data 115 | * - Meetings 116 | - Array 117 | - List of races and associated sessions 118 | * - Key 119 | - Integer 120 | - Unique identifier for the meeting 121 | * - Name 122 | - String 123 | - Name of the meeting 124 | * - Sessions 125 | - Array 126 | - List of sessions for the meeting 127 | 128 | .. seealso:: 129 | You can download season data by using LiveF1 ``get_season`` function. 130 | 131 | 3. Session Topics 132 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 133 | 134 | **Endpoint:** :code:`https://livetiming.formula1.com/static/2024/2024-06-23_Spanish_Grand_Prix/2024-06-23_Race/Index.json` 135 | 136 | **Description:** Provides available data topics for a specific session. 137 | 138 | .. note:: 139 | The data feeds is further explained in :ref:`data_topics` section. 140 | 141 | **Response Example:** 142 | 143 | .. code-block:: json 144 | 145 | { 146 | "Feeds": { 147 | "SessionInfo": { 148 | "KeyFramePath": "SessionInfo.json", 149 | "StreamPath": "SessionInfo.jsonStream" 150 | }, 151 | "TrackStatus": { 152 | "KeyFramePath": "TrackStatus.json", 153 | "StreamPath": "TrackStatus.jsonStream" 154 | }, 155 | // ...other data topics 156 | } 157 | } 158 | 159 | .. list-table:: 160 | :header-rows: 1 161 | 162 | * - Key 163 | - Type 164 | - Description 165 | * - Feeds 166 | - Object 167 | - Contains different data feed categories 168 | * - SessionInfo 169 | - Object 170 | - Details about the session 171 | * - KeyFramePath 172 | - String 173 | - Path to the keyframe data 174 | * - StreamPath 175 | - String 176 | - Path to the data stream 177 | * - TrackStatus 178 | - Object 179 | - Details about the track status 180 | 181 | .. seealso:: 182 | You can download session's data by using LiveF1 ``get_season`` function. 183 | 184 | 4. Example: Session Details 185 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 186 | 187 | **Endpoint:** :code:`https://livetiming.formula1.com/static/2024/2024-06-23_Spanish_Grand_Prix/2024-06-23_Race/SessionInfo.json` 188 | 189 | **Description:** Contains detailed information about the session. 190 | 191 | **Response Example:** 192 | 193 | .. code-block:: json 194 | 195 | { 196 | "Meeting": { 197 | "Name": "Spanish Grand Prix", 198 | "Location": "Barcelona", 199 | "Country": { 200 | "Code": "ESP", 201 | "Name": "Spain" 202 | } 203 | }, 204 | "Key": 9539, 205 | "Type": "Race", 206 | "Name": "Race", 207 | "StartDate": "2024-06-23T15:00:00", 208 | "EndDate": "2024-06-23T17:00:00" 209 | } 210 | 211 | .. seealso:: 212 | You can download session's data by using LiveF1 ``get_data`` function. 213 | 214 | 215 | .. Downloading Data 216 | .. -------------------------------------------- 217 | .. To access the live timing data: 218 | 219 | .. - Use an appropriate API client or toolkit such as `LiveF1`. 220 | .. - Subscribe to the desired topics based on your analysis requirements. 221 | .. - Ensure robust handling for real-time streaming or archival for offline analysis. -------------------------------------------------------------------------------- /livef1/data_processing/etl.py: -------------------------------------------------------------------------------- 1 | # Standard Library Imports 2 | import json 3 | 4 | # Third-Party Library Imports 5 | import pandas as pd 6 | from typing import Optional, Union 7 | 8 | # Internal Project Imports 9 | from ..utils.helper import * 10 | from ..utils.exceptions import ( 11 | MissingFunctionError, 12 | ETLError 13 | ) 14 | from .parse_functions import * 15 | 16 | # function_map = 17 | 18 | class livef1SessionETL: 19 | """ 20 | A class that handles Extract-Transform-Load (ETL) operations for F1 session data. 21 | 22 | Parameters 23 | ---------- 24 | session : :class:`~Session` 25 | The session object. 26 | 27 | Attributes 28 | ---------- 29 | session : :class:`~Session` 30 | The session object containing session-related information. 31 | function_map :class:`dict` 32 | A dictionary mapping various session data titles to their corresponding parsing functions. 33 | """ 34 | 35 | def __init__(self, session): 36 | """ 37 | Initializes the livef1SessionETL class with a session object and function map. 38 | """ 39 | self.session = session 40 | self.function_map = function_map 41 | 42 | def unified_parse(self, title, data): 43 | """ 44 | Unified parsing function that selects the appropriate parser function based on the title. 45 | 46 | Parameters 47 | ---------- 48 | title : :class:`str` 49 | The title of the data to be parsed. 50 | data : :class:`dict` 51 | The session data to be parsed. 52 | 53 | Returns 54 | ---------- 55 | Parsed data from the respective function in the function map. 56 | """ 57 | if title not in self.function_map: 58 | logger.error(f"No parser function found for title: {title}") 59 | raise MissingFunctionError(f"No parser function available for title: {title}") 60 | 61 | try: 62 | # Perform the parsing 63 | logger.debug(f"Using parser function for title: {title}") 64 | parsed_data = self.function_map[title](data, self.session.key, session_path = self.session.path) 65 | logger.debug(f"Parsing successful for title: {title}") 66 | return parsed_data 67 | except Exception as e: 68 | logger.error("Parsing failed.") 69 | raise ETLError(f"Parser of {title} failed.") 70 | 71 | def process_bronze_level(self): 72 | """ 73 | Process raw data to produce bronze level data. 74 | 75 | Returns 76 | ------- 77 | :class:`~BronzeResult` 78 | An object containing the bronze level data. 79 | """ 80 | raw_data = self.session.get_data(dataNames="Raw_Data") 81 | # Perform any additional processing if needed 82 | return BronzeResult(data=raw_data.value) 83 | 84 | def process_silver_level(self): 85 | """ 86 | Process bronze level data to produce silver level data. 87 | 88 | Returns 89 | ------- 90 | :class:`~SilverResult` 91 | An object containing the silver level data. 92 | """ 93 | bronze_data = self.process_bronze_level() 94 | silver_data = self.clean_data(bronze_data.value) 95 | return SilverResult(data=silver_data) 96 | 97 | def process_gold_level(self): 98 | """ 99 | Process silver level data to produce gold level data. 100 | 101 | Returns 102 | ------- 103 | :class:`~GoldResult` 104 | An object containing the gold level data. 105 | """ 106 | silver_data = self.process_silver_level() 107 | gold_data = self.aggregate_data(silver_data.value) 108 | return GoldResult(data=gold_data) 109 | 110 | def clean_data(self, data): 111 | """ 112 | Clean the raw data to produce silver level data. 113 | 114 | Parameters 115 | ---------- 116 | data : :class:`list` 117 | The raw data to be cleaned. 118 | 119 | Returns 120 | ------- 121 | :class:`list` 122 | The cleaned data. 123 | """ 124 | # Implement the cleaning logic here 125 | cleaned_data = [] 126 | for record in data: 127 | # Perform cleaning operations 128 | cleaned_record = record # Placeholder for actual cleaning logic 129 | cleaned_data.append(cleaned_record) 130 | return cleaned_data 131 | 132 | def aggregate_data(self, data): 133 | """ 134 | Aggregate the cleaned data to produce gold level data. 135 | 136 | Parameters 137 | ---------- 138 | data : :class:`list` 139 | The cleaned data to be aggregated. 140 | 141 | Returns 142 | ------- 143 | :class:`list` 144 | The aggregated data. 145 | """ 146 | # Implement the aggregation logic here 147 | aggregated_data = [] 148 | for record in data: 149 | # Perform aggregation operations 150 | aggregated_record = record # Placeholder for actual aggregation logic 151 | aggregated_data.append(aggregated_record) 152 | return aggregated_data 153 | 154 | # Parsing functions 155 | function_map = { 156 | 'SessionInfo': parse_session_info, 157 | 'ArchiveStatus': None, 158 | 'TrackStatus': parse_session_info, 159 | 'SessionData': parse_session_data, 160 | 'ContentStreams': None, 161 | 'AudioStreams': None, 162 | 'ExtrapolatedClock': parse_extrapolated_clock, 163 | 'DriverList': parse_driver_list, 164 | 'TimingDataF1': parse_timing_data, 165 | 'TimingData': parse_timing_data, # Potential duplicate with TimingDataF1 166 | 'LapSeries': parse_lap_series, 167 | 'TopThree': parse_top_three, 168 | 'TimingAppData': None, 169 | 'TimingStats': parse_timing_data, # Same function used as TimingData 170 | 'SessionStatus': parse_session_status, 171 | 'TyreStintSeries': parse_tyre_stint_series, 172 | 'Heartbeat': parse_hearthbeat, 173 | 'Position.z': parse_position_z, 174 | 'WeatherData': parse_weather_data, 175 | 'WeatherDataSeries': None, 176 | 'CarData.z': parse_car_data_z, 177 | 'TeamRadio': parse_team_radio, 178 | 'TlaRcm': parse_tlarcm, 179 | 'RaceControlMessages': parse_race_control_messages, 180 | 'PitLaneTimeCollection': parse_pit_lane_time, 181 | 'CurrentTyres': parse_current_tyres, 182 | 'DriverRaceInfo': parse_driver_race_info, 183 | 'ChampionshipPrediction': parse_championship_prediction, 184 | # 'ChampionshipPredictionDrivers': parse_session_info, 185 | 'OvertakeSeries': None, 186 | 'DriverScore': None, 187 | 'SPFeed': None, 188 | 'PitStopSeries': parse_pit_stop_series, 189 | 'PitStop': parse_basic, 190 | 'LapCount': parse_basic 191 | } 192 | 193 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # LiveF1 - An Open-Source Formula 1 Data Processing Toolkit 2 | 3 | ![Written in Python](https://img.shields.io/static/v1?label=&message=Python&color=3C78A9&logo=python&logoColor=FFFFFF) 4 | [![PyPI version](https://badge.fury.io/py/livef1.svg)](https://badge.fury.io/py/livef1) 5 | [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT) 6 | [![View livef1 on GitHub](https://img.shields.io/github/stars/goktugocal/livef1?color=232323&label=livef1&logo=github&labelColor=232323)](https://github.com/goktugocal/livef1) 7 | [![Author goktugocal](https://img.shields.io/badge/goktugocal-b820f9?labelColor=b820f9&logo=githubsponsors&logoColor=fff)](https://github.com/goktugocal) 8 | 9 | 10 | LiveF1 is a powerful Python toolkit for accessing and analyzing Formula 1 data in real-time or from historical archives. It's designed for developers, analysts, and F1 fans building applications around Formula 1 insights. 11 | 12 |

13 | LiveF1 Data Flow 14 |

15 | 16 | ### Features 17 | - **Real-Time Race Data**: Provides live telemetry, timing, and position updates, ideal for powering dashboards and live analytics. 18 | - **Historical Data Access**: Includes comprehensive race data from past seasons, perfect for performance analysis and comparisons. 19 | - **Data Processing Modules**: Built-in ETL tools make raw data immediately usable, supporting analysis and seamless data storage. 20 | - **Easy Integration**: Simple API for both real-time and historical data 21 | 22 | In a nutshell: 23 | 24 | **Using LiveF1, you can access real-time and historical racing data, making it easy to feed analytics and visualizations.** 25 | 26 | ## Installation 27 | 28 | Install using pip: 29 | 30 | ```bash 31 | pip install livef1 32 | ``` 33 | 34 | ## Quick Start 35 | 36 | ### Historical Data 37 | 38 | Access data from past races: 39 | 40 | ```python 41 | import livef1 42 | 43 | # Get a specific race session 44 | session = livef1.get_session( 45 | season=2024, 46 | meeting_identifier="Spa", 47 | session_identifier="Race" 48 | ) 49 | 50 | # Load position data 51 | position_data = session.get_data( 52 | dataNames="Position.z" 53 | ) 54 | 55 | print(position_data.head()) 56 | ``` 57 | 58 | ```text 59 | | | SessionKey | timestamp | Utc | DriverNo | Status | X | Y | Z | 60 | |---:|-------------:|:-------------|:-----------------------------|-----------:|:---------|----:|----:|----:| 61 | | 0 | 9574 | 00:01:45.570 | 2024-07-28T12:10:22.7877313Z | 1 | OnTrack | 0 | 0 | 0 | 62 | | 1 | 9574 | 00:01:45.570 | 2024-07-28T12:10:22.7877313Z | 2 | OnTrack | 0 | 0 | 0 | 63 | | 2 | 9574 | 00:01:45.570 | 2024-07-28T12:10:22.7877313Z | 3 | OnTrack | 0 | 0 | 0 | 64 | | 3 | 9574 | 00:01:45.570 | 2024-07-28T12:10:22.7877313Z | 4 | OnTrack | 0 | 0 | 0 | 65 | | 4 | 9574 | 00:01:45.570 | 2024-07-28T12:10:22.7877313Z | 10 | OnTrack | 0 | 0 | 0 | 66 | ``` 67 | 68 | #### Data Processing 69 | 70 | LiveF1 uses a medallion architecture to process F1 data into analysis-ready formats: 71 | 72 | ```python 73 | # Generate processed data tables 74 | session.generate(silver=True) 75 | 76 | # Access refined data 77 | laps_data = session.get_laps() 78 | telemetry_data = session.get_car_telemetry() 79 | 80 | print(laps_data.head()) 81 | ``` 82 | 83 | ```text 84 | | | lap_number | lap_time | in_pit | pit_out | sector1_time | sector2_time | sector3_time | None | speed_I1 | speed_I2 | speed_FL | speed_ST | no_pits | lap_start_time | DriverNo | lap_start_date | 85 | |---:|-------------:|:-----------------------|:-----------------------|:----------|:-----------------------|:-----------------------|:-----------------------|:-------|-----------:|-----------:|-----------:|-----------:|----------:|:-----------------------|-----------:|:---------------------------| 86 | | 0 | 1 | NaT | 0 days 00:17:07.661000 | NaT | NaT | 0 days 00:00:48.663000 | 0 days 00:00:29.571000 | | 314 | 204 | | 303 | 0 | NaT | 16 | 2024-07-28 13:03:52.742000 | 87 | | 1 | 2 | 0 days 00:01:50.240000 | NaT | NaT | 0 days 00:00:31.831000 | 0 days 00:00:48.675000 | 0 days 00:00:29.734000 | | 303 | 203 | 219 | | 0 | 0 days 00:57:07.067000 | 16 | 2024-07-28 13:05:45.045000 | 88 | | 2 | 3 | 0 days 00:01:50.519000 | NaT | NaT | 0 days 00:00:31.833000 | 0 days 00:00:49.132000 | 0 days 00:00:29.554000 | | 311 | 202 | 215 | 304 | 0 | 0 days 00:58:57.307000 | 16 | 2024-07-28 13:07:35.285000 | 89 | | 3 | 4 | 0 days 00:01:49.796000 | NaT | NaT | 0 days 00:00:31.592000 | 0 days 00:00:48.778000 | 0 days 00:00:29.426000 | | 312 | 201 | 217 | 309 | 0 | 0 days 01:00:47.870000 | 16 | 2024-07-28 13:09:25.848000 | 90 | | 4 | 5 | 0 days 00:01:49.494000 | NaT | NaT | 0 days 00:00:31.394000 | 0 days 00:00:48.729000 | 0 days 00:00:29.371000 | | 313 | 197 | 217 | 311 | 0 | 0 days 01:02:37.721000 | 16 | 2024-07-28 13:11:15.699000 | 91 | ``` 92 | 93 | ### Real-Time Data 94 | 95 | Stream live race data: 96 | 97 | ```python 98 | from livef1.adapters import RealF1Client 99 | 100 | # Initialize client with topics to subscribe 101 | client = RealF1Client( 102 | topics=["CarData.z", "Position.z"], 103 | log_file_name="race_data.json" # Optional: log data to file 104 | ) 105 | 106 | # Define callback for incoming data 107 | @client.callback("telemetry_handler") 108 | async def handle_data(records): 109 | for record in records: 110 | print(record) # Process incoming data 111 | 112 | # Start receiving data 113 | client.run() 114 | ``` 115 | 116 | ## Documentation 117 | 118 | For detailed documentation, examples, and API reference, visit our [documentation page](https://livef1.readthedocs.io/). 119 | 120 | ## Contributing 121 | 122 | Contributions are welcome! Please feel free to submit a Pull Request. For major changes, please open an issue first to discuss what you would like to change. 123 | 124 | ## License 125 | 126 | This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details. 127 | 128 | ## Support 129 | 130 | - Report bugs and request features in [GitHub Issues](https://github.com/GoktugOcal/LiveF1/issues) 131 | - For questions and discussions, use [GitHub Discussions](https://github.com/GoktugOcal/LiveF1/discussions) 132 | 133 | ## FEEL FREE TO [CONTACT ME](https://www.goktugocal.com/contact.html) 134 | -------------------------------------------------------------------------------- /livef1/models/season.py: -------------------------------------------------------------------------------- 1 | # Standard Library Imports 2 | import urllib 3 | import json 4 | import dateutil 5 | import sys 6 | 7 | # Third-Party Library Imports 8 | import pandas as pd 9 | 10 | # Internal Project Imports 11 | from ..adapters import download_data 12 | from ..adapters.other import parse_schedule_from_f1com 13 | 14 | from ..models.meeting import Meeting 15 | from ..utils.helper import json_parser_for_objects, build_session_endpoint 16 | from ..utils.constants import SESSIONS_COLUMN_MAP 17 | 18 | 19 | class Season: 20 | """ 21 | Represents a Formula 1 season, containing methods to load and manage the season's meetings and sessions. 22 | 23 | Attributes 24 | ---------- 25 | year : :class:`int` 26 | The year of the season. 27 | meetings : list of :class:`~Meeting` 28 | A list of :class:`Meeting` objects for the season. 29 | """ 30 | 31 | def __init__(self, year, meetings): 32 | """ 33 | Initializes the Season object with the given year and meetings. 34 | 35 | Parameters 36 | ---------- 37 | year : :class:`int` 38 | The year of the season. 39 | meetings : list 40 | Raw meetings data to initialize the season. 41 | """ 42 | 43 | self.year = year 44 | self.load() # Load the data for the season upon initialization. 45 | 46 | def load(self): 47 | """ 48 | Loads the season data from the API and populates the `meetings` attribute. 49 | """ 50 | self.json_data = download_data(self.year) # Download data for the specified year. 51 | 52 | # Set attributes from the downloaded data using a helper function. 53 | for key, value in json_parser_for_objects(self.json_data).items(): 54 | setattr(self, key.lower(), value) 55 | 56 | self.meetings_json = self.meetings # Store raw meeting data. 57 | self.meetings = [] # Initialize meetings list. 58 | 59 | self.parse_sessions() # Parse sessions from the meetings. 60 | self.set_meetings() # Create Meeting objects for each meeting. 61 | 62 | def set_meetings(self): 63 | """ 64 | Creates :class:`~Meeting` objects for each meeting in the `meetings_json` attribute 65 | and adds them to the `meetings` list. 66 | """ 67 | self.meetings = [] # Reset meetings list. 68 | 69 | # Iterate through each meeting in the raw meeting data. 70 | for meeting in self.meetings_json: 71 | self.meetings.append( 72 | Meeting( 73 | season=self, 74 | loaded=True, 75 | **json_parser_for_objects(meeting) # Unpack the meeting data into the Meeting object. 76 | ) 77 | ) 78 | 79 | def parse_sessions(self): 80 | """ 81 | Parses session data from the meetings and organizes it into a DataFrame. 82 | 83 | The resulting DataFrame is stored in the `meetings_table` attribute, indexed by 84 | `season_year`, `meeting_location`, and `session_type`. 85 | """ 86 | session_all_data = [] # List to hold all session data. 87 | 88 | # Iterate through each meeting in the meetings_json attribute. 89 | for meeting in self.meetings_json: 90 | for session in meeting["Sessions"]: # Iterate through each session in the meeting. 91 | session_data = { 92 | "season_year": dateutil.parser.parse(session["StartDate"]).year, 93 | "meeting_code": meeting["Code"], 94 | "meeting_key": meeting["Key"], 95 | "meeting_number": meeting["Number"], 96 | "meeting_location": meeting["Location"], 97 | "meeting_offname": meeting["OfficialName"], 98 | "meeting_name": meeting["Name"], 99 | "meeting_country_key": meeting["Country"]["Key"], 100 | "meeting_country_code": meeting["Country"]["Code"], 101 | "meeting_country_name": meeting["Country"]["Name"], 102 | "meeting_circuit_key": meeting["Circuit"]["Key"], 103 | "meeting_circuit_shortname": meeting["Circuit"]["ShortName"], 104 | "session_key": session.get("Key", None), 105 | "session_type": session["Type"] + " " + str(session["Number"]) if "Number" in session else session["Type"], 106 | "session_name": session.get("Name", None), 107 | "session_startDate": session.get("StartDate", None), 108 | "session_endDate": session.get("EndDate", None), 109 | "gmtoffset": session.get("GmtOffset", None), 110 | "path": session.get("Path", None), 111 | } 112 | session_all_data.append(session_data) # Add the session data to the list. 113 | 114 | # Create a DataFrame to organize the sessions data. 115 | self.season_table = pd.DataFrame(session_all_data).set_index(["meeting_key"]) 116 | self.season_table["session_startDate"] = pd.to_datetime(self.season_table["session_startDate"]) 117 | self.season_table["session_endDate"] = pd.to_datetime(self.season_table["session_endDate"]) 118 | 119 | self.meetings_table = self.season_table \ 120 | .groupby("meeting_key") \ 121 | .agg( 122 | { 123 | "meeting_code": "first", 124 | "meeting_name": "first", 125 | "meeting_offname": "first", 126 | "meeting_circuit_shortname": "first", 127 | "session_name": "count" 128 | } 129 | ) \ 130 | .join( 131 | self.season_table[self.season_table["session_type"] == "Race"]["session_startDate"] 132 | ) \ 133 | .sort_values("session_startDate") \ 134 | .reset_index() \ 135 | .rename( 136 | columns = { 137 | **SESSIONS_COLUMN_MAP, 138 | **{ 139 | "session_startDate" : "Race Startdate", 140 | "session_name" : "No. Sessions" 141 | } 142 | } 143 | ) 144 | 145 | self.season_table = self.season_table.rename(columns = SESSIONS_COLUMN_MAP) 146 | 147 | def get_schedule(self): 148 | self.schedule = parse_schedule_from_f1com(self.year) # Parse the schedule from F1.com for the season. 149 | return self.schedule 150 | 151 | def __repr__(self): 152 | """ 153 | Returns a string representation of the `meetings_table` for display. 154 | """ 155 | if "IPython" not in sys.modules: 156 | # definitely not in IPython 157 | return self.__str__() # Print the meetings table. 158 | else: 159 | display(self.season_table) # Display the meetings table. 160 | # return self.__str__() 161 | return "" 162 | 163 | def __str__(self): 164 | """ 165 | Returns a string representation of the `meetings_table` for easy reading. 166 | """ 167 | return self.season_table.__str__() # Return the string representation of the meetings table. 168 | -------------------------------------------------------------------------------- /livef1/api.py: -------------------------------------------------------------------------------- 1 | from .adapters import LivetimingF1adapters, livetimingF1_request 2 | from .models import ( 3 | Session, 4 | Season, 5 | Meeting, 6 | Circuit 7 | ) 8 | from .adapters import download_data 9 | from .utils.helper import json_parser_for_objects, find_most_similar_vectorized, print_found_model 10 | from .utils.logger import logger 11 | from .utils.exceptions import * 12 | from .utils.constants import SESSIONS_COLUMN_MAP 13 | 14 | from datetime import datetime 15 | 16 | def get_season(season: int) -> Season: 17 | """ 18 | Retrieve data for a specified Formula 1 season. 19 | 20 | Parameters 21 | ---------- 22 | season : :class:`int` 23 | The year of the season to retrieve. 24 | 25 | Returns 26 | ------- 27 | Season 28 | A `Season` object containing all meetings and sessions for the specified year. 29 | 30 | Raises 31 | ------ 32 | livef1Exception 33 | If no data is available for the specified season. 34 | """ 35 | logger.debug(f"Getting season {season}.") 36 | season_data = download_data(season_identifier=season) 37 | season = Season(**json_parser_for_objects(season_data)) 38 | logger.debug("Got the season.") 39 | return season 40 | 41 | def get_meeting( 42 | season: int, 43 | meeting_identifier: str = None, 44 | meeting_key: int = None, 45 | meeting_offname: str = None, 46 | ) -> Meeting: 47 | """ 48 | Retrieve data for a specific meeting in a given season. 49 | 50 | Parameters 51 | ---------- 52 | season : :class:`int` 53 | The year of the season to retrieve the meeting from. 54 | meeting_identifier : :class:`str` 55 | The identifier (e.g., circuit name, grand prix name) of the meeting. 56 | The identifier is going to be searched in the season's meeting table columns: 57 | - "Meeting Official Name" 58 | - "Meeting Name" 59 | - "Circuit Short Name" 60 | Therefore, it is suggested to use keywords that is distinguishable among meetings. 61 | Another suggestion is using circuit names for querying. 62 | meeting_key : :class:`int` 63 | The key of the meeting to get the desired meeting whose key is matching. 64 | 65 | Returns 66 | ------- 67 | Meeting 68 | A `Meeting` object containing sessions and metadata for the specified meeting. 69 | 70 | Raises 71 | ------ 72 | livef1Exception 73 | If the meeting cannot be found based on the provided parameters. 74 | """ 75 | 76 | # Check if sufficient arguments have been provided 77 | if (meeting_identifier == None) and (meeting_key == None) and (meeting_offname == None): 78 | try: 79 | raise ArgumentError(f"One of the following arguments needs to be defined: 'meeting_identifier', 'meeting_key', 'meeting_offname'.") 80 | except ArgumentError as e: 81 | logger.error(f"An error occured {e}") 82 | 83 | season_obj = get_season(season=season) 84 | required_cols = ["Meeting Offname","Meeting Name","Meeting Circuit Shortname"] 85 | 86 | if meeting_offname != None: 87 | found_meetings = [meeting for meeting in season_obj.meetings if meeting.officialname == meeting_offname] 88 | 89 | if len(found_meetings) > 0: 90 | meeting_obj = found_meetings[0] 91 | meeting_key = meeting_obj.key 92 | else: 93 | raise LiveF1Error(f"The meeting with official name '{meeting_offname}' could not be found in the season {season}.") 94 | 95 | else: 96 | search_df_season = season_obj.meetings_table.reset_index()[required_cols].drop_duplicates() 97 | if meeting_identifier: 98 | logger.debug("Getting meeting by meeting identifier.") 99 | result_meeting = find_most_similar_vectorized(search_df_season, meeting_identifier) 100 | meeting_key = season_obj.meetings_table.iloc[result_meeting["row_index"]]["Meeting Key"] 101 | elif meeting_key: 102 | logger.debug("Getting meeting by meeting key.") 103 | pass 104 | 105 | meeting_obj = [meeting for meeting in season_obj.meetings if meeting.key == meeting_key][0] 106 | 107 | print_found_model( 108 | df = season_obj.season_table, 109 | key = meeting_key, 110 | cols = required_cols 111 | ) 112 | logger.info("Got the meeting.") 113 | 114 | return meeting_obj 115 | 116 | def get_session( 117 | season: int, 118 | meeting_identifier: str = None, 119 | session_identifier: str = None, 120 | meeting_key: int = None, 121 | session_key: int = None 122 | ) -> Session: 123 | """ 124 | Retrieve data for a specific session within a meeting and season. 125 | 126 | Parameters 127 | ---------- 128 | season : :class:`int` 129 | The year of the season. 130 | meeting_identifier : :class:`str` 131 | The identifier (e.g., circuit name, grand prix name) of the meeting. 132 | The identifier is going to be searched in the season's meeting table columns: 133 | - "Meeting Official Name" 134 | - "Meeting Name" 135 | - "Circuit Short Name" 136 | Therefore, it is suggested to use keywords that is distinguishable among meetings. 137 | Another suggestion is using circuit names for querying. 138 | meeting_key : :class:`int` 139 | The key of the meeting to get the desired meeting whose key is matching. 140 | session_identifier : :class:`str` 141 | The identifier of the session (e.g., "Practice 1", "Qualifying"). 142 | The identifier is going to be searched in the meeting's sessions table. 143 | session_key : :class:`int` 144 | The key of the session to get the desired session whose key is matching. 145 | 146 | Returns 147 | ------- 148 | Session 149 | A `Session` object containing data about the specified session. 150 | 151 | Raises 152 | ------ 153 | livef1Exception 154 | If the session cannot be found based on the provided parameters. 155 | """ 156 | 157 | # Check if sufficient arguments have been provided 158 | if (session_identifier == None) and (session_key == None): 159 | try: 160 | raise ArgumentError(f"One of the following arguments needs to be defined: 'session_identifier', 'session_key'.") 161 | except ArgumentError as e: 162 | logger.error(f"An error occured {e}") 163 | raise 164 | 165 | meeting_obj = get_meeting( 166 | season=season, 167 | meeting_identifier=meeting_identifier, 168 | meeting_key=meeting_key 169 | ) 170 | required_cols = ["session_name"] 171 | search_df_season = meeting_obj.sessions_table.reset_index()[required_cols].drop_duplicates() 172 | 173 | if session_identifier: 174 | logger.debug("Getting session by identifier.") 175 | result_session = find_most_similar_vectorized(search_df_season, session_identifier) 176 | session_key = meeting_obj.sessions_table.iloc[result_session["row_index"]].name 177 | 178 | elif session_key: 179 | logger.debug("Getting session by key.") 180 | pass 181 | 182 | session_obj = [session for name, session in meeting_obj.sessions.items() if session.key == session_key][0] 183 | print_found_model( 184 | df = meeting_obj.sessions_table, 185 | key = session_key, 186 | cols = required_cols 187 | ) 188 | logger.info("The session was received successfully.") 189 | 190 | session_obj.load_session_data() 191 | return session_obj -------------------------------------------------------------------------------- /livef1/data/starting_coordinates.json: -------------------------------------------------------------------------------- 1 | { 2 | "Silverstone": { 3 | "start_coordinates": [ 4 | -1719.0, 5 | 1259.0696915293308 6 | ], 7 | "start_direction": [ 8 | 1.0, 9 | 1.0 10 | ] 11 | }, 12 | "Hungaroring": { 13 | "start_coordinates": [ 14 | -1503.1760607788005, 15 | -97.79438368267355 16 | ], 17 | "start_direction": [ 18 | -1.0, 19 | 1.0 20 | ] 21 | }, 22 | "Imola": { 23 | "start_coordinates": [ 24 | -1567.6526404890294, 25 | -1609.0 26 | ], 27 | "start_direction": [ 28 | -1.0, 29 | -1.0 30 | ] 31 | }, 32 | "Spa-Francorchamps": { 33 | "start_coordinates": [ 34 | -434.74837653627947, 35 | 1306.8582922622738 36 | ], 37 | "start_direction": [ 38 | -1.0, 39 | 1.0 40 | ] 41 | }, 42 | "Austin": { 43 | "start_coordinates": [ 44 | -1296.857864097475, 45 | -418.4940661710967 46 | ], 47 | "start_direction": [ 48 | 1.0, 49 | -1.0 50 | ] 51 | }, 52 | "Melbourne": { 53 | "start_coordinates": [ 54 | -1117.044473859799, 55 | -1407.8614972009418 56 | ], 57 | "start_direction": [ 58 | 1.0, 59 | -1.0 60 | ] 61 | }, 62 | "Interlagos": { 63 | "start_coordinates": [ 64 | -3640.956428946304, 65 | -5403.238457322315 66 | ], 67 | "start_direction": [ 68 | 1.0, 69 | -1.0 70 | ] 71 | }, 72 | "Catalunya": { 73 | "start_coordinates": [ 74 | 1333.0, 75 | -318.0 76 | ], 77 | "start_direction": [ 78 | -1.0, 79 | -1.0 80 | ] 81 | }, 82 | "Spielberg": { 83 | "start_coordinates": [ 84 | 1051.9430707003812, 85 | -1221.0 86 | ], 87 | "start_direction": [ 88 | -1.0, 89 | -1.0 90 | ] 91 | }, 92 | "Monte Carlo": { 93 | "start_coordinates": [ 94 | -7666.021463369087, 95 | -6542.6118060293875 96 | ], 97 | "start_direction": [ 98 | -1.0, 99 | 1.0 100 | ] 101 | }, 102 | "Montreal": { 103 | "start_coordinates": [ 104 | 3374.964298911727, 105 | 754.2371366743143 106 | ], 107 | "start_direction": [ 108 | 1.0, 109 | -1.0 110 | ] 111 | }, 112 | "Paul Ricard": { 113 | "start_coordinates": [ 114 | -442.0, 115 | -1382.0 116 | ], 117 | "start_direction": [ 118 | -1.0, 119 | 1.0 120 | ] 121 | }, 122 | "Monza": { 123 | "start_coordinates": [ 124 | -1373.7278248908085, 125 | -678.8106088197458 126 | ], 127 | "start_direction": [ 128 | 1.0, 129 | 1.0 130 | ] 131 | }, 132 | "Suzuka": { 133 | "start_coordinates": [ 134 | 1747.021795551225, 135 | -703.9474171350629 136 | ], 137 | "start_direction": [ 138 | 1.0, 139 | -1.0 140 | ] 141 | }, 142 | "Shanghai": { 143 | "start_coordinates": [ 144 | 30.519037360871856, 145 | -1502.3037163443846 146 | ], 147 | "start_direction": [ 148 | -1.0, 149 | -1.0 150 | ] 151 | }, 152 | "Zandvoort": { 153 | "start_coordinates": [ 154 | 520.7570772162252, 155 | 3808.0258182955977 156 | ], 157 | "start_direction": [ 158 | 1.0, 159 | 1.0 160 | ] 161 | }, 162 | "Istanbul": { 163 | "start_coordinates": [ 164 | 582.4348669934898, 165 | -1402.0 166 | ], 167 | "start_direction": [ 168 | 1.0, 169 | 1.0 170 | ] 171 | }, 172 | "Singapore": { 173 | "start_coordinates": [ 174 | 1056.5297920098192, 175 | -325.4606469128227 176 | ], 177 | "start_direction": [ 178 | -1.0, 179 | 1.0 180 | ] 181 | }, 182 | "Sakhir": { 183 | "start_coordinates": [ 184 | -376.0, 185 | 1345.0 186 | ], 187 | "start_direction": [ 188 | 1.0, 189 | 1.0 190 | ] 191 | }, 192 | "Mexico City": { 193 | "start_coordinates": [ 194 | -162.85000000000014, 195 | 1208.321410000574 196 | ], 197 | "start_direction": [ 198 | 1.0, 199 | -1.0 200 | ] 201 | }, 202 | "Yas Marina Circuit": { 203 | "start_coordinates": [ 204 | 626.9032354136568, 205 | 2088.0 206 | ], 207 | "start_direction": [ 208 | 1.0, 209 | 1.0 210 | ] 211 | }, 212 | "N\u00fcrburgring": { 213 | "start_coordinates": [ 214 | 1653.0, 215 | -346.22388097316923 216 | ], 217 | "start_direction": [ 218 | -1.0, 219 | -1.0 220 | ] 221 | }, 222 | "Sochi": { 223 | "start_coordinates": [ 224 | 1221.9475505080843, 225 | -502.7054245901702 226 | ], 227 | "start_direction": [ 228 | -1.0, 229 | -1.0 230 | ] 231 | }, 232 | "Baku": { 233 | "start_coordinates": [ 234 | 1123.0, 235 | -566.0 236 | ], 237 | "start_direction": [ 238 | 1.0, 239 | 1.0 240 | ] 241 | }, 242 | "Mugello": { 243 | "start_coordinates": [ 244 | -954.6910821331189, 245 | -332.13248185993336 246 | ], 247 | "start_direction": [ 248 | -1.0, 249 | -1.0 250 | ] 251 | }, 252 | "Algarve International Circuit": { 253 | "start_coordinates": [ 254 | -1887.7766731353843, 255 | 1568.8178947218707 256 | ], 257 | "start_direction": [ 258 | -1.0, 259 | 1.0 260 | ] 261 | }, 262 | "Sakhir Outer Track": { 263 | "start_coordinates": [ 264 | -365.0, 265 | 1383.1499999999999 266 | ], 267 | "start_direction": [ 268 | 1.0, 269 | 1.0 270 | ] 271 | }, 272 | "Jeddah": { 273 | "start_coordinates": [ 274 | -1316.0, 275 | 286.0 276 | ], 277 | "start_direction": [ 278 | -1.0, 279 | 1.0 280 | ] 281 | }, 282 | "Miami": { 283 | "start_coordinates": [ 284 | 1938.7747920436402, 285 | 65.07787460514575 286 | ], 287 | "start_direction": [ 288 | 1.0, 289 | -1.0 290 | ] 291 | }, 292 | "Las Vegas": { 293 | "start_coordinates": [ 294 | 2180.991946047756, 295 | -485.05109463819497 296 | ], 297 | "start_direction": [ 298 | 1.0, 299 | 1.0 300 | ] 301 | }, 302 | "Lusail": { 303 | "start_coordinates": [ 304 | -1107.0, 305 | -974.6627996487782 306 | ], 307 | "start_direction": [ 308 | -1.0, 309 | 1.0 310 | ] 311 | } 312 | } -------------------------------------------------------------------------------- /livef1/models/meeting.py: -------------------------------------------------------------------------------- 1 | # Standard Library Imports 2 | import dateutil 3 | import sys 4 | import json 5 | 6 | # Third-Party Library Imports 7 | import pandas as pd 8 | from typing import List, Dict 9 | 10 | # Internal Project Imports 11 | from ..adapters import download_data 12 | from ..models.session import Session 13 | from ..models.circuit import Circuit 14 | from ..utils.helper import json_parser_for_objects, build_session_endpoint 15 | from ..utils.constants import SESSIONS_COLUMN_MAP 16 | 17 | 18 | class Meeting: 19 | """ 20 | Represents a meeting in a specific season with relevant details and associated sessions. 21 | 22 | Attributes 23 | ---------- 24 | season : :class:`~Season` 25 | The season this meeting belongs to. 26 | year : :class:`int` 27 | The year of the meeting. 28 | code : :class:`int` 29 | The unique code for the meeting. 30 | key : :class:`str` 31 | The unique identifier for the meeting. 32 | number : :class:`int` 33 | The sequential number of the meeting in the season. 34 | location : :class:`str` 35 | The location (e.g., circuit name) of the meeting. 36 | officialname : :class:`str` 37 | The official name of the meeting. 38 | name : :class:`str` 39 | The name of the meeting. 40 | country : :class:`dict` 41 | Details about the country where the meeting takes place (e.g., key, code, name). 42 | circuit : :class:`dict` 43 | Details about the circuit where the meeting takes place (e.g., key, short name). 44 | sessions : :class:`list` 45 | List of session objects associated with the meeting. 46 | loaded : :class:`bool` 47 | Indicates whether the meeting data has been loaded. 48 | """ 49 | 50 | def __init__( 51 | self, 52 | season: "Season" = None, 53 | year: int = None, 54 | code: int = None, 55 | key: str = None, 56 | number: int = None, 57 | location: str = None, 58 | officialname: str = None, 59 | name: str = None, 60 | country: Dict = None, 61 | circuit: Dict = None, 62 | sessions: List = None, 63 | loaded: bool = False, 64 | **kwargs # In case new information comes from the API in future 65 | ): 66 | self.season = season 67 | self.loaded = loaded 68 | 69 | # Iterate over the kwargs and set them as attributes of the instance 70 | for key, value in locals().items(): 71 | if value: 72 | setattr(self, key.lower(), value) 73 | 74 | # Load Circuit 75 | self.circuit = Circuit(self.circuit["Key"],self.circuit["ShortName"]) 76 | self.circuit._load_start_coordinates() 77 | 78 | if hasattr(self, "sessions"): 79 | self.sessions_json = self.sessions 80 | self.sessions = {} 81 | self.set_sessions() 82 | 83 | self.parse_sessions() 84 | 85 | def load(self, force=False): 86 | """ 87 | Load or reload meeting data from the API. 88 | 89 | .. note:: 90 | Reloading is useful when updated data is required. 91 | 92 | Parameters 93 | ---------- 94 | force : bool, optional 95 | If True, forces the reload of meeting data even if already loaded. Defaults to False. 96 | 97 | 98 | """ 99 | if (not self.loaded) | (force): 100 | if force: 101 | print("Force load...") 102 | 103 | if hasattr(self, "year"): 104 | self.json_data = download_data(self.year, self.location) 105 | elif hasattr(self, "season"): 106 | self.json_data = download_data(self.season.year, self.location) 107 | 108 | for key, value in json_parser_for_objects(self.json_data).items(): 109 | setattr(self, key.lower(), value) 110 | 111 | self.sessions_json = self.sessions 112 | self.sessions = [] 113 | 114 | self.parse_sessions() 115 | self.set_sessions() 116 | else: 117 | print("The meeting has already been loaded. If you want to load anyway, use `force=True`.") 118 | 119 | def set_sessions(self): 120 | """ 121 | Create session objects for the meeting using the session JSON data. 122 | 123 | .. note:: 124 | This method populates the `sessions` attribute with `Session` objects derived from `sessions_json`. 125 | """ 126 | for session_data in self.sessions_json: 127 | if "Name" in session_data: k = session_data["Name"] 128 | else: k = session_data["Key"] 129 | self.sessions[k] = Session( 130 | season=self.season, 131 | meeting=self, 132 | **json_parser_for_objects(session_data) 133 | ) 134 | 135 | def parse_sessions(self): 136 | """ 137 | Parse session data to generate a detailed DataFrame of session metadata. 138 | 139 | .. note:: 140 | The resulting DataFrame is stored in the `sessions_table` attribute and indexed by season year, meeting location, and session type. 141 | """ 142 | session_all_data = [] 143 | 144 | for session in self.sessions_json: 145 | session_data = { 146 | "season_year": dateutil.parser.parse(session["StartDate"]).year, 147 | "meeting_code": self.code, 148 | "meeting_key": self.key, 149 | "meeting_number": self.number, 150 | "meeting_location": self.location, 151 | "meeting_offname": self.officialname, 152 | "meeting_name": self.name, 153 | "meeting_country_key": self.country["Key"], 154 | "meeting_country_code": self.country["Code"], 155 | "meeting_country_name": self.country["Name"], 156 | "circuit": self.circuit, 157 | "session_key": session.get("Key", None), 158 | "session_type": session["Type"] + " " + str(session["Number"]) if "Number" in session else session["Type"], 159 | "session_name": session.get("Name", None), 160 | "session_startDate": session.get("StartDate", None), 161 | "session_endDate": session.get("EndDate", None), 162 | "gmtoffset": session.get("GmtOffset", None), 163 | "path": session.get("Path", None), 164 | } 165 | session_all_data.append(session_data) 166 | 167 | self.meeting_table = pd.DataFrame(session_all_data).set_index(["season_year", "meeting_location", "session_type"]) 168 | self.meeting_table["session_startDate"] = pd.to_datetime(self.meeting_table["session_startDate"]) 169 | self.meeting_table["session_endDate"] = pd.to_datetime(self.meeting_table["session_endDate"]) 170 | 171 | self.sessions_table = self.meeting_table[["meeting_key","session_key","session_name","session_startDate","session_endDate","gmtoffset","path"]].set_index("session_key") 172 | # self.meeting_table = self.meeting_table.reset_index().rename(columns = SESSIONS_COLUMN_MAP) 173 | 174 | 175 | 176 | # self.meeting_table = 177 | 178 | def __repr__(self): 179 | """ 180 | Return a detailed string representation of the meeting. 181 | 182 | Returns 183 | ------- 184 | str 185 | The string representation of the meeting's session table. 186 | """ 187 | if "IPython" not in sys.modules: 188 | # definitely not in IPython 189 | return self.meeting_table.__str__() # Print the meetings table. 190 | else: 191 | display(self.meeting_table) # Display the meetings table. 192 | return "" 193 | 194 | def __str__(self): 195 | """ 196 | Return a readable string representation of the meeting. 197 | 198 | Returns 199 | ------- 200 | str 201 | The string representation of the meeting's session table. 202 | """ 203 | return self.meeting_table.__str__() -------------------------------------------------------------------------------- /docs/source/user_guide/historical_data.rst: -------------------------------------------------------------------------------- 1 | .. _historical_data: 2 | 3 | Historical Data 4 | ============== 5 | 6 | The LiveF1 package provides access to historical Formula 1 data through Session objects, which contain comprehensive race data including timing, telemetry, and other metrics. 7 | 8 | Accessing Historical Data 9 | ------------------------ 10 | 11 | Direct Session Access 12 | ^^^^^^^^^^^^^^^ 13 | 14 | The most common way to access historical data is through sessions: 15 | 16 | .. code-block:: python 17 | 18 | import livef1 19 | 20 | # Get specific session directly 21 | race = livef1.get_session( 22 | season=2024, 23 | meeting_identifier="Spa", # Circuit/Location name 24 | session_identifier="Race" # Session type 25 | ) 26 | 27 | .. note:: 28 | You can also access data through Season and Meeting objects for broader data exploration: 29 | 30 | .. code-block:: python 31 | 32 | # Alternative: Browse through season/meeting structure 33 | season = livef1.get_season(2024) 34 | belgian_gp = livef1.get_meeting(season=2024, meeting_identifier="Spa") 35 | 36 | # View available sessions 37 | print(belgian_gp.sessions_table) 38 | 39 | Working with Session Data 40 | ------------------------ 41 | 42 | Once you have a session object, you can access different types of data: 43 | 44 | 1. Raw Data Access 45 | ^^^^^^^^^^^^^^^ 46 | 47 | .. code-block:: python 48 | 49 | # Access specific data 50 | weather_data = race.get_data("WeatherData") 51 | car_data = race.get_data("CarData.z") 52 | 53 | display(weather_data.head()) 54 | display(car_data.head()) 55 | 56 | .. rst-class:: sphx-glr-script-out 57 | 58 | .. code-block:: none 59 | 60 | | | SessionKey | timestamp | AirTemp | Humidity | Pressure | Rainfall | TrackTemp | WindDirection | WindSpeed | 61 | |---:|-------------:|:-------------|----------:|-----------:|-----------:|-----------:|------------:|----------------:|------------:| 62 | | 0 | 9574 | 00:00:14.052 | 21 | 52 | 978.1 | 0 | 42.1 | 217 | 0.5 | 63 | | 1 | 9574 | 00:01:14.050 | 20.6 | 53 | 978 | 0 | 42.1 | 0 | 1.1 | 64 | | 2 | 9574 | 00:02:14.047 | 20.6 | 52 | 978 | 0 | 42.2 | 284 | 0.7 | 65 | | 3 | 9574 | 00:03:14.053 | 20.7 | 51 | 978.1 | 0 | 42 | 244 | 1.4 | 66 | | 4 | 9574 | 00:04:14.057 | 20.7 | 51 | 978.1 | 0 | 41.1 | 13 | 1.2 | 67 | 68 | | | SessionKey | timestamp | Utc | DriverNo | rpm | speed | n_gear | throttle | brake | drs | 69 | |---:|-------------:|:-------------|:-----------------------------|-----------:|------:|--------:|---------:|-----------:|--------:|------:| 70 | | 0 | 9574 | 00:02:28.746 | 2024-07-28T12:11:06.7233441Z | 1 | 0 | 0 | 0 | 0 | 0 | 0 | 71 | | 1 | 9574 | 00:02:28.746 | 2024-07-28T12:11:06.7233441Z | 2 | 0 | 0 | 0 | 0 | 0 | 0 | 72 | | 2 | 9574 | 00:02:28.746 | 2024-07-28T12:11:06.7233441Z | 3 | 0 | 0 | 0 | 0 | 0 | 0 | 73 | | 3 | 9574 | 00:02:28.746 | 2024-07-28T12:11:06.7233441Z | 4 | 0 | 0 | 0 | 0 | 0 | 0 | 74 | | 4 | 9574 | 00:02:28.746 | 2024-07-28T12:11:06.7233441Z | 10 | 0 | 0 | 0 | 0 | 0 | 0 | 75 | 76 | .. seealso:: 77 | For a complete list of available topics and their descriptions, see :ref:`data_topics` 78 | 79 | 2. Processed Data 80 | ^^^^^^^^^^^^^^^ 81 | 82 | Generate processed data tables using the medallion architecture: 83 | 84 | .. code-block:: python 85 | 86 | # Generate silver and gold tables 87 | race.generate() 88 | 89 | # Access processed data 90 | laps = race.laps 91 | telemetry = race.carTelemetry 92 | 93 | display(laps.head()) 94 | 95 | .. rst-class:: sphx-glr-script-out 96 | 97 | .. code-block:: none 98 | 99 | | | lap_number | lap_time | in_pit | pit_out | sector1_time | sector2_time | sector3_time | None | speed_I1 | speed_I2 | speed_FL | speed_ST | no_pits | lap_start_time | DriverNo | lap_start_date | 100 | |---:|-------------:|:-----------------------|:-----------------------|:----------|:-----------------------|:-----------------------|:-----------------------|:-------|-----------:|-----------:|-----------:|-----------:|----------:|:-----------------------|-----------:|:---------------------------| 101 | | 0 | 1 | NaT | 0 days 00:17:07.661000 | NaT | NaT | 0 days 00:00:48.663000 | 0 days 00:00:29.571000 | | 314 | 204 | | 303 | 0 | NaT | 16 | 2024-07-28 13:03:52.742000 | 102 | | 1 | 2 | 0 days 00:01:50.240000 | NaT | NaT | 0 days 00:00:31.831000 | 0 days 00:00:48.675000 | 0 days 00:00:29.734000 | | 303 | 203 | 219 | | 0 | 0 days 00:57:07.067000 | 16 | 2024-07-28 13:05:45.045000 | 103 | | 2 | 3 | 0 days 00:01:50.519000 | NaT | NaT | 0 days 00:00:31.833000 | 0 days 00:00:49.132000 | 0 days 00:00:29.554000 | | 311 | 202 | 215 | 304 | 0 | 0 days 00:58:57.307000 | 16 | 2024-07-28 13:07:35.285000 | 104 | | 3 | 4 | 0 days 00:01:49.796000 | NaT | NaT | 0 days 00:00:31.592000 | 0 days 00:00:48.778000 | 0 days 00:00:29.426000 | | 312 | 201 | 217 | 309 | 0 | 0 days 01:00:47.870000 | 16 | 2024-07-28 13:09:25.848000 | 105 | | 4 | 5 | 0 days 00:01:49.494000 | NaT | NaT | 0 days 00:00:31.394000 | 0 days 00:00:48.729000 | 0 days 00:00:29.371000 | | 313 | 197 | 217 | 311 | 0 | 0 days 01:02:37.721000 | 16 | 2024-07-28 13:11:15.699000 | 106 | 107 | 108 | Example: Complete Historical Data Analysis 109 | ---------------------------------------- 110 | 111 | Here's a complete example showing how to access and analyze historical race data: 112 | 113 | .. code-block:: python 114 | 115 | import livef1 116 | 117 | # Get a specific race session 118 | race = livef1.get_session( 119 | season=2023, 120 | meeting_identifier="Monaco", 121 | session_identifier="Race" 122 | ) 123 | 124 | # Generate processed data 125 | race.generate() 126 | 127 | # Get lap times and telemetry 128 | laps_data = race.laps 129 | telemetry = race.carTelemetry 130 | 131 | # Analyze fastest laps 132 | fastest_laps = laps_data.sort_values('LapTime').groupby('DriverNumber').first() 133 | print("Fastest laps by driver:\n****************") 134 | print(fastest_laps[['LapTime', 'LapNumber']]) 135 | 136 | .. rst-class:: sphx-glr-script-out 137 | 138 | .. code-block:: none 139 | 140 | Fastest laps by driver: 141 | **************** 142 | | DriverNo | lap_time | lap_number | 143 | |-----------:|:-----------------------|-------------:| 144 | | 1 | 0 days 00:01:46.128000 | 32 | 145 | | 10 | 0 days 00:01:47.418000 | 30 | 146 | | 11 | 0 days 00:01:44.701000 | 44 | 147 | | 14 | 0 days 00:01:48.051000 | 42 | 148 | | 16 | 0 days 00:01:47.013000 | 33 | 149 | | 18 | 0 days 00:01:48.105000 | 44 | 150 | | 2 | 0 days 00:01:47.490000 | 43 | 151 | | 20 | 0 days 00:01:47.848000 | 44 | 152 | | 22 | 0 days 00:01:47.969000 | 44 | 153 | | 23 | 0 days 00:01:47.996000 | 44 | 154 | | 24 | 0 days 00:01:52.099000 | 2 | 155 | | 27 | 0 days 00:01:48.954000 | 44 | 156 | | 3 | 0 days 00:01:47.435000 | 37 | 157 | | 31 | 0 days 00:01:46.957000 | 43 | 158 | | 4 | 0 days 00:01:45.563000 | 31 | 159 | | 44 | 0 days 00:01:46.653000 | 33 | 160 | | 55 | 0 days 00:01:46.364000 | 44 | 161 | | 63 | 0 days 00:01:47.113000 | 44 | 162 | | 77 | 0 days 00:01:47.019000 | 37 | 163 | | 81 | 0 days 00:01:45.840000 | 32 | 164 | 165 | .. Parallel Data Loading 166 | .. ------------------- 167 | 168 | .. When working with multiple data topics, LiveF1 supports parallel data loading to improve performance. This is especially useful when retrieving multiple large datasets simultaneously. For best results, consider grouping related topics together, monitor memory usage when loading multiple topics in parallel, implement proper error handling for parallel requests, and handle partial failures gracefully. It's also recommended to use parallel loading only when retrieving multiple large datasets, as the overhead of parallelization may outweigh the benefits for small datasets. 169 | 170 | .. Basic Usage 171 | .. ^^^^^^^^^^ 172 | 173 | .. .. code-block:: python 174 | 175 | .. # Load multiple topics in parallel (default behavior) 176 | .. data = session.get_data( 177 | .. ["CarData.z", "Position.z", "SessionStatus"] 178 | .. ) 179 | 180 | .. # Load topics sequentially (parallel disabled) 181 | .. data = session.get_data( 182 | .. ["CarData.z", "Position.z", "SessionStatus"], 183 | .. parallel=False 184 | .. ) -------------------------------------------------------------------------------- /livef1/adapters/realtime_client.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | import concurrent 3 | import requests 4 | import inspect 5 | import random 6 | 7 | import time 8 | import json 9 | from urllib.parse import urljoin 10 | 11 | from ..data_processing.etl import function_map 12 | from .signalr_aio._connection import Connection 13 | from ..utils.logger import logger 14 | from ..utils.constants import ( 15 | DEFAULT_METHOD, 16 | BASE_URL, 17 | SIGNALR_ENDPOINT, 18 | REALTIME_CALLBACK_DEFAULT_PARAMETERS 19 | ) 20 | from ..utils.exceptions import ( 21 | ArgumentError, 22 | ParsingError 23 | ) 24 | 25 | class RealF1Client: 26 | """ 27 | A client for managing real-time Formula 1 data streaming. 28 | 29 | Attributes 30 | ---------- 31 | topics : list 32 | List of topics to subscribe to for receiving live data. 33 | headers : dict 34 | HTTP headers used for the connection. 35 | _connection_url : str 36 | URL for the SignalR connection. 37 | _log_file_name : str 38 | Path to the log file. 39 | _log_file_mode : str 40 | Mode for opening the log file. 41 | _test : bool 42 | Indicates if the client is in test mode. 43 | _log_file : file object 44 | Log file object for writing messages (used in test mode). 45 | _handlers : dict 46 | Mapping of methods to their respective handlers. 47 | 48 | Parameters 49 | ---------- 50 | topics : str or list 51 | Topic(s) to subscribe to for live updates. 52 | log_file_name : str, optional 53 | Name of the log file (default is None). 54 | log_file_mode : str, optional 55 | Mode for opening the log file (default is "w"). 56 | 57 | """ 58 | def __init__( 59 | self, 60 | topics, 61 | log_file_name = None, 62 | log_file_mode = "w", 63 | ): 64 | 65 | self._connection_url = urljoin(BASE_URL, SIGNALR_ENDPOINT) 66 | self.headers = { 67 | 'User-agent': 'BestHTTP', 68 | 'Accept-Encoding': 'gzip, identity', 69 | 'Connection': 'keep-alive, Upgrade'} 70 | 71 | if isinstance(topics, str): self.topics = [topics] 72 | elif isinstance(topics, list): self.topics = topics 73 | else: raise ArgumentError("You need to give list of topics you want to subscribe") 74 | 75 | self._log_file_name = log_file_name 76 | self._log_file_mode = log_file_mode 77 | self._handlers = {} 78 | 79 | if self._log_file_name: 80 | self._log_file = open(self._log_file_name, self._log_file_mode) 81 | @self.callback("default logger") 82 | async def print_callback( 83 | records 84 | ): 85 | for topic, data in records.items(): 86 | for record in data: 87 | await self._file_logger(f"{topic} > {record}") 88 | 89 | def _create_session(self): 90 | """ 91 | Create an HTTP session with the required headers. 92 | 93 | Returns 94 | ------- 95 | requests.Session 96 | A configured HTTP session. 97 | """ 98 | session = requests.Session() 99 | session.headers = self.headers 100 | return session 101 | 102 | async def _on_message(self, msg): 103 | """ 104 | Handle incoming messages asynchronously. 105 | 106 | Parameters 107 | ---------- 108 | msg : dict 109 | The incoming message to process. 110 | """ 111 | self._t_last_message = time.time() 112 | loop = asyncio.get_running_loop() 113 | try: 114 | with concurrent.futures.ThreadPoolExecutor() as pool: 115 | await loop.run_in_executor( 116 | pool, print, str(msg) 117 | ) 118 | except Exception as e: 119 | raise RealF1Error(e) 120 | 121 | async def _file_logger(self, msg): 122 | """ 123 | Log incoming messages to a file. 124 | 125 | Parameters 126 | ---------- 127 | msg : dict 128 | The incoming message to log. 129 | """ 130 | 131 | if msg != {} and msg: 132 | self._log_file.write(str(msg) + '\n') 133 | self._log_file.flush() 134 | 135 | def on_message(self, method, handler): 136 | """ 137 | Register a handler for a specific method. 138 | 139 | Parameters 140 | ---------- 141 | method : str 142 | The method to handle. 143 | handler : callable 144 | The function to handle the method. 145 | """ 146 | if method not in self._handlers: 147 | func = MessageHandlerTemplate(handler).get 148 | self._handlers[method] = func 149 | 150 | def callback(self, method): 151 | """ 152 | Decorator to register a callback function for a specific method. 153 | 154 | This decorator allows you to associate a callback function with a particular 155 | method. The function being registered must have arguments matching the 156 | required parameters defined in `REALTIME_CALLBACK_DEFAULT_PARAMETERS`. 157 | 158 | Raises 159 | ------ 160 | TypeError 161 | If the provided callback function does not have the required arguments. 162 | 163 | Notes 164 | ----- 165 | - The `REALTIME_CALLBACK_DEFAULT_PARAMETERS` is a predefined list of parameter 166 | names that the callback function must include. 167 | - Once the function is successfully registered, it will handle messages for the 168 | specified method. 169 | 170 | Examples 171 | -------- 172 | Registering a callback for a method: 173 | 174 | .. code-block:: python 175 | 176 | from livef1.adapters.realtime_client import RealF1Client 177 | 178 | client = RealF1Client( 179 | topics = ["CarData.z", "SessionInfo"], 180 | log_file_name="./output.json" 181 | ) 182 | 183 | @client.callback("new one") 184 | async def print_callback(records): # records argument have to be set 185 | print(records) # or you can do whatever you want with incoming data 186 | 187 | client.run() 188 | 189 | """ 190 | def inner(func): 191 | # Check if the provided function has the required arguments 192 | has_args = set(REALTIME_CALLBACK_DEFAULT_PARAMETERS) == set(inspect.signature(func).parameters.keys()) 193 | args_diff = set(REALTIME_CALLBACK_DEFAULT_PARAMETERS).difference(set(inspect.signature(func).parameters.keys())) 194 | if not has_args: 195 | raise ArgumentError(f"The provided callback function does not have following required arguments. {args_diff}") 196 | else: 197 | # Register the function as a handler for the given method 198 | self.on_message(method,func) 199 | logger.debug(f"Custom callback method with '{method}' has successfully inserted.") 200 | return func 201 | return inner 202 | 203 | def run(self): 204 | """ 205 | Start the client in asynchronous mode. 206 | """ 207 | start = time.time() 208 | self._async_engine_run() 209 | logger.info(f"Client have run for {(time.time() - start):.2f} seconds.") 210 | 211 | def _async_engine_run(self): 212 | """ 213 | Execute the asynchronous engine. 214 | """ 215 | try: 216 | asyncio.run(self._async_run()) 217 | except KeyboardInterrupt: 218 | logger.info("Keyboard interrupt - exiting...") 219 | 220 | async def _async_run(self): 221 | """ 222 | Run the client asynchronously. 223 | """ 224 | logger.info(f"Starting LiveF1 live timing client") 225 | await asyncio.gather( 226 | asyncio.ensure_future(self._forever_check()), 227 | asyncio.ensure_future(self._run()) 228 | ) 229 | logger.info("Exiting...") 230 | 231 | async def _forever_check(self): 232 | """ 233 | Keep the client running indefinitely. 234 | """ 235 | while True: 236 | await asyncio.sleep(1) 237 | 238 | def _sync_engine_run(self): 239 | pass 240 | 241 | def _sync_engine(self): 242 | pass 243 | 244 | async def _run(self): 245 | """ 246 | Set up the SignalR connection and register handlers. 247 | """ 248 | # Create connection 249 | self._connection = Connection(self._connection_url, session=self._create_session()) 250 | # Register hub 251 | hub = self._connection.register_hub('Streaming') 252 | # Set default message handler 253 | for method, handler in self._handlers.items(): 254 | hub.client.on(method, handler) 255 | 256 | # Subscribe topics in interest 257 | hub.server.invoke("Subscribe", self.topics) 258 | # Start the client 259 | loop = asyncio.get_event_loop() 260 | executor = concurrent.futures.ThreadPoolExecutor() 261 | await loop.run_in_executor(executor, self._connection.start) 262 | 263 | 264 | class MessageHandlerTemplate: 265 | """ 266 | A template for handling incoming SignalR messages. 267 | 268 | This class serves as a message handler for SignalR streams, where incoming 269 | messages are processed and passed to a user-defined function. 270 | 271 | Parameters 272 | ---------- 273 | func : callable 274 | A user-defined asynchronous function that processes the parsed records 275 | from incoming SignalR messages. The function must accept the processed 276 | records as its input. 277 | """ 278 | def __init__( 279 | self, 280 | func 281 | ): 282 | self._func = func 283 | 284 | async def get(self, msg): 285 | """ 286 | Process incoming messages and invoke the handler function. 287 | 288 | The method handles two types of incoming message formats: 289 | - Messages in the "R" key: Representing topics with associated data. 290 | - Messages in the "M" key: Representing method calls with data payloads. 291 | 292 | For each message, the method uses a `function_map` to parse the data and 293 | sends the resulting records to the user-defined handler function. 294 | 295 | Parameters 296 | ---------- 297 | msg : dict 298 | The incoming message in SignalR format. It is expected to contain either: 299 | - "R": A dictionary where the keys represent topic names and the values 300 | are the associated data for that topic. 301 | - "M": A list of dictionaries, where each dictionary contains: 302 | - "M": The method name. 303 | - "A": A list of message components including topic name, data, and timestamp. 304 | 305 | """ 306 | batch = msg 307 | 308 | if ("R" in batch.keys()) or (batch.get("M") and batch.get("M") != []): 309 | if batch.get("R"): 310 | for key in batch.get("R").keys(): 311 | try: 312 | topic_name = key 313 | data = batch.get("R")[key] 314 | timestamp = None 315 | records = list(function_map[topic_name]([(timestamp, data)], None)) 316 | records = {topic_name: records} 317 | await self._func(records) 318 | # await self._func( 319 | # topic_name = key, 320 | # data = batch.get("R")[key], 321 | # timestamp = None) 322 | except Exception as e: 323 | raise ParsingError(e) 324 | 325 | elif batch.get("M"): 326 | for data in batch.get("M"): 327 | method = data["M"] 328 | message = data["A"] 329 | 330 | topic_name = message[0] 331 | data = message[1] 332 | timestamp = message[2] 333 | records = list(function_map[topic_name]([(timestamp, data)], None)) 334 | records = {topic_name: records} 335 | 336 | await self._func(records) 337 | # await self._func( 338 | # topic_name = message[0], 339 | # data = message[1], 340 | # timestamp = message[2]) -------------------------------------------------------------------------------- /2025_schedule.csv: -------------------------------------------------------------------------------- 1 | Meeting Shortname,Meeting Offname,Session Name,Start Date,End Date 2 | 2025 F1 Pre-Season Testing,FORMULA 1 ARAMCO PRE-SEASON TESTING 2025,Day 1,2025-02-26 07:00:00,2025-02-26 16:00:00 3 | 2025 F1 Pre-Season Testing,FORMULA 1 ARAMCO PRE-SEASON TESTING 2025,Day 2,2025-02-27 07:00:00,2025-02-27 16:00:00 4 | 2025 F1 Pre-Season Testing,FORMULA 1 ARAMCO PRE-SEASON TESTING 2025,Day 3,2025-02-28 07:00:00,2025-02-28 16:00:00 5 | Australian,FORMULA 1 LOUIS VUITTON AUSTRALIAN GRAND PRIX 2025,Practice 1,2025-03-14 01:30:00,2025-03-14 02:30:00 6 | Australian,FORMULA 1 LOUIS VUITTON AUSTRALIAN GRAND PRIX 2025,Practice 2,2025-03-14 05:00:00,2025-03-14 06:00:00 7 | Australian,FORMULA 1 LOUIS VUITTON AUSTRALIAN GRAND PRIX 2025,Practice 3,2025-03-15 01:30:00,2025-03-15 02:30:00 8 | Australian,FORMULA 1 LOUIS VUITTON AUSTRALIAN GRAND PRIX 2025,Qualifying,2025-03-15 05:00:00,2025-03-15 06:00:00 9 | Australian,FORMULA 1 LOUIS VUITTON AUSTRALIAN GRAND PRIX 2025,Race,2025-03-16 04:00:00, 10 | Chinese,FORMULA 1 HEINEKEN CHINESE GRAND PRIX 2025,Practice 1,2025-03-21 03:30:00,2025-03-21 04:30:00 11 | Chinese,FORMULA 1 HEINEKEN CHINESE GRAND PRIX 2025,Sprint Qualifying,2025-03-21 07:30:00,2025-03-21 08:14:00 12 | Chinese,FORMULA 1 HEINEKEN CHINESE GRAND PRIX 2025,Sprint,2025-03-22 03:00:00,2025-03-22 04:00:00 13 | Chinese,FORMULA 1 HEINEKEN CHINESE GRAND PRIX 2025,Qualifying,2025-03-22 07:00:00,2025-03-22 08:00:00 14 | Chinese,FORMULA 1 HEINEKEN CHINESE GRAND PRIX 2025,Race,2025-03-23 07:00:00, 15 | Japanese,FORMULA 1 LENOVO JAPANESE GRAND PRIX 2025 ,Practice 1,2025-04-04 02:30:00,2025-04-04 03:30:00 16 | Japanese,FORMULA 1 LENOVO JAPANESE GRAND PRIX 2025 ,Practice 2,2025-04-04 06:00:00,2025-04-04 07:00:00 17 | Japanese,FORMULA 1 LENOVO JAPANESE GRAND PRIX 2025 ,Practice 3,2025-04-05 02:30:00,2025-04-05 03:30:00 18 | Japanese,FORMULA 1 LENOVO JAPANESE GRAND PRIX 2025 ,Qualifying,2025-04-05 06:00:00,2025-04-05 07:00:00 19 | Japanese,FORMULA 1 LENOVO JAPANESE GRAND PRIX 2025 ,Race,2025-04-06 05:00:00, 20 | Bahrain,FORMULA 1 GULF AIR BAHRAIN GRAND PRIX 2025,Practice 1,2025-04-11 11:30:00,2025-04-11 12:30:00 21 | Bahrain,FORMULA 1 GULF AIR BAHRAIN GRAND PRIX 2025,Practice 2,2025-04-11 15:00:00,2025-04-11 16:00:00 22 | Bahrain,FORMULA 1 GULF AIR BAHRAIN GRAND PRIX 2025,Practice 3,2025-04-12 12:30:00,2025-04-12 13:30:00 23 | Bahrain,FORMULA 1 GULF AIR BAHRAIN GRAND PRIX 2025,Qualifying,2025-04-12 16:00:00,2025-04-12 17:00:00 24 | Bahrain,FORMULA 1 GULF AIR BAHRAIN GRAND PRIX 2025,Race,2025-04-13 15:00:00, 25 | Saudi Arabian,FORMULA 1 STC SAUDI ARABIAN GRAND PRIX 2025,Practice 1,2025-04-18 13:30:00,2025-04-18 14:30:00 26 | Saudi Arabian,FORMULA 1 STC SAUDI ARABIAN GRAND PRIX 2025,Practice 2,2025-04-18 17:00:00,2025-04-18 18:00:00 27 | Saudi Arabian,FORMULA 1 STC SAUDI ARABIAN GRAND PRIX 2025,Practice 3,2025-04-19 13:30:00,2025-04-19 14:30:00 28 | Saudi Arabian,FORMULA 1 STC SAUDI ARABIAN GRAND PRIX 2025,Qualifying,2025-04-19 17:00:00,2025-04-19 18:00:00 29 | Saudi Arabian,FORMULA 1 STC SAUDI ARABIAN GRAND PRIX 2025,Race,2025-04-20 17:00:00, 30 | Miami,FORMULA 1 CRYPTO.COM MIAMI GRAND PRIX 2025,Practice 1,2025-05-02 16:30:00,2025-05-02 17:30:00 31 | Miami,FORMULA 1 CRYPTO.COM MIAMI GRAND PRIX 2025,Sprint Qualifying,2025-05-02 20:30:00,2025-05-02 21:14:00 32 | Miami,FORMULA 1 CRYPTO.COM MIAMI GRAND PRIX 2025,Sprint,2025-05-03 16:00:00,2025-05-03 17:00:00 33 | Miami,FORMULA 1 CRYPTO.COM MIAMI GRAND PRIX 2025,Qualifying,2025-05-03 20:00:00,2025-05-03 21:00:00 34 | Miami,FORMULA 1 CRYPTO.COM MIAMI GRAND PRIX 2025,Race,2025-05-04 20:00:00, 35 | Emilia-Romagna,FORMULA 1 AWS GRAN PREMIO DEL MADE IN ITALY E DELL'EMILIA-ROMAGNA 2025,Practice 1,2025-05-16 11:30:00,2025-05-16 12:30:00 36 | Emilia-Romagna,FORMULA 1 AWS GRAN PREMIO DEL MADE IN ITALY E DELL'EMILIA-ROMAGNA 2025,Practice 2,2025-05-16 15:00:00,2025-05-16 16:00:00 37 | Emilia-Romagna,FORMULA 1 AWS GRAN PREMIO DEL MADE IN ITALY E DELL'EMILIA-ROMAGNA 2025,Practice 3,2025-05-17 10:30:00,2025-05-17 11:30:00 38 | Emilia-Romagna,FORMULA 1 AWS GRAN PREMIO DEL MADE IN ITALY E DELL'EMILIA-ROMAGNA 2025,Qualifying,2025-05-17 14:00:00,2025-05-17 15:00:00 39 | Emilia-Romagna,FORMULA 1 AWS GRAN PREMIO DEL MADE IN ITALY E DELL'EMILIA-ROMAGNA 2025,Race,2025-05-18 13:00:00, 40 | Monaco,FORMULA 1 TAG HEUER GRAND PRIX DE MONACO 2025,Practice 1,2025-05-23 11:30:00,2025-05-23 12:30:00 41 | Monaco,FORMULA 1 TAG HEUER GRAND PRIX DE MONACO 2025,Practice 2,2025-05-23 15:00:00,2025-05-23 16:00:00 42 | Monaco,FORMULA 1 TAG HEUER GRAND PRIX DE MONACO 2025,Practice 3,2025-05-24 10:30:00,2025-05-24 11:30:00 43 | Monaco,FORMULA 1 TAG HEUER GRAND PRIX DE MONACO 2025,Qualifying,2025-05-24 14:00:00,2025-05-24 15:00:00 44 | Monaco,FORMULA 1 TAG HEUER GRAND PRIX DE MONACO 2025,Race,2025-05-25 13:00:00, 45 | Spanish,FORMULA 1 ARAMCO GRAN PREMIO DE ESPAÑA 2025,Practice 1,2025-05-30 11:30:00,2025-05-30 12:30:00 46 | Spanish,FORMULA 1 ARAMCO GRAN PREMIO DE ESPAÑA 2025,Practice 2,2025-05-30 15:00:00,2025-05-30 16:00:00 47 | Spanish,FORMULA 1 ARAMCO GRAN PREMIO DE ESPAÑA 2025,Practice 3,2025-05-31 10:30:00,2025-05-31 11:30:00 48 | Spanish,FORMULA 1 ARAMCO GRAN PREMIO DE ESPAÑA 2025,Qualifying,2025-05-31 14:00:00,2025-05-31 15:00:00 49 | Spanish,FORMULA 1 ARAMCO GRAN PREMIO DE ESPAÑA 2025,Race,2025-06-01 13:00:00, 50 | Canadian,FORMULA 1 PIRELLI GRAND PRIX DU CANADA 2025,Practice 1,2025-06-13 17:30:00,2025-06-13 18:30:00 51 | Canadian,FORMULA 1 PIRELLI GRAND PRIX DU CANADA 2025,Practice 2,2025-06-13 21:00:00,2025-06-13 22:00:00 52 | Canadian,FORMULA 1 PIRELLI GRAND PRIX DU CANADA 2025,Practice 3,2025-06-14 16:30:00,2025-06-14 17:30:00 53 | Canadian,FORMULA 1 PIRELLI GRAND PRIX DU CANADA 2025,Qualifying,2025-06-14 20:00:00,2025-06-14 21:00:00 54 | Canadian,FORMULA 1 PIRELLI GRAND PRIX DU CANADA 2025,Race,2025-06-15 18:00:00, 55 | Austrian,FORMULA 1 MSC CRUISES AUSTRIAN GRAND PRIX 2025,Practice 1,2025-06-27 11:30:00,2025-06-27 12:30:00 56 | Austrian,FORMULA 1 MSC CRUISES AUSTRIAN GRAND PRIX 2025,Practice 2,2025-06-27 15:00:00,2025-06-27 16:00:00 57 | Austrian,FORMULA 1 MSC CRUISES AUSTRIAN GRAND PRIX 2025,Practice 3,2025-06-28 10:30:00,2025-06-28 11:30:00 58 | Austrian,FORMULA 1 MSC CRUISES AUSTRIAN GRAND PRIX 2025,Qualifying,2025-06-28 14:00:00,2025-06-28 15:00:00 59 | Austrian,FORMULA 1 MSC CRUISES AUSTRIAN GRAND PRIX 2025,Race,2025-06-29 13:00:00, 60 | British,FORMULA 1 QATAR AIRWAYS BRITISH GRAND PRIX 2025,Practice 1,2025-07-04 11:30:00,2025-07-04 12:30:00 61 | British,FORMULA 1 QATAR AIRWAYS BRITISH GRAND PRIX 2025,Practice 2,2025-07-04 15:00:00,2025-07-04 16:00:00 62 | British,FORMULA 1 QATAR AIRWAYS BRITISH GRAND PRIX 2025,Practice 3,2025-07-05 10:30:00,2025-07-05 11:30:00 63 | British,FORMULA 1 QATAR AIRWAYS BRITISH GRAND PRIX 2025,Qualifying,2025-07-05 14:00:00,2025-07-05 15:00:00 64 | British,FORMULA 1 QATAR AIRWAYS BRITISH GRAND PRIX 2025,Race,2025-07-06 14:00:00, 65 | Belgian,FORMULA 1 MOËT & CHANDON BELGIAN GRAND PRIX 2025,Practice 1,2025-07-25 10:30:00,2025-07-25 11:30:00 66 | Belgian,FORMULA 1 MOËT & CHANDON BELGIAN GRAND PRIX 2025,Sprint Qualifying,2025-07-25 14:30:00,2025-07-25 15:14:00 67 | Belgian,FORMULA 1 MOËT & CHANDON BELGIAN GRAND PRIX 2025,Sprint,2025-07-26 10:00:00,2025-07-26 11:00:00 68 | Belgian,FORMULA 1 MOËT & CHANDON BELGIAN GRAND PRIX 2025,Qualifying,2025-07-26 14:00:00,2025-07-26 15:00:00 69 | Belgian,FORMULA 1 MOËT & CHANDON BELGIAN GRAND PRIX 2025,Race,2025-07-27 13:00:00, 70 | Hungarian,FORMULA 1 LENOVO HUNGARIAN GRAND PRIX 2025,Practice 1,2025-08-01 11:30:00,2025-08-01 12:30:00 71 | Hungarian,FORMULA 1 LENOVO HUNGARIAN GRAND PRIX 2025,Practice 2,2025-08-01 15:00:00,2025-08-01 16:00:00 72 | Hungarian,FORMULA 1 LENOVO HUNGARIAN GRAND PRIX 2025,Practice 3,2025-08-02 10:30:00,2025-08-02 11:30:00 73 | Hungarian,FORMULA 1 LENOVO HUNGARIAN GRAND PRIX 2025,Qualifying,2025-08-02 14:00:00,2025-08-02 15:00:00 74 | Hungarian,FORMULA 1 LENOVO HUNGARIAN GRAND PRIX 2025,Race,2025-08-03 13:00:00, 75 | Dutch,FORMULA 1 HEINEKEN DUTCH GRAND PRIX 2025,Practice 1,2025-08-29 10:30:00,2025-08-29 11:30:00 76 | Dutch,FORMULA 1 HEINEKEN DUTCH GRAND PRIX 2025,Practice 2,2025-08-29 14:00:00,2025-08-29 15:00:00 77 | Dutch,FORMULA 1 HEINEKEN DUTCH GRAND PRIX 2025,Practice 3,2025-08-30 09:30:00,2025-08-30 10:30:00 78 | Dutch,FORMULA 1 HEINEKEN DUTCH GRAND PRIX 2025,Qualifying,2025-08-30 13:00:00,2025-08-30 14:00:00 79 | Dutch,FORMULA 1 HEINEKEN DUTCH GRAND PRIX 2025,Race,2025-08-31 13:00:00, 80 | Italian,FORMULA 1 PIRELLI GRAN PREMIO D’ITALIA 2025,Practice 1,2025-09-05 11:30:00,2025-09-05 12:30:00 81 | Italian,FORMULA 1 PIRELLI GRAN PREMIO D’ITALIA 2025,Practice 2,2025-09-05 15:00:00,2025-09-05 16:00:00 82 | Italian,FORMULA 1 PIRELLI GRAN PREMIO D’ITALIA 2025,Practice 3,2025-09-06 10:30:00,2025-09-06 11:30:00 83 | Italian,FORMULA 1 PIRELLI GRAN PREMIO D’ITALIA 2025,Qualifying,2025-09-06 14:00:00,2025-09-06 15:00:00 84 | Italian,FORMULA 1 PIRELLI GRAN PREMIO D’ITALIA 2025,Race,2025-09-07 13:00:00, 85 | Azerbaijan,FORMULA 1 QATAR AIRWAYS AZERBAIJAN GRAND PRIX 2025,Practice 1,2025-09-19 08:30:00,2025-09-19 09:30:00 86 | Azerbaijan,FORMULA 1 QATAR AIRWAYS AZERBAIJAN GRAND PRIX 2025,Practice 2,2025-09-19 12:00:00,2025-09-19 13:00:00 87 | Azerbaijan,FORMULA 1 QATAR AIRWAYS AZERBAIJAN GRAND PRIX 2025,Practice 3,2025-09-20 08:30:00,2025-09-20 09:30:00 88 | Azerbaijan,FORMULA 1 QATAR AIRWAYS AZERBAIJAN GRAND PRIX 2025,Qualifying,2025-09-20 12:00:00,2025-09-20 13:00:00 89 | Azerbaijan,FORMULA 1 QATAR AIRWAYS AZERBAIJAN GRAND PRIX 2025,Race,2025-09-21 11:00:00, 90 | Singapore,FORMULA 1 SINGAPORE AIRLINES SINGAPORE GRAND PRIX 2025,Practice 1,2025-10-03 09:30:00,2025-10-03 10:30:00 91 | Singapore,FORMULA 1 SINGAPORE AIRLINES SINGAPORE GRAND PRIX 2025,Practice 2,2025-10-03 13:00:00,2025-10-03 14:00:00 92 | Singapore,FORMULA 1 SINGAPORE AIRLINES SINGAPORE GRAND PRIX 2025,Practice 3,2025-10-04 09:30:00,2025-10-04 10:30:00 93 | Singapore,FORMULA 1 SINGAPORE AIRLINES SINGAPORE GRAND PRIX 2025,Qualifying,2025-10-04 13:00:00,2025-10-04 14:00:00 94 | Singapore,FORMULA 1 SINGAPORE AIRLINES SINGAPORE GRAND PRIX 2025,Race,2025-10-05 12:00:00, 95 | United States,FORMULA 1 MSC CRUISES UNITED STATES GRAND PRIX 2025,Practice 1,2025-10-17 17:30:00,2025-10-17 18:30:00 96 | United States,FORMULA 1 MSC CRUISES UNITED STATES GRAND PRIX 2025,Sprint Qualifying,2025-10-17 21:30:00,2025-10-17 22:14:00 97 | United States,FORMULA 1 MSC CRUISES UNITED STATES GRAND PRIX 2025,Sprint,2025-10-18 17:00:00,2025-10-18 18:00:00 98 | United States,FORMULA 1 MSC CRUISES UNITED STATES GRAND PRIX 2025,Qualifying,2025-10-18 21:00:00,2025-10-18 22:00:00 99 | United States,FORMULA 1 MSC CRUISES UNITED STATES GRAND PRIX 2025,Race,2025-10-19 19:00:00, 100 | Mexico City,FORMULA 1 GRAN PREMIO DE LA CIUDAD DE MÉXICO 2025,Practice 1,2025-10-24 18:30:00,2025-10-24 19:30:00 101 | Mexico City,FORMULA 1 GRAN PREMIO DE LA CIUDAD DE MÉXICO 2025,Practice 2,2025-10-24 22:00:00,2025-10-24 23:00:00 102 | Mexico City,FORMULA 1 GRAN PREMIO DE LA CIUDAD DE MÉXICO 2025,Practice 3,2025-10-25 17:30:00,2025-10-25 18:30:00 103 | Mexico City,FORMULA 1 GRAN PREMIO DE LA CIUDAD DE MÉXICO 2025,Qualifying,2025-10-25 21:00:00,2025-10-25 22:00:00 104 | Mexico City,FORMULA 1 GRAN PREMIO DE LA CIUDAD DE MÉXICO 2025,Race,2025-10-26 20:00:00, 105 | Sao Paulo,FORMULA 1 MSC CRUISES GRANDE PRÊMIO DE SÃO PAULO 2025,Practice 1,2025-11-07 14:30:00,2025-11-07 15:30:00 106 | Sao Paulo,FORMULA 1 MSC CRUISES GRANDE PRÊMIO DE SÃO PAULO 2025,Sprint Qualifying,2025-11-07 18:30:00,2025-11-07 19:14:00 107 | Sao Paulo,FORMULA 1 MSC CRUISES GRANDE PRÊMIO DE SÃO PAULO 2025,Sprint,2025-11-08 14:00:00,2025-11-08 15:00:00 108 | Sao Paulo,FORMULA 1 MSC CRUISES GRANDE PRÊMIO DE SÃO PAULO 2025,Qualifying,2025-11-08 18:00:00,2025-11-08 19:00:00 109 | Sao Paulo,FORMULA 1 MSC CRUISES GRANDE PRÊMIO DE SÃO PAULO 2025,Race,2025-11-09 17:00:00, 110 | Las Vegas,FORMULA 1 HEINEKEN LAS VEGAS GRAND PRIX 2025,Practice 1,2025-11-21 00:30:00,2025-11-21 01:30:00 111 | Las Vegas,FORMULA 1 HEINEKEN LAS VEGAS GRAND PRIX 2025,Practice 2,2025-11-21 04:00:00,2025-11-21 05:00:00 112 | Las Vegas,FORMULA 1 HEINEKEN LAS VEGAS GRAND PRIX 2025,Practice 3,2025-11-22 00:30:00,2025-11-22 01:30:00 113 | Las Vegas,FORMULA 1 HEINEKEN LAS VEGAS GRAND PRIX 2025,Qualifying,2025-11-22 04:00:00,2025-11-22 05:00:00 114 | Las Vegas,FORMULA 1 HEINEKEN LAS VEGAS GRAND PRIX 2025,Race,2025-11-23 04:00:00, 115 | Qatar,FORMULA 1 QATAR AIRWAYS QATAR GRAND PRIX 2025,Practice 1,2025-11-28 13:30:00,2025-11-28 14:30:00 116 | Qatar,FORMULA 1 QATAR AIRWAYS QATAR GRAND PRIX 2025,Sprint Qualifying,2025-11-28 17:30:00,2025-11-28 18:14:00 117 | Qatar,FORMULA 1 QATAR AIRWAYS QATAR GRAND PRIX 2025,Sprint,2025-11-29 14:00:00,2025-11-29 15:00:00 118 | Qatar,FORMULA 1 QATAR AIRWAYS QATAR GRAND PRIX 2025,Qualifying,2025-11-29 18:00:00,2025-11-29 19:00:00 119 | Qatar,FORMULA 1 QATAR AIRWAYS QATAR GRAND PRIX 2025,Race,2025-11-30 16:00:00, 120 | Abu Dhabi,FORMULA 1 ETIHAD AIRWAYS ABU DHABI GRAND PRIX 2025 ,Practice 1,2025-12-05 09:30:00,2025-12-05 10:30:00 121 | Abu Dhabi,FORMULA 1 ETIHAD AIRWAYS ABU DHABI GRAND PRIX 2025 ,Practice 2,2025-12-05 13:00:00,2025-12-05 14:00:00 122 | Abu Dhabi,FORMULA 1 ETIHAD AIRWAYS ABU DHABI GRAND PRIX 2025 ,Practice 3,2025-12-06 10:30:00,2025-12-06 11:30:00 123 | Abu Dhabi,FORMULA 1 ETIHAD AIRWAYS ABU DHABI GRAND PRIX 2025 ,Qualifying,2025-12-06 14:00:00,2025-12-06 15:00:00 124 | Abu Dhabi,FORMULA 1 ETIHAD AIRWAYS ABU DHABI GRAND PRIX 2025 ,Race,2025-12-07 13:00:00, 125 | --------------------------------------------------------------------------------