Skip to main content

A thin, unopinionated wrapper of the F1 livetiming API. Provides typed values on a modern data stack.

Project description

timingtower

Motivation

The F1 livetiming API at https://livetiming.formula1.com exposes a rich set of data feeds - car telemetry, position data, timing, tyre stints, race control messages, weather, pit lane times, and more - going back to 2018. Existing tools access parts of this API but make tradeoffs that aren't right for every use case.

FastF1 is an excellent analysis-first library, but it consumes raw feeds internally and surfaces its own higher-level abstractions built on Pandas. While they deliver high quality, processed views of the data, some power-analysts may want to handle the raw data directly.

OpenF1 is a hosted REST API that proxies a subset of the feed data into simplified JSON endpoints. It's convenient for quick queries but only covers 2023 onward and restructures the data into its own schema.

timingtower takes a different approach. It maps the livetiming feeds directly, preserving their original structure while wrapping them in Pydantic V2 models and Polars DataFrames. The raw data stays intact - timingtower doesn't decide what's relevant or how fields should be combined. It gives you typed, validated access to the feeds as they exist, on a modern stack that's faster and more ergonomic than Pandas.

Like its namesake, timingtower sits close to the action - just one layer above the raw data.

Data Model

The livetiming API is a hierarchical API that progressively provides more information. The base of the API is https://livetiming.formula1.com/static (from here on, we will refer to that as root - '/' and reference the API layers as starting from '/'). Most layers provide an Index.json that we can learn about the available endpoints to dive deeper into. For example, /Index.json shows the current year (why it doesn't show every available year is not known to me). /{year}/Index.json provides a list of meetings (race or testing weekends) with their relevant sessions (FP1, Qualifying, etc.). Oddly, /{year}/{meeting} does not provide an Index.json file. /{year}/{meeting}/{session}/Index.json displays all available 'feeds' for a session. A 'feed' refers to a single collection of data submitted either as a .json file (a 'keyframe') or a .jsonStream (aka .jsonl) file (a 'stream').

A keyframe represents a static json, while streams are dynamic, partial updates throughout the course of a session. This makes Pydantic a natural package to model the keyframe. Streams are timestamped by the session time as a duration. Polars is used to represent the streamed data.

Consistency is prioritized across returned models. Each model contains a 'keyframe' and 'stream' (with the exceptions of Season, Meeting, and Session, which offer only a keyframe since they only have an Index.json file). It is up to the user to determine whether they need the data from the stream or the keyframe, as some feeds are represented better in the stream (ie. CarData and Position keyframes are not useful for many applications). In anticipation of many use cases relying on the stream's dataframe, a property is provided directly on the F1DataContainer object: obj.df.

Getting Started

Install

# uv
uv add timingtower

# pip
pip install timingtower

Data Exploration

from timingtower import DirectClient

with DirectClient() as client:
    # Available seasons
    client.get_available_seasons()

    # Get meetings from specific year
    season = client.get_season(year=2026) # Using convenience method
    season.meetings # Aliases season.keyframe.meetings for convenience

    # Get sessions from specific meeting
    meeting = season.get_meeting(meeting="Australia")
    meeting.sessions

    # Get a specific session
    meeting.get_session(name="Qualifying")
    # Using convenience properties
    meeting.q # Qualifying

    # Get session directly from client and look at available data
    session_index = client.get(year=2026, meeting="Australia", session="Qualifying")
    session_index.available_feeds

Get data

Synchronously

from timingtower import DirectClient


# Can also use DirectClient as a long-lived instance:
# client = DirectClient()
# val = client.get(...)

with DirectClient() as client:
    # Request CarData and Position from livetiming API
    car_data = client.get(model="CarData", year=2026, meeting="Shanghai", session="Race") # Returns a Keyframe+Stream of CarData
    position = client.get(model="Position", year=2026, meeting="Shanghai", session="Race") # Returns a Keyframe+Stream of Position

car_df = car_data.df
position_df = position.df

joined_df = car_df.join_asof(
    position_df, on="timestamp", by="racing_number", strategy="nearest"
)

Asynchronously

import asyncio
from timingtower import AsyncDirectClient

