timingtower 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (66) hide show
  1. timingtower-0.1.0/.github/workflows/ci.yml +39 -0
  2. timingtower-0.1.0/.github/workflows/publish.yml +41 -0
  3. timingtower-0.1.0/.gitignore +18 -0
  4. timingtower-0.1.0/.pre-commit-config.yaml +6 -0
  5. timingtower-0.1.0/.python-version +1 -0
  6. timingtower-0.1.0/CONTRIBUTING.md +66 -0
  7. timingtower-0.1.0/PKG-INFO +181 -0
  8. timingtower-0.1.0/README.md +165 -0
  9. timingtower-0.1.0/examples/average_rcm.py +112 -0
  10. timingtower-0.1.0/examples/car_and_position.py +70 -0
  11. timingtower-0.1.0/justfile +20 -0
  12. timingtower-0.1.0/pyproject.toml +103 -0
  13. timingtower-0.1.0/src/timingtower/__init__.py +3 -0
  14. timingtower-0.1.0/src/timingtower/api_handler/__init__.py +13 -0
  15. timingtower-0.1.0/src/timingtower/api_handler/client.py +565 -0
  16. timingtower-0.1.0/src/timingtower/api_handler/models/__init__.py +73 -0
  17. timingtower-0.1.0/src/timingtower/api_handler/models/archive_status.py +25 -0
  18. timingtower-0.1.0/src/timingtower/api_handler/models/base.py +181 -0
  19. timingtower-0.1.0/src/timingtower/api_handler/models/car_data.py +113 -0
  20. timingtower-0.1.0/src/timingtower/api_handler/models/championship_prediction.py +102 -0
  21. timingtower-0.1.0/src/timingtower/api_handler/models/circuit.py +7 -0
  22. timingtower-0.1.0/src/timingtower/api_handler/models/content_streams.py +79 -0
  23. timingtower-0.1.0/src/timingtower/api_handler/models/country.py +8 -0
  24. timingtower-0.1.0/src/timingtower/api_handler/models/current_tyres.py +90 -0
  25. timingtower-0.1.0/src/timingtower/api_handler/models/driver_list.py +90 -0
  26. timingtower-0.1.0/src/timingtower/api_handler/models/driver_race_info.py +253 -0
  27. timingtower-0.1.0/src/timingtower/api_handler/models/driver_tracker.py +109 -0
  28. timingtower-0.1.0/src/timingtower/api_handler/models/extrapolated_clock.py +66 -0
  29. timingtower-0.1.0/src/timingtower/api_handler/models/heartbeat.py +52 -0
  30. timingtower-0.1.0/src/timingtower/api_handler/models/lap_count.py +82 -0
  31. timingtower-0.1.0/src/timingtower/api_handler/models/lap_series.py +68 -0
  32. timingtower-0.1.0/src/timingtower/api_handler/models/meeting.py +138 -0
  33. timingtower-0.1.0/src/timingtower/api_handler/models/meeting_data.py +14 -0
  34. timingtower-0.1.0/src/timingtower/api_handler/models/overtake_series.py +83 -0
  35. timingtower-0.1.0/src/timingtower/api_handler/models/pit_lane_time_collection.py +99 -0
  36. timingtower-0.1.0/src/timingtower/api_handler/models/pit_stop.py +55 -0
  37. timingtower-0.1.0/src/timingtower/api_handler/models/pit_stop_series.py +80 -0
  38. timingtower-0.1.0/src/timingtower/api_handler/models/position.py +66 -0
  39. timingtower-0.1.0/src/timingtower/api_handler/models/race_control_messages.py +216 -0
  40. timingtower-0.1.0/src/timingtower/api_handler/models/season.py +84 -0
  41. timingtower-0.1.0/src/timingtower/api_handler/models/session.py +323 -0
  42. timingtower-0.1.0/src/timingtower/api_handler/models/session_data.py +84 -0
  43. timingtower-0.1.0/src/timingtower/api_handler/models/session_info.py +115 -0
  44. timingtower-0.1.0/src/timingtower/api_handler/models/session_status.py +28 -0
  45. timingtower-0.1.0/src/timingtower/api_handler/models/timing_app_data.py +134 -0
  46. timingtower-0.1.0/src/timingtower/api_handler/models/timing_data.py +215 -0
  47. timingtower-0.1.0/src/timingtower/api_handler/models/timing_stats.py +208 -0
  48. timingtower-0.1.0/src/timingtower/api_handler/models/tla_rcm.py +51 -0
  49. timingtower-0.1.0/src/timingtower/api_handler/models/top_three.py +81 -0
  50. timingtower-0.1.0/src/timingtower/api_handler/models/track_status.py +29 -0
  51. timingtower-0.1.0/src/timingtower/api_handler/models/tyre_stint_series.py +78 -0
  52. timingtower-0.1.0/src/timingtower/api_handler/models/weather_data.py +39 -0
  53. timingtower-0.1.0/src/timingtower/api_handler/models/weather_data_series.py +91 -0
  54. timingtower-0.1.0/src/timingtower/api_handler/registry.py +20 -0
  55. timingtower-0.1.0/src/timingtower/api_handler/settings.py +11 -0
  56. timingtower-0.1.0/tests/__init__.py +0 -0
  57. timingtower-0.1.0/tests/conftest.py +91 -0
  58. timingtower-0.1.0/tests/data/base_index.json +1 -0
  59. timingtower-0.1.0/tests/data/season_index.json +1 -0
  60. timingtower-0.1.0/tests/data/timing_data.json +1 -0
  61. timingtower-0.1.0/tests/data/timing_data.jsonStream +61660 -0
  62. timingtower-0.1.0/tests/helpers.py +248 -0
  63. timingtower-0.1.0/tests/test_async_client.py +264 -0
  64. timingtower-0.1.0/tests/test_sync_client.py +255 -0
  65. timingtower-0.1.0/todo.md +5 -0
  66. timingtower-0.1.0/uv.lock +856 -0
