fastnascar 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.
- fastnascar-0.1.0/LICENSE +21 -0
- fastnascar-0.1.0/MANIFEST.in +13 -0
- fastnascar-0.1.0/PKG-INFO +209 -0
- fastnascar-0.1.0/README.md +187 -0
- fastnascar-0.1.0/examples/analyze_race.py +9 -0
- fastnascar-0.1.0/examples/hocevar_bristol_2026.ps1 +26 -0
- fastnascar-0.1.0/examples/hocevar_bristol_2026.py +123 -0
- fastnascar-0.1.0/examples/live_timing.py +6 -0
- fastnascar-0.1.0/pyproject.toml +31 -0
- fastnascar-0.1.0/setup.cfg +4 -0
- fastnascar-0.1.0/src/fastnascar/__init__.py +13 -0
- fastnascar-0.1.0/src/fastnascar/_time.py +18 -0
- fastnascar-0.1.0/src/fastnascar/api.py +155 -0
- fastnascar-0.1.0/src/fastnascar/cache.py +56 -0
- fastnascar-0.1.0/src/fastnascar/core.py +440 -0
- fastnascar-0.1.0/src/fastnascar/exceptions.py +25 -0
- fastnascar-0.1.0/src/fastnascar/live.py +212 -0
- fastnascar-0.1.0/src/fastnascar/plotting.py +35 -0
- fastnascar-0.1.0/src/fastnascar.egg-info/PKG-INFO +209 -0
- fastnascar-0.1.0/src/fastnascar.egg-info/SOURCES.txt +27 -0
- fastnascar-0.1.0/src/fastnascar.egg-info/dependency_links.txt +1 -0
- fastnascar-0.1.0/src/fastnascar.egg-info/requires.txt +9 -0
- fastnascar-0.1.0/src/fastnascar.egg-info/top_level.txt +1 -0
- fastnascar-0.1.0/tests/fixtures/README.md +16 -0
- fastnascar-0.1.0/tests/fixtures/lap-times.json +2199 -0
- fastnascar-0.1.0/tests/fixtures/live-feed.json +272 -0
- fastnascar-0.1.0/tests/fixtures/race_list_basic.json +332 -0
- fastnascar-0.1.0/tests/fixtures/weekend-feed.json +958 -0
- fastnascar-0.1.0/tests/test_fastnascar.py +227 -0
fastnascar-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 FastNASCAR contributors
|
|
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.
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
include README.md LICENSE pyproject.toml
|
|
2
|
+
recursive-include src *.py
|
|
3
|
+
recursive-include examples *.py *.ps1
|
|
4
|
+
recursive-include tests *.py *.json *.md
|
|
5
|
+
prune build
|
|
6
|
+
prune dist
|
|
7
|
+
prune outputs
|
|
8
|
+
prune .plot-deps
|
|
9
|
+
prune .matplotlib
|
|
10
|
+
prune .fastnascar-cache
|
|
11
|
+
prune .release-tools
|
|
12
|
+
prune .release-install
|
|
13
|
+
global-exclude __pycache__ *.py[cod]
|
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: fastnascar
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Pandas-based NASCAR sessions, laps, results, and live timing
|
|
5
|
+
License-Expression: MIT
|
|
6
|
+
Keywords: nascar,motorsport,timing,pandas
|
|
7
|
+
Classifier: Development Status :: 3 - Alpha
|
|
8
|
+
Classifier: Programming Language :: Python :: 3
|
|
9
|
+
Classifier: Operating System :: OS Independent
|
|
10
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
11
|
+
Requires-Python: >=3.10
|
|
12
|
+
Description-Content-Type: text/markdown
|
|
13
|
+
License-File: LICENSE
|
|
14
|
+
Requires-Dist: pandas<4,>=2.2
|
|
15
|
+
Provides-Extra: plot
|
|
16
|
+
Requires-Dist: matplotlib>=3.8; extra == "plot"
|
|
17
|
+
Provides-Extra: dev
|
|
18
|
+
Requires-Dist: pytest>=8; extra == "dev"
|
|
19
|
+
Requires-Dist: build>=1; extra == "dev"
|
|
20
|
+
Requires-Dist: twine>=6; extra == "dev"
|
|
21
|
+
Dynamic: license-file
|
|
22
|
+
|
|
23
|
+
# FastNASCAR
|
|
24
|
+
|
|
25
|
+
FastNASCAR provides NASCAR Cup lap times, results, and live timing in pandas.
|
|
26
|
+
Select a race, load the session, and work with its lap data:
|
|
27
|
+
|
|
28
|
+
```python
|
|
29
|
+
import fastnascar
|
|
30
|
+
|
|
31
|
+
session = fastnascar.get_session(2026, race_id=5626).load()
|
|
32
|
+
hocevar = session.laps.pick_driver("77")
|
|
33
|
+
print(hocevar.pick_fastest())
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
Early release. Race laps and weekend results work; historical practice and
|
|
37
|
+
qualifying laps aren't supported yet. No throttle, brake, RPM, or GPS telemetry.
|
|
38
|
+
This project is unaffiliated with NASCAR.
|
|
39
|
+
|
|
40
|
+
## Install
|
|
41
|
+
|
|
42
|
+
Python 3.10+ is required. From a checkout:
|
|
43
|
+
|
|
44
|
+
```sh
|
|
45
|
+
python -m pip install -e .
|
|
46
|
+
python -m pip install -e ".[plot]" # optional matplotlib charts
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
## Pick a race
|
|
50
|
+
|
|
51
|
+
```python
|
|
52
|
+
schedule = fastnascar.get_event_schedule(2026)
|
|
53
|
+
print(schedule[["RoundNumber", "RaceId", "EventName", "EventDate"]])
|
|
54
|
+
|
|
55
|
+
session = fastnascar.get_session(2026, 29)
|
|
56
|
+
session = fastnascar.get_session(2026, "Bass Pro Shops Night Race")
|
|
57
|
+
session = fastnascar.get_session(2026, race_id=5626)
|
|
58
|
+
session.load()
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
Rounds count points races in scheduled order. Use
|
|
62
|
+
`include_exhibition=True` to include exhibitions in the schedule; select those
|
|
63
|
+
by name or race ID. Repeated names raise `AmbiguousEventError`, with the matching
|
|
64
|
+
race IDs. Schedule dates retain NASCAR's local timestamps without a timezone.
|
|
65
|
+
|
|
66
|
+
## Work with laps
|
|
67
|
+
|
|
68
|
+
```python
|
|
69
|
+
hocevar = session.laps.pick_driver("77")
|
|
70
|
+
print(hocevar.pick_fastest())
|
|
71
|
+
print(hocevar.pick_laps([1, 2, 3]))
|
|
72
|
+
print(hocevar.pick_quicklaps().LapTime.dt.total_seconds().mean())
|
|
73
|
+
print(session.results[["Position", "DriverNumber", "FullName", "Points"]])
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
| Table | Columns |
|
|
77
|
+
| --- | --- |
|
|
78
|
+
| `session.laps` | Driver, DriverNumber, DriverId, LapNumber, LapTime, Speed, Position |
|
|
79
|
+
| `session.results` | DriverNumber, DriverId, FullName, TeamName, Manufacturer, Position, GridPosition, Laps, LapsLed, Points, Status, BestLapTime |
|
|
80
|
+
| `session.track_status` | LeaderLapNumber, FlagState |
|
|
81
|
+
|
|
82
|
+
Lap times are pandas timedeltas. Speeds are lap-average mph. Car numbers are
|
|
83
|
+
strings; missing values stay missing. Lap zero is excluded because it records
|
|
84
|
+
the starting order.
|
|
85
|
+
|
|
86
|
+
`pick_drivers()` accepts one or more car numbers or exact full names.
|
|
87
|
+
`pick_quicklaps(1.07)` keeps times below 107% of the fastest lap in the current
|
|
88
|
+
selection. Select a driver first to use that driver's pace. The filter doesn't
|
|
89
|
+
identify cautions or pit laps.
|
|
90
|
+
|
|
91
|
+
Flags refer to leader laps and stay separate from car laps. A lapped car's lap
|
|
92
|
+
number doesn't line up with the leader's. FlagState contains raw feed codes.
|
|
93
|
+
|
|
94
|
+
## Practice and qualifying
|
|
95
|
+
|
|
96
|
+
```python
|
|
97
|
+
practice = fastnascar.get_session(2026, race_id=5626, identifier="P")
|
|
98
|
+
practice.load(laps=False)
|
|
99
|
+
qualifying = fastnascar.get_session(2026, race_id=5626, identifier="Q")
|
|
100
|
+
qualifying.load(laps=False)
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
These sessions provide results and best lap times. When a weekend has several
|
|
104
|
+
runs of the same type, pass the exact `run_name` from the weekend feed.
|
|
105
|
+
Qualifying results may cover only the final round.
|
|
106
|
+
|
|
107
|
+
## Plot a race
|
|
108
|
+
|
|
109
|
+
```python
|
|
110
|
+
from fastnascar.plotting import plot_lap_times
|
|
111
|
+
import matplotlib.pyplot as plt
|
|
112
|
+
|
|
113
|
+
plot_lap_times(session.laps, drivers=["77", "5"])
|
|
114
|
+
plt.show()
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
The Bristol example saves a full-race chart, a pace detail view, and a CSV:
|
|
118
|
+
|
|
119
|
+
```sh
|
|
120
|
+
python examples/hocevar_bristol_2026.py
|
|
121
|
+
python examples/hocevar_bristol_2026.py --race spring --show
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
The default is the September 19, 2026 night race. Files go into `outputs/`.
|
|
125
|
+
On Windows, `examples/hocevar_bristol_2026.ps1` also runs the script using
|
|
126
|
+
an available Python installation or the bundled desktop runtime.
|
|
127
|
+
|
|
128
|
+
## Live timing
|
|
129
|
+
|
|
130
|
+
```python
|
|
131
|
+
import fastnascar
|
|
132
|
+
|
|
133
|
+
snapshot = fastnascar.get_live_session()
|
|
134
|
+
print(snapshot.timing)
|
|
135
|
+
print(snapshot.pit_stops)
|
|
136
|
+
|
|
137
|
+
for snapshot in fastnascar.iter_live(interval=2, max_updates=10):
|
|
138
|
+
print(snapshot.timestamp)
|
|
139
|
+
print(snapshot.timing.query("DriverNumber == '77'"))
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
The live feed serves the currently active session. It can't replay an earlier
|
|
143
|
+
race. Requests bypass the cache. Updates can repeat or become stale; check the
|
|
144
|
+
source timestamp. Pass `race_id=...` to require a specific race.
|
|
145
|
+
|
|
146
|
+
Polling raises an error if the feed switches race, timing run, or series.
|
|
147
|
+
The default delay is two seconds, with a minimum of one second. LastLapTime
|
|
148
|
+
and ElapsedTime are timedeltas; gaps and status retain the feed's values.
|
|
149
|
+
Polling can miss laps between updates.
|
|
150
|
+
|
|
151
|
+
Record and replay snapshots:
|
|
152
|
+
|
|
153
|
+
```python
|
|
154
|
+
fastnascar.record_live("race.jsonl", interval=2, max_updates=100)
|
|
155
|
+
for snapshot in fastnascar.replay_live("race.jsonl"):
|
|
156
|
+
print(snapshot.timestamp, snapshot.timing.head())
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
Recordings append to the file and flush after each update. Replay runs offline.
|
|
160
|
+
|
|
161
|
+
## Cache
|
|
162
|
+
|
|
163
|
+
```python
|
|
164
|
+
fastnascar.Cache.enable_cache(".fastnascar-cache")
|
|
165
|
+
session = fastnascar.get_session(2026, race_id=5626).load()
|
|
166
|
+
|
|
167
|
+
# Reuse downloaded data without network access.
|
|
168
|
+
fastnascar.Cache.enable_cache(".fastnascar-cache", offline=True)
|
|
169
|
+
session = fastnascar.get_session(2026, race_id=5626).load()
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
Settings apply across the process. Cached responses expire after an hour.
|
|
173
|
+
`refresh=True` on `load()` fetches new data. Offline mode accepts old entries
|
|
174
|
+
and raises on a cache miss, including live requests.
|
|
175
|
+
`Cache.disable_cache()` restores network access without deleting files;
|
|
176
|
+
`Cache.clear_cache()` removes this package's cached responses.
|
|
177
|
+
|
|
178
|
+
Unavailable feeds raise `DataNotAvailableError`; unsupported schemas raise
|
|
179
|
+
`InvalidDataError`. Accessing a table before loading it raises
|
|
180
|
+
`SessionNotLoadedError`. A failed reload keeps the previous session tables.
|
|
181
|
+
Function docstrings describe arguments, return values, and errors.
|
|
182
|
+
|
|
183
|
+
## Data
|
|
184
|
+
|
|
185
|
+
Feeds are hosted at `https://cf.nascar.com`:
|
|
186
|
+
|
|
187
|
+
- Schedule: `cacher/{year}/race_list_basic.json`
|
|
188
|
+
- Results: `cacher/{year}/1/{race_id}/weekend-feed.json`
|
|
189
|
+
- Race laps and flags: `cacher/{year}/1/{race_id}/lap-times.json`
|
|
190
|
+
- Live timing: `live/feeds/live-feed.json`
|
|
191
|
+
|
|
192
|
+
Coverage depends on what NASCAR makes available. Verified examples include
|
|
193
|
+
the 2024 Food City 500, the 2026 Bristol night race, and Cup live timing.
|
|
194
|
+
Feed URLs and schemas can change.
|
|
195
|
+
|
|
196
|
+
Code is MIT licensed. NASCAR feed data and test excerpts aren't covered by
|
|
197
|
+
that license.
|
|
198
|
+
|
|
199
|
+
## Development
|
|
200
|
+
|
|
201
|
+
```sh
|
|
202
|
+
python -m pip install -e ".[dev]"
|
|
203
|
+
python -m unittest discover -s tests -v
|
|
204
|
+
python -m build
|
|
205
|
+
python -m twine check dist/*
|
|
206
|
+
```
|
|
207
|
+
|
|
208
|
+
Tests run offline with recorded feed excerpts and mocked requests. Examples
|
|
209
|
+
make real requests. CI runs the tests on Python 3.10, 3.12, and 3.13.
|
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
# FastNASCAR
|
|
2
|
+
|
|
3
|
+
FastNASCAR provides NASCAR Cup lap times, results, and live timing in pandas.
|
|
4
|
+
Select a race, load the session, and work with its lap data:
|
|
5
|
+
|
|
6
|
+
```python
|
|
7
|
+
import fastnascar
|
|
8
|
+
|
|
9
|
+
session = fastnascar.get_session(2026, race_id=5626).load()
|
|
10
|
+
hocevar = session.laps.pick_driver("77")
|
|
11
|
+
print(hocevar.pick_fastest())
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
Early release. Race laps and weekend results work; historical practice and
|
|
15
|
+
qualifying laps aren't supported yet. No throttle, brake, RPM, or GPS telemetry.
|
|
16
|
+
This project is unaffiliated with NASCAR.
|
|
17
|
+
|
|
18
|
+
## Install
|
|
19
|
+
|
|
20
|
+
Python 3.10+ is required. From a checkout:
|
|
21
|
+
|
|
22
|
+
```sh
|
|
23
|
+
python -m pip install -e .
|
|
24
|
+
python -m pip install -e ".[plot]" # optional matplotlib charts
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
## Pick a race
|
|
28
|
+
|
|
29
|
+
```python
|
|
30
|
+
schedule = fastnascar.get_event_schedule(2026)
|
|
31
|
+
print(schedule[["RoundNumber", "RaceId", "EventName", "EventDate"]])
|
|
32
|
+
|
|
33
|
+
session = fastnascar.get_session(2026, 29)
|
|
34
|
+
session = fastnascar.get_session(2026, "Bass Pro Shops Night Race")
|
|
35
|
+
session = fastnascar.get_session(2026, race_id=5626)
|
|
36
|
+
session.load()
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
Rounds count points races in scheduled order. Use
|
|
40
|
+
`include_exhibition=True` to include exhibitions in the schedule; select those
|
|
41
|
+
by name or race ID. Repeated names raise `AmbiguousEventError`, with the matching
|
|
42
|
+
race IDs. Schedule dates retain NASCAR's local timestamps without a timezone.
|
|
43
|
+
|
|
44
|
+
## Work with laps
|
|
45
|
+
|
|
46
|
+
```python
|
|
47
|
+
hocevar = session.laps.pick_driver("77")
|
|
48
|
+
print(hocevar.pick_fastest())
|
|
49
|
+
print(hocevar.pick_laps([1, 2, 3]))
|
|
50
|
+
print(hocevar.pick_quicklaps().LapTime.dt.total_seconds().mean())
|
|
51
|
+
print(session.results[["Position", "DriverNumber", "FullName", "Points"]])
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
| Table | Columns |
|
|
55
|
+
| --- | --- |
|
|
56
|
+
| `session.laps` | Driver, DriverNumber, DriverId, LapNumber, LapTime, Speed, Position |
|
|
57
|
+
| `session.results` | DriverNumber, DriverId, FullName, TeamName, Manufacturer, Position, GridPosition, Laps, LapsLed, Points, Status, BestLapTime |
|
|
58
|
+
| `session.track_status` | LeaderLapNumber, FlagState |
|
|
59
|
+
|
|
60
|
+
Lap times are pandas timedeltas. Speeds are lap-average mph. Car numbers are
|
|
61
|
+
strings; missing values stay missing. Lap zero is excluded because it records
|
|
62
|
+
the starting order.
|
|
63
|
+
|
|
64
|
+
`pick_drivers()` accepts one or more car numbers or exact full names.
|
|
65
|
+
`pick_quicklaps(1.07)` keeps times below 107% of the fastest lap in the current
|
|
66
|
+
selection. Select a driver first to use that driver's pace. The filter doesn't
|
|
67
|
+
identify cautions or pit laps.
|
|
68
|
+
|
|
69
|
+
Flags refer to leader laps and stay separate from car laps. A lapped car's lap
|
|
70
|
+
number doesn't line up with the leader's. FlagState contains raw feed codes.
|
|
71
|
+
|
|
72
|
+
## Practice and qualifying
|
|
73
|
+
|
|
74
|
+
```python
|
|
75
|
+
practice = fastnascar.get_session(2026, race_id=5626, identifier="P")
|
|
76
|
+
practice.load(laps=False)
|
|
77
|
+
qualifying = fastnascar.get_session(2026, race_id=5626, identifier="Q")
|
|
78
|
+
qualifying.load(laps=False)
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
These sessions provide results and best lap times. When a weekend has several
|
|
82
|
+
runs of the same type, pass the exact `run_name` from the weekend feed.
|
|
83
|
+
Qualifying results may cover only the final round.
|
|
84
|
+
|
|
85
|
+
## Plot a race
|
|
86
|
+
|
|
87
|
+
```python
|
|
88
|
+
from fastnascar.plotting import plot_lap_times
|
|
89
|
+
import matplotlib.pyplot as plt
|
|
90
|
+
|
|
91
|
+
plot_lap_times(session.laps, drivers=["77", "5"])
|
|
92
|
+
plt.show()
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
The Bristol example saves a full-race chart, a pace detail view, and a CSV:
|
|
96
|
+
|
|
97
|
+
```sh
|
|
98
|
+
python examples/hocevar_bristol_2026.py
|
|
99
|
+
python examples/hocevar_bristol_2026.py --race spring --show
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
The default is the September 19, 2026 night race. Files go into `outputs/`.
|
|
103
|
+
On Windows, `examples/hocevar_bristol_2026.ps1` also runs the script using
|
|
104
|
+
an available Python installation or the bundled desktop runtime.
|
|
105
|
+
|
|
106
|
+
## Live timing
|
|
107
|
+
|
|
108
|
+
```python
|
|
109
|
+
import fastnascar
|
|
110
|
+
|
|
111
|
+
snapshot = fastnascar.get_live_session()
|
|
112
|
+
print(snapshot.timing)
|
|
113
|
+
print(snapshot.pit_stops)
|
|
114
|
+
|
|
115
|
+
for snapshot in fastnascar.iter_live(interval=2, max_updates=10):
|
|
116
|
+
print(snapshot.timestamp)
|
|
117
|
+
print(snapshot.timing.query("DriverNumber == '77'"))
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
The live feed serves the currently active session. It can't replay an earlier
|
|
121
|
+
race. Requests bypass the cache. Updates can repeat or become stale; check the
|
|
122
|
+
source timestamp. Pass `race_id=...` to require a specific race.
|
|
123
|
+
|
|
124
|
+
Polling raises an error if the feed switches race, timing run, or series.
|
|
125
|
+
The default delay is two seconds, with a minimum of one second. LastLapTime
|
|
126
|
+
and ElapsedTime are timedeltas; gaps and status retain the feed's values.
|
|
127
|
+
Polling can miss laps between updates.
|
|
128
|
+
|
|
129
|
+
Record and replay snapshots:
|
|
130
|
+
|
|
131
|
+
```python
|
|
132
|
+
fastnascar.record_live("race.jsonl", interval=2, max_updates=100)
|
|
133
|
+
for snapshot in fastnascar.replay_live("race.jsonl"):
|
|
134
|
+
print(snapshot.timestamp, snapshot.timing.head())
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
Recordings append to the file and flush after each update. Replay runs offline.
|
|
138
|
+
|
|
139
|
+
## Cache
|
|
140
|
+
|
|
141
|
+
```python
|
|
142
|
+
fastnascar.Cache.enable_cache(".fastnascar-cache")
|
|
143
|
+
session = fastnascar.get_session(2026, race_id=5626).load()
|
|
144
|
+
|
|
145
|
+
# Reuse downloaded data without network access.
|
|
146
|
+
fastnascar.Cache.enable_cache(".fastnascar-cache", offline=True)
|
|
147
|
+
session = fastnascar.get_session(2026, race_id=5626).load()
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
Settings apply across the process. Cached responses expire after an hour.
|
|
151
|
+
`refresh=True` on `load()` fetches new data. Offline mode accepts old entries
|
|
152
|
+
and raises on a cache miss, including live requests.
|
|
153
|
+
`Cache.disable_cache()` restores network access without deleting files;
|
|
154
|
+
`Cache.clear_cache()` removes this package's cached responses.
|
|
155
|
+
|
|
156
|
+
Unavailable feeds raise `DataNotAvailableError`; unsupported schemas raise
|
|
157
|
+
`InvalidDataError`. Accessing a table before loading it raises
|
|
158
|
+
`SessionNotLoadedError`. A failed reload keeps the previous session tables.
|
|
159
|
+
Function docstrings describe arguments, return values, and errors.
|
|
160
|
+
|
|
161
|
+
## Data
|
|
162
|
+
|
|
163
|
+
Feeds are hosted at `https://cf.nascar.com`:
|
|
164
|
+
|
|
165
|
+
- Schedule: `cacher/{year}/race_list_basic.json`
|
|
166
|
+
- Results: `cacher/{year}/1/{race_id}/weekend-feed.json`
|
|
167
|
+
- Race laps and flags: `cacher/{year}/1/{race_id}/lap-times.json`
|
|
168
|
+
- Live timing: `live/feeds/live-feed.json`
|
|
169
|
+
|
|
170
|
+
Coverage depends on what NASCAR makes available. Verified examples include
|
|
171
|
+
the 2024 Food City 500, the 2026 Bristol night race, and Cup live timing.
|
|
172
|
+
Feed URLs and schemas can change.
|
|
173
|
+
|
|
174
|
+
Code is MIT licensed. NASCAR feed data and test excerpts aren't covered by
|
|
175
|
+
that license.
|
|
176
|
+
|
|
177
|
+
## Development
|
|
178
|
+
|
|
179
|
+
```sh
|
|
180
|
+
python -m pip install -e ".[dev]"
|
|
181
|
+
python -m unittest discover -s tests -v
|
|
182
|
+
python -m build
|
|
183
|
+
python -m twine check dist/*
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
Tests run offline with recorded feed excerpts and mocked requests. Examples
|
|
187
|
+
make real requests. CI runs the tests on Python 3.10, 3.12, and 3.13.
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
"""Run after installing: python examples/analyze_race.py"""
|
|
2
|
+
import fastnascar
|
|
3
|
+
|
|
4
|
+
fastnascar.Cache.enable_cache(".fastnascar-cache")
|
|
5
|
+
session = fastnascar.get_session(2024, race_id=5392).load()
|
|
6
|
+
print(session.results[["Position", "DriverNumber", "FullName", "Points"]].head(10))
|
|
7
|
+
hamlin = session.laps.pick_driver("11")
|
|
8
|
+
print(hamlin.pick_fastest())
|
|
9
|
+
print(hamlin.pick_quicklaps().LapTime.dt.total_seconds().describe())
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
# Run from PowerShell: .\examples\hocevar_bristol_2026.ps1
|
|
2
|
+
# Pass Python script options, for example: --race spring --show
|
|
3
|
+
$taskRoot = Split-Path $PSScriptRoot -Parent
|
|
4
|
+
$taskPython = Get-Command python -ErrorAction SilentlyContinue
|
|
5
|
+
$taskBundledPython = Join-Path $env:USERPROFILE '.cache\codex-runtimes\codex-primary-runtime\dependencies\python\python.exe'
|
|
6
|
+
if ($taskPython) {
|
|
7
|
+
$taskPythonPath = $taskPython.Source
|
|
8
|
+
} elseif (Test-Path -LiteralPath $taskBundledPython) {
|
|
9
|
+
$taskPythonPath = $taskBundledPython
|
|
10
|
+
} else {
|
|
11
|
+
throw 'Python is required. Install Python 3.10 or newer and the package with its plot extra.'
|
|
12
|
+
}
|
|
13
|
+
$taskOldPythonPath = $env:PYTHONPATH
|
|
14
|
+
$taskOldMplConfig = $env:MPLCONFIGDIR
|
|
15
|
+
try {
|
|
16
|
+
$taskPlotDeps = Join-Path $taskRoot '.plot-deps'
|
|
17
|
+
if (Test-Path -LiteralPath $taskPlotDeps) {
|
|
18
|
+
$env:PYTHONPATH = "$taskPlotDeps;$taskOldPythonPath"
|
|
19
|
+
}
|
|
20
|
+
$env:MPLCONFIGDIR = Join-Path $taskRoot '.matplotlib'
|
|
21
|
+
& $taskPythonPath (Join-Path $PSScriptRoot 'hocevar_bristol_2026.py') @args
|
|
22
|
+
if ($LASTEXITCODE -ne 0) { throw "Chart script failed with exit code $LASTEXITCODE" }
|
|
23
|
+
} finally {
|
|
24
|
+
$env:PYTHONPATH = $taskOldPythonPath
|
|
25
|
+
$env:MPLCONFIGDIR = $taskOldMplConfig
|
|
26
|
+
}
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
"""Chart Carson Hocevar's lap times at Bristol in 2026 using real NASCAR data.
|
|
2
|
+
|
|
3
|
+
From the repository root:
|
|
4
|
+
python -m pip install -e ".[plot]"
|
|
5
|
+
python examples/hocevar_bristol_2026.py
|
|
6
|
+
python examples/hocevar_bristol_2026.py --race spring --show
|
|
7
|
+
|
|
8
|
+
The default is the September 19 Bass Pro Shops Night Race (race ID 5626).
|
|
9
|
+
"""
|
|
10
|
+
import argparse
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
import sys
|
|
13
|
+
|
|
14
|
+
# Also works directly from the checkout when pandas/matplotlib are installed.
|
|
15
|
+
sys.path.insert(0, str(Path(__file__).resolve().parents[1] / "src"))
|
|
16
|
+
|
|
17
|
+
import fastnascar
|
|
18
|
+
|
|
19
|
+
RACES = {"fall": 5626, "spring": 5604}
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def main():
|
|
23
|
+
"""Parse CLI options, load Hocevar's race laps, and write PNG/SVG/CSV files.
|
|
24
|
+
|
|
25
|
+
Returns:
|
|
26
|
+
None
|
|
27
|
+
|
|
28
|
+
Raises:
|
|
29
|
+
SystemExit: Invalid CLI options, missing plotting dependencies, unavailable
|
|
30
|
+
race data, or no usable Hocevar lap times.
|
|
31
|
+
OSError: Output files cannot be created.
|
|
32
|
+
"""
|
|
33
|
+
parser = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
|
|
34
|
+
parser.add_argument("--race", choices=RACES, default="fall")
|
|
35
|
+
parser.add_argument("--output-dir", type=Path, default=Path(__file__).resolve().parents[1] / "outputs")
|
|
36
|
+
parser.add_argument("--show", action="store_true", help="Open the chart in a matplotlib window")
|
|
37
|
+
parser.add_argument("--refresh", action="store_true", help="Fetch fresh lap and result data")
|
|
38
|
+
args = parser.parse_args()
|
|
39
|
+
|
|
40
|
+
try:
|
|
41
|
+
import matplotlib
|
|
42
|
+
if not args.show:
|
|
43
|
+
matplotlib.use("Agg")
|
|
44
|
+
import matplotlib.pyplot as plt
|
|
45
|
+
from matplotlib.ticker import MultipleLocator
|
|
46
|
+
except ImportError:
|
|
47
|
+
parser.exit(1, 'Plotting requires matplotlib. Run: python -m pip install -e ".[plot]"\n')
|
|
48
|
+
|
|
49
|
+
output_dir = args.output_dir.expanduser().resolve()
|
|
50
|
+
output_dir.mkdir(parents=True, exist_ok=True)
|
|
51
|
+
fastnascar.Cache.enable_cache(Path(__file__).resolve().parents[1] / ".fastnascar-cache")
|
|
52
|
+
try:
|
|
53
|
+
session = fastnascar.get_session(2026, race_id=RACES[args.race]).load(refresh=args.refresh)
|
|
54
|
+
except fastnascar.FastNASCARException as exc:
|
|
55
|
+
parser.exit(1, f"Could not load the Bristol race: {exc}\n")
|
|
56
|
+
|
|
57
|
+
# Results use a plain full name; lap feeds may add a '(C)' playoff suffix.
|
|
58
|
+
driver = session.get_driver("Carson Hocevar")
|
|
59
|
+
laps = session.laps.loc[session.laps.DriverId == driver.DriverId].sort_values("LapNumber").copy()
|
|
60
|
+
seconds = laps.LapTime.dt.total_seconds()
|
|
61
|
+
if laps.empty or seconds.notna().sum() == 0:
|
|
62
|
+
parser.exit(1, "No lap times were available for Carson Hocevar.\n")
|
|
63
|
+
|
|
64
|
+
# Reindex missing lap numbers so the line breaks across any absent records.
|
|
65
|
+
plot_seconds = seconds.set_axis(laps.LapNumber.astype(int)).reindex(range(1, int(laps.LapNumber.max()) + 1))
|
|
66
|
+
median = plot_seconds.rolling(10, min_periods=10).median()
|
|
67
|
+
fastest = laps.pick_fastest()
|
|
68
|
+
base = f"hocevar_bristol_2026_{args.race}"
|
|
69
|
+
csv_path = output_dir / f"{base}.csv"
|
|
70
|
+
png_path = output_dir / f"{base}.png"
|
|
71
|
+
svg_path = output_dir / f"{base}.svg"
|
|
72
|
+
export = laps.copy()
|
|
73
|
+
export["LapTimeSeconds"] = seconds
|
|
74
|
+
export["Rolling10LapMedianSeconds"] = laps.LapNumber.map(median)
|
|
75
|
+
export.to_csv(csv_path, index=False)
|
|
76
|
+
|
|
77
|
+
with plt.rc_context({"font.family": "DejaVu Sans", "font.size": 10,
|
|
78
|
+
"axes.spines.top": False, "axes.spines.right": False}):
|
|
79
|
+
fig, axes = plt.subplots(2, 1, figsize=(13, 8), sharex=True,
|
|
80
|
+
gridspec_kw={"height_ratios": [1, 1.4]}, layout="constrained")
|
|
81
|
+
fig.patch.set_facecolor("#f7f9fc")
|
|
82
|
+
date = session.event.EventDate.strftime("%B %d, %Y")
|
|
83
|
+
fig.suptitle(f"Carson Hocevar · #{driver.DriverNumber} · Bristol 2026\n"
|
|
84
|
+
f"{session.event.EventName} · {date}", fontsize=17, fontweight="bold")
|
|
85
|
+
for ax in axes:
|
|
86
|
+
ax.set_facecolor("white")
|
|
87
|
+
ax.plot(plot_seconds.index, plot_seconds, color="#2374ab", linewidth=0.8,
|
|
88
|
+
marker=".", markersize=2.4, alpha=0.85, label="Recorded lap time")
|
|
89
|
+
ax.set_ylabel("Lap time (seconds)")
|
|
90
|
+
ax.grid(axis="y", color="#dce3ea", linewidth=0.6)
|
|
91
|
+
ax.set_xlim(1, max(2, int(laps.LapNumber.max())))
|
|
92
|
+
ax.xaxis.set_major_locator(MultipleLocator(50))
|
|
93
|
+
axes[0].set_title("All laps · full time range, including slow laps", loc="left", fontsize=11)
|
|
94
|
+
axes[0].legend(loc="upper right", frameon=False)
|
|
95
|
+
axes[1].plot(median.index, median, color="#e07826", linewidth=1.8,
|
|
96
|
+
label="10-lap rolling median (all recorded laps)")
|
|
97
|
+
# Same data in both panels; only the lower panel's displayed y-range is limited.
|
|
98
|
+
lower = max(0, float(seconds.min()) - 0.5)
|
|
99
|
+
upper = max(lower + 3, float(seconds.quantile(0.75)) + 2)
|
|
100
|
+
axes[1].set_ylim(lower, upper)
|
|
101
|
+
axes[1].set_title(f"Pace detail · y-axis zoom {lower:.1f}–{upper:.1f} s; slower laps remain above",
|
|
102
|
+
loc="left", fontsize=11)
|
|
103
|
+
axes[1].scatter([int(fastest.LapNumber)], [fastest.LapTime.total_seconds()],
|
|
104
|
+
color="#b93645", s=38, zorder=5,
|
|
105
|
+
label=f"Fastest: {fastest.LapTime.total_seconds():.3f} s (lap {fastest.LapNumber})")
|
|
106
|
+
axes[1].legend(loc="upper right", frameon=False, fontsize=9)
|
|
107
|
+
axes[1].set_xlabel("Hocevar's completed car lap")
|
|
108
|
+
fig.supxlabel("Source: NASCAR lap-times feed · No laps filtered · Gaps indicate missing data", fontsize=9)
|
|
109
|
+
fig.savefig(png_path, dpi=180, facecolor=fig.get_facecolor())
|
|
110
|
+
fig.savefig(svg_path, facecolor=fig.get_facecolor())
|
|
111
|
+
if args.show:
|
|
112
|
+
plt.show()
|
|
113
|
+
plt.close(fig)
|
|
114
|
+
|
|
115
|
+
print(f"{session.event.EventName} (race ID {session.event.RaceId})")
|
|
116
|
+
print(f"#{driver.DriverNumber} {driver.FullName}: {len(laps)} laps; finish P{driver.Position}")
|
|
117
|
+
print(f"Fastest lap: {fastest.LapTime.total_seconds():.3f}s on lap {fastest.LapNumber}")
|
|
118
|
+
for path in (png_path, svg_path, csv_path):
|
|
119
|
+
print(path)
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
if __name__ == "__main__":
|
|
123
|
+
main()
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
"""Show ten updates from the currently active NASCAR Cup feed."""
|
|
2
|
+
import fastnascar
|
|
3
|
+
|
|
4
|
+
for snapshot in fastnascar.iter_live(interval=2, max_updates=10):
|
|
5
|
+
print(f"Race {snapshot.race_id}, lap {snapshot.lap_number}, flag {snapshot.flag_state}")
|
|
6
|
+
print(snapshot.timing[["Position", "DriverNumber", "Driver", "LastLapTime"]].head())
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=77.0.3"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "fastnascar"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "Pandas-based NASCAR sessions, laps, results, and live timing"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.10"
|
|
11
|
+
license = "MIT"
|
|
12
|
+
license-files = ["LICENSE"]
|
|
13
|
+
keywords = ["nascar", "motorsport", "timing", "pandas"]
|
|
14
|
+
classifiers = [
|
|
15
|
+
"Development Status :: 3 - Alpha",
|
|
16
|
+
"Programming Language :: Python :: 3",
|
|
17
|
+
"Operating System :: OS Independent",
|
|
18
|
+
"Topic :: Software Development :: Libraries :: Python Modules",
|
|
19
|
+
]
|
|
20
|
+
dependencies = ["pandas>=2.2,<4"]
|
|
21
|
+
|
|
22
|
+
[project.optional-dependencies]
|
|
23
|
+
plot = ["matplotlib>=3.8"]
|
|
24
|
+
dev = ["pytest>=8", "build>=1", "twine>=6"]
|
|
25
|
+
|
|
26
|
+
[tool.setuptools.packages.find]
|
|
27
|
+
where = ["src"]
|
|
28
|
+
|
|
29
|
+
[tool.pytest.ini_options]
|
|
30
|
+
pythonpath = ["src"]
|
|
31
|
+
testpaths = ["tests"]
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
"""FastNASCAR: NASCAR Cup data for pandas analysis."""
|
|
2
|
+
from .api import NASCARClient
|
|
3
|
+
from .cache import Cache
|
|
4
|
+
from .core import Laps, Session, get_event, get_event_schedule, get_session
|
|
5
|
+
from .exceptions import (AmbiguousEventError, DataNotAvailableError, EventNotFoundError,
|
|
6
|
+
FastNASCARException, InvalidDataError, SessionNotLoadedError)
|
|
7
|
+
from .live import LiveSnapshot, get_live_session, iter_live, record_live, replay_live
|
|
8
|
+
|
|
9
|
+
__version__ = "0.1.0"
|
|
10
|
+
__all__ = ["Cache", "NASCARClient", "Laps", "Session", "get_event", "get_event_schedule",
|
|
11
|
+
"get_session", "LiveSnapshot", "get_live_session", "iter_live", "record_live", "replay_live",
|
|
12
|
+
"FastNASCARException", "DataNotAvailableError", "InvalidDataError", "SessionNotLoadedError",
|
|
13
|
+
"EventNotFoundError", "AmbiguousEventError"]
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
"""Conversions shared by historical and live timing parsers."""
|
|
2
|
+
import pandas as pd
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
def seconds_to_timedelta(values):
|
|
6
|
+
"""Convert a Series of seconds to nanosecond-resolution pandas timedeltas.
|
|
7
|
+
|
|
8
|
+
Args:
|
|
9
|
+
values (pandas.Series): Numeric seconds or strings that represent seconds.
|
|
10
|
+
|
|
11
|
+
Returns:
|
|
12
|
+
pandas.Series: Timedeltas with the original index. Missing, nonnumeric,
|
|
13
|
+
and infinite values become ``NaT``. Empty Series keep the same dtype.
|
|
14
|
+
"""
|
|
15
|
+
numeric = pd.to_numeric(values, errors="coerce")
|
|
16
|
+
numeric = numeric.mask(numeric.isin([float("inf"), -float("inf")]))
|
|
17
|
+
converted = pd.to_timedelta(numeric.fillna(0), unit="s").astype("timedelta64[ns]")
|
|
18
|
+
return converted.mask(numeric.isna())
|