# Use the async client as a context manager
async with AsyncDirectClient() as client:
    # Launch multiple jobs at the same time (this sends 4 requests - 1 for keyframe and 1 for stream in each client.get)
    car_data, position = await asyncio.gather(
        client.get(model="CarData", year=2024, meeting="Monza", session="Race"),
        client.get(model="Position", year=2024, meeting="Monza", session="Race"),
    )
car_df = car_data.df # Alias to car_data.stream.data
position_df = position.df

joined_df = car_df.join_asof(
    position_df, on="timestamp", by="racing_number", strategy="nearest"
)

Modify (Async)DirectClient settings

Since your connection to livetiming may be different from the testing environment, we allow for customization of ClientSettings. Check settings::ClientSettings for what can be changed.

from timingtower import AsyncDirectClient, ClientSettings

settings = ClientSettings(
    total_timeout = 10, # Set max allowed time to wait for client to 10s
    request_timeout = 2 # Set max allowed time to wait per request to 2s
)

async_client = AsyncDirectClient(settings=settings)
# Note: For DirectClient, total_timeout does not have any effect at the moment.
# Each request is still limited to request timeout, so the max waited for is 3x request_timeout
sync_client = DirectClient(settings=settings)

sync_client.get(year=2026)

Supported Feeds

Supported Feeds (32/32)
Feed Keyframe (.json) Stream (.jsonStream)
TimingDataF1
TimingStats
TimingAppData
LapSeries
TyreStintSeries
DriverTracker
OvertakeSeries
PitStop
PitStopSeries
CurrentTyres
TimingData
LapCount
TopThree
CarData.z
Position.z
RaceControlMessages
TrackStatus
TlaRcm
TeamRadio
WeatherData
WeatherDataSeries
DriverList
PitLaneTimeCollection
ChampionshipPrediction
DriverRaceInfo
SessionInfo
SessionData
SessionStatus
ArchiveStatus
Heartbeat
ExtrapolatedClock
ContentStreams
AudioStreams

Contributing

timingtower is in early development and contributions are welcome - whether that's new feed implementations, tests, docs, or bug reports. See [CONTRIBUTING.md] for setup instructions and guidelines.

Disclaimer

timingtower is an unofficial project and is not affiliated with Formula 1 companies. All F1-related trademarks are owned by Formula One Licensing B.V.

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

timingtower-0.1.0.tar.gz (707.6 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

timingtower-0.1.0-py3-none-any.whl (54.3 kB view details)

Uploaded Python 3

File details

Details for the file timingtower-0.1.0.tar.gz.

File metadata

  • Download URL: timingtower-0.1.0.tar.gz
  • Upload date:
  • Size: 707.6 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.12

File hashes

Hashes for timingtower-0.1.0.tar.gz
Algorithm Hash digest
SHA256 e9a823a46e8ba0ef649cfde16fe0dee814297f6fd0308f95f7f2f879b74b6ae5
MD5 a836cbe940131d214fd737bdfd0eac3d
BLAKE2b-256 f91eddb56dccd43a34bdc2e2286f50fce7bc48165a50815a656b901fe8b42415

See more details on using hashes here.

Provenance

The following attestation bundles were made for timingtower-0.1.0.tar.gz:

Publisher: publish.yml on bfoley12/timingtower

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file timingtower-0.1.0-py3-none-any.whl.

File metadata

  • Download URL: timingtower-0.1.0-py3-none-any.whl
  • Upload date:
  • Size: 54.3 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.12

File hashes

Hashes for timingtower-0.1.0-py3-none-any.whl
Algorithm Hash digest
SHA256 c81d6dffd674a567fd5b24658fc30e8c6f423c24f9d4b4c705b942a4ecc2e779
MD5 d54b8aac1f223d0fe1d9e93cda126e05
BLAKE2b-256 5850b2fe1983298077b0c82314ae938ce76043deae0dc5b97bd385285055f03d

See more details on using hashes here.

Provenance

The following attestation bundles were made for timingtower-0.1.0-py3-none-any.whl:

Publisher: publish.yml on bfoley12/timingtower

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page