dunkapi 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.
dunkapi/__init__.py ADDED
@@ -0,0 +1,25 @@
1
+ """DunkAPI: Python client for the DunkAPI NBA data API."""
2
+ from ._core import (
3
+ APIConnectionError,
4
+ APIError,
5
+ AuthError,
6
+ InsufficientCreditsError,
7
+ NotFoundError,
8
+ RateLimitError,
9
+ to_dataframe,
10
+ )
11
+ from ._version import __version__
12
+ from .client import TOOLS, Client
13
+
14
+ __all__ = [
15
+ "APIConnectionError",
16
+ "APIError",
17
+ "AuthError",
18
+ "Client",
19
+ "InsufficientCreditsError",
20
+ "NotFoundError",
21
+ "RateLimitError",
22
+ "TOOLS",
23
+ "__version__",
24
+ "to_dataframe",
25
+ ]
dunkapi/_core.py ADDED
@@ -0,0 +1,307 @@
1
+ """Shared runtime for the DunkAPI Python client.
2
+
3
+ Standard library only. The generated methods live in client.py and call
4
+ ``Client._call``; everything else (auth, retries, errors, credits headers) is here.
5
+ """
6
+ from __future__ import annotations
7
+
8
+ import json
9
+ import os
10
+ import socket
11
+ import ssl
12
+ import time
13
+ import urllib.error
14
+ import urllib.request
15
+ from typing import Any, Callable, Dict, List, Optional, Tuple, Union
16
+
17
+ from ._version import __version__
18
+
19
+ BRAND = "DunkAPI"
20
+ ENV_VAR = "DUNKAPI_API_KEY"
21
+ DEFAULT_BASE_URL = "https://mcp.dunkapi.com"
22
+ SIGNUP_URL = "https://dunkapi.com/signup"
23
+ DEFAULT_TIMEOUT = 60.0
24
+ MAX_RETRIES = 3
25
+ MAX_RETRY_WAIT = 30.0
26
+
27
+ # A transport takes (method, url, headers, body, timeout) and returns
28
+ # (status, headers, body). Header names in the returned dict are lowercase.
29
+ Transport = Callable[
30
+ [str, str, Dict[str, str], Optional[bytes], float],
31
+ Tuple[int, Dict[str, str], bytes],
32
+ ]
33
+
34
+
35
+ class APIError(Exception):
36
+ """Any error response from the API, or a failed connection."""
37
+
38
+ def __init__(
39
+ self,
40
+ message: str,
41
+ status_code: Optional[int] = None,
42
+ body: Any = None,
43
+ ) -> None:
44
+ super().__init__(message)
45
+ self.message = message
46
+ self.status_code = status_code
47
+ self.body = body
48
+
49
+ def __str__(self) -> str:
50
+ if self.status_code:
51
+ return "%s (HTTP %s)" % (self.message, self.status_code)
52
+ return self.message
53
+
54
+
55
+ class AuthError(APIError):
56
+ """401: the API key is missing, invalid or revoked."""
57
+
58
+
59
+ class InsufficientCreditsError(APIError):
60
+ """402: the account does not have enough credits for this call."""
61
+
62
+
63
+ class NotFoundError(APIError):
64
+ """404: the game, player, team or other id does not exist."""
65
+
66
+
67
+ class RateLimitError(APIError):
68
+ """429: too many requests. ``retry_after`` is in seconds when the server sent it."""
69
+
70
+ def __init__(
71
+ self,
72
+ message: str,
73
+ status_code: Optional[int] = 429,
74
+ body: Any = None,
75
+ retry_after: Optional[float] = None,
76
+ ) -> None:
77
+ super().__init__(message, status_code, body)
78
+ self.retry_after = retry_after
79
+
80
+
81
+ class APIConnectionError(APIError):
82
+ """The request never got a response (DNS, TLS, timeout, reset)."""
83
+
84
+
85
+ def _ssl_context() -> Optional[ssl.SSLContext]:
86
+ """Use certifi's CA bundle when it is installed, else the system default.
87
+
88
+ Some python.org installs on macOS ship without root certificates. Installing
89
+ certifi (or running the "Install Certificates" script) fixes the resulting
90
+ CERTIFICATE_VERIFY_FAILED error.
91
+ """
92
+ try:
93
+ import certifi
94
+ except ImportError:
95
+ return None
96
+ return ssl.create_default_context(cafile=certifi.where())
97
+
98
+
99
+ def _urllib_transport(
100
+ method: str,
101
+ url: str,
102
+ headers: Dict[str, str],
103
+ body: Optional[bytes],
104
+ timeout: float,
105
+ ) -> Tuple[int, Dict[str, str], bytes]:
106
+ req = urllib.request.Request(url, data=body, headers=headers, method=method)
107
+ try:
108
+ with urllib.request.urlopen(req, timeout=timeout, context=_ssl_context()) as resp:
109
+ return (
110
+ resp.status,
111
+ {k.lower(): v for k, v in resp.headers.items()},
112
+ resp.read(),
113
+ )
114
+ except urllib.error.HTTPError as exc:
115
+ try:
116
+ payload = exc.read()
117
+ finally:
118
+ exc.close()
119
+ return (
120
+ exc.code,
121
+ {k.lower(): v for k, v in (exc.headers or {}).items()},
122
+ payload,
123
+ )
124
+ except (urllib.error.URLError, socket.timeout, ConnectionError, OSError) as exc:
125
+ reason = getattr(exc, "reason", exc)
126
+ raise APIConnectionError("Could not reach the API: %s" % (reason,)) from exc
127
+
128
+
129
+ def _to_number(value: Optional[str]) -> Optional[Union[int, float]]:
130
+ if value is None:
131
+ return None
132
+ try:
133
+ return int(value)
134
+ except ValueError:
135
+ try:
136
+ return float(value)
137
+ except ValueError:
138
+ return None
139
+
140
+
141
+ def _error_message(parsed: Any, raw: bytes, status: int) -> str:
142
+ if isinstance(parsed, dict):
143
+ err = parsed.get("error")
144
+ if isinstance(err, str) and err:
145
+ return err
146
+ if isinstance(err, dict) and isinstance(err.get("message"), str):
147
+ return err["message"]
148
+ text = raw.decode("utf-8", "replace").strip()
149
+ if text:
150
+ return text[:300]
151
+ return "HTTP %s" % status
152
+
153
+
154
+ class BaseClient:
155
+ """Holds the key, base URL, retry policy and the last credit counters."""
156
+
157
+ def __init__(
158
+ self,
159
+ api_key: Optional[str] = None,
160
+ *,
161
+ base_url: Optional[str] = None,
162
+ timeout: float = DEFAULT_TIMEOUT,
163
+ max_retries: int = MAX_RETRIES,
164
+ transport: Optional[Transport] = None,
165
+ ) -> None:
166
+ key = api_key or os.environ.get(ENV_VAR)
167
+ if not key:
168
+ raise AuthError(
169
+ "No API key. Pass api_key=... or set %s. "
170
+ "A free key is at %s" % (ENV_VAR, SIGNUP_URL)
171
+ )
172
+ self.api_key = key
173
+ self.base_url = (base_url or os.environ.get(BRAND.upper() + "_BASE_URL") or DEFAULT_BASE_URL).rstrip("/")
174
+ self.timeout = timeout
175
+ self.max_retries = max(0, int(max_retries))
176
+ self._transport: Transport = transport or _urllib_transport
177
+ self._sleep: Callable[[float], None] = time.sleep
178
+ self.last_credits_used: Optional[Union[int, float]] = None
179
+ self.last_credits_remaining: Optional[Union[int, float]] = None
180
+
181
+ def __repr__(self) -> str:
182
+ return "<%s.Client base_url=%r>" % (BRAND, self.base_url)
183
+
184
+ def call(self, tool: str, arguments: Optional[Dict[str, Any]] = None) -> Any:
185
+ """Call any tool by name. Useful for tools newer than this package."""
186
+ return self._call(tool, arguments or {})
187
+
188
+ def _call(self, tool: str, arguments: Dict[str, Any]) -> Any:
189
+ payload = {k: v for k, v in arguments.items() if v is not None}
190
+ url = "%s/v1/%s" % (self.base_url, tool)
191
+ headers = {
192
+ "x-api-key": self.api_key,
193
+ "content-type": "application/json",
194
+ "accept": "application/json",
195
+ "user-agent": "%s-python/%s" % (BRAND.lower(), __version__),
196
+ }
197
+ body = json.dumps(payload).encode("utf-8")
198
+
199
+ attempt = 0
200
+ while True:
201
+ try:
202
+ status, resp_headers, raw = self._transport("POST", url, headers, body, self.timeout)
203
+ except APIConnectionError:
204
+ if attempt >= self.max_retries:
205
+ raise
206
+ self._sleep(self._backoff(attempt, None))
207
+ attempt += 1
208
+ continue
209
+
210
+ self._record_credits(resp_headers)
211
+ if status == 429 or status >= 500:
212
+ if attempt < self.max_retries:
213
+ self._sleep(self._backoff(attempt, self._retry_after(resp_headers)))
214
+ attempt += 1
215
+ continue
216
+ return self._handle(status, resp_headers, raw)
217
+
218
+ @staticmethod
219
+ def _retry_after(headers: Dict[str, str]) -> Optional[float]:
220
+ value = headers.get("retry-after")
221
+ if value is None:
222
+ return None
223
+ try:
224
+ return max(0.0, float(value))
225
+ except ValueError:
226
+ return None
227
+
228
+ @staticmethod
229
+ def _backoff(attempt: int, retry_after: Optional[float]) -> float:
230
+ if retry_after is not None:
231
+ return min(retry_after, MAX_RETRY_WAIT)
232
+ return min(0.5 * (2 ** attempt), MAX_RETRY_WAIT)
233
+
234
+ def _record_credits(self, headers: Dict[str, str]) -> None:
235
+ used = _to_number(headers.get("x-credits-used"))
236
+ remaining = _to_number(headers.get("x-credits-remaining"))
237
+ if used is not None:
238
+ self.last_credits_used = used
239
+ if remaining is not None:
240
+ self.last_credits_remaining = remaining
241
+
242
+ def _handle(self, status: int, headers: Dict[str, str], raw: bytes) -> Any:
243
+ try:
244
+ parsed = json.loads(raw.decode("utf-8")) if raw else None
245
+ except ValueError:
246
+ parsed = None
247
+
248
+ if 200 <= status < 300:
249
+ if parsed is None:
250
+ raise APIError("The API returned a response that is not JSON", status, raw[:300])
251
+ if isinstance(parsed, dict) and "data" in parsed:
252
+ return parsed["data"]
253
+ return parsed
254
+
255
+ message = _error_message(parsed, raw, status)
256
+ if status == 401:
257
+ raise AuthError(message, status, parsed)
258
+ if status == 402:
259
+ raise InsufficientCreditsError(message, status, parsed)
260
+ if status == 404:
261
+ raise NotFoundError(message, status, parsed)
262
+ if status == 429:
263
+ raise RateLimitError(message, status, parsed, self._retry_after(headers))
264
+ raise APIError(message, status, parsed)
265
+
266
+
267
+ def _rows(data: Any, key: Optional[str]) -> List[Any]:
268
+ if key is not None:
269
+ cur = data
270
+ for part in key.split("."):
271
+ if not isinstance(cur, dict) or part not in cur:
272
+ raise KeyError("No %r in the response. Keys: %s" % (key, sorted(cur) if isinstance(cur, dict) else type(cur).__name__))
273
+ cur = cur[part]
274
+ if isinstance(cur, list):
275
+ return cur
276
+ return [cur]
277
+ if isinstance(data, list):
278
+ return data
279
+ if isinstance(data, dict):
280
+ lists = {k: v for k, v in data.items() if isinstance(v, list)}
281
+ records = {k: v for k, v in lists.items() if v and all(isinstance(r, dict) for r in v)}
282
+ pool = records or lists
283
+ if pool:
284
+ return pool[max(pool, key=lambda k: len(pool[k]))]
285
+ for v in data.values():
286
+ if isinstance(v, dict) and any(isinstance(x, list) for x in v.values()):
287
+ return _rows(v, None)
288
+ return [data]
289
+ return [data]
290
+
291
+
292
+ def to_dataframe(data: Any, key: Optional[str] = None) -> Any:
293
+ """Turn a response into a pandas DataFrame.
294
+
295
+ ``data`` is what a client method returned. ``key`` names the list to use
296
+ (dots reach into nested dicts, for example ``"props"``). Without a key the
297
+ longest list of records in the response is used. Nested dicts are flattened
298
+ into dotted columns. pandas is imported here, so it stays optional.
299
+ """
300
+ try:
301
+ import pandas as pd
302
+ except ImportError as exc: # pragma: no cover
303
+ raise ImportError("to_dataframe needs pandas: pip install pandas") from exc
304
+ rows = _rows(data, key)
305
+ if rows and all(isinstance(r, dict) for r in rows):
306
+ return pd.json_normalize(rows)
307
+ return pd.DataFrame(rows)
dunkapi/_version.py ADDED
@@ -0,0 +1 @@
1
+ __version__ = "0.1.0"
dunkapi/client.py ADDED
@@ -0,0 +1,731 @@
1
+ """Generated by packages/python/generate.py from the DunkAPI OpenAPI spec. Do not edit by hand."""
2
+ from __future__ import annotations
3
+
4
+ from typing import Any, Dict, List, Literal, Optional, Union
5
+
6
+ from ._core import BaseClient
7
+
8
+ #: Credits each tool costs, from the spec.
9
+ TOOLS: Dict[str, int] = {
10
+ "list_teams": 1,
11
+ "list_games": 2,
12
+ "get_schedule": 2,
13
+ "get_game": 5,
14
+ "get_standings": 2,
15
+ "get_injuries": 2,
16
+ "search_players": 1,
17
+ "get_player": 5,
18
+ "get_player_games": 5,
19
+ "get_leaders": 5,
20
+ "get_team_stats": 5,
21
+ "get_plays": 10,
22
+ "get_shot_chart": 10,
23
+ "get_odds": 5,
24
+ "get_line_movement": 10,
25
+ "get_ratings": 2,
26
+ "get_player_props": 10,
27
+ "get_prop_history": 10,
28
+ }
29
+
30
+
31
+ class Client(BaseClient):
32
+ """Client for the DunkAPI API (NBA).
33
+
34
+ Create one with ``Client()`` (reads DUNKAPI_API_KEY) or ``Client(api_key="...")``.
35
+ Every method returns the unwrapped ``data`` from the response.
36
+ ``last_credits_used`` and ``last_credits_remaining`` hold the counters from
37
+ the most recent response.
38
+ """
39
+
40
+ def list_teams(
41
+ self,
42
+ *,
43
+ season: Optional[Union[int, float, str]] = None,
44
+ league: Optional[Literal["nba", "wnba", "ncaam", "ncaaw"]] = None,
45
+ ) -> Any:
46
+ """List teams.
47
+
48
+ Use to find team ids and abbreviations for a season (default: the latest). Every other tool
49
+ takes a team as an id or abbreviation. Pass league for the WNBA (wnba) or college (ncaam,
50
+ ncaaw); default NBA. Costs 1 credit.
51
+
52
+ Costs 1 credit per call.
53
+
54
+ Args:
55
+ season (optional): Season by the year it ends: 2025 = 2024-25. WNBA: the calendar year.
56
+ league (optional): League. Defaults to nba. One of: nba, wnba, ncaam, ncaaw.
57
+
58
+ Returns:
59
+ The unwrapped ``data`` object from the response.
60
+ """
61
+ return self._call(
62
+ "list_teams",
63
+ {
64
+ "season": season,
65
+ "league": league,
66
+ },
67
+ )
68
+
69
+ def list_games(
70
+ self,
71
+ *,
72
+ season: Optional[Union[int, float, str]] = None,
73
+ season_type: Optional[Literal["regular", "postseason", "play_in", "preseason"]] = None,
74
+ team: Optional[str] = None,
75
+ date: Optional[str] = None,
76
+ from_: Optional[str] = None,
77
+ to: Optional[str] = None,
78
+ status: Optional[Literal["scheduled", "final"]] = None,
79
+ limit: Optional[int] = None,
80
+ offset: Optional[int] = None,
81
+ league: Optional[Literal["nba", "wnba", "ncaam", "ncaaw"]] = None,
82
+ ) -> Any:
83
+ """List games.
84
+
85
+ Use to list games with scores by season, date, date range or team, newest first. For upcoming
86
+ games with lines use get_schedule; for one game's box score use get_game. Costs 2 credits.
87
+
88
+ Costs 2 credits per call.
89
+
90
+ Args:
91
+ season (optional): Season by the year it ends: 2025 = 2024-25.
92
+ season_type (optional): regular, postseason, play_in or preseason.
93
+ team (optional): Team id or abbreviation.
94
+ date (optional): One date, YYYY-MM-DD (US Eastern).
95
+ from_ (optional): From date, YYYY-MM-DD.
96
+ to (optional): To date, YYYY-MM-DD.
97
+ status (optional): One of: scheduled, final.
98
+ limit (optional): Max games (default 50).
99
+ offset (optional): Page offset; use next_offset.
100
+ league (optional): League. Defaults to nba. One of: nba, wnba, ncaam, ncaaw.
101
+
102
+ Returns:
103
+ The unwrapped ``data`` object from the response.
104
+ """
105
+ return self._call(
106
+ "list_games",
107
+ {
108
+ "season": season,
109
+ "season_type": season_type,
110
+ "team": team,
111
+ "date": date,
112
+ "from": from_,
113
+ "to": to,
114
+ "status": status,
115
+ "limit": limit,
116
+ "offset": offset,
117
+ "league": league,
118
+ },
119
+ )
120
+
121
+ def get_schedule(
122
+ self,
123
+ *,
124
+ days: Optional[int] = None,
125
+ team: Optional[str] = None,
126
+ league: Optional[Literal["nba", "wnba", "ncaam", "ncaaw"]] = None,
127
+ ) -> Any:
128
+ """Schedule.
129
+
130
+ Use for upcoming games (next 1-30 days, default 7) with the median line across posted
131
+ sportsbooks: spread, total and moneylines. For every book's line use get_odds. Costs 2 credits.
132
+
133
+ Costs 2 credits per call.
134
+
135
+ Args:
136
+ days (optional): Days ahead (default 7).
137
+ team (optional): Team id or abbreviation.
138
+ league (optional): League. Defaults to nba. One of: nba, wnba, ncaam, ncaaw.
139
+
140
+ Returns:
141
+ The unwrapped ``data`` object from the response.
142
+ """
143
+ return self._call(
144
+ "get_schedule",
145
+ {
146
+ "days": days,
147
+ "team": team,
148
+ "league": league,
149
+ },
150
+ )
151
+
152
+ def get_game(
153
+ self,
154
+ game_id: str,
155
+ *,
156
+ league: Optional[Literal["nba", "wnba", "ncaam", "ncaaw"]] = None,
157
+ ) -> Any:
158
+ """Game.
159
+
160
+ Use for one game in full: score, both teams' box scores, every player's line, each sportsbook's
161
+ opening and closing line, and the win-probability range. For the play-by-play use get_plays.
162
+ Costs 5 credits.
163
+
164
+ Costs 5 credits per call.
165
+
166
+ Args:
167
+ game_id: ESPN game id, from list_games or get_schedule.
168
+ league (optional): League. Defaults to nba. One of: nba, wnba, ncaam, ncaaw.
169
+
170
+ Returns:
171
+ The unwrapped ``data`` object from the response.
172
+ """
173
+ return self._call(
174
+ "get_game",
175
+ {
176
+ "game_id": game_id,
177
+ "league": league,
178
+ },
179
+ )
180
+
181
+ def get_standings(
182
+ self,
183
+ *,
184
+ season: Optional[Union[int, float, str]] = None,
185
+ league: Optional[Literal["nba", "wnba", "ncaam", "ncaaw"]] = None,
186
+ ) -> Any:
187
+ """Standings.
188
+
189
+ Use for a season's regular-season standings computed from every final game: overall, conference,
190
+ home and away records, points for and against, margin and last 10. Costs 2 credits.
191
+
192
+ Costs 2 credits per call.
193
+
194
+ Args:
195
+ season (optional): Season by the year it ends (default: the latest).
196
+ league (optional): League. Defaults to nba. One of: nba, wnba, ncaam, ncaaw.
197
+
198
+ Returns:
199
+ The unwrapped ``data`` object from the response.
200
+ """
201
+ return self._call(
202
+ "get_standings",
203
+ {
204
+ "season": season,
205
+ "league": league,
206
+ },
207
+ )
208
+
209
+ def get_injuries(
210
+ self,
211
+ *,
212
+ team: Optional[str] = None,
213
+ date: Optional[str] = None,
214
+ league: Optional[Literal["nba", "wnba"]] = None,
215
+ ) -> Any:
216
+ """Injuries.
217
+
218
+ Use for the injury report: every listed player's status (out, day-to-day), injury and expected
219
+ return, as captured on the latest day (or the latest on or before date). NBA and WNBA. Costs 2
220
+ credits.
221
+
222
+ Costs 2 credits per call.
223
+
224
+ Args:
225
+ team (optional): One team (id or abbreviation).
226
+ date (optional): The report as of this day, YYYY-MM-DD (default: the latest).
227
+ league (optional): League. Defaults to nba. One of: nba, wnba.
228
+
229
+ Returns:
230
+ The unwrapped ``data`` object from the response.
231
+ """
232
+ return self._call(
233
+ "get_injuries",
234
+ {
235
+ "team": team,
236
+ "date": date,
237
+ "league": league,
238
+ },
239
+ )
240
+
241
+ def search_players(
242
+ self,
243
+ name: str,
244
+ *,
245
+ limit: Optional[int] = None,
246
+ league: Optional[Literal["nba", "wnba", "ncaam", "ncaaw"]] = None,
247
+ ) -> Any:
248
+ """Search players.
249
+
250
+ Use to find a player's id by name (partial names work), most recent players first. Pass league
251
+ for the WNBA (wnba) or college (ncaam, ncaaw); default NBA. Costs 1 credit.
252
+
253
+ Costs 1 credit per call.
254
+
255
+ Args:
256
+ name: Full or partial name.
257
+ limit (optional): limit
258
+ league (optional): League. Defaults to nba. One of: nba, wnba, ncaam, ncaaw.
259
+
260
+ Returns:
261
+ The unwrapped ``data`` object from the response.
262
+ """
263
+ return self._call(
264
+ "search_players",
265
+ {
266
+ "name": name,
267
+ "limit": limit,
268
+ "league": league,
269
+ },
270
+ )
271
+
272
+ def get_player(
273
+ self,
274
+ player_id: str,
275
+ *,
276
+ league: Optional[Literal["nba", "wnba", "ncaam", "ncaaw"]] = None,
277
+ ) -> Any:
278
+ """Player.
279
+
280
+ Use for a player's profile, per-season averages and shooting splits (regular season and
281
+ postseason separately) and last 10 games. For every game use get_player_games. Costs 5 credits.
282
+
283
+ Costs 5 credits per call.
284
+
285
+ Args:
286
+ player_id: ESPN athlete id, from search_players.
287
+ league (optional): League. Defaults to nba. One of: nba, wnba, ncaam, ncaaw.
288
+
289
+ Returns:
290
+ The unwrapped ``data`` object from the response.
291
+ """
292
+ return self._call(
293
+ "get_player",
294
+ {
295
+ "player_id": player_id,
296
+ "league": league,
297
+ },
298
+ )
299
+
300
+ def get_player_games(
301
+ self,
302
+ player_id: str,
303
+ *,
304
+ season: Optional[Union[int, float, str]] = None,
305
+ season_type: Optional[Literal["regular", "postseason", "play_in", "preseason"]] = None,
306
+ limit: Optional[int] = None,
307
+ offset: Optional[int] = None,
308
+ league: Optional[Literal["nba", "wnba", "ncaam", "ncaaw"]] = None,
309
+ ) -> Any:
310
+ """Player games.
311
+
312
+ Use for a player's game log: every game's box-score line with opponent and result, newest first,
313
+ optionally one season. Costs 5 credits.
314
+
315
+ Costs 5 credits per call.
316
+
317
+ Args:
318
+ player_id: ESPN athlete id.
319
+ season (optional): Season by the year it ends: 2025 = 2024-25 (default: the latest).
320
+ season_type (optional): One of: regular, postseason, play_in, preseason.
321
+ limit (optional): Max games (default 100).
322
+ offset (optional): offset
323
+ league (optional): League. Defaults to nba. One of: nba, wnba, ncaam, ncaaw.
324
+
325
+ Returns:
326
+ The unwrapped ``data`` object from the response.
327
+ """
328
+ return self._call(
329
+ "get_player_games",
330
+ {
331
+ "player_id": player_id,
332
+ "season": season,
333
+ "season_type": season_type,
334
+ "limit": limit,
335
+ "offset": offset,
336
+ "league": league,
337
+ },
338
+ )
339
+
340
+ def get_leaders(
341
+ self,
342
+ stat: Literal["pts", "reb", "ast", "stl", "blk", "tpm", "min", "plus_minus", "fg_pct", "tp_pct", "ft_pct", "ts_pct", "total_pts"],
343
+ *,
344
+ season: Optional[Union[int, float, str]] = None,
345
+ season_type: Optional[Literal["regular", "postseason", "play_in", "preseason"]] = None,
346
+ min_games: Optional[int] = None,
347
+ limit: Optional[int] = None,
348
+ league: Optional[Literal["nba", "wnba", "ncaam", "ncaaw"]] = None,
349
+ ) -> Any:
350
+ """Leaders.
351
+
352
+ Use for a season's statistical leaders (per game, shooting percentages with a volume floor, or
353
+ totals) from every box score. Costs 5 credits.
354
+
355
+ Costs 5 credits per call.
356
+
357
+ Args:
358
+ stat: Stat to rank by. One of: pts, reb, ast, stl, blk, tpm, min, plus_minus, fg_pct,
359
+ tp_pct, ft_pct, ts_pct, total_pts.
360
+ season (optional): Season by the year it ends: 2025 = 2024-25 (default: the latest).
361
+ season_type (optional): Default regular. One of: regular, postseason, play_in, preseason.
362
+ min_games (optional): Minimum games played (default: a quarter of the season).
363
+ limit (optional): limit
364
+ league (optional): League. Defaults to nba. One of: nba, wnba, ncaam, ncaaw.
365
+
366
+ Returns:
367
+ The unwrapped ``data`` object from the response.
368
+ """
369
+ return self._call(
370
+ "get_leaders",
371
+ {
372
+ "stat": stat,
373
+ "season": season,
374
+ "season_type": season_type,
375
+ "min_games": min_games,
376
+ "limit": limit,
377
+ "league": league,
378
+ },
379
+ )
380
+
381
+ def get_team_stats(
382
+ self,
383
+ *,
384
+ season: Optional[Union[int, float, str]] = None,
385
+ season_type: Optional[Literal["regular", "postseason", "play_in", "preseason"]] = None,
386
+ team: Optional[str] = None,
387
+ league: Optional[Literal["nba", "wnba", "ncaam", "ncaaw"]] = None,
388
+ ) -> Any:
389
+ """Team stats.
390
+
391
+ Use for team efficiency for a season: pace, offensive, defensive and net rating per 100
392
+ possessions, the four factors (eFG%, turnover %, offensive rebound %, free throw rate) and
393
+ three-point rate, from every box score. Costs 5 credits.
394
+
395
+ Costs 5 credits per call.
396
+
397
+ Args:
398
+ season (optional): Season by the year it ends: 2025 = 2024-25 (default: the latest).
399
+ season_type (optional): One of: regular, postseason, play_in, preseason.
400
+ team (optional): One team (id or abbreviation).
401
+ league (optional): League. Defaults to nba. One of: nba, wnba, ncaam, ncaaw.
402
+
403
+ Returns:
404
+ The unwrapped ``data`` object from the response.
405
+ """
406
+ return self._call(
407
+ "get_team_stats",
408
+ {
409
+ "season": season,
410
+ "season_type": season_type,
411
+ "team": team,
412
+ "league": league,
413
+ },
414
+ )
415
+
416
+ def get_plays(
417
+ self,
418
+ *,
419
+ game_id: Optional[str] = None,
420
+ player_id: Optional[str] = None,
421
+ team: Optional[str] = None,
422
+ season: Optional[Union[int, float, str]] = None,
423
+ season_type: Optional[Literal["regular", "postseason", "play_in", "preseason"]] = None,
424
+ shots_only: Optional[bool] = None,
425
+ made: Optional[bool] = None,
426
+ zone: Optional[Literal["restricted_area", "paint", "midrange", "corner_3", "above_break_3", "free_throw"]] = None,
427
+ period: Optional[int] = None,
428
+ clutch: Optional[bool] = None,
429
+ limit: Optional[int] = None,
430
+ offset: Optional[int] = None,
431
+ league: Optional[Literal["nba", "wnba", "ncaam", "ncaaw"]] = None,
432
+ ) -> Any:
433
+ """Plays.
434
+
435
+ Use for play-by-play: every event of a game, or a player's or team's plays across a season, with
436
+ shot location (feet), zone, our expected points, the score and the home win probability after
437
+ each play (2018 on). Filter to shots, makes or misses, a zone, a period or clutch time. Pass
438
+ league for the WNBA (wnba) or college (ncaam, ncaaw); default NBA. Costs 10 credits.
439
+
440
+ Costs 10 credits per call.
441
+
442
+ Args:
443
+ game_id (optional): One game: every play in order.
444
+ player_id (optional): A player's plays (with season).
445
+ team (optional): A team's plays (id or abbreviation).
446
+ season (optional): Season by the year it ends (default: the latest; ignored with game_id).
447
+ season_type (optional): One of: regular, postseason, play_in, preseason.
448
+ shots_only (optional): Field goal and free throw attempts only.
449
+ made (optional): Only made (true) or missed (false) shots.
450
+ zone (optional): One shot zone. One of: restricted_area, paint, midrange, corner_3,
451
+ above_break_3, free_throw.
452
+ period (optional): period
453
+ clutch (optional): Last 5 minutes of the 4th quarter (college men: 2nd half) or overtime,
454
+ score within 5.
455
+ limit (optional): Max plays (default 200).
456
+ offset (optional): offset
457
+ league (optional): League. Defaults to nba. One of: nba, wnba, ncaam, ncaaw.
458
+
459
+ Returns:
460
+ The unwrapped ``data`` object from the response.
461
+ """
462
+ return self._call(
463
+ "get_plays",
464
+ {
465
+ "game_id": game_id,
466
+ "player_id": player_id,
467
+ "team": team,
468
+ "season": season,
469
+ "season_type": season_type,
470
+ "shots_only": shots_only,
471
+ "made": made,
472
+ "zone": zone,
473
+ "period": period,
474
+ "clutch": clutch,
475
+ "limit": limit,
476
+ "offset": offset,
477
+ "league": league,
478
+ },
479
+ )
480
+
481
+ def get_shot_chart(
482
+ self,
483
+ *,
484
+ player_id: Optional[str] = None,
485
+ team: Optional[str] = None,
486
+ opponent: Optional[bool] = None,
487
+ game_id: Optional[str] = None,
488
+ season: Optional[Union[int, float, str]] = None,
489
+ season_type: Optional[Literal["regular", "postseason", "play_in", "preseason"]] = None,
490
+ include_shots: Optional[bool] = None,
491
+ league: Optional[Literal["nba", "wnba"]] = None,
492
+ ) -> Any:
493
+ """Shot chart.
494
+
495
+ Use for a shot profile: attempts, makes, percentage, points per shot and expected points per
496
+ shot in each zone (restricted area, paint, midrange, corner three, above-the-break three, free
497
+ throws) for a player, a team, a team's opponents or one game. include_shots adds every located
498
+ shot's x/y and result for drawing a chart. Costs 10 credits.
499
+
500
+ Costs 10 credits per call.
501
+
502
+ Args:
503
+ player_id (optional): player_id
504
+ team (optional): Team id or abbreviation.
505
+ opponent (optional): With team: the shots its opponents took (its defence).
506
+ game_id (optional): game_id
507
+ season (optional): Season by the year it ends (default: the latest).
508
+ season_type (optional): One of: regular, postseason, play_in, preseason.
509
+ include_shots (optional): Add each shot's x, y and result (up to 3,000).
510
+ league (optional): League. Defaults to nba. One of: nba, wnba.
511
+
512
+ Returns:
513
+ The unwrapped ``data`` object from the response.
514
+ """
515
+ return self._call(
516
+ "get_shot_chart",
517
+ {
518
+ "player_id": player_id,
519
+ "team": team,
520
+ "opponent": opponent,
521
+ "game_id": game_id,
522
+ "season": season,
523
+ "season_type": season_type,
524
+ "include_shots": include_shots,
525
+ "league": league,
526
+ },
527
+ )
528
+
529
+ def get_odds(
530
+ self,
531
+ *,
532
+ game_id: Optional[str] = None,
533
+ date: Optional[str] = None,
534
+ league: Optional[Literal["nba", "wnba", "ncaam", "ncaaw"]] = None,
535
+ ) -> Any:
536
+ """Odds.
537
+
538
+ Use for betting lines: each sportsbook's moneyline, spread and total for a game (opening and
539
+ closing per book from July 2020 from The Odds API, and from 2023-24 from ESPN; ESPN's final line
540
+ only before that), with implied probabilities and, for finished games, whether the home side
541
+ covered and the total went over against the median close. Pass game_id for one game (5 credits)
542
+ or date for a day's games (10 credits).
543
+
544
+ Costs 5 credits per call.
545
+
546
+ Args:
547
+ game_id (optional): One game.
548
+ date (optional): A day's games, YYYY-MM-DD (US Eastern).
549
+ league (optional): League. Defaults to nba. One of: nba, wnba, ncaam, ncaaw.
550
+
551
+ Returns:
552
+ The unwrapped ``data`` object from the response.
553
+ """
554
+ return self._call(
555
+ "get_odds",
556
+ {
557
+ "game_id": game_id,
558
+ "date": date,
559
+ "league": league,
560
+ },
561
+ )
562
+
563
+ def get_line_movement(
564
+ self,
565
+ game_id: str,
566
+ *,
567
+ provider: Optional[str] = None,
568
+ league: Optional[Literal["nba", "wnba", "ncaam", "ncaaw"]] = None,
569
+ ) -> Any:
570
+ """Line movement.
571
+
572
+ Use to see how a game's line moved before tip-off: our pre-game snapshots of each sportsbook's
573
+ moneyline, spread and total (ESPN's once a day, The Odds API's every hour for games in the next
574
+ two days). Costs 10 credits.
575
+
576
+ Costs 10 credits per call.
577
+
578
+ Args:
579
+ game_id: Game id.
580
+ provider (optional): One sportsbook, e.g. DraftKings.
581
+ league (optional): League. Defaults to nba. One of: nba, wnba, ncaam, ncaaw.
582
+
583
+ Returns:
584
+ The unwrapped ``data`` object from the response.
585
+ """
586
+ return self._call(
587
+ "get_line_movement",
588
+ {
589
+ "game_id": game_id,
590
+ "provider": provider,
591
+ "league": league,
592
+ },
593
+ )
594
+
595
+ def get_ratings(
596
+ self,
597
+ *,
598
+ season: Optional[Union[int, float, str]] = None,
599
+ league: Optional[Literal["nba", "wnba", "ncaam", "ncaaw"]] = None,
600
+ ) -> Any:
601
+ """Ratings.
602
+
603
+ Use for our team power ratings (Elo from every final game in our data), ranked, as of the end of
604
+ a season or today. Costs 2 credits.
605
+
606
+ Costs 2 credits per call.
607
+
608
+ Args:
609
+ season (optional): Season by the year it ends (default: the latest).
610
+ league (optional): League. Defaults to nba. One of: nba, wnba, ncaam, ncaaw.
611
+
612
+ Returns:
613
+ The unwrapped ``data`` object from the response.
614
+ """
615
+ return self._call(
616
+ "get_ratings",
617
+ {
618
+ "season": season,
619
+ "league": league,
620
+ },
621
+ )
622
+
623
+ def get_player_props(
624
+ self,
625
+ *,
626
+ game_id: Optional[str] = None,
627
+ player_id: Optional[str] = None,
628
+ from_date: Optional[str] = None,
629
+ to_date: Optional[str] = None,
630
+ market: Optional[Literal["player_points", "player_rebounds", "player_assists", "player_threes", "player_points_rebounds_assists", "player_blocks", "player_steals"]] = None,
631
+ bookmaker: Optional[str] = None,
632
+ snapshot_type: Optional[Literal["opening", "closing"]] = None,
633
+ estimate_only: Optional[bool] = None,
634
+ league: Optional[Literal["nba", "wnba", "ncaam"]] = None,
635
+ ) -> Any:
636
+ """Player props.
637
+
638
+ Use for player prop lines by sportsbook: over/under on points, rebounds, assists, threes, points
639
+ + rebounds + assists, blocks and steals, from May 2023 for the NBA and WNBA and the 2023-24
640
+ season for men's college basketball (DraftKings, FanDuel, BetMGM, ESPN BET, Pinnacle and
641
+ others). Pass game_id for every player's props in one game (10 credits), or player_id with
642
+ from_date (and to_date) for one player across games (10 credits per game returned, up to 100).
643
+ Returns the closing line by default (the last before the game starts) or the opening line, with
644
+ each book's over and under prices. Set estimate_only to get the price without being charged.
645
+
646
+ Costs 10 credits per call.
647
+
648
+ Args:
649
+ game_id (optional): Game id: every player's props for that game.
650
+ player_id (optional): Player id (e.g. 3945274) from search_players. With from_date: that
651
+ player's props across games.
652
+ from_date (optional): With player_id: first date, YYYY-MM-DD (the game's date).
653
+ to_date (optional): With player_id: last date (default: no end), YYYY-MM-DD (the game's
654
+ date).
655
+ market (optional): One market: player_points (points), player_rebounds (rebounds),
656
+ player_assists (assists), player_threes (three-pointers made),
657
+ player_points_rebounds_assists (points + rebounds + assists), player_blocks (blocks),
658
+ player_steals (steals)
659
+ bookmaker (optional): Bookmaker key, e.g. draftkings, fanduel, pinnacle.
660
+ snapshot_type (optional): closing (default): the last line before the game starts. opening:
661
+ the first one captured.
662
+ estimate_only (optional): Return what this call would cost, without charging or running it.
663
+ league (optional): League. Defaults to nba. One of: nba, wnba, ncaam.
664
+
665
+ Returns:
666
+ The unwrapped ``data`` object from the response.
667
+ """
668
+ return self._call(
669
+ "get_player_props",
670
+ {
671
+ "game_id": game_id,
672
+ "player_id": player_id,
673
+ "from_date": from_date,
674
+ "to_date": to_date,
675
+ "market": market,
676
+ "bookmaker": bookmaker,
677
+ "snapshot_type": snapshot_type,
678
+ "estimate_only": estimate_only,
679
+ "league": league,
680
+ },
681
+ )
682
+
683
+ def get_prop_history(
684
+ self,
685
+ player_id: str,
686
+ market: Literal["player_points", "player_rebounds", "player_assists", "player_threes", "player_points_rebounds_assists", "player_blocks", "player_steals"],
687
+ *,
688
+ from_date: Optional[str] = None,
689
+ to_date: Optional[str] = None,
690
+ bookmaker: Optional[str] = None,
691
+ limit: Optional[int] = None,
692
+ league: Optional[Literal["nba", "wnba", "ncaam"]] = None,
693
+ ) -> Any:
694
+ """Prop history.
695
+
696
+ Use to see how a player did against his prop line over time: for one market (points, rebounds,
697
+ assists, threes, points + rebounds + assists, blocks or steals), one row per game with the
698
+ closing line (the median across books, or one book's), the opening line, the prices, what the
699
+ player actually recorded from our box scores and the result against the line, plus the over
700
+ rate. Costs 10 credits for up to 200 games.
701
+
702
+ Costs 10 credits per call.
703
+
704
+ Args:
705
+ player_id: Player id (e.g. 3945274) from search_players.
706
+ market: The market: player_points (points), player_rebounds (rebounds), player_assists
707
+ (assists), player_threes (three-pointers made), player_points_rebounds_assists (points +
708
+ rebounds + assists), player_blocks (blocks), player_steals (steals)
709
+ from_date (optional): First date, YYYY-MM-DD (the game's date).
710
+ to_date (optional): Last date, YYYY-MM-DD (the game's date).
711
+ bookmaker (optional): One book's closing line instead of the median across books, e.g.
712
+ draftkings.
713
+ limit (optional): Games to return, oldest first (default 50, max 200). next_from_date says
714
+ where to continue.
715
+ league (optional): League. Defaults to nba. One of: nba, wnba, ncaam.
716
+
717
+ Returns:
718
+ The unwrapped ``data`` object from the response.
719
+ """
720
+ return self._call(
721
+ "get_prop_history",
722
+ {
723
+ "player_id": player_id,
724
+ "market": market,
725
+ "from_date": from_date,
726
+ "to_date": to_date,
727
+ "bookmaker": bookmaker,
728
+ "limit": limit,
729
+ "league": league,
730
+ },
731
+ )
dunkapi/py.typed ADDED
File without changes
@@ -0,0 +1,204 @@
1
+ Metadata-Version: 2.5
2
+ Name: dunkapi
3
+ Version: 0.1.0
4
+ Summary: Python client for the DunkAPI NBA data API. No dependencies.
5
+ Project-URL: Homepage, https://dunkapi.com
6
+ Project-URL: Documentation, https://dunkapi.com/docs
7
+ Project-URL: Repository, https://github.com/noahowsh/puckapi
8
+ Project-URL: Sign up, https://dunkapi.com/signup
9
+ Author-email: DunkAPI <support@dunkapi.com>
10
+ License-Expression: MIT
11
+ License-File: LICENSE
12
+ Keywords: api-client,betting,dunkapi,nba,odds,player-props,sports-data
13
+ Classifier: Development Status :: 4 - Beta
14
+ Classifier: Intended Audience :: Developers
15
+ Classifier: Operating System :: OS Independent
16
+ Classifier: Programming Language :: Python :: 3
17
+ Classifier: Programming Language :: Python :: 3.9
18
+ Classifier: Programming Language :: Python :: 3.10
19
+ Classifier: Programming Language :: Python :: 3.11
20
+ Classifier: Programming Language :: Python :: 3.12
21
+ Classifier: Programming Language :: Python :: 3.13
22
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
23
+ Classifier: Typing :: Typed
24
+ Requires-Python: >=3.9
25
+ Provides-Extra: pandas
26
+ Requires-Dist: pandas>=1.5; extra == 'pandas'
27
+ Provides-Extra: test
28
+ Requires-Dist: pytest>=7; extra == 'test'
29
+ Description-Content-Type: text/markdown
30
+
31
+ # DunkAPI for Python
32
+
33
+ Python client for the [DunkAPI](https://dunkapi.com) NBA data API. It has no required dependencies (standard library only) and works on Python 3.9 and newer. Pandas is optional.
34
+
35
+ ## Install
36
+
37
+ ```bash
38
+ pip install dunkapi
39
+ pip install "dunkapi[pandas]" # only if you want DataFrames
40
+ ```
41
+
42
+ ## Set your key
43
+
44
+ Get a free key at https://dunkapi.com/signup. Free accounts get 500 credits a month.
45
+
46
+ ```bash
47
+ export DUNKAPI_API_KEY=your_key_here
48
+ ```
49
+
50
+ ```python
51
+ from dunkapi import Client
52
+
53
+ client = Client() # reads DUNKAPI_API_KEY
54
+ client = Client(api_key="your_key") # or pass it in
55
+ ```
56
+
57
+ ## Examples
58
+
59
+ ### 1. Player props for one game
60
+
61
+ `get_player_props` returns every player's line for one market, by sportsbook.
62
+
63
+ ```python
64
+ from dunkapi import Client, to_dataframe
65
+
66
+ client = Client()
67
+
68
+ props = client.get_player_props(
69
+ game_id="401810726",
70
+ market="player_points",
71
+ bookmaker="draftkings",
72
+ )
73
+ df = to_dataframe(props, key="props")
74
+ print(df[["player_name", "line", "over_odds", "under_odds"]].head(10))
75
+ ```
76
+
77
+ ### 2. One player's prop history
78
+
79
+ `get_prop_history` shows how a player's line for a market moved across games. Search for the player id first.
80
+
81
+ ```python
82
+ from dunkapi import Client, to_dataframe
83
+
84
+ client = Client()
85
+
86
+ jokic = client.search_players("Jokic", limit=1)["players"][0]
87
+ history = client.get_prop_history(
88
+ jokic["player_id"],
89
+ "player_points",
90
+ from_date="2026-03-01",
91
+ to_date="2026-03-08",
92
+ bookmaker="draftkings",
93
+ limit=10,
94
+ )
95
+ print(to_dataframe(history).head())
96
+ ```
97
+
98
+ ### 3. Games on a date, and scoring leaders
99
+
100
+ `list_games` needs a filter such as `date`, `season` or `team`.
101
+
102
+ ```python
103
+ from dunkapi import Client
104
+
105
+ client = Client()
106
+
107
+ games = client.list_games(date="2026-03-15", limit=3)["games"]
108
+ for g in games:
109
+ print(g["away"]["abbrev"], g["away"]["score"], "at", g["home"]["abbrev"], g["home"]["score"])
110
+
111
+ leaders = client.get_leaders("pts", limit=5)
112
+ for row in leaders["leaders"]:
113
+ print(row["name"], row["team"], row["value"])
114
+ ```
115
+
116
+ ## Credits
117
+
118
+ Every method returns the `data` object from the response. After each call, `client.last_credits_used` and `client.last_credits_remaining` hold the counters from the `X-Credits-Used` and `X-Credits-Remaining` headers. Methods that take `estimate_only=True` return the price of a call without running or charging it.
119
+
120
+ ## Pandas
121
+
122
+ ```python
123
+ from dunkapi import to_dataframe
124
+
125
+ df = to_dataframe(data) # finds the main list in the response
126
+ df = to_dataframe(data, key="rows") # or name the list yourself
127
+ ```
128
+
129
+ Nested fields become dotted columns. Install pandas separately or with `pip install "dunkapi[pandas]"`.
130
+
131
+ ## Errors
132
+
133
+ Every error is an `APIError` subclass, with the server's message and the HTTP status.
134
+
135
+ | Exception | Status | Meaning |
136
+ | --- | --- | --- |
137
+ | `AuthError` | 401 | Missing, invalid or revoked key |
138
+ | `InsufficientCreditsError` | 402 | Not enough credits for the call |
139
+ | `NotFoundError` | 404 | The id or name does not exist |
140
+ | `RateLimitError` | 429 | Too many requests; has `.retry_after` when the server sends it |
141
+ | `APIError` | 400, 5xx | Bad arguments or a server problem |
142
+ | `APIConnectionError` | none | No response (network or timeout) |
143
+
144
+ ```python
145
+ from dunkapi import Client, APIError, InsufficientCreditsError, RateLimitError
146
+
147
+ client = Client()
148
+ try:
149
+ data = client.list_teams()
150
+ except InsufficientCreditsError:
151
+ print("Out of credits, top up at https://dunkapi.com")
152
+ except RateLimitError as exc:
153
+ print("Slow down, retry after", exc.retry_after)
154
+ except APIError as exc:
155
+ print(exc.status_code, exc.message)
156
+ ```
157
+
158
+ The client retries 429 and 5xx responses and connection failures up to 3 times with backoff, and honors `Retry-After`. Set `Client(max_retries=0)` to turn that off.
159
+
160
+ If you see `CERTIFICATE_VERIFY_FAILED` on a python.org install for macOS, run the "Install Certificates.command" script that came with Python, or `pip install certifi` (the client uses it when present).
161
+
162
+ ## Options
163
+
164
+ ```python
165
+ Client(
166
+ api_key=None, # defaults to $DUNKAPI_API_KEY
167
+ base_url="https://mcp.dunkapi.com", # point at another host
168
+ timeout=60.0, # seconds per request
169
+ max_retries=3,
170
+ )
171
+ ```
172
+
173
+ `client.call("tool_name", {"arg": "value"})` calls any tool by name, which helps when the API adds a tool before this package does.
174
+
175
+ ## Tools
176
+
177
+ Method names match the API tool names. Arguments are keyword arguments; required ones can also be passed positionally. Each method's docstring lists its arguments (`help(client.list_teams)`).
178
+
179
+ | Method | Credits | What it does |
180
+ | --- | --- | --- |
181
+ | `list_teams` | 1 | List teams |
182
+ | `list_games` | 2 | List games |
183
+ | `get_schedule` | 2 | Schedule |
184
+ | `get_game` | 5 | Game |
185
+ | `get_standings` | 2 | Standings |
186
+ | `get_injuries` | 2 | Injuries |
187
+ | `search_players` | 1 | Search players |
188
+ | `get_player` | 5 | Player |
189
+ | `get_player_games` | 5 | Player games |
190
+ | `get_leaders` | 5 | Leaders |
191
+ | `get_team_stats` | 5 | Team stats |
192
+ | `get_plays` | 10 | Plays |
193
+ | `get_shot_chart` | 10 | Shot chart |
194
+ | `get_odds` | 5 | Odds |
195
+ | `get_line_movement` | 10 | Line movement |
196
+ | `get_ratings` | 2 | Ratings |
197
+ | `get_player_props` | 10 | Player props |
198
+ | `get_prop_history` | 10 | Prop history |
199
+
200
+ Full API reference: https://dunkapi.com/docs
201
+
202
+ ## License
203
+
204
+ MIT
@@ -0,0 +1,9 @@
1
+ dunkapi/__init__.py,sha256=ATLWrzFMYlP1Z6Cnu0xg4jj8hW5mXHuPAjGX4J_ZfhY,505
2
+ dunkapi/_core.py,sha256=VL8fy7Vf3_a71xVhrlWQ4-dzSuGDrjg8HcdeuEcvVR4,10594
3
+ dunkapi/_version.py,sha256=kUR5RAFc7HCeiqdlX36dZOHkUI5wI6V_43RpEcD8b-0,22
4
+ dunkapi/client.py,sha256=zfhgbvdNJBty163Is2wweuRN5qhGg9Eavv05und-Zco,27251
5
+ dunkapi/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
6
+ dunkapi-0.1.0.dist-info/METADATA,sha256=KraEaS2acYiyI1_CHYN_ngGGGZXeq3M4DR2QkdOHvM4,6559
7
+ dunkapi-0.1.0.dist-info/WHEEL,sha256=W3fkpkm7-wf9vBI5Z-7s0eWkeM-spu78I8Neb98DeEg,87
8
+ dunkapi-0.1.0.dist-info/licenses/LICENSE,sha256=u471lqnuQ34I5CiZKd7-BtYYdeajVU9iMFrJhy11V2U,1064
9
+ dunkapi-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.32.4
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 DunkAPI
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.