pitchapi 0.1.1

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.
package/dist/index.js ADDED
@@ -0,0 +1,360 @@
1
+ // src/errors.ts
2
+ var PitchAPIError = class extends Error {
3
+ constructor(message, options = {}) {
4
+ super(message, options.cause === void 0 ? void 0 : { cause: options.cause });
5
+ this.name = new.target.name;
6
+ this.code = options.code;
7
+ this.status = options.status;
8
+ this.requestId = options.requestId;
9
+ }
10
+ };
11
+ var APIConnectionError = class extends PitchAPIError {
12
+ };
13
+ var InvalidParameterError = class extends PitchAPIError {
14
+ };
15
+ var AuthError = class extends PitchAPIError {
16
+ };
17
+ var SubscriptionSuspendedError = class extends PitchAPIError {
18
+ };
19
+ var PlanUpgradeRequiredError = class extends PitchAPIError {
20
+ };
21
+ var NotFoundError = class extends PitchAPIError {
22
+ };
23
+ var RateLimitError = class extends PitchAPIError {
24
+ constructor(message, options = {}) {
25
+ super(message, options);
26
+ this.retryAfter = options.retryAfter;
27
+ }
28
+ };
29
+ var ServerError = class extends PitchAPIError {
30
+ };
31
+ var CODE_MAP = {
32
+ INVALID_PARAMETER: InvalidParameterError,
33
+ UNAUTHORIZED: AuthError,
34
+ SUBSCRIPTION_SUSPENDED: SubscriptionSuspendedError,
35
+ PLAN_UPGRADE_REQUIRED: PlanUpgradeRequiredError,
36
+ RESOURCE_NOT_FOUND: NotFoundError,
37
+ ANALYTICS_UNAVAILABLE: NotFoundError,
38
+ HALVES_UNAVAILABLE: NotFoundError,
39
+ RATE_LIMIT_EXCEEDED: RateLimitError,
40
+ INTERNAL_SERVER_ERROR: ServerError
41
+ };
42
+ function errorClassFor(code, status) {
43
+ if (code !== void 0 && Object.prototype.hasOwnProperty.call(CODE_MAP, code)) {
44
+ return CODE_MAP[code];
45
+ }
46
+ if (status === 400) return InvalidParameterError;
47
+ if (status === 401) return AuthError;
48
+ if (status === 403) return PlanUpgradeRequiredError;
49
+ if (status === 404) return NotFoundError;
50
+ if (status === 429) return RateLimitError;
51
+ if (status >= 500) return ServerError;
52
+ return PitchAPIError;
53
+ }
54
+
55
+ // src/version.ts
56
+ var VERSION = "0.1.1";
57
+
58
+ // src/transport.ts
59
+ var DEFAULT_BASE_URL = "https://api.pitchapi.dev";
60
+ var API_PREFIX = "/v1";
61
+ var RETRY_STATUSES = /* @__PURE__ */ new Set([429, 500, 502, 503, 504]);
62
+ var defaultSleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
63
+ function isServerRuntime() {
64
+ return typeof globalThis.document === "undefined";
65
+ }
66
+ function buildHeaders(apiKey) {
67
+ const headers = {
68
+ "X-API-KEY": apiKey,
69
+ Accept: "application/json"
70
+ };
71
+ if (isServerRuntime()) headers["User-Agent"] = `pitchapi-js/${VERSION}`;
72
+ return headers;
73
+ }
74
+ function parseRetryAfter(value) {
75
+ if (!value) return void 0;
76
+ const n = Number.parseInt(value, 10);
77
+ return Number.isFinite(n) && n >= 0 ? n : void 0;
78
+ }
79
+ function backoffMs(attempt, retryAfter) {
80
+ if (retryAfter !== void 0) return retryAfter * 1e3;
81
+ const base = Math.min(2 ** attempt, 8);
82
+ return Math.random() * base * 1e3;
83
+ }
84
+ function buildQuery(params) {
85
+ if (!params) return "";
86
+ const qs = new URLSearchParams();
87
+ for (const [k, v] of Object.entries(params)) {
88
+ if (v !== void 0) qs.set(k, v);
89
+ }
90
+ const s = qs.toString();
91
+ return s ? `?${s}` : "";
92
+ }
93
+ function unwrap(status, payload, requestId, retryAfter) {
94
+ if (status >= 200 && status < 300) {
95
+ if (payload !== null && typeof payload === "object" && "data" in payload) {
96
+ return payload.data;
97
+ }
98
+ throw new PitchAPIError("malformed success response: missing 'data' envelope", { status, requestId });
99
+ }
100
+ let code;
101
+ let message = `HTTP ${status}`;
102
+ if (payload !== null && typeof payload === "object" && "error" in payload) {
103
+ const err = payload.error;
104
+ if (err !== null && typeof err === "object") {
105
+ const e = err;
106
+ if (typeof e.code === "string") code = e.code;
107
+ if (typeof e.message === "string") message = e.message;
108
+ }
109
+ }
110
+ const Cls = errorClassFor(code, status);
111
+ if (Cls === RateLimitError) {
112
+ throw new RateLimitError(message, { code, status, requestId, retryAfter });
113
+ }
114
+ if (Cls === ServerError && code === void 0) message = `server error (HTTP ${status})`;
115
+ throw new Cls(message, { code, status, requestId });
116
+ }
117
+ var Transport = class {
118
+ constructor(opts) {
119
+ this.opts = opts;
120
+ this.headers = buildHeaders(opts.apiKey);
121
+ this.sleep = opts.sleep ?? defaultSleep;
122
+ }
123
+ async get(path, query, signal) {
124
+ const url = this.opts.baseUrl + API_PREFIX + path + buildQuery(query);
125
+ let attempt = 0;
126
+ for (; ; ) {
127
+ let response;
128
+ try {
129
+ response = await this.fetchOnce(url, signal);
130
+ } catch (err) {
131
+ if (signal?.aborted) throw err;
132
+ if (attempt < this.opts.maxRetries) {
133
+ await this.sleep(backoffMs(attempt, void 0));
134
+ attempt++;
135
+ continue;
136
+ }
137
+ throw new APIConnectionError(`request failed: ${describe(err)}`, { cause: err });
138
+ }
139
+ const status = response.status;
140
+ const retryAfter = parseRetryAfter(response.headers.get("Retry-After"));
141
+ if (attempt < this.opts.maxRetries && RETRY_STATUSES.has(status)) {
142
+ await response.body?.cancel().catch(() => void 0);
143
+ await this.sleep(backoffMs(attempt, retryAfter));
144
+ attempt++;
145
+ continue;
146
+ }
147
+ const requestId = response.headers.get("X-Request-ID") ?? void 0;
148
+ let payload = null;
149
+ try {
150
+ payload = await response.json();
151
+ } catch {
152
+ }
153
+ return unwrap(status, payload, requestId, retryAfter);
154
+ }
155
+ }
156
+ async fetchOnce(url, signal) {
157
+ const timeout = AbortSignal.timeout(this.opts.timeoutMs);
158
+ const combined = signal ? anySignal([signal, timeout]) : timeout;
159
+ return this.opts.fetch(url, { method: "GET", headers: this.headers, signal: combined });
160
+ }
161
+ };
162
+ function anySignal(signals) {
163
+ const any = AbortSignal.any;
164
+ if (any) return any(signals);
165
+ const controller = new AbortController();
166
+ for (const s of signals) {
167
+ if (s.aborted) {
168
+ controller.abort(s.reason);
169
+ break;
170
+ }
171
+ s.addEventListener("abort", () => controller.abort(s.reason), { once: true });
172
+ }
173
+ return controller.signal;
174
+ }
175
+ function describe(err) {
176
+ return err instanceof Error ? err.message : String(err);
177
+ }
178
+
179
+ // src/client.ts
180
+ function env(name) {
181
+ const proc = globalThis.process;
182
+ return proc?.env?.[name] || void 0;
183
+ }
184
+ function formatDate(value) {
185
+ return typeof value === "string" ? value : value.toISOString().slice(0, 10);
186
+ }
187
+ var seg = encodeURIComponent;
188
+ var Resource = class {
189
+ constructor(t) {
190
+ this.t = t;
191
+ }
192
+ request(path, opts, query) {
193
+ return this.t.get(path, query, opts?.signal);
194
+ }
195
+ };
196
+ var DateResource = class extends Resource {
197
+ /**
198
+ * Every match on a day. `date` is a `YYYY-MM-DD` string or a `Date`, which
199
+ * is read in UTC. An upcoming fixture carries a kickoff `time_utc` and a
200
+ * `status` but no score yet.
201
+ */
202
+ get(date, opts) {
203
+ return this.request(`/date/${seg(formatDate(date))}`, opts, { status: opts?.status });
204
+ }
205
+ };
206
+ var MatchesResource = class extends Resource {
207
+ /** Match summary — league, teams, score, referee, stadium. */
208
+ get(matchId, opts) {
209
+ return this.request(`/matches/${seg(matchId)}`, opts);
210
+ }
211
+ /** Every shot, grouped by period, with pitch coordinates and xG. */
212
+ shots(matchId, opts) {
213
+ return this.request(`/matches/${seg(matchId)}/shots`, opts);
214
+ }
215
+ /** A single shot by its opaque ID. */
216
+ shot(matchId, shotId, opts) {
217
+ return this.request(`/matches/${seg(matchId)}/shots/${seg(shotId)}`, opts);
218
+ }
219
+ /** Match events (goals, cards, subs, ...). */
220
+ events(matchId, opts) {
221
+ return this.request(`/matches/${seg(matchId)}/events`, opts);
222
+ }
223
+ /** Starting lineups and substitutes. Before kickoff the XI may be predicted. */
224
+ lineups(matchId, opts) {
225
+ return this.request(`/matches/${seg(matchId)}/lineups`, opts);
226
+ }
227
+ /** Possession-momentum timeline. */
228
+ momentum(matchId, opts) {
229
+ return this.request(`/matches/${seg(matchId)}/momentum`, opts);
230
+ }
231
+ /** Team-level match statistics. */
232
+ stats(matchId, opts) {
233
+ return this.request(`/matches/${seg(matchId)}/stats`, opts);
234
+ }
235
+ /** Per-player match stat lines. */
236
+ players(matchId, opts) {
237
+ return this.request(`/matches/${seg(matchId)}/players`, opts);
238
+ }
239
+ /** One player's match stat line. */
240
+ player(matchId, playerId, opts) {
241
+ return this.request(`/matches/${seg(matchId)}/players/${seg(playerId)}`, opts);
242
+ }
243
+ /**
244
+ * Every player's stat line for the first and the second half. Throws
245
+ * `NotFoundError` (code `HALVES_UNAVAILABLE`) for a match without them.
246
+ */
247
+ playersHalves(matchId, opts) {
248
+ return this.request(`/matches/${seg(matchId)}/players/halves`, opts);
249
+ }
250
+ /** One player's stat line for each half they played. */
251
+ playerHalves(matchId, playerId, opts) {
252
+ return this.request(`/matches/${seg(matchId)}/players/${seg(playerId)}/halves`, opts);
253
+ }
254
+ /** A single player's shots in the match. */
255
+ playerShots(matchId, playerId, opts) {
256
+ return this.request(`/matches/${seg(matchId)}/players/${seg(playerId)}/shots`, opts);
257
+ }
258
+ /** Head-to-head history for the two sides. */
259
+ h2h(matchId, opts) {
260
+ return this.request(`/matches/${seg(matchId)}/h2h`, opts);
261
+ }
262
+ /**
263
+ * Team advanced rollups (PPDA, field tilt, xG timeline). Throws
264
+ * `NotFoundError` (code `ANALYTICS_UNAVAILABLE`) for a match not yet rated.
265
+ */
266
+ advanced(matchId, opts) {
267
+ return this.request(`/matches/${seg(matchId)}/advanced`, opts);
268
+ }
269
+ /** Pass networks for both sides. */
270
+ advancedNetwork(matchId, opts) {
271
+ return this.request(`/matches/${seg(matchId)}/advanced/network`, opts);
272
+ }
273
+ /** Player-level advanced rollups. */
274
+ advancedPlayers(matchId, opts) {
275
+ return this.request(`/matches/${seg(matchId)}/advanced/players`, opts);
276
+ }
277
+ /** One player's advanced rollup. */
278
+ advancedPlayer(matchId, playerId, opts) {
279
+ return this.request(`/matches/${seg(matchId)}/advanced/players/${seg(playerId)}`, opts);
280
+ }
281
+ /**
282
+ * Binned activity for both teams and every player who touched the ball.
283
+ * Cells are sparse `[cell_x, cell_y, actions]` triples on a 16x12 grid.
284
+ */
285
+ heatmaps(matchId, opts) {
286
+ return this.request(`/matches/${seg(matchId)}/heatmaps`, opts, { frame: opts?.frame });
287
+ }
288
+ };
289
+ var LeaguesResource = class extends Resource {
290
+ /** All leagues, each with its accepted seasons and coverage. */
291
+ list(opts) {
292
+ return this.request("/leagues", opts);
293
+ }
294
+ /** League detail. */
295
+ get(leagueId, opts) {
296
+ return this.request(`/leagues/${seg(leagueId)}`, opts);
297
+ }
298
+ /**
299
+ * Matches in a league. `season` is `"2025/2026"` for a fall-spring league
300
+ * and `"2026"` for a calendar-year one; omitted, the latest season with
301
+ * data. The season is resolved before `status` applies, so `upcoming` on a
302
+ * finished season is an empty list.
303
+ */
304
+ matches(leagueId, opts) {
305
+ return this.request(`/leagues/${seg(leagueId)}/matches`, opts, {
306
+ season: opts?.season,
307
+ status: opts?.status
308
+ });
309
+ }
310
+ };
311
+ var TeamsResource = class extends Resource {
312
+ /** Team profile. */
313
+ get(teamId, opts) {
314
+ return this.request(`/teams/${seg(teamId)}`, opts);
315
+ }
316
+ };
317
+ var PlayersResource = class extends Resource {
318
+ /** Player profile. */
319
+ get(playerId, opts) {
320
+ return this.request(`/players/${seg(playerId)}`, opts);
321
+ }
322
+ };
323
+ var PitchAPI = class {
324
+ constructor(options = {}) {
325
+ const apiKey = options.apiKey ?? env("PITCHAPI_API_KEY");
326
+ if (!apiKey) {
327
+ throw new Error("an API key is required: pass { apiKey } or set the PITCHAPI_API_KEY environment variable");
328
+ }
329
+ const fetchImpl = options.fetch ?? globalThis.fetch;
330
+ if (!fetchImpl) {
331
+ throw new Error("no global fetch: use Node 18+ or pass { fetch }");
332
+ }
333
+ const transport = new Transport({
334
+ apiKey,
335
+ baseUrl: (options.baseUrl ?? env("PITCHAPI_BASE_URL") ?? DEFAULT_BASE_URL).replace(/\/+$/, ""),
336
+ timeoutMs: options.timeoutMs ?? 3e4,
337
+ maxRetries: options.maxRetries ?? 2,
338
+ // Bound to globalThis: some runtimes throw on an unbound fetch.
339
+ fetch: options.fetch ?? fetchImpl.bind(globalThis)
340
+ });
341
+ this.date = new DateResource(transport);
342
+ this.matches = new MatchesResource(transport);
343
+ this.leagues = new LeaguesResource(transport);
344
+ this.teams = new TeamsResource(transport);
345
+ this.players = new PlayersResource(transport);
346
+ }
347
+ };
348
+ export {
349
+ APIConnectionError,
350
+ AuthError,
351
+ InvalidParameterError,
352
+ NotFoundError,
353
+ PitchAPI,
354
+ PitchAPIError,
355
+ PlanUpgradeRequiredError,
356
+ RateLimitError,
357
+ ServerError,
358
+ SubscriptionSuspendedError,
359
+ VERSION
360
+ };
package/package.json ADDED
@@ -0,0 +1,59 @@
1
+ {
2
+ "name": "pitchapi",
3
+ "version": "0.1.1",
4
+ "description": "TypeScript SDK for the PitchAPI football-data API",
5
+ "keywords": [
6
+ "football",
7
+ "soccer",
8
+ "xg",
9
+ "analytics",
10
+ "api",
11
+ "sdk"
12
+ ],
13
+ "license": "MIT",
14
+ "author": "PitchAPI",
15
+ "homepage": "https://pitchapi.dev",
16
+ "type": "module",
17
+ "exports": {
18
+ ".": {
19
+ "import": {
20
+ "types": "./dist/index.d.ts",
21
+ "default": "./dist/index.js"
22
+ },
23
+ "require": {
24
+ "types": "./dist/index.d.cts",
25
+ "default": "./dist/index.cjs"
26
+ }
27
+ },
28
+ "./package.json": "./package.json"
29
+ },
30
+ "main": "./dist/index.cjs",
31
+ "module": "./dist/index.js",
32
+ "types": "./dist/index.d.ts",
33
+ "files": [
34
+ "dist",
35
+ "README.md",
36
+ "LICENSE"
37
+ ],
38
+ "sideEffects": false,
39
+ "engines": {
40
+ "node": ">=18"
41
+ },
42
+ "scripts": {
43
+ "gen": "node scripts/gen.mjs",
44
+ "build": "tsup",
45
+ "typecheck": "tsc --noEmit && tsc --noEmit -p test",
46
+ "test": "vitest run",
47
+ "check": "publint --strict && attw --pack . --profile node16",
48
+ "prepublishOnly": "npm run typecheck && npm test && npm run build && npm run check"
49
+ },
50
+ "devDependencies": {
51
+ "@arethetypeswrong/cli": "0.18.5",
52
+ "@types/node": "^22.20.5",
53
+ "openapi-typescript": "7.13.0",
54
+ "publint": "0.3.25",
55
+ "tsup": "8.5.1",
56
+ "typescript": "5.9.3",
57
+ "vitest": "3.2.7"
58
+ }
59
+ }