openwec 0.1.0__py3-none-any.whl

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/__init__.py ADDED
@@ -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
+ ]
openwec/client.py ADDED
@@ -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)."""
openwec/plotting.py ADDED
@@ -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
openwec/session.py ADDED
@@ -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,139 @@
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
+ Requires-Dist: requests>=2.31
24
+ Requires-Dist: pandas>=2.0
25
+ Provides-Extra: plotting
26
+ Requires-Dist: matplotlib>=3.7; extra == "plotting"
27
+ Provides-Extra: dev
28
+ Requires-Dist: matplotlib>=3.7; extra == "dev"
29
+ Requires-Dist: jupyter>=1.0; extra == "dev"
30
+ Requires-Dist: ipykernel>=6.0; extra == "dev"
31
+ Requires-Dist: twine>=5.0; extra == "dev"
32
+ Requires-Dist: build>=1.0; extra == "dev"
33
+
34
+ # openwec
35
+
36
+ **Python SDK for endurance racing data — WEC, ELMS, ALMS, Le Mans Cup, IMSA.**
37
+
38
+ 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.
39
+
40
+ → **[openwec.com](https://openwec.com)** — Live dashboard
41
+ → **[api.openwec.com/docs](https://api.openwec.com/docs)** — API reference
42
+ → **[github.com/palomacdev/openwec](https://github.com/palomacdev/openwec)** — Source
43
+
44
+ ---
45
+
46
+ ## Installation
47
+
48
+ ```bash
49
+ pip install openwec
50
+
51
+ # With plotting support
52
+ pip install openwec[plotting]
53
+ ```
54
+
55
+ ---
56
+
57
+ ## Quick start
58
+
59
+ ```python
60
+ import openwec
61
+
62
+ # Configure — public endpoints work without a key
63
+ # Request a free key at https://openwec.com/api-keys
64
+ openwec.configure(
65
+ base_url="https://api.openwec.com/api/v1",
66
+ api_key="your-key-here" # required for laps and analytics
67
+ )
68
+
69
+ # Load any session
70
+ session = openwec.Session("WEC", 2026, "Le Mans", "Race")
71
+ print(session)
72
+ # Session(WEC 2026 LE MANS — Race, id=6556)
73
+
74
+ # Results as a DataFrame (no key needed)
75
+ results = session.results()
76
+ print(results[["position", "car_number", "car_class", "team", "drivers"]].head(10))
77
+
78
+ # Lap-by-lap data (key required)
79
+ laps = session.laps(car="7")
80
+ print(laps[["lap_number", "lap_time_s", "s1_s", "s2_s", "s3_s"]].head())
81
+
82
+ # Stints and pace (key required)
83
+ stints = session.stints(car_class="HYPERCAR")
84
+ pace = session.pace(car_class="HYPERCAR")
85
+
86
+ # Pit window estimate
87
+ pit_window = session.pit_window(car="7")
88
+
89
+ # Plots (requires matplotlib)
90
+ session.plot_lap_evolution(car="7")
91
+ session.plot_stint_chart(car_class="HYPERCAR")
92
+ session.plot_gap_to_leader(car_class="HYPERCAR")
93
+ ```
94
+
95
+ ---
96
+
97
+ ## Coverage
98
+
99
+ | Series | Seasons |
100
+ |--------|---------|
101
+ | FIA WEC | 2012–2026 |
102
+ | ELMS | 2012–2026 |
103
+ | ALMS (Asian) | 2022–2026 |
104
+ | Le Mans Cup | 2017–2026 |
105
+ | IMSA | 2014–2026 |
106
+
107
+ **1.77M+ laps** across all series.
108
+
109
+ ---
110
+
111
+ ## API key
112
+
113
+ Public endpoints (results, driver profiles, team profiles) require no key.
114
+ Lap-by-lap data and analytics endpoints require a free API key.
115
+
116
+ → **[Request a key at openwec.com/api-keys](https://openwec.com/api-keys)**
117
+
118
+ ---
119
+
120
+ ## Session methods
121
+
122
+ | Method | Returns | Key required |
123
+ |--------|---------|-------------|
124
+ | `.results()` | DataFrame | No |
125
+ | `.laps(car=None)` | DataFrame | Yes |
126
+ | `.stints(car_class=None)` | DataFrame | Yes |
127
+ | `.pace(car_class=None)` | DataFrame | Yes |
128
+ | `.gaps(car_class=None)` | DataFrame | Yes |
129
+ | `.pit_window(car=None)` | DataFrame | Yes |
130
+ | `.plot_lap_evolution(car)` | Figure | Yes |
131
+ | `.plot_stint_chart()` | Figure | Yes |
132
+ | `.plot_gap_to_leader()` | Figure | Yes |
133
+
134
+ ---
135
+
136
+ ## License
137
+
138
+ MIT — data sourced from Al Kamel Systems public timing exports.
139
+ Not affiliated with ACO, FIA, or any racing organization.
@@ -0,0 +1,8 @@
1
+ openwec/__init__.py,sha256=wJJc3T6sGgKbPLo7L3BVJU3Hgc_d8c-GqXDvja9cljs,1108
2
+ openwec/client.py,sha256=1xwcBXIrHF0m4i3-KvT23xKPSr6AtgNefq4Sg3ZeQ9Q,1669
3
+ openwec/plotting.py,sha256=iAsA8a7PhYB4ybUoRok9i0jXV4b6ssEmlC22kg4nFk4,3943
4
+ openwec/session.py,sha256=8cQ7a-WR6Qkgv59ymYQJWFtt10OCWzfX2LY9Oc4YLzs,11106
5
+ openwec-0.1.0.dist-info/METADATA,sha256=7FDjcf_9Z7Rpt9_wTUqNZ24vsz7zFDIVIJMcdh2YWKQ,4362
6
+ openwec-0.1.0.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
7
+ openwec-0.1.0.dist-info/top_level.txt,sha256=zWvhMMbttx58mLwB47zGzQn0uIvWqg56F027jpgK19k,8
8
+ openwec-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (82.0.1)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1 @@
1
+ openwec