@@ -0,0 +1,39 @@
1
+ name: CI
2
+
3
+ on:
4
+ push:
5
+ branches: [main]
6
+ pull_request:
7
+ branches: [main]
8
+
9
+ jobs:
10
+ check:
11
+ runs-on: ubuntu-latest
12
+ steps:
13
+ - uses: actions/checkout@v4
14
+
15
+ - uses: actions/setup-python@v5
16
+ with:
17
+ python-version: "3.14"
18
+ allow-prereleases: true
19
+
20
+ - uses: astral-sh/setup-uv@v4
21
+ with:
22
+ enable-cache: true
23
+
24
+ - run: uv sync
25
+
26
+ - run: uv tool install ruff
27
+ - run: uv tool install basedpyright
28
+
29
+ - name: Format check
30
+ run: ruff format --check src tests
31
+
32
+ - name: Lint
33
+ run: ruff check src tests
34
+
35
+ - name: Type check
36
+ run: basedpyright
37
+
38
+ - name: Test
39
+ run: uv run pytest
@@ -0,0 +1,41 @@
1
+ name: Publish to PyPI
2
+
3
+ on:
4
+ push:
5
+ tags:
6
+ - "v*"
7
+
8
+ jobs:
9
+ build:
10
+ runs-on: ubuntu-latest
11
+ steps:
12
+ - uses: actions/checkout@v4
13
+
14
+ - name: Install uv
15
+ uses: astral-sh/setup-uv@v4
16
+
17
+ - name: Build
18
+ run: uv build
19
+
20
+ - name: Upload dist
21
+ uses: actions/upload-artifact@v4
22
+ with:
23
+ name: dist
24
+ path: dist/
25
+
26
+ publish:
27
+ needs: build
28
+ runs-on: ubuntu-latest
29
+ environment: pypi
30
+ permissions:
31
+ id-token: write
32
+
33
+ steps:
34
+ - name: Download dist
35
+ uses: actions/download-artifact@v4
36
+ with:
37
+ name: dist
38
+ path: dist/
39
+
40
+ - name: Publish to PyPI
41
+ uses: pypa/gh-action-pypi-publish@release/v1
@@ -0,0 +1,18 @@
1
+ # Python-generated files
2
+ __pycache__/
3
+ *.py[oc]
4
+ build/
5
+ dist/
6
+ wheels/
7
+ *.egg-info
8
+
9
+ # Virtual environments
10
+ .venv
11
+
12
+ .env
13
+
14
+ .pytest_cache/
15
+ .ruff_cache/
16
+
17
+ # Marimo
18
+ __marimo__/
@@ -0,0 +1,6 @@
1
+ repos:
2
+ - repo: https://github.com/astral-sh/ruff-pre-commit
3
+ rev: v0.15.6
4
+ hooks:
5
+ - id: ruff-format
6
+ args: [src, tests]
@@ -0,0 +1 @@
1
+ 3.14
@@ -0,0 +1,66 @@
1
+ # Contributing to timingtower
2
+
3
+ Thanks for your interest in contributing! timingtower is in early development (`0.x`), so the API is still evolving - but that also means there's plenty of room to shape things.
4
+
5
+ ## Setup
6
+ Ensure that [uv](https://docs.astral.sh/uv/) and [just](https://just.systems/man/en/installation.html) are installed.
7
+
8
+ ```bash
9
+ git clone https://github.com/bfoley12/timingtower.git
10
+ cd timingtower
11
+ just setup
12
+ ```
13
+
14
+ ## Development
15
+ Run tests, lint, and type check before submitting anything:
16
+
17
+ ```bash
18
+ just check
19
+ ```
20
+
21
+ Or individually:
22
+
23
+ ```bash
24
+ ruff format
25
+ ruff check
26
+ basedpyright
27
+ uv run pytest
28
+ ```
29
+
30
+ Both must pass clean.
31
+
32
+ ## What to work on
33
+
34
+ Check [open issues](https://github.com/bfoley12/timingtower/issues) for anything tagged `good first issue` or `help wanted`. Some areas where contributions are especially useful:
35
+
36
+ - **Test coverage** for existing feeds
37
+ - **Documentation** of existing classes and functions
38
+ - **Examples** of loading and basic transformations/uses
39
+
40
+ If you want to tackle something not listed, open an issue first so we can discuss the approach.
41
+
42
+ ## Pull requests
43
+
44
+ - Keep PRs focused - one feed, one fix, one feature.
45
+ - Include tests for new feeds or changed behavior.
46
+ - All tests and basedpyright must pass.
47
+ - Use clear commit messages. No strict format required, just be descriptive.
48
+
49
+ ## Code conventions
50
+
51
+ - **Pydantic v2** models for all feed .json.
52
+ - **Polars** for all DataFrames - no Pandas - especially in feed .jsonStream.
53
+ - **httpx** for HTTP.
54
+ - This will change to niquest when I have time to read up on it. Stay with httpx for now.
55
+ - Store raw strings at ingest (e.g., timestamps as `pl.String`). Parsing and casting belong in the transformation layer.
56
+ - Fail loudly on malformed data rather than silently skipping.
57
+
58
+ ## Architecture quick reference
59
+
60
+ - `fetch()` is a thin HTTP/URL-builder layer. It does not resolve sessions or meetings.
61
+ - `get()` handles all domain resolution (meeting, session, folder lookup) and calls `fetch()` with fully resolved paths.
62
+ - Feed models live in their own modules and are registered for class-based `client.get(F1DataContainer)` calls.
63
+
64
+ ## Questions?
65
+
66
+ Open a GitHub issue or discussion. There's no Discord/Slack yet.
@@ -0,0 +1,181 @@
1
+ Metadata-Version: 2.4
2
+ Name: timingtower
3
+ Version: 0.1.0
4
+ Summary: A thin, unopinionated wrapper of the F1 livetiming API. Provides typed values on a modern data stack.
5
+ Project-URL: Homepage, https://github.com/bfoley12/timingtower
6
+ Project-URL: Repository, https://github.com/bfoley12/timingtower
7
+ Author-email: Brendan Foley <brendanfoley1214@proton.me>
8
+ License: MIT
9
+ Requires-Python: >=3.14
10
+ Requires-Dist: httpx>=0.28.1
11
+ Requires-Dist: polars>=1.39.0
12
+ Requires-Dist: pydantic-settings>=2.13.1
13
+ Requires-Dist: pydantic>=2.12.5
14
+ Requires-Dist: tenacity>=9.1.4
15
+ Description-Content-Type: text/markdown
16
+
17
+ # timingtower
18
+ ## Motivation
19
+ 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.
20
+
21
+ [FastF1](https://github.com/theOehrly/Fast-F1) 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.
22
+
23
+ [OpenF1](https://github.com/br-g/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.
24
+
25
+ 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.
26
+
27
+ Like its namesake, timingtower sits close to the action - just one layer above the raw data.
28
+
29
+
30
+ ## Data Model
31
+ 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').
32
+
33
+ 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.
34
+
35
+ 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.
36
+
37
+ ## Getting Started
38
+
39
+ ### Install
40
+ ```bash
41
+ # uv
42
+ uv add timingtower
43
+
44
+ # pip
45
+ pip install timingtower
46
+ ```
47
+
48
+ ### Data Exploration
49
+ ```python
50
+ from timingtower import DirectClient
51
+
52
+ with DirectClient() as client:
53
+ # Available seasons
54
+ client.get_available_seasons()
55
+
56
+ # Get meetings from specific year
57
+ season = client.get_season(year=2026) # Using convenience method
58
+ season.meetings # Aliases season.keyframe.meetings for convenience
59
+
60
+ # Get sessions from specific meeting
61
+ meeting = season.get_meeting(meeting="Australia")
62
+ meeting.sessions
63
+
64
+ # Get a specific session
65
+ meeting.get_session(name="Qualifying")
66
+ # Using convenience properties
67
+ meeting.q # Qualifying
68
+
69
+ # Get session directly from client and look at available data
70
+ session_index = client.get(year=2026, meeting="Australia", session="Qualifying")
71
+ session_index.available_feeds
72
+ ```
73
+
74
+ ### Get data
75
+ #### Synchronously
76
+ ```python
77
+ from timingtower import DirectClient
78
+
79
+
80
+ # Can also use DirectClient as a long-lived instance:
81
+ # client = DirectClient()
82
+ # val = client.get(...)
83
+
84
+ with DirectClient() as client:
85
+ # Request CarData and Position from livetiming API
86
+ car_data = client.get(model="CarData", year=2026, meeting="Shanghai", session="Race") # Returns a Keyframe+Stream of CarData
87
+ position = client.get(model="Position", year=2026, meeting="Shanghai", session="Race") # Returns a Keyframe+Stream of Position
88
+
89
+ car_df = car_data.df
90
+ position_df = position.df
91
+
92
+ joined_df = car_df.join_asof(
93
+ position_df, on="timestamp", by="racing_number", strategy="nearest"
94
+ )
95
+ ```
96
+ #### Asynchronously
97
+
98
+ ```python
99
+ import asyncio
100
+ from timingtower import AsyncDirectClient
101
+
102
+ # Use the async client as a context manager
103
+ async with AsyncDirectClient() as client:
104
+ # Launch multiple jobs at the same time (this sends 4 requests - 1 for keyframe and 1 for stream in each client.get)
105
+ car_data, position = await asyncio.gather(
106
+ client.get(model="CarData", year=2024, meeting="Monza", session="Race"),
107
+ client.get(model="Position", year=2024, meeting="Monza", session="Race"),
108
+ )
109
+ car_df = car_data.df # Alias to car_data.stream.data
110
+ position_df = position.df
111
+
112
+ joined_df = car_df.join_asof(
113
+ position_df, on="timestamp", by="racing_number", strategy="nearest"
114
+ )
115
+ ```
116
+
117
+ ### Modify (Async)DirectClient settings
118
+ 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.
119
+ ```python
120
+ from timingtower import AsyncDirectClient, ClientSettings
121
+
122
+ settings = ClientSettings(
123
+ total_timeout = 10, # Set max allowed time to wait for client to 10s
124
+ request_timeout = 2 # Set max allowed time to wait per request to 2s
125
+ )
126
+
127
+ async_client = AsyncDirectClient(settings=settings)
128
+ # Note: For DirectClient, total_timeout does not have any effect at the moment.
129
+ # Each request is still limited to request timeout, so the max waited for is 3x request_timeout
130
+ sync_client = DirectClient(settings=settings)
131
+
132
+ sync_client.get(year=2026)
133
+ ```
134
+
135
+ ## Supported Feeds
136
+ <details>
137
+ <summary>Supported Feeds (32/32)</summary>
138
+
139
+ | Feed | Keyframe (`.json`) | Stream (`.jsonStream`) |
140
+ |------|:-----:|:-----:|
141
+ | [TimingDataF1](src/timingtower/api_handler/models/timing_data.py) | ✅ | ✅ |
142
+ | [TimingStats](src/timingtower/api_handler/models/timing_stats.py) | ✅ | ✅ |
143
+ | [TimingAppData](src/timingtower/api_handler/models/timing_app_data.py) | ✅ | ✅ |
144
+ | [LapSeries](src/timingtower/api_handler/models/lap_series.py) | ✅ | ✅ |
145
+ | [TyreStintSeries](src/timingtower/api_handler/models/tyre_stint_series.py) | ✅ | ✅ |
146
+ | [DriverTracker](src/timingtower/api_handler/models/driver_tracker.py) | ✅ | ✅ |
147
+ | [OvertakeSeries](src/timingtower/api_handler/models/overtake_series.py) | ✅ | ✅ |
148
+ | [PitStop](src/timingtower/api_handler/models/pit_stop.py) | ✅ | ✅ |
149
+ | [PitStopSeries](src/timingtower/api_handler/models/pit_stop_series.py) | ✅ | ✅ |
150
+ | [CurrentTyres](src/timingtower/api_handler/models/current_tyres.py) | ✅ | ✅ |
151
+ | [TimingData](src/timingtower/api_handler/models/timing_data.py) | ✅ | ✅ |
152
+ | [LapCount](src/timingtower/api_handler/models/lap_count.py) | ✅ | ✅ |
153
+ | [TopThree](src/timingtower/api_handler/models/top_three.py) | ✅ | ✅ |
154
+ | [CarData.z](src/timingtower/api_handler/models/car_data.py) | ✅ | ✅ |
155
+ | [Position.z](src/timingtower/api_handler/models/position.py) | ✅ | ✅ |
156
+ | [RaceControlMessages](src/timingtower/api_handler/models/race_control_messages.py) | ✅ | ✅ |
157
+ | [TrackStatus](src/timingtower/api_handler/models/track_status.py) | ✅ | ✅ |
158
+ | [TlaRcm](src/timingtower/api_handler/models/tla_rcm.py) | ✅ | ✅ |
159
+ | [TeamRadio](src/timingtower/api_handler/models/team_radio.py) | ✅ | ✅ |
160
+ | [WeatherData](src/timingtower/api_handler/models/weather_data.py) | ✅ | ✅ |
161
+ | [WeatherDataSeries](src/timingtower/api_handler/models/weather_data_series.py) | ✅ | ✅ |
162
+ | [DriverList](src/timingtower/api_handler/models/driver_list.py) | ✅ | ✅ |
163
+ | [PitLaneTimeCollection](src/timingtower/api_handler/models/pit_lane_time_collection.py) | ✅ | ✅ |
164
+ | [ChampionshipPrediction](src/timingtower/api_handler/models/championship_prediction.py) | ✅ | ✅ |
165
+ | [DriverRaceInfo](src/timingtower/api_handler/models/driver_race_info.py) | ✅ | ✅ |
166
+ | [SessionInfo](src/timingtower/api_handler/models/session_info.py) | ✅ | ✅ |
167
+ | [SessionData](src/timingtower/api_handler/models/session_data.py) | ✅ | ✅ |
168
+ | [SessionStatus](src/timingtower/api_handler/models/session_status.py) | ✅ | ✅ |
169
+ | [ArchiveStatus](src/timingtower/api_handler/models/archive_status.py) | ✅ | ✅ |
170
+ | [Heartbeat](src/timingtower/api_handler/models/heartbeat.py) | ✅ | ✅ |
171
+ | [ExtrapolatedClock](src/timingtower/api_handler/models/extrapolated_clock.py) | ✅ | ✅ |
172
+ | [ContentStreams](src/timingtower/api_handler/models/content_streams.py) | ✅ | ✅ |
173
+ | [AudioStreams](src/timingtower/api_handler/models/audio_streams.py) | ✅ | ✅ |
174
+
175
+ </details>
176
+
177
+ ## Contributing
178
+ 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.
179
+
180
+ ## Disclaimer
181
+ 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.
@@ -0,0 +1,165 @@
1
+ # timingtower
2
+ ## Motivation
3
+ 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.
4
+
5
+ [FastF1](https://github.com/theOehrly/Fast-F1) 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.
6
+
7
+ [OpenF1](https://github.com/br-g/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.
8
+
9
+ 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.
10
+
11
+ Like its namesake, timingtower sits close to the action - just one layer above the raw data.
12
+
13
+
14
+ ## Data Model
15
+ 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').
16
+
17
+ 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.
18
+
19
+ 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.
20
+
21
+ ## Getting Started
22
+
23
+ ### Install
24
+ ```bash
25
+ # uv
26
+ uv add timingtower
27
+
28
+ # pip
29
+ pip install timingtower
30
+ ```
31
+
32
+ ### Data Exploration
33
+ ```python
34
+ from timingtower import DirectClient
35
+
36
+ with DirectClient() as client:
37
+ # Available seasons
38
+ client.get_available_seasons()
39
+
40
+ # Get meetings from specific year
41
+ season = client.get_season(year=2026) # Using convenience method
42
+ season.meetings # Aliases season.keyframe.meetings for convenience
43
+
44
+ # Get sessions from specific meeting
45
+ meeting = season.get_meeting(meeting="Australia")
46
+ meeting.sessions
47
+
48
+ # Get a specific session
49
+ meeting.get_session(name="Qualifying")
50
+ # Using convenience properties
51
+ meeting.q # Qualifying
52
+
53
+ # Get session directly from client and look at available data
54
+ session_index = client.get(year=2026, meeting="Australia", session="Qualifying")
55
+ session_index.available_feeds
56
+ ```
57
+
58
+ ### Get data
59
+ #### Synchronously
60
+ ```python
61
+ from timingtower import DirectClient
62
+
63
+
64
+ # Can also use DirectClient as a long-lived instance:
65
+ # client = DirectClient()
66
+ # val = client.get(...)
67
+
68
+ with DirectClient() as client:
69
+ # Request CarData and Position from livetiming API
70
+ car_data = client.get(model="CarData", year=2026, meeting="Shanghai", session="Race") # Returns a Keyframe+Stream of CarData
71
+ position = client.get(model="Position", year=2026, meeting="Shanghai", session="Race") # Returns a Keyframe+Stream of Position
72
+
73
+ car_df = car_data.df
74
+ position_df = position.df
75
+
76
+ joined_df = car_df.join_asof(
77
+ position_df, on="timestamp", by="racing_number", strategy="nearest"
78
+ )
79
+ ```
80
+ #### Asynchronously
81
+
82
+ ```python
83
+ import asyncio
84
+ from timingtower import AsyncDirectClient
85
+
86
+ # Use the async client as a context manager
87
+ async with AsyncDirectClient() as client:
88
+ # Launch multiple jobs at the same time (this sends 4 requests - 1 for keyframe and 1 for stream in each client.get)
89
+ car_data, position = await asyncio.gather(
90
+ client.get(model="CarData", year=2024, meeting="Monza", session="Race"),
91
+ client.get(model="Position", year=2024, meeting="Monza", session="Race"),
92
+ )
93
+ car_df = car_data.df # Alias to car_data.stream.data
94
+ position_df = position.df
95
+
96
+ joined_df = car_df.join_asof(
97
+ position_df, on="timestamp", by="racing_number", strategy="nearest"
98
+ )
99
+ ```
100
+
101
+ ### Modify (Async)DirectClient settings
102
+ 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.
103
+ ```python
104
+ from timingtower import AsyncDirectClient, ClientSettings
105
+
106
+ settings = ClientSettings(
107
+ total_timeout = 10, # Set max allowed time to wait for client to 10s
108
+ request_timeout = 2 # Set max allowed time to wait per request to 2s
109
+ )
110
+
111
+ async_client = AsyncDirectClient(settings=settings)
112
+ # Note: For DirectClient, total_timeout does not have any effect at the moment.
113
+ # Each request is still limited to request timeout, so the max waited for is 3x request_timeout
114
+ sync_client = DirectClient(settings=settings)
115
+
116
+ sync_client.get(year=2026)
117
+ ```
118
+
119
+ ## Supported Feeds
120
+ <details>
121
+ <summary>Supported Feeds (32/32)</summary>
122
+
123
+ | Feed | Keyframe (`.json`) | Stream (`.jsonStream`) |
124
+ |------|:-----:|:-----:|
125
+ | [TimingDataF1](src/timingtower/api_handler/models/timing_data.py) | ✅ | ✅ |
126
+ | [TimingStats](src/timingtower/api_handler/models/timing_stats.py) | ✅ | ✅ |
127
+ | [TimingAppData](src/timingtower/api_handler/models/timing_app_data.py) | ✅ | ✅ |
128
+ | [LapSeries](src/timingtower/api_handler/models/lap_series.py) | ✅ | ✅ |
129
+ | [TyreStintSeries](src/timingtower/api_handler/models/tyre_stint_series.py) | ✅ | ✅ |
130
+ | [DriverTracker](src/timingtower/api_handler/models/driver_tracker.py) | ✅ | ✅ |
131
+ | [OvertakeSeries](src/timingtower/api_handler/models/overtake_series.py) | ✅ | ✅ |
132
+ | [PitStop](src/timingtower/api_handler/models/pit_stop.py) | ✅ | ✅ |
133
+ | [PitStopSeries](src/timingtower/api_handler/models/pit_stop_series.py) | ✅ | ✅ |
134
+ | [CurrentTyres](src/timingtower/api_handler/models/current_tyres.py) | ✅ | ✅ |
135
+ | [TimingData](src/timingtower/api_handler/models/timing_data.py) | ✅ | ✅ |
136
+ | [LapCount](src/timingtower/api_handler/models/lap_count.py) | ✅ | ✅ |
137
+ | [TopThree](src/timingtower/api_handler/models/top_three.py) | ✅ | ✅ |
138
+ | [CarData.z](src/timingtower/api_handler/models/car_data.py) | ✅ | ✅ |
139
+ | [Position.z](src/timingtower/api_handler/models/position.py) | ✅ | ✅ |
140
+ | [RaceControlMessages](src/timingtower/api_handler/models/race_control_messages.py) | ✅ | ✅ |
141
+ | [TrackStatus](src/timingtower/api_handler/models/track_status.py) | ✅ | ✅ |
142
+ | [TlaRcm](src/timingtower/api_handler/models/tla_rcm.py) | ✅ | ✅ |
143
+ | [TeamRadio](src/timingtower/api_handler/models/team_radio.py) | ✅ | ✅ |
144
+ | [WeatherData](src/timingtower/api_handler/models/weather_data.py) | ✅ | ✅ |
145
+ | [WeatherDataSeries](src/timingtower/api_handler/models/weather_data_series.py) | ✅ | ✅ |
146
+ | [DriverList](src/timingtower/api_handler/models/driver_list.py) | ✅ | ✅ |
147
+ | [PitLaneTimeCollection](src/timingtower/api_handler/models/pit_lane_time_collection.py) | ✅ | ✅ |
148
+ | [ChampionshipPrediction](src/timingtower/api_handler/models/championship_prediction.py) | ✅ | ✅ |
149
+ | [DriverRaceInfo](src/timingtower/api_handler/models/driver_race_info.py) | ✅ | ✅ |
150
+ | [SessionInfo](src/timingtower/api_handler/models/session_info.py) | ✅ | ✅ |
151
+ | [SessionData](src/timingtower/api_handler/models/session_data.py) | ✅ | ✅ |
152
+ | [SessionStatus](src/timingtower/api_handler/models/session_status.py) | ✅ | ✅ |
153
+ | [ArchiveStatus](src/timingtower/api_handler/models/archive_status.py) | ✅ | ✅ |
154
+ | [Heartbeat](src/timingtower/api_handler/models/heartbeat.py) | ✅ | ✅ |
155
+ | [ExtrapolatedClock](src/timingtower/api_handler/models/extrapolated_clock.py) | ✅ | ✅ |
156
+ | [ContentStreams](src/timingtower/api_handler/models/content_streams.py) | ✅ | ✅ |
157
+ | [AudioStreams](src/timingtower/api_handler/models/audio_streams.py) | ✅ | ✅ |
158
+
159
+ </details>
160
+
161
+ ## Contributing
162
+ 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.
163
+
164
+ ## Disclaimer
165
+ 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.
@@ -0,0 +1,112 @@
1
+ import marimo
2
+
3
+ __generated_with = "0.22.0"
4
+ app = marimo.App(width="full")
5
+
6
+
7
+ @app.cell
8
+ def _():
9
+ import plotly.express as px
10
+ import polars as pl
11
+
12
+ from timingtower import DirectClient
13
+
14
+ return (DirectClient,)
15
+
16
+
17
+ @app.cell
18
+ def _(DirectClient):
19
+ client = DirectClient()
20
+ return (client,)
21
+
22
+
23
+ @app.cell
24
+ def _(client):
25
+ season = client.get_season(year=2026)
26
+ return (season,)
27
+
28
+
29
+ @app.cell
30
+ def _(season) -> None:
31
+ season.keyframe.meetings[0].sessions[0]
32
+ return
33
+
34
+
35
+ @app.cell
36
+ def _(client) -> None:
37
+ client.get(year=2026, model="Season").keyframe.meetings
38
+ return
39
+
40
+
41
+ @app.cell
42
+ def _(client, season) -> None:
43
+ data_list = []
44
+ for meeting in season.keyframe.meetings:
45
+ for session in meeting.sessions:
46
+ print(session)
47
+ data_list.append(client.get(session=session, model="RaceControlMessages"))
48
+ return
49
+
50
+
51
+ @app.cell
52
+ def _(client) -> None:
53
+ client.get(year=2026, meeting="Shanghai", session="Race", model="TlaRcm")
54
+ return
55
+
56
+
57
+ @app.cell
58
+ async def _():
59
+ import asyncio
60
+
61
+ from timingtower import AsyncDirectClient
62
+
63
+ # Use the async client as a context manager
64
+ async with AsyncDirectClient() as async_client:
65
+ # Launch multiple jobs at the same time (this sends 4 requests - 1 for keyframe and 1 for stream in each client.get)
66
+ car_data, _position = await asyncio.gather(
67
+ async_client.get("CarData", year=2024, meeting="Monza", session="Race"),
68
+ async_client.get("Position", year=2024, meeting="Monza", session="Race"),
69
+ )
70
+ car_df = car_data.df
71
+ position_df = car_data.df
72
+
73
+ car_df.join_asof(
74
+ position_df, on="timestamp", by="racing_number", strategy="nearest"
75
+ )
76
+ return (AsyncDirectClient,)
77
+
78
+
79
+ @app.cell
80
+ async def _(AsyncDirectClient) -> None:
81
+ from pprint import pprint
82
+
83
+ async with AsyncDirectClient() as _client:
84
+ pprint(await _client.get_available_seasons())
85
+ return
86
+
87
+
88
+ @app.cell
89
+ def _(DirectClient) -> None:
90
+ with DirectClient() as _client:
91
+ # Available years
92
+ _client.get_available_seasons()
93
+
94
+ # Get meetings from specific year
95
+ _season = _client.get_season(year=2026) # Using convenience method
96
+ _season.keyframe.meetings
97
+ _season.meetings # Aliases season.keyframe.meetings for convenience
98
+
99
+ # Get sessions from specific meeting
100
+ _meeting = _season.get_meeting(name="Australia")
101
+ _meeting.sessions
102
+
103
+ # Get a specific session
104
+ _meeting.get_session(name="Qualifying")
105
+ # Using convenience properties
106
+ _meeting.fp1 # Free Practice 1
107
+ _meeting.q # Qualifying
108
+ return
109
+
110
+
111
+ if __name__ == "__main__":
112
+ app.run()