openwec 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.
- openwec-0.1.0/LICENSE +21 -0
- openwec-0.1.0/PKG-INFO +141 -0
- openwec-0.1.0/README.md +106 -0
- openwec-0.1.0/openwec/__init__.py +45 -0
- openwec-0.1.0/openwec/client.py +62 -0
- openwec-0.1.0/openwec/plotting.py +124 -0
- openwec-0.1.0/openwec/session.py +304 -0
- openwec-0.1.0/openwec.egg-info/PKG-INFO +141 -0
- openwec-0.1.0/openwec.egg-info/SOURCES.txt +12 -0
- openwec-0.1.0/openwec.egg-info/dependency_links.txt +1 -0
- openwec-0.1.0/openwec.egg-info/requires.txt +12 -0
- openwec-0.1.0/openwec.egg-info/top_level.txt +1 -0
- openwec-0.1.0/pyproject.toml +58 -0
- openwec-0.1.0/setup.cfg +4 -0
openwec-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Paloma Cordeiro
|
|
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.
|
openwec-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: openwec
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Python SDK for endurance racing data — WEC, ELMS, ALMS, Le Mans Cup, IMSA
|
|
5
|
+
Author: Paloma C.
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Project-URL: Homepage, https://openwec.com
|
|
8
|
+
Project-URL: Documentation, https://api.openwec.com/docs
|
|
9
|
+
Project-URL: Repository, https://github.com/palomacdev/openwec
|
|
10
|
+
Project-URL: Bug Tracker, https://github.com/palomacdev/openwec/issues
|
|
11
|
+
Keywords: motorsport,endurance racing,WEC,Le Mans,ELMS,IMSA,timing data,lap times,pandas
|
|
12
|
+
Classifier: Development Status :: 3 - Alpha
|
|
13
|
+
Classifier: Intended Audience :: Developers
|
|
14
|
+
Classifier: Intended Audience :: Science/Research
|
|
15
|
+
Classifier: Programming Language :: Python :: 3
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
19
|
+
Classifier: Topic :: Scientific/Engineering :: Information Analysis
|
|
20
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
21
|
+
Requires-Python: >=3.10
|
|
22
|
+
Description-Content-Type: text/markdown
|
|
23
|
+
License-File: LICENSE
|
|
24
|
+
Requires-Dist: requests>=2.31
|
|
25
|
+
Requires-Dist: pandas>=2.0
|
|
26
|
+
Provides-Extra: plotting
|
|
27
|
+
Requires-Dist: matplotlib>=3.7; extra == "plotting"
|
|
28
|
+
Provides-Extra: dev
|
|
29
|
+
Requires-Dist: matplotlib>=3.7; extra == "dev"
|
|
30
|
+
Requires-Dist: jupyter>=1.0; extra == "dev"
|
|
31
|
+
Requires-Dist: ipykernel>=6.0; extra == "dev"
|
|
32
|
+
Requires-Dist: twine>=5.0; extra == "dev"
|
|
33
|
+
Requires-Dist: build>=1.0; extra == "dev"
|
|
34
|
+
Dynamic: license-file
|
|
35
|
+
|
|
36
|
+
# openwec
|
|
37
|
+
|
|
38
|
+
**Python SDK for endurance racing data — WEC, ELMS, ALMS, Le Mans Cup, IMSA.**
|
|
39
|
+
|
|
40
|
+
Inspired by [FastF1](https://github.com/theOehrly/Fast-F1), OpenWEC gives you lap-by-lap timing data, stint analytics, and race results from endurance racing as pandas DataFrames — in one line.
|
|
41
|
+
|
|
42
|
+
→ **[openwec.com](https://openwec.com)** — Live dashboard
|
|
43
|
+
→ **[api.openwec.com/docs](https://api.openwec.com/docs)** — API reference
|
|
44
|
+
→ **[github.com/palomacdev/openwec](https://github.com/palomacdev/openwec)** — Source
|
|
45
|
+
|
|
46
|
+
---
|
|
47
|
+
|
|
48
|
+
## Installation
|
|
49
|
+
|
|
50
|
+
```bash
|
|
51
|
+
pip install openwec
|
|
52
|
+
|
|
53
|
+
# With plotting support
|
|
54
|
+
pip install openwec[plotting]
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
---
|
|
58
|
+
|
|
59
|
+
## Quick start
|
|
60
|
+
|
|
61
|
+
```python
|
|
62
|
+
import openwec
|
|
63
|
+
|
|
64
|
+
# Configure — public endpoints work without a key
|
|
65
|
+
# Request a free key at https://openwec.com/api-keys
|
|
66
|
+
openwec.configure(
|
|
67
|
+
base_url="https://api.openwec.com/api/v1",
|
|
68
|
+
api_key="your-key-here" # required for laps and analytics
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
# Load any session
|
|
72
|
+
session = openwec.Session("WEC", 2026, "Le Mans", "Race")
|
|
73
|
+
print(session)
|
|
74
|
+
# Session(WEC 2026 LE MANS — Race, id=6556)
|
|
75
|
+
|
|
76
|
+
# Results as a DataFrame (no key needed)
|
|
77
|
+
results = session.results()
|
|
78
|
+
print(results[["position", "car_number", "car_class", "team", "drivers"]].head(10))
|
|
79
|
+
|
|
80
|
+
# Lap-by-lap data (key required)
|
|
81
|
+
laps = session.laps(car="7")
|
|
82
|
+
print(laps[["lap_number", "lap_time_s", "s1_s", "s2_s", "s3_s"]].head())
|
|
83
|
+
|
|
84
|
+
# Stints and pace (key required)
|
|
85
|
+
stints = session.stints(car_class="HYPERCAR")
|
|
86
|
+
pace = session.pace(car_class="HYPERCAR")
|
|
87
|
+
|
|
88
|
+
# Pit window estimate
|
|
89
|
+
pit_window = session.pit_window(car="7")
|
|
90
|
+
|
|
91
|
+
# Plots (requires matplotlib)
|
|
92
|
+
session.plot_lap_evolution(car="7")
|
|
93
|
+
session.plot_stint_chart(car_class="HYPERCAR")
|
|
94
|
+
session.plot_gap_to_leader(car_class="HYPERCAR")
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
---
|
|
98
|
+
|
|
99
|
+
## Coverage
|
|
100
|
+
|
|
101
|
+
| Series | Seasons |
|
|
102
|
+
|--------|---------|
|
|
103
|
+
| FIA WEC | 2012–2026 |
|
|
104
|
+
| ELMS | 2012–2026 |
|
|
105
|
+
| ALMS (Asian) | 2022–2026 |
|
|
106
|
+
| Le Mans Cup | 2017–2026 |
|
|
107
|
+
| IMSA | 2014–2026 |
|
|
108
|
+
|
|
109
|
+
**1.77M+ laps** across all series.
|
|
110
|
+
|
|
111
|
+
---
|
|
112
|
+
|
|
113
|
+
## API key
|
|
114
|
+
|
|
115
|
+
Public endpoints (results, driver profiles, team profiles) require no key.
|
|
116
|
+
Lap-by-lap data and analytics endpoints require a free API key.
|
|
117
|
+
|
|
118
|
+
→ **[Request a key at openwec.com/api-keys](https://openwec.com/api-keys)**
|
|
119
|
+
|
|
120
|
+
---
|
|
121
|
+
|
|
122
|
+
## Session methods
|
|
123
|
+
|
|
124
|
+
| Method | Returns | Key required |
|
|
125
|
+
|--------|---------|-------------|
|
|
126
|
+
| `.results()` | DataFrame | No |
|
|
127
|
+
| `.laps(car=None)` | DataFrame | Yes |
|
|
128
|
+
| `.stints(car_class=None)` | DataFrame | Yes |
|
|
129
|
+
| `.pace(car_class=None)` | DataFrame | Yes |
|
|
130
|
+
| `.gaps(car_class=None)` | DataFrame | Yes |
|
|
131
|
+
| `.pit_window(car=None)` | DataFrame | Yes |
|
|
132
|
+
| `.plot_lap_evolution(car)` | Figure | Yes |
|
|
133
|
+
| `.plot_stint_chart()` | Figure | Yes |
|
|
134
|
+
| `.plot_gap_to_leader()` | Figure | Yes |
|
|
135
|
+
|
|
136
|
+
---
|
|
137
|
+
|
|
138
|
+
## License
|
|
139
|
+
|
|
140
|
+
MIT — data sourced from Al Kamel Systems public timing exports.
|
|
141
|
+
Not affiliated with ACO, FIA, or any racing organization.
|
openwec-0.1.0/README.md
ADDED
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
# openwec
|
|
2
|
+
|
|
3
|
+
**Python SDK for endurance racing data — WEC, ELMS, ALMS, Le Mans Cup, IMSA.**
|
|
4
|
+
|
|
5
|
+
Inspired by [FastF1](https://github.com/theOehrly/Fast-F1), OpenWEC gives you lap-by-lap timing data, stint analytics, and race results from endurance racing as pandas DataFrames — in one line.
|
|
6
|
+
|
|
7
|
+
→ **[openwec.com](https://openwec.com)** — Live dashboard
|
|
8
|
+
→ **[api.openwec.com/docs](https://api.openwec.com/docs)** — API reference
|
|
9
|
+
→ **[github.com/palomacdev/openwec](https://github.com/palomacdev/openwec)** — Source
|
|
10
|
+
|
|
11
|
+
---
|
|
12
|
+
|
|
13
|
+
## Installation
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
pip install openwec
|
|
17
|
+
|
|
18
|
+
# With plotting support
|
|
19
|
+
pip install openwec[plotting]
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
---
|
|
23
|
+
|
|
24
|
+
## Quick start
|
|
25
|
+
|
|
26
|
+
```python
|
|
27
|
+
import openwec
|
|
28
|
+
|
|
29
|
+
# Configure — public endpoints work without a key
|
|
30
|
+
# Request a free key at https://openwec.com/api-keys
|
|
31
|
+
openwec.configure(
|
|
32
|
+
base_url="https://api.openwec.com/api/v1",
|
|
33
|
+
api_key="your-key-here" # required for laps and analytics
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
# Load any session
|
|
37
|
+
session = openwec.Session("WEC", 2026, "Le Mans", "Race")
|
|
38
|
+
print(session)
|
|
39
|
+
# Session(WEC 2026 LE MANS — Race, id=6556)
|
|
40
|
+
|
|
41
|
+
# Results as a DataFrame (no key needed)
|
|
42
|
+
results = session.results()
|
|
43
|
+
print(results[["position", "car_number", "car_class", "team", "drivers"]].head(10))
|
|
44
|
+
|
|
45
|
+
# Lap-by-lap data (key required)
|
|
46
|
+
laps = session.laps(car="7")
|
|
47
|
+
print(laps[["lap_number", "lap_time_s", "s1_s", "s2_s", "s3_s"]].head())
|
|
48
|
+
|
|
49
|
+
# Stints and pace (key required)
|
|
50
|
+
stints = session.stints(car_class="HYPERCAR")
|
|
51
|
+
pace = session.pace(car_class="HYPERCAR")
|
|
52
|
+
|
|
53
|
+
# Pit window estimate
|
|
54
|
+
pit_window = session.pit_window(car="7")
|
|
55
|
+
|
|
56
|
+
# Plots (requires matplotlib)
|
|
57
|
+
session.plot_lap_evolution(car="7")
|
|
58
|
+
session.plot_stint_chart(car_class="HYPERCAR")
|
|
59
|
+
session.plot_gap_to_leader(car_class="HYPERCAR")
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
---
|
|
63
|
+
|
|
64
|
+
## Coverage
|
|
65
|
+
|
|
66
|
+
| Series | Seasons |
|
|
67
|
+
|--------|---------|
|
|
68
|
+
| FIA WEC | 2012–2026 |
|
|
69
|
+
| ELMS | 2012–2026 |
|
|
70
|
+
| ALMS (Asian) | 2022–2026 |
|
|
71
|
+
| Le Mans Cup | 2017–2026 |
|
|
72
|
+
| IMSA | 2014–2026 |
|
|
73
|
+
|
|
74
|
+
**1.77M+ laps** across all series.
|
|
75
|
+
|
|
76
|
+
---
|
|
77
|
+
|
|
78
|
+
## API key
|
|
79
|
+
|
|
80
|
+
Public endpoints (results, driver profiles, team profiles) require no key.
|
|
81
|
+
Lap-by-lap data and analytics endpoints require a free API key.
|
|
82
|
+
|
|
83
|
+
→ **[Request a key at openwec.com/api-keys](https://openwec.com/api-keys)**
|
|
84
|
+
|
|
85
|
+
---
|
|
86
|
+
|
|
87
|
+
## Session methods
|
|
88
|
+
|
|
89
|
+
| Method | Returns | Key required |
|
|
90
|
+
|--------|---------|-------------|
|
|
91
|
+
| `.results()` | DataFrame | No |
|
|
92
|
+
| `.laps(car=None)` | DataFrame | Yes |
|
|
93
|
+
| `.stints(car_class=None)` | DataFrame | Yes |
|
|
94
|
+
| `.pace(car_class=None)` | DataFrame | Yes |
|
|
95
|
+
| `.gaps(car_class=None)` | DataFrame | Yes |
|
|
96
|
+
| `.pit_window(car=None)` | DataFrame | Yes |
|
|
97
|
+
| `.plot_lap_evolution(car)` | Figure | Yes |
|
|
98
|
+
| `.plot_stint_chart()` | Figure | Yes |
|
|
99
|
+
| `.plot_gap_to_leader()` | Figure | Yes |
|
|
100
|
+
|
|
101
|
+
---
|
|
102
|
+
|
|
103
|
+
## License
|
|
104
|
+
|
|
105
|
+
MIT — data sourced from Al Kamel Systems public timing exports.
|
|
106
|
+
Not affiliated with ACO, FIA, or any racing organization.
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
"""
|
|
2
|
+
OpenWEC SDK — Alpha
|
|
3
|
+
|
|
4
|
+
A FastF1-style Python interface for endurance racing data
|
|
5
|
+
(WEC, ELMS, ALMS, Le Mans Cup, IMSA).
|
|
6
|
+
|
|
7
|
+
Usage:
|
|
8
|
+
import openwec
|
|
9
|
+
|
|
10
|
+
# Optional: configure API URL and key
|
|
11
|
+
openwec.configure(base_url="http://localhost:8000/api/v1", api_key="your-key")
|
|
12
|
+
|
|
13
|
+
# Load a session
|
|
14
|
+
session = openwec.Session("WEC", 2024, "Le Mans", "Race")
|
|
15
|
+
|
|
16
|
+
# Results (public, no API key needed)
|
|
17
|
+
results = session.results()
|
|
18
|
+
print(results.head())
|
|
19
|
+
|
|
20
|
+
# Laps (requires API key)
|
|
21
|
+
laps = session.laps()
|
|
22
|
+
laps_50 = session.laps(car="50")
|
|
23
|
+
|
|
24
|
+
# Analytics (requires API key)
|
|
25
|
+
stints = session.stints()
|
|
26
|
+
pace = session.pace()
|
|
27
|
+
|
|
28
|
+
# Plots (requires matplotlib + API key)
|
|
29
|
+
session.plot_lap_evolution(car="50")
|
|
30
|
+
session.plot_stint_chart()
|
|
31
|
+
session.plot_gap_to_leader()
|
|
32
|
+
"""
|
|
33
|
+
|
|
34
|
+
from .client import configure, OpenWECError, OpenWECNotFoundError, OpenWECAuthError
|
|
35
|
+
from .session import Session
|
|
36
|
+
|
|
37
|
+
__version__ = "0.1.0-alpha"
|
|
38
|
+
|
|
39
|
+
__all__ = [
|
|
40
|
+
"Session",
|
|
41
|
+
"configure",
|
|
42
|
+
"OpenWECError",
|
|
43
|
+
"OpenWECNotFoundError",
|
|
44
|
+
"OpenWECAuthError",
|
|
45
|
+
]
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
"""
|
|
2
|
+
OpenWEC SDK — Client configuration and HTTP wrapper.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
import requests
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class _Config:
|
|
9
|
+
base_url: str = "http://localhost:8000/api/v1"
|
|
10
|
+
api_key: str | None = None
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
_config = _Config()
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def configure(base_url: str | None = None, api_key: str | None = None):
|
|
17
|
+
"""
|
|
18
|
+
Configure the SDK.
|
|
19
|
+
|
|
20
|
+
Args:
|
|
21
|
+
base_url: API base URL (default: http://localhost:8000/api/v1)
|
|
22
|
+
api_key: API key for protected endpoints (laps, analytics)
|
|
23
|
+
"""
|
|
24
|
+
if base_url:
|
|
25
|
+
_config.base_url = base_url.rstrip("/")
|
|
26
|
+
if api_key:
|
|
27
|
+
_config.api_key = api_key
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def _headers() -> dict:
|
|
31
|
+
headers = {}
|
|
32
|
+
if _config.api_key:
|
|
33
|
+
headers["X-API-Key"] = _config.api_key
|
|
34
|
+
return headers
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def _get(path: str, params: dict | None = None) -> dict | list:
|
|
38
|
+
"""Performs a GET request to the OpenWEC API. Raises on error."""
|
|
39
|
+
url = f"{_config.base_url}{path}"
|
|
40
|
+
resp = requests.get(url, params=params, headers=_headers(), timeout=30)
|
|
41
|
+
|
|
42
|
+
if resp.status_code == 404:
|
|
43
|
+
raise OpenWECNotFoundError(f"Not found: {path} ({resp.text})")
|
|
44
|
+
if resp.status_code == 401:
|
|
45
|
+
raise OpenWECAuthError(
|
|
46
|
+
"Unauthorized — this endpoint requires an API key. "
|
|
47
|
+
"Call openwec.configure(api_key='...') first."
|
|
48
|
+
)
|
|
49
|
+
resp.raise_for_status()
|
|
50
|
+
return resp.json()
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
class OpenWECError(Exception):
|
|
54
|
+
"""Base exception for OpenWEC SDK errors."""
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
class OpenWECNotFoundError(OpenWECError):
|
|
58
|
+
"""Raised when a resource is not found (404)."""
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
class OpenWECAuthError(OpenWECError):
|
|
62
|
+
"""Raised when an API key is required but missing/invalid (401)."""
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
"""
|
|
2
|
+
OpenWEC SDK — Plotting
|
|
3
|
+
Basic matplotlib charts, inspired by FastF1's plotting helpers.
|
|
4
|
+
|
|
5
|
+
These are intentionally simple — alpha quality. Returns a matplotlib
|
|
6
|
+
Figure so users can further customize (fig.savefig, etc).
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
import matplotlib.pyplot as plt
|
|
11
|
+
import matplotlib.colors as mcolors
|
|
12
|
+
import pandas as pd
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def plot_lap_evolution(laps: pd.DataFrame, car: str | None = None, ax=None):
|
|
16
|
+
"""
|
|
17
|
+
Plots lap time evolution over the race.
|
|
18
|
+
|
|
19
|
+
Args:
|
|
20
|
+
laps: DataFrame from session.laps(car=...)
|
|
21
|
+
car: car number, used for the plot title
|
|
22
|
+
ax: optional matplotlib axes to plot on
|
|
23
|
+
"""
|
|
24
|
+
if ax is None:
|
|
25
|
+
fig, ax = plt.subplots(figsize=(10, 5))
|
|
26
|
+
else:
|
|
27
|
+
fig = ax.figure
|
|
28
|
+
|
|
29
|
+
df = laps.copy()
|
|
30
|
+
df = df[df["lap_time_s"].notna()]
|
|
31
|
+
df = df[df["lap_time_s"] < 600] # exclude formation/SC laps with huge times
|
|
32
|
+
|
|
33
|
+
# Highlight pit laps
|
|
34
|
+
pit_laps = df[df["crossing_finish_in_pit"] == True]
|
|
35
|
+
clean = df[df["crossing_finish_in_pit"] == False]
|
|
36
|
+
|
|
37
|
+
ax.plot(clean["lap_number"], clean["lap_time_s"],
|
|
38
|
+
marker="o", markersize=3, linewidth=1, label="Lap time")
|
|
39
|
+
|
|
40
|
+
if not pit_laps.empty:
|
|
41
|
+
ax.scatter(pit_laps["lap_number"], pit_laps["lap_time_s"],
|
|
42
|
+
color="red", marker="x", s=50, label="Pit lap", zorder=5)
|
|
43
|
+
|
|
44
|
+
ax.set_xlabel("Lap")
|
|
45
|
+
ax.set_ylabel("Lap time (s)")
|
|
46
|
+
title = f"Lap Evolution — Car #{car}" if car else "Lap Evolution"
|
|
47
|
+
ax.set_title(title)
|
|
48
|
+
ax.legend()
|
|
49
|
+
ax.grid(alpha=0.3)
|
|
50
|
+
|
|
51
|
+
fig.tight_layout()
|
|
52
|
+
return fig
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def plot_stint_chart(stints: pd.DataFrame, ax=None):
|
|
56
|
+
"""
|
|
57
|
+
Plots a horizontal stint chart — one row per car, colored bars per stint.
|
|
58
|
+
Similar to FastF1's strategy chart.
|
|
59
|
+
|
|
60
|
+
Args:
|
|
61
|
+
stints: DataFrame from session.stints() (all cars)
|
|
62
|
+
ax: optional matplotlib axes to plot on
|
|
63
|
+
"""
|
|
64
|
+
if ax is None:
|
|
65
|
+
fig, ax = plt.subplots(figsize=(12, max(4, len(stints["car_number"].unique()) * 0.3)))
|
|
66
|
+
else:
|
|
67
|
+
fig = ax.figure
|
|
68
|
+
|
|
69
|
+
cars = sorted(stints["car_number"].unique(), key=lambda x: int(x) if x.isdigit() else 999)
|
|
70
|
+
|
|
71
|
+
# Color map by stint number (cycles)
|
|
72
|
+
cmap = plt.get_cmap("tab10")
|
|
73
|
+
|
|
74
|
+
for i, car in enumerate(cars):
|
|
75
|
+
car_stints = stints[stints["car_number"] == car].sort_values("stint_number")
|
|
76
|
+
for _, stint in car_stints.iterrows():
|
|
77
|
+
duration = stint["end_lap"] - stint["start_lap"] + 1
|
|
78
|
+
color = cmap(int(stint["stint_number"]) % 10)
|
|
79
|
+
ax.barh(i, duration, left=stint["start_lap"], color=color,
|
|
80
|
+
edgecolor="white", height=0.6)
|
|
81
|
+
|
|
82
|
+
ax.set_yticks(range(len(cars)))
|
|
83
|
+
ax.set_yticklabels([f"#{c}" for c in cars])
|
|
84
|
+
ax.set_xlabel("Lap")
|
|
85
|
+
ax.set_title("Stint Chart")
|
|
86
|
+
ax.invert_yaxis()
|
|
87
|
+
ax.grid(axis="x", alpha=0.3)
|
|
88
|
+
|
|
89
|
+
fig.tight_layout()
|
|
90
|
+
return fig
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def plot_gap_to_leader(gaps: pd.DataFrame, ax=None):
|
|
94
|
+
"""
|
|
95
|
+
Plots gap to leader over race distance.
|
|
96
|
+
|
|
97
|
+
Args:
|
|
98
|
+
gaps: DataFrame from session.gaps() — needs lap_number, car_number, cumulative_s
|
|
99
|
+
ax: optional matplotlib axes to plot on
|
|
100
|
+
"""
|
|
101
|
+
if ax is None:
|
|
102
|
+
fig, ax = plt.subplots(figsize=(10, 6))
|
|
103
|
+
else:
|
|
104
|
+
fig = ax.figure
|
|
105
|
+
|
|
106
|
+
df = gaps.copy()
|
|
107
|
+
df = df[df["cumulative_s"].notna()]
|
|
108
|
+
|
|
109
|
+
# Leader = minimum cumulative_s per lap
|
|
110
|
+
leader_per_lap = df.groupby("lap_number")["cumulative_s"].min()
|
|
111
|
+
|
|
112
|
+
for car, group in df.groupby("car_number"):
|
|
113
|
+
group = group.set_index("lap_number")
|
|
114
|
+
gap = group["cumulative_s"] - leader_per_lap.reindex(group.index)
|
|
115
|
+
ax.plot(gap.index, gap.values, label=f"#{car}", linewidth=1)
|
|
116
|
+
|
|
117
|
+
ax.set_xlabel("Lap")
|
|
118
|
+
ax.set_ylabel("Gap to leader (s)")
|
|
119
|
+
ax.set_title("Gap to Leader")
|
|
120
|
+
ax.legend(fontsize=8, ncol=2)
|
|
121
|
+
ax.grid(alpha=0.3)
|
|
122
|
+
|
|
123
|
+
fig.tight_layout()
|
|
124
|
+
return fig
|
|
@@ -0,0 +1,304 @@
|
|
|
1
|
+
"""
|
|
2
|
+
OpenWEC SDK — Session
|
|
3
|
+
The main object-style interface, inspired by FastF1.
|
|
4
|
+
|
|
5
|
+
Usage:
|
|
6
|
+
import openwec
|
|
7
|
+
|
|
8
|
+
session = openwec.Session("WEC", 2024, "Le Mans", "Race")
|
|
9
|
+
results = session.results()
|
|
10
|
+
laps = session.laps()
|
|
11
|
+
laps_50 = session.laps(car="50")
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
from __future__ import annotations
|
|
15
|
+
import pandas as pd
|
|
16
|
+
|
|
17
|
+
from .client import _get, OpenWECNotFoundError
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class Session:
|
|
21
|
+
"""
|
|
22
|
+
Represents a single session (e.g. WEC 2024 Le Mans Race).
|
|
23
|
+
|
|
24
|
+
Resolution is done by matching series/season/event/session names
|
|
25
|
+
against the OpenWEC catalog — matching is case-insensitive and
|
|
26
|
+
substring-based for event and session names.
|
|
27
|
+
|
|
28
|
+
Args:
|
|
29
|
+
series: series key, e.g. "WEC", "ELMS", "IMSA"
|
|
30
|
+
year: season year, e.g. 2024
|
|
31
|
+
event: event name or substring, e.g. "Le Mans"
|
|
32
|
+
session: session name or substring, e.g. "Race", "Qualifying"
|
|
33
|
+
"""
|
|
34
|
+
|
|
35
|
+
def __init__(self, series: str, year: int, event: str, session: str):
|
|
36
|
+
self.series_key = series.upper()
|
|
37
|
+
self.year = year
|
|
38
|
+
self.event_query = event
|
|
39
|
+
self.session_query = session
|
|
40
|
+
|
|
41
|
+
self._session_id: int | None = None
|
|
42
|
+
self._event_id: int | None = None
|
|
43
|
+
self._event_name: str | None = None
|
|
44
|
+
self._session_name: str | None = None
|
|
45
|
+
self._session_type: str | None = None
|
|
46
|
+
self._session_at: str | None = None
|
|
47
|
+
|
|
48
|
+
self._results_cache: pd.DataFrame | None = None
|
|
49
|
+
self._laps_cache: dict[str | None, pd.DataFrame] = {}
|
|
50
|
+
|
|
51
|
+
self._resolve()
|
|
52
|
+
|
|
53
|
+
# ── Resolution ────────────────────────────────────────────
|
|
54
|
+
|
|
55
|
+
def _resolve(self):
|
|
56
|
+
"""Resolves series/year/event/session to a session_id via the API."""
|
|
57
|
+
|
|
58
|
+
# 1. Find the event
|
|
59
|
+
events = _get(f"/series/{self.series_key}/seasons/{self.year}/events")
|
|
60
|
+
if not isinstance(events, list):
|
|
61
|
+
raise OpenWECNotFoundError(f"No events found for {self.series_key} {self.year}")
|
|
62
|
+
|
|
63
|
+
event_match = _find_best_match(events, "name", self.event_query)
|
|
64
|
+
if not event_match:
|
|
65
|
+
available = ", ".join(e["name"] for e in events)
|
|
66
|
+
raise OpenWECNotFoundError(
|
|
67
|
+
f"No event matching '{self.event_query}' in {self.series_key} {self.year}. "
|
|
68
|
+
f"Available: {available}"
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
self._event_id = event_match["id"]
|
|
72
|
+
self._event_name = event_match["name"]
|
|
73
|
+
|
|
74
|
+
# 2. Find the session within that event
|
|
75
|
+
sessions = _get(
|
|
76
|
+
f"/series/{self.series_key}/seasons/{self.year}/events/{self._event_id}/sessions"
|
|
77
|
+
)
|
|
78
|
+
if not isinstance(sessions, list):
|
|
79
|
+
raise OpenWECNotFoundError(f"No sessions found for event {self._event_name}")
|
|
80
|
+
|
|
81
|
+
session_match = _find_best_match(sessions, "name", self.session_query)
|
|
82
|
+
if not session_match:
|
|
83
|
+
available = ", ".join(s["name"] for s in sessions)
|
|
84
|
+
raise OpenWECNotFoundError(
|
|
85
|
+
f"No session matching '{self.session_query}' in {self._event_name}. "
|
|
86
|
+
f"Available: {available}"
|
|
87
|
+
)
|
|
88
|
+
|
|
89
|
+
self._session_id = session_match["id"]
|
|
90
|
+
self._session_name = session_match["name"]
|
|
91
|
+
self._session_type = session_match["session_type"]
|
|
92
|
+
self._session_at = session_match.get("session_at")
|
|
93
|
+
|
|
94
|
+
# ── Properties ────────────────────────────────────────────
|
|
95
|
+
|
|
96
|
+
@property
|
|
97
|
+
def id(self) -> int:
|
|
98
|
+
"""The resolved session_id."""
|
|
99
|
+
return self._session_id
|
|
100
|
+
|
|
101
|
+
@property
|
|
102
|
+
def event_name(self) -> str:
|
|
103
|
+
return self._event_name
|
|
104
|
+
|
|
105
|
+
@property
|
|
106
|
+
def name(self) -> str:
|
|
107
|
+
return self._session_name
|
|
108
|
+
|
|
109
|
+
@property
|
|
110
|
+
def session_type(self) -> str:
|
|
111
|
+
return self._session_type
|
|
112
|
+
|
|
113
|
+
def __repr__(self) -> str:
|
|
114
|
+
return (
|
|
115
|
+
f"Session({self.series_key} {self.year} "
|
|
116
|
+
f"{self._event_name} — {self._session_name}, id={self._session_id})"
|
|
117
|
+
)
|
|
118
|
+
|
|
119
|
+
# ── Data ──────────────────────────────────────────────────
|
|
120
|
+
|
|
121
|
+
def results(self) -> pd.DataFrame:
|
|
122
|
+
"""
|
|
123
|
+
Returns the final classification as a DataFrame.
|
|
124
|
+
One row per car, sorted by position.
|
|
125
|
+
Public endpoint — no API key required.
|
|
126
|
+
"""
|
|
127
|
+
if self._results_cache is not None:
|
|
128
|
+
return self._results_cache
|
|
129
|
+
|
|
130
|
+
data = _get(f"/sessions/{self._session_id}/results")
|
|
131
|
+
df = pd.json_normalize(data)
|
|
132
|
+
|
|
133
|
+
# Flatten drivers list into a readable column
|
|
134
|
+
if "drivers" in df.columns:
|
|
135
|
+
df["drivers"] = df["drivers"].apply(_format_drivers)
|
|
136
|
+
|
|
137
|
+
self._results_cache = df
|
|
138
|
+
return df
|
|
139
|
+
|
|
140
|
+
def laps(self, car: str | None = None) -> pd.DataFrame:
|
|
141
|
+
"""
|
|
142
|
+
Returns lap-by-lap data as a DataFrame.
|
|
143
|
+
Requires API key — call openwec.configure(api_key=...) first.
|
|
144
|
+
|
|
145
|
+
Args:
|
|
146
|
+
car: optional car number to filter (e.g. "50").
|
|
147
|
+
If None, returns all laps (paginated automatically).
|
|
148
|
+
"""
|
|
149
|
+
cache_key = car
|
|
150
|
+
if cache_key in self._laps_cache:
|
|
151
|
+
return self._laps_cache[cache_key]
|
|
152
|
+
|
|
153
|
+
if car:
|
|
154
|
+
data = _get(f"/sessions/{self._session_id}/laps/{car}")
|
|
155
|
+
df = pd.DataFrame(data)
|
|
156
|
+
else:
|
|
157
|
+
# Paginate through all laps
|
|
158
|
+
all_rows = []
|
|
159
|
+
page = 1
|
|
160
|
+
while True:
|
|
161
|
+
resp = _get(f"/sessions/{self._session_id}/laps", params={
|
|
162
|
+
"page": page, "page_size": 500
|
|
163
|
+
})
|
|
164
|
+
rows = resp.get("results", [])
|
|
165
|
+
all_rows.extend(rows)
|
|
166
|
+
if len(rows) < 500:
|
|
167
|
+
break
|
|
168
|
+
page += 1
|
|
169
|
+
df = pd.DataFrame(all_rows)
|
|
170
|
+
|
|
171
|
+
self._laps_cache[cache_key] = df
|
|
172
|
+
return df
|
|
173
|
+
|
|
174
|
+
# ── Analytics ─────────────────────────────────────────────
|
|
175
|
+
|
|
176
|
+
def stints(self, car: str | None = None, car_class: str | None = None) -> pd.DataFrame:
|
|
177
|
+
"""
|
|
178
|
+
Returns stint breakdown (baseline pace, degradation, consistency) per car.
|
|
179
|
+
Requires API key.
|
|
180
|
+
"""
|
|
181
|
+
params = {}
|
|
182
|
+
if car:
|
|
183
|
+
params["car"] = car
|
|
184
|
+
if car_class:
|
|
185
|
+
params["car_class"] = car_class
|
|
186
|
+
data = _get(f"/sessions/{self._session_id}/stints", params=params)
|
|
187
|
+
return pd.DataFrame(data)
|
|
188
|
+
|
|
189
|
+
def pace(self, car_class: str | None = None) -> pd.DataFrame:
|
|
190
|
+
"""
|
|
191
|
+
Returns average green-flag pace per car, sorted fastest first.
|
|
192
|
+
Requires API key.
|
|
193
|
+
"""
|
|
194
|
+
params = {}
|
|
195
|
+
if car_class:
|
|
196
|
+
params["car_class"] = car_class
|
|
197
|
+
data = _get(f"/sessions/{self._session_id}/pace", params=params)
|
|
198
|
+
return pd.DataFrame(data)
|
|
199
|
+
|
|
200
|
+
def gaps(self, car: str | None = None, car_class: str | None = None,
|
|
201
|
+
max_laps: int = 50) -> pd.DataFrame:
|
|
202
|
+
"""
|
|
203
|
+
Returns cumulative lap time evolution — useful for gap-to-leader charts.
|
|
204
|
+
Requires API key.
|
|
205
|
+
"""
|
|
206
|
+
params = {"max_laps": max_laps}
|
|
207
|
+
if car:
|
|
208
|
+
params["car"] = car
|
|
209
|
+
if car_class:
|
|
210
|
+
params["car_class"] = car_class
|
|
211
|
+
data = _get(f"/sessions/{self._session_id}/gaps", params=params)
|
|
212
|
+
return pd.DataFrame(data)
|
|
213
|
+
|
|
214
|
+
def pit_window(self, car: str | None = None, car_class: str | None = None,
|
|
215
|
+
pit_loss_s: float | None = None) -> pd.DataFrame:
|
|
216
|
+
"""
|
|
217
|
+
Returns estimated optimal pit window per stint per car.
|
|
218
|
+
Requires API key.
|
|
219
|
+
"""
|
|
220
|
+
params = {}
|
|
221
|
+
if car:
|
|
222
|
+
params["car"] = car
|
|
223
|
+
if car_class:
|
|
224
|
+
params["car_class"] = car_class
|
|
225
|
+
if pit_loss_s:
|
|
226
|
+
params["pit_loss_s"] = pit_loss_s
|
|
227
|
+
|
|
228
|
+
data = _get(f"/sessions/{self._session_id}/pit-window", params=params)
|
|
229
|
+
|
|
230
|
+
# Flatten nested stints into rows
|
|
231
|
+
rows = []
|
|
232
|
+
for car_data in data:
|
|
233
|
+
for stint in car_data["stints"]:
|
|
234
|
+
rows.append({
|
|
235
|
+
"car_number": car_data["car_number"],
|
|
236
|
+
"car_class": car_data["car_class"],
|
|
237
|
+
"team": car_data["team"],
|
|
238
|
+
"pit_loss_s": car_data["pit_loss_s"],
|
|
239
|
+
**stint,
|
|
240
|
+
})
|
|
241
|
+
return pd.DataFrame(rows)
|
|
242
|
+
|
|
243
|
+
# ── Plots ─────────────────────────────────────────────────
|
|
244
|
+
|
|
245
|
+
def plot_lap_evolution(self, car: str, ax=None):
|
|
246
|
+
"""
|
|
247
|
+
Plots lap time evolution for a car. Requires matplotlib.
|
|
248
|
+
Requires API key (uses session.laps()).
|
|
249
|
+
"""
|
|
250
|
+
from . import plotting
|
|
251
|
+
laps = self.laps(car=car)
|
|
252
|
+
return plotting.plot_lap_evolution(laps, car=car, ax=ax)
|
|
253
|
+
|
|
254
|
+
def plot_stint_chart(self, car_class: str | None = None, ax=None):
|
|
255
|
+
"""
|
|
256
|
+
Plots a stint chart (strategy overview) for all cars.
|
|
257
|
+
Requires matplotlib and API key.
|
|
258
|
+
"""
|
|
259
|
+
from . import plotting
|
|
260
|
+
stints = self.stints(car_class=car_class)
|
|
261
|
+
return plotting.plot_stint_chart(stints, ax=ax)
|
|
262
|
+
|
|
263
|
+
def plot_gap_to_leader(self, car_class: str | None = None, max_laps: int = 50, ax=None):
|
|
264
|
+
"""
|
|
265
|
+
Plots gap to leader over race distance.
|
|
266
|
+
Requires matplotlib and API key.
|
|
267
|
+
"""
|
|
268
|
+
from . import plotting
|
|
269
|
+
gaps = self.gaps(car_class=car_class, max_laps=max_laps)
|
|
270
|
+
return plotting.plot_gap_to_leader(gaps, ax=ax)
|
|
271
|
+
|
|
272
|
+
|
|
273
|
+
# ── Helpers ───────────────────────────────────────────────────
|
|
274
|
+
|
|
275
|
+
def _find_best_match(items: list[dict], key: str, query: str) -> dict | None:
|
|
276
|
+
"""
|
|
277
|
+
Finds the best match for `query` against `item[key]`.
|
|
278
|
+
Priority: exact match (case-insensitive) > substring match.
|
|
279
|
+
"""
|
|
280
|
+
query_lower = query.strip().lower()
|
|
281
|
+
|
|
282
|
+
# Exact match first
|
|
283
|
+
for item in items:
|
|
284
|
+
if item[key].strip().lower() == query_lower:
|
|
285
|
+
return item
|
|
286
|
+
|
|
287
|
+
# Substring match
|
|
288
|
+
for item in items:
|
|
289
|
+
if query_lower in item[key].strip().lower():
|
|
290
|
+
return item
|
|
291
|
+
|
|
292
|
+
return None
|
|
293
|
+
|
|
294
|
+
|
|
295
|
+
def _format_drivers(drivers: list[dict]) -> str:
|
|
296
|
+
"""Formats a list of driver dicts into 'First Last / First Last' string."""
|
|
297
|
+
if not drivers:
|
|
298
|
+
return ""
|
|
299
|
+
names = []
|
|
300
|
+
for d in sorted(drivers, key=lambda x: x.get("slot", 0)):
|
|
301
|
+
first = d.get("first_name", "")
|
|
302
|
+
last = d.get("last_name", "")
|
|
303
|
+
names.append(f"{first} {last}".strip())
|
|
304
|
+
return " / ".join(names)
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: openwec
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Python SDK for endurance racing data — WEC, ELMS, ALMS, Le Mans Cup, IMSA
|
|
5
|
+
Author: Paloma C.
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Project-URL: Homepage, https://openwec.com
|
|
8
|
+
Project-URL: Documentation, https://api.openwec.com/docs
|
|
9
|
+
Project-URL: Repository, https://github.com/palomacdev/openwec
|
|
10
|
+
Project-URL: Bug Tracker, https://github.com/palomacdev/openwec/issues
|
|
11
|
+
Keywords: motorsport,endurance racing,WEC,Le Mans,ELMS,IMSA,timing data,lap times,pandas
|
|
12
|
+
Classifier: Development Status :: 3 - Alpha
|
|
13
|
+
Classifier: Intended Audience :: Developers
|
|
14
|
+
Classifier: Intended Audience :: Science/Research
|
|
15
|
+
Classifier: Programming Language :: Python :: 3
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
19
|
+
Classifier: Topic :: Scientific/Engineering :: Information Analysis
|
|
20
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
21
|
+
Requires-Python: >=3.10
|
|
22
|
+
Description-Content-Type: text/markdown
|
|
23
|
+
License-File: LICENSE
|
|
24
|
+
Requires-Dist: requests>=2.31
|
|
25
|
+
Requires-Dist: pandas>=2.0
|
|
26
|
+
Provides-Extra: plotting
|
|
27
|
+
Requires-Dist: matplotlib>=3.7; extra == "plotting"
|
|
28
|
+
Provides-Extra: dev
|
|
29
|
+
Requires-Dist: matplotlib>=3.7; extra == "dev"
|
|
30
|
+
Requires-Dist: jupyter>=1.0; extra == "dev"
|
|
31
|
+
Requires-Dist: ipykernel>=6.0; extra == "dev"
|
|
32
|
+
Requires-Dist: twine>=5.0; extra == "dev"
|
|
33
|
+
Requires-Dist: build>=1.0; extra == "dev"
|
|
34
|
+
Dynamic: license-file
|
|
35
|
+
|
|
36
|
+
# openwec
|
|
37
|
+
|
|
38
|
+
**Python SDK for endurance racing data — WEC, ELMS, ALMS, Le Mans Cup, IMSA.**
|
|
39
|
+
|
|
40
|
+
Inspired by [FastF1](https://github.com/theOehrly/Fast-F1), OpenWEC gives you lap-by-lap timing data, stint analytics, and race results from endurance racing as pandas DataFrames — in one line.
|
|
41
|
+
|
|
42
|
+
→ **[openwec.com](https://openwec.com)** — Live dashboard
|
|
43
|
+
→ **[api.openwec.com/docs](https://api.openwec.com/docs)** — API reference
|
|
44
|
+
→ **[github.com/palomacdev/openwec](https://github.com/palomacdev/openwec)** — Source
|
|
45
|
+
|
|
46
|
+
---
|
|
47
|
+
|
|
48
|
+
## Installation
|
|
49
|
+
|
|
50
|
+
```bash
|
|
51
|
+
pip install openwec
|
|
52
|
+
|
|
53
|
+
# With plotting support
|
|
54
|
+
pip install openwec[plotting]
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
---
|
|
58
|
+
|
|
59
|
+
## Quick start
|
|
60
|
+
|
|
61
|
+
```python
|
|
62
|
+
import openwec
|
|
63
|
+
|
|
64
|
+
# Configure — public endpoints work without a key
|
|
65
|
+
# Request a free key at https://openwec.com/api-keys
|
|
66
|
+
openwec.configure(
|
|
67
|
+
base_url="https://api.openwec.com/api/v1",
|
|
68
|
+
api_key="your-key-here" # required for laps and analytics
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
# Load any session
|
|
72
|
+
session = openwec.Session("WEC", 2026, "Le Mans", "Race")
|
|
73
|
+
print(session)
|
|
74
|
+
# Session(WEC 2026 LE MANS — Race, id=6556)
|
|
75
|
+
|
|
76
|
+
# Results as a DataFrame (no key needed)
|
|
77
|
+
results = session.results()
|
|
78
|
+
print(results[["position", "car_number", "car_class", "team", "drivers"]].head(10))
|
|
79
|
+
|
|
80
|
+
# Lap-by-lap data (key required)
|
|
81
|
+
laps = session.laps(car="7")
|
|
82
|
+
print(laps[["lap_number", "lap_time_s", "s1_s", "s2_s", "s3_s"]].head())
|
|
83
|
+
|
|
84
|
+
# Stints and pace (key required)
|
|
85
|
+
stints = session.stints(car_class="HYPERCAR")
|
|
86
|
+
pace = session.pace(car_class="HYPERCAR")
|
|
87
|
+
|
|
88
|
+
# Pit window estimate
|
|
89
|
+
pit_window = session.pit_window(car="7")
|
|
90
|
+
|
|
91
|
+
# Plots (requires matplotlib)
|
|
92
|
+
session.plot_lap_evolution(car="7")
|
|
93
|
+
session.plot_stint_chart(car_class="HYPERCAR")
|
|
94
|
+
session.plot_gap_to_leader(car_class="HYPERCAR")
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
---
|
|
98
|
+
|
|
99
|
+
## Coverage
|
|
100
|
+
|
|
101
|
+
| Series | Seasons |
|
|
102
|
+
|--------|---------|
|
|
103
|
+
| FIA WEC | 2012–2026 |
|
|
104
|
+
| ELMS | 2012–2026 |
|
|
105
|
+
| ALMS (Asian) | 2022–2026 |
|
|
106
|
+
| Le Mans Cup | 2017–2026 |
|
|
107
|
+
| IMSA | 2014–2026 |
|
|
108
|
+
|
|
109
|
+
**1.77M+ laps** across all series.
|
|
110
|
+
|
|
111
|
+
---
|
|
112
|
+
|
|
113
|
+
## API key
|
|
114
|
+
|
|
115
|
+
Public endpoints (results, driver profiles, team profiles) require no key.
|
|
116
|
+
Lap-by-lap data and analytics endpoints require a free API key.
|
|
117
|
+
|
|
118
|
+
→ **[Request a key at openwec.com/api-keys](https://openwec.com/api-keys)**
|
|
119
|
+
|
|
120
|
+
---
|
|
121
|
+
|
|
122
|
+
## Session methods
|
|
123
|
+
|
|
124
|
+
| Method | Returns | Key required |
|
|
125
|
+
|--------|---------|-------------|
|
|
126
|
+
| `.results()` | DataFrame | No |
|
|
127
|
+
| `.laps(car=None)` | DataFrame | Yes |
|
|
128
|
+
| `.stints(car_class=None)` | DataFrame | Yes |
|
|
129
|
+
| `.pace(car_class=None)` | DataFrame | Yes |
|
|
130
|
+
| `.gaps(car_class=None)` | DataFrame | Yes |
|
|
131
|
+
| `.pit_window(car=None)` | DataFrame | Yes |
|
|
132
|
+
| `.plot_lap_evolution(car)` | Figure | Yes |
|
|
133
|
+
| `.plot_stint_chart()` | Figure | Yes |
|
|
134
|
+
| `.plot_gap_to_leader()` | Figure | Yes |
|
|
135
|
+
|
|
136
|
+
---
|
|
137
|
+
|
|
138
|
+
## License
|
|
139
|
+
|
|
140
|
+
MIT — data sourced from Al Kamel Systems public timing exports.
|
|
141
|
+
Not affiliated with ACO, FIA, or any racing organization.
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
LICENSE
|
|
2
|
+
README.md
|
|
3
|
+
pyproject.toml
|
|
4
|
+
openwec/__init__.py
|
|
5
|
+
openwec/client.py
|
|
6
|
+
openwec/plotting.py
|
|
7
|
+
openwec/session.py
|
|
8
|
+
openwec.egg-info/PKG-INFO
|
|
9
|
+
openwec.egg-info/SOURCES.txt
|
|
10
|
+
openwec.egg-info/dependency_links.txt
|
|
11
|
+
openwec.egg-info/requires.txt
|
|
12
|
+
openwec.egg-info/top_level.txt
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
openwec
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=68", "wheel"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "openwec"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "Python SDK for endurance racing data — WEC, ELMS, ALMS, Le Mans Cup, IMSA"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
license = "MIT"
|
|
11
|
+
license-files = ["LICENSE"]
|
|
12
|
+
requires-python = ">=3.10"
|
|
13
|
+
|
|
14
|
+
authors = [
|
|
15
|
+
{ name = "Paloma C."}
|
|
16
|
+
]
|
|
17
|
+
|
|
18
|
+
keywords = [
|
|
19
|
+
"motorsport", "endurance racing", "WEC", "Le Mans",
|
|
20
|
+
"ELMS", "IMSA", "timing data", "lap times", "pandas"
|
|
21
|
+
]
|
|
22
|
+
|
|
23
|
+
classifiers = [
|
|
24
|
+
"Development Status :: 3 - Alpha",
|
|
25
|
+
"Intended Audience :: Developers",
|
|
26
|
+
"Intended Audience :: Science/Research",
|
|
27
|
+
"Programming Language :: Python :: 3",
|
|
28
|
+
"Programming Language :: Python :: 3.10",
|
|
29
|
+
"Programming Language :: Python :: 3.11",
|
|
30
|
+
"Programming Language :: Python :: 3.12",
|
|
31
|
+
"Topic :: Scientific/Engineering :: Information Analysis",
|
|
32
|
+
"Topic :: Software Development :: Libraries :: Python Modules",
|
|
33
|
+
]
|
|
34
|
+
|
|
35
|
+
dependencies = [
|
|
36
|
+
"requests>=2.31",
|
|
37
|
+
"pandas>=2.0",
|
|
38
|
+
]
|
|
39
|
+
|
|
40
|
+
[project.optional-dependencies]
|
|
41
|
+
plotting = ["matplotlib>=3.7"]
|
|
42
|
+
dev = [
|
|
43
|
+
"matplotlib>=3.7",
|
|
44
|
+
"jupyter>=1.0",
|
|
45
|
+
"ipykernel>=6.0",
|
|
46
|
+
"twine>=5.0",
|
|
47
|
+
"build>=1.0",
|
|
48
|
+
]
|
|
49
|
+
|
|
50
|
+
[project.urls]
|
|
51
|
+
Homepage = "https://openwec.com"
|
|
52
|
+
Documentation = "https://api.openwec.com/docs"
|
|
53
|
+
Repository = "https://github.com/palomacdev/openwec"
|
|
54
|
+
"Bug Tracker" = "https://github.com/palomacdev/openwec/issues"
|
|
55
|
+
|
|
56
|
+
[tool.setuptools.packages.find]
|
|
57
|
+
where = ["."]
|
|
58
|
+
include = ["openwec*"]
|
openwec-0.1.0/setup.cfg
ADDED