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 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.
@@ -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,12 @@
1
+ requests>=2.31
2
+ pandas>=2.0
3
+
4
+ [dev]
5
+ matplotlib>=3.7
6
+ jupyter>=1.0
7
+ ipykernel>=6.0
8
+ twine>=5.0
9
+ build>=1.0
10
+
11
+ [plotting]
12
+ matplotlib>=3.7
@@ -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*"]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+