birdieapi 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.
- birdieapi/__init__.py +25 -0
- birdieapi/_core.py +307 -0
- birdieapi/_version.py +1 -0
- birdieapi/client.py +536 -0
- birdieapi/py.typed +0 -0
- birdieapi-0.1.0.dist-info/METADATA +184 -0
- birdieapi-0.1.0.dist-info/RECORD +9 -0
- birdieapi-0.1.0.dist-info/WHEEL +4 -0
- birdieapi-0.1.0.dist-info/licenses/LICENSE +21 -0
birdieapi/__init__.py
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
"""BirdieAPI: Python client for the BirdieAPI golf 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
|
+
]
|
birdieapi/_core.py
ADDED
|
@@ -0,0 +1,307 @@
|
|
|
1
|
+
"""Shared runtime for the BirdieAPI 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 = "BirdieAPI"
|
|
20
|
+
ENV_VAR = "BIRDIEAPI_API_KEY"
|
|
21
|
+
DEFAULT_BASE_URL = "https://mcp.birdieapi.com"
|
|
22
|
+
SIGNUP_URL = "https://birdieapi.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)
|
birdieapi/_version.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "0.1.0"
|
birdieapi/client.py
ADDED
|
@@ -0,0 +1,536 @@
|
|
|
1
|
+
"""Generated by packages/python/generate.py from the BirdieAPI 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_tournaments": 1,
|
|
11
|
+
"get_schedule": 1,
|
|
12
|
+
"get_leaderboard": 2,
|
|
13
|
+
"get_course": 2,
|
|
14
|
+
"search_players": 1,
|
|
15
|
+
"get_player": 5,
|
|
16
|
+
"get_player_results": 5,
|
|
17
|
+
"get_player_stats": 5,
|
|
18
|
+
"get_scorecard": 2,
|
|
19
|
+
"get_course_history": 5,
|
|
20
|
+
"get_head_to_head": 5,
|
|
21
|
+
"get_ratings": 2,
|
|
22
|
+
"get_leaders": 5,
|
|
23
|
+
"get_winner_odds": 5,
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class Client(BaseClient):
|
|
28
|
+
"""Client for the BirdieAPI API (golf).
|
|
29
|
+
|
|
30
|
+
Create one with ``Client()`` (reads BIRDIEAPI_API_KEY) or ``Client(api_key="...")``.
|
|
31
|
+
Every method returns the unwrapped ``data`` from the response.
|
|
32
|
+
``last_credits_used`` and ``last_credits_remaining`` hold the counters from
|
|
33
|
+
the most recent response.
|
|
34
|
+
"""
|
|
35
|
+
|
|
36
|
+
def list_tournaments(
|
|
37
|
+
self,
|
|
38
|
+
*,
|
|
39
|
+
season: Optional[Union[str, int, float]] = None,
|
|
40
|
+
status: Optional[Literal["scheduled", "live", "final"]] = None,
|
|
41
|
+
major: Optional[bool] = None,
|
|
42
|
+
limit: Optional[int] = None,
|
|
43
|
+
offset: Optional[int] = None,
|
|
44
|
+
league: Optional[Literal["golf", "lpga", "liv", "dpwt", "kornferry"]] = None,
|
|
45
|
+
) -> Any:
|
|
46
|
+
"""List tournaments.
|
|
47
|
+
|
|
48
|
+
Use to find tournaments. Lists a season's tournaments (ascending) or the latest ones
|
|
49
|
+
(descending) with dates, status, purse, whether it is a major, the winner and winning score,
|
|
50
|
+
field size and the courses. Filter by season, status and major. Costs 1 credit.
|
|
51
|
+
|
|
52
|
+
Costs 1 credit per call.
|
|
53
|
+
|
|
54
|
+
Args:
|
|
55
|
+
season (optional): Season year, e.g. 2025.
|
|
56
|
+
status (optional): Only upcoming, in-progress or completed tournaments. One of: scheduled,
|
|
57
|
+
live, final.
|
|
58
|
+
major (optional): Only the majors.
|
|
59
|
+
limit (optional): Max tournaments (default 60).
|
|
60
|
+
offset (optional): Page offset; use next_offset.
|
|
61
|
+
league (optional): League. Defaults to golf. One of: golf, lpga, liv, dpwt, kornferry.
|
|
62
|
+
|
|
63
|
+
Returns:
|
|
64
|
+
The unwrapped ``data`` object from the response.
|
|
65
|
+
"""
|
|
66
|
+
return self._call(
|
|
67
|
+
"list_tournaments",
|
|
68
|
+
{
|
|
69
|
+
"season": season,
|
|
70
|
+
"status": status,
|
|
71
|
+
"major": major,
|
|
72
|
+
"limit": limit,
|
|
73
|
+
"offset": offset,
|
|
74
|
+
"league": league,
|
|
75
|
+
},
|
|
76
|
+
)
|
|
77
|
+
|
|
78
|
+
def get_schedule(
|
|
79
|
+
self,
|
|
80
|
+
*,
|
|
81
|
+
days: Optional[int] = None,
|
|
82
|
+
league: Optional[Literal["golf", "lpga", "liv", "dpwt", "kornferry"]] = None,
|
|
83
|
+
) -> Any:
|
|
84
|
+
"""Schedule.
|
|
85
|
+
|
|
86
|
+
Use for what is on now and next: tournaments in play or starting in the next N days (default
|
|
87
|
+
30), with the courses and the defending champion. Costs 1 credit.
|
|
88
|
+
|
|
89
|
+
Costs 1 credit per call.
|
|
90
|
+
|
|
91
|
+
Args:
|
|
92
|
+
days (optional): Days ahead (default 30, max 365).
|
|
93
|
+
league (optional): League. Defaults to golf. One of: golf, lpga, liv, dpwt, kornferry.
|
|
94
|
+
|
|
95
|
+
Returns:
|
|
96
|
+
The unwrapped ``data`` object from the response.
|
|
97
|
+
"""
|
|
98
|
+
return self._call(
|
|
99
|
+
"get_schedule",
|
|
100
|
+
{
|
|
101
|
+
"days": days,
|
|
102
|
+
"league": league,
|
|
103
|
+
},
|
|
104
|
+
)
|
|
105
|
+
|
|
106
|
+
def get_leaderboard(
|
|
107
|
+
self,
|
|
108
|
+
tournament_id: str,
|
|
109
|
+
*,
|
|
110
|
+
limit: Optional[int] = None,
|
|
111
|
+
league: Optional[Literal["golf", "lpga", "liv", "dpwt", "kornferry"]] = None,
|
|
112
|
+
) -> Any:
|
|
113
|
+
"""Leaderboard.
|
|
114
|
+
|
|
115
|
+
Use for a tournament's results: every player in the field in finishing order with position (T5),
|
|
116
|
+
status (cut, wd, dq), score to par, total strokes, each round's score and strokes versus the
|
|
117
|
+
field, earnings and season points, plus any playoff. Costs 2 credits.
|
|
118
|
+
|
|
119
|
+
Costs 2 credits per call.
|
|
120
|
+
|
|
121
|
+
Args:
|
|
122
|
+
tournament_id: Tournament id from list_tournaments or get_schedule.
|
|
123
|
+
limit (optional): Max players (default the whole field).
|
|
124
|
+
league (optional): League. Defaults to golf. One of: golf, lpga, liv, dpwt, kornferry.
|
|
125
|
+
|
|
126
|
+
Returns:
|
|
127
|
+
The unwrapped ``data`` object from the response.
|
|
128
|
+
"""
|
|
129
|
+
return self._call(
|
|
130
|
+
"get_leaderboard",
|
|
131
|
+
{
|
|
132
|
+
"tournament_id": tournament_id,
|
|
133
|
+
"limit": limit,
|
|
134
|
+
"league": league,
|
|
135
|
+
},
|
|
136
|
+
)
|
|
137
|
+
|
|
138
|
+
def get_course(
|
|
139
|
+
self,
|
|
140
|
+
tournament_id: str,
|
|
141
|
+
*,
|
|
142
|
+
course_id: Optional[str] = None,
|
|
143
|
+
league: Optional[Literal["golf", "lpga", "liv", "dpwt", "kornferry"]] = None,
|
|
144
|
+
) -> Any:
|
|
145
|
+
"""Course.
|
|
146
|
+
|
|
147
|
+
Use for a course as it was set up for a tournament: par and yards per hole, and how each hole
|
|
148
|
+
played (scoring average, to par, birdie and bogey rates, difficulty rank) from every round on
|
|
149
|
+
it. For a tournament on more than one course pass course_id. Costs 2 credits.
|
|
150
|
+
|
|
151
|
+
Costs 2 credits per call.
|
|
152
|
+
|
|
153
|
+
Args:
|
|
154
|
+
tournament_id: Tournament id from list_tournaments.
|
|
155
|
+
course_id (optional): Course id, for tournaments played on more than one course
|
|
156
|
+
(list_tournaments lists them).
|
|
157
|
+
league (optional): League. Defaults to golf. One of: golf, lpga, liv, dpwt, kornferry.
|
|
158
|
+
|
|
159
|
+
Returns:
|
|
160
|
+
The unwrapped ``data`` object from the response.
|
|
161
|
+
"""
|
|
162
|
+
return self._call(
|
|
163
|
+
"get_course",
|
|
164
|
+
{
|
|
165
|
+
"tournament_id": tournament_id,
|
|
166
|
+
"course_id": course_id,
|
|
167
|
+
"league": league,
|
|
168
|
+
},
|
|
169
|
+
)
|
|
170
|
+
|
|
171
|
+
def search_players(
|
|
172
|
+
self,
|
|
173
|
+
query: str,
|
|
174
|
+
*,
|
|
175
|
+
limit: Optional[int] = None,
|
|
176
|
+
league: Optional[Literal["golf", "lpga", "liv", "dpwt", "kornferry"]] = None,
|
|
177
|
+
) -> Any:
|
|
178
|
+
"""Search players.
|
|
179
|
+
|
|
180
|
+
Use to find a player's id by name (accents optional). Returns country, tournaments played, wins
|
|
181
|
+
and the seasons played. Costs 1 credit.
|
|
182
|
+
|
|
183
|
+
Costs 1 credit per call.
|
|
184
|
+
|
|
185
|
+
Args:
|
|
186
|
+
query: Part of the player's name, e.g. 'scheffler'.
|
|
187
|
+
limit (optional): Max players (default 20).
|
|
188
|
+
league (optional): League. Defaults to golf. One of: golf, lpga, liv, dpwt, kornferry.
|
|
189
|
+
|
|
190
|
+
Returns:
|
|
191
|
+
The unwrapped ``data`` object from the response.
|
|
192
|
+
"""
|
|
193
|
+
return self._call(
|
|
194
|
+
"search_players",
|
|
195
|
+
{
|
|
196
|
+
"query": query,
|
|
197
|
+
"limit": limit,
|
|
198
|
+
"league": league,
|
|
199
|
+
},
|
|
200
|
+
)
|
|
201
|
+
|
|
202
|
+
def get_player(
|
|
203
|
+
self,
|
|
204
|
+
player_id: str,
|
|
205
|
+
*,
|
|
206
|
+
league: Optional[Literal["golf", "lpga", "liv", "dpwt", "kornferry"]] = None,
|
|
207
|
+
) -> Any:
|
|
208
|
+
"""Player.
|
|
209
|
+
|
|
210
|
+
Use for a player's overview: profile, career totals (starts, wins, top tens, cuts, money,
|
|
211
|
+
scoring average, strokes versus the field), our current rating, a line per season and the last
|
|
212
|
+
ten results. Costs 5 credits.
|
|
213
|
+
|
|
214
|
+
Costs 5 credits per call.
|
|
215
|
+
|
|
216
|
+
Args:
|
|
217
|
+
player_id: Player id from search_players or get_leaderboard.
|
|
218
|
+
league (optional): League. Defaults to golf. One of: golf, lpga, liv, dpwt, kornferry.
|
|
219
|
+
|
|
220
|
+
Returns:
|
|
221
|
+
The unwrapped ``data`` object from the response.
|
|
222
|
+
"""
|
|
223
|
+
return self._call(
|
|
224
|
+
"get_player",
|
|
225
|
+
{
|
|
226
|
+
"player_id": player_id,
|
|
227
|
+
"league": league,
|
|
228
|
+
},
|
|
229
|
+
)
|
|
230
|
+
|
|
231
|
+
def get_player_results(
|
|
232
|
+
self,
|
|
233
|
+
player_id: str,
|
|
234
|
+
*,
|
|
235
|
+
season: Optional[Union[str, int, float]] = None,
|
|
236
|
+
limit: Optional[int] = None,
|
|
237
|
+
offset: Optional[int] = None,
|
|
238
|
+
league: Optional[Literal["golf", "lpga", "liv", "dpwt", "kornferry"]] = None,
|
|
239
|
+
) -> Any:
|
|
240
|
+
"""Player results.
|
|
241
|
+
|
|
242
|
+
Use for a player's tournament-by-tournament record: finish, score, earnings and each round with
|
|
243
|
+
the field's average and strokes versus the field (raw and field-strength adjusted). Newest
|
|
244
|
+
first; filter by season. Costs 5 credits.
|
|
245
|
+
|
|
246
|
+
Costs 5 credits per call.
|
|
247
|
+
|
|
248
|
+
Args:
|
|
249
|
+
player_id: Player id from search_players.
|
|
250
|
+
season (optional): Season year, e.g. 2025.
|
|
251
|
+
limit (optional): Max tournaments (default 50).
|
|
252
|
+
offset (optional): Page offset; use next_offset.
|
|
253
|
+
league (optional): League. Defaults to golf. One of: golf, lpga, liv, dpwt, kornferry.
|
|
254
|
+
|
|
255
|
+
Returns:
|
|
256
|
+
The unwrapped ``data`` object from the response.
|
|
257
|
+
"""
|
|
258
|
+
return self._call(
|
|
259
|
+
"get_player_results",
|
|
260
|
+
{
|
|
261
|
+
"player_id": player_id,
|
|
262
|
+
"season": season,
|
|
263
|
+
"limit": limit,
|
|
264
|
+
"offset": offset,
|
|
265
|
+
"league": league,
|
|
266
|
+
},
|
|
267
|
+
)
|
|
268
|
+
|
|
269
|
+
def get_player_stats(
|
|
270
|
+
self,
|
|
271
|
+
player_id: str,
|
|
272
|
+
season: Union[str, int, float],
|
|
273
|
+
*,
|
|
274
|
+
league: Optional[Literal["golf", "lpga", "liv", "dpwt", "kornferry"]] = None,
|
|
275
|
+
) -> Any:
|
|
276
|
+
"""Player stats.
|
|
277
|
+
|
|
278
|
+
Use for a player's season in numbers: results (starts, wins, top tens, cuts, money), scoring
|
|
279
|
+
average and strokes versus the field, hole scoring from every hole played (birdies and bogeys
|
|
280
|
+
per round, par 3/4/5 averages) and, for the PGA Tour and Korn Ferry Tour, strokes versus the
|
|
281
|
+
field by hole type (sg_par3, sg_par4, sg_par5; score-based, not shot data) and, where ESPN keeps
|
|
282
|
+
them, driving distance and accuracy, greens in regulation and putts per green. Costs 5 credits.
|
|
283
|
+
|
|
284
|
+
Costs 5 credits per call.
|
|
285
|
+
|
|
286
|
+
Args:
|
|
287
|
+
player_id: Player id from search_players.
|
|
288
|
+
season: Season year, e.g. 2025.
|
|
289
|
+
league (optional): League. Defaults to golf. One of: golf, lpga, liv, dpwt, kornferry.
|
|
290
|
+
|
|
291
|
+
Returns:
|
|
292
|
+
The unwrapped ``data`` object from the response.
|
|
293
|
+
"""
|
|
294
|
+
return self._call(
|
|
295
|
+
"get_player_stats",
|
|
296
|
+
{
|
|
297
|
+
"player_id": player_id,
|
|
298
|
+
"season": season,
|
|
299
|
+
"league": league,
|
|
300
|
+
},
|
|
301
|
+
)
|
|
302
|
+
|
|
303
|
+
def get_scorecard(
|
|
304
|
+
self,
|
|
305
|
+
tournament_id: str,
|
|
306
|
+
player_id: str,
|
|
307
|
+
*,
|
|
308
|
+
league: Optional[Literal["golf", "kornferry"]] = None,
|
|
309
|
+
) -> Any:
|
|
310
|
+
"""Scorecard.
|
|
311
|
+
|
|
312
|
+
Use for one player's tournament hole by hole: every round's strokes on each hole with par, yards
|
|
313
|
+
and the result (birdie, bogey), the course played, out and in, and strokes versus the field.
|
|
314
|
+
Costs 2 credits.
|
|
315
|
+
|
|
316
|
+
Costs 2 credits per call.
|
|
317
|
+
|
|
318
|
+
Args:
|
|
319
|
+
tournament_id: Tournament id from list_tournaments.
|
|
320
|
+
player_id: Player id from get_leaderboard or search_players.
|
|
321
|
+
league (optional): League. Defaults to golf. One of: golf, kornferry.
|
|
322
|
+
|
|
323
|
+
Returns:
|
|
324
|
+
The unwrapped ``data`` object from the response.
|
|
325
|
+
"""
|
|
326
|
+
return self._call(
|
|
327
|
+
"get_scorecard",
|
|
328
|
+
{
|
|
329
|
+
"tournament_id": tournament_id,
|
|
330
|
+
"player_id": player_id,
|
|
331
|
+
"league": league,
|
|
332
|
+
},
|
|
333
|
+
)
|
|
334
|
+
|
|
335
|
+
def get_course_history(
|
|
336
|
+
self,
|
|
337
|
+
player_id: str,
|
|
338
|
+
*,
|
|
339
|
+
tournament_id: Optional[str] = None,
|
|
340
|
+
course_id: Optional[str] = None,
|
|
341
|
+
league: Optional[Literal["golf", "lpga", "liv", "dpwt", "kornferry"]] = None,
|
|
342
|
+
) -> Any:
|
|
343
|
+
"""Course history.
|
|
344
|
+
|
|
345
|
+
Use for course or event form: a player's record at one tournament across every year (pass any
|
|
346
|
+
edition's tournament_id) or at one course across every tournament played there (course_id), with
|
|
347
|
+
finishes, rounds and strokes versus the field, and a summary. Costs 5 credits.
|
|
348
|
+
|
|
349
|
+
Costs 5 credits per call.
|
|
350
|
+
|
|
351
|
+
Args:
|
|
352
|
+
player_id: Player id from search_players.
|
|
353
|
+
tournament_id (optional): Any year's id of the tournament; every edition is returned.
|
|
354
|
+
course_id (optional): A course id (list_tournaments lists each tournament's courses).
|
|
355
|
+
league (optional): League. Defaults to golf. One of: golf, lpga, liv, dpwt, kornferry.
|
|
356
|
+
|
|
357
|
+
Returns:
|
|
358
|
+
The unwrapped ``data`` object from the response.
|
|
359
|
+
"""
|
|
360
|
+
return self._call(
|
|
361
|
+
"get_course_history",
|
|
362
|
+
{
|
|
363
|
+
"player_id": player_id,
|
|
364
|
+
"tournament_id": tournament_id,
|
|
365
|
+
"course_id": course_id,
|
|
366
|
+
"league": league,
|
|
367
|
+
},
|
|
368
|
+
)
|
|
369
|
+
|
|
370
|
+
def get_head_to_head(
|
|
371
|
+
self,
|
|
372
|
+
player_a: str,
|
|
373
|
+
player_b: str,
|
|
374
|
+
*,
|
|
375
|
+
season: Optional[Union[str, int, float]] = None,
|
|
376
|
+
league: Optional[Literal["golf", "lpga", "liv", "dpwt", "kornferry"]] = None,
|
|
377
|
+
) -> Any:
|
|
378
|
+
"""Head to head.
|
|
379
|
+
|
|
380
|
+
Use to compare two players: every tournament both played with each one's finish, who finished
|
|
381
|
+
ahead (making the cut beats missing it), round-by-round strokes on the same course, and a
|
|
382
|
+
summary. Filter by season. Costs 5 credits.
|
|
383
|
+
|
|
384
|
+
Costs 5 credits per call.
|
|
385
|
+
|
|
386
|
+
Args:
|
|
387
|
+
player_a: First player id.
|
|
388
|
+
player_b: Second player id.
|
|
389
|
+
season (optional): Season year, e.g. 2025.
|
|
390
|
+
league (optional): League. Defaults to golf. One of: golf, lpga, liv, dpwt, kornferry.
|
|
391
|
+
|
|
392
|
+
Returns:
|
|
393
|
+
The unwrapped ``data`` object from the response.
|
|
394
|
+
"""
|
|
395
|
+
return self._call(
|
|
396
|
+
"get_head_to_head",
|
|
397
|
+
{
|
|
398
|
+
"player_a": player_a,
|
|
399
|
+
"player_b": player_b,
|
|
400
|
+
"season": season,
|
|
401
|
+
"league": league,
|
|
402
|
+
},
|
|
403
|
+
)
|
|
404
|
+
|
|
405
|
+
def get_ratings(
|
|
406
|
+
self,
|
|
407
|
+
*,
|
|
408
|
+
limit: Optional[int] = None,
|
|
409
|
+
as_of: Optional[str] = None,
|
|
410
|
+
min_rounds: Optional[int] = None,
|
|
411
|
+
league: Optional[Literal["golf", "lpga", "liv", "dpwt", "kornferry"]] = None,
|
|
412
|
+
) -> Any:
|
|
413
|
+
"""Ratings.
|
|
414
|
+
|
|
415
|
+
Use for who is playing best: our strokes-versus-field rating for every player active in the past
|
|
416
|
+
year, best first. A rating of 2.0 means two strokes a round better than an average player in the
|
|
417
|
+
same fields, adjusted for field strength and weighted toward recent rounds. Pass as_of for the
|
|
418
|
+
ratings on a past date. Costs 2 credits.
|
|
419
|
+
|
|
420
|
+
Costs 2 credits per call.
|
|
421
|
+
|
|
422
|
+
Args:
|
|
423
|
+
limit (optional): Max players (default 50).
|
|
424
|
+
as_of (optional): A date (YYYY-MM-DD): the ratings as they stood then. Default the latest.
|
|
425
|
+
min_rounds (optional): Minimum decay-weighted rounds behind a rating (default 20).
|
|
426
|
+
league (optional): League. Defaults to golf. One of: golf, lpga, liv, dpwt, kornferry.
|
|
427
|
+
|
|
428
|
+
Returns:
|
|
429
|
+
The unwrapped ``data`` object from the response.
|
|
430
|
+
"""
|
|
431
|
+
return self._call(
|
|
432
|
+
"get_ratings",
|
|
433
|
+
{
|
|
434
|
+
"limit": limit,
|
|
435
|
+
"as_of": as_of,
|
|
436
|
+
"min_rounds": min_rounds,
|
|
437
|
+
"league": league,
|
|
438
|
+
},
|
|
439
|
+
)
|
|
440
|
+
|
|
441
|
+
def get_leaders(
|
|
442
|
+
self,
|
|
443
|
+
stat: Literal["scoring_average", "adj_vs_field", "vs_field", "birdies_per_round", "bogey_avoidance", "par3_average", "par4_average", "par5_average", "sg_par3", "sg_par4", "sg_par5", "earnings", "cup_points", "wins", "top_tens", "driving_distance", "driving_accuracy", "greens_in_regulation", "putts_per_gir"],
|
|
444
|
+
season: Union[str, int, float],
|
|
445
|
+
*,
|
|
446
|
+
min: Optional[int] = None,
|
|
447
|
+
limit: Optional[int] = None,
|
|
448
|
+
league: Optional[Literal["golf", "lpga", "liv", "dpwt", "kornferry"]] = None,
|
|
449
|
+
) -> Any:
|
|
450
|
+
"""Leaders.
|
|
451
|
+
|
|
452
|
+
Use for a season's leaders in one stat, with each player's sample. Stats from rounds and holes:
|
|
453
|
+
scoring_average, vs_field, adj_vs_field, birdies_per_round, bogey_avoidance, par3_average,
|
|
454
|
+
par4_average, par5_average, and sg_par3, sg_par4, sg_par5 (strokes versus the field on par 3, 4
|
|
455
|
+
or 5 holes, field-strength adjusted; PGA Tour and Korn Ferry Tour only, score-based, not shot
|
|
456
|
+
data). From results: earnings, cup_points, wins, top_tens. From ESPN's tournament lines (PGA
|
|
457
|
+
Tour from 2011): driving_distance, driving_accuracy, greens_in_regulation, putts_per_gir. Costs
|
|
458
|
+
5 credits.
|
|
459
|
+
|
|
460
|
+
Costs 5 credits per call.
|
|
461
|
+
|
|
462
|
+
Args:
|
|
463
|
+
stat: The stat to rank. One of: scoring_average, adj_vs_field, vs_field, birdies_per_round,
|
|
464
|
+
bogey_avoidance, par3_average, par4_average, par5_average, sg_par3, sg_par4, sg_par5,
|
|
465
|
+
earnings, cup_points, wins, top_tens, driving_distance, driving_accuracy,
|
|
466
|
+
greens_in_regulation, putts_per_gir.
|
|
467
|
+
season: Season year, e.g. 2025.
|
|
468
|
+
min (optional): Minimum sample: rounds (default 40), starts (1) or tournaments with a stat
|
|
469
|
+
line (8).
|
|
470
|
+
limit (optional): Max players (default 25).
|
|
471
|
+
league (optional): League. Defaults to golf. One of: golf, lpga, liv, dpwt, kornferry.
|
|
472
|
+
|
|
473
|
+
Returns:
|
|
474
|
+
The unwrapped ``data`` object from the response.
|
|
475
|
+
"""
|
|
476
|
+
return self._call(
|
|
477
|
+
"get_leaders",
|
|
478
|
+
{
|
|
479
|
+
"stat": stat,
|
|
480
|
+
"season": season,
|
|
481
|
+
"min": min,
|
|
482
|
+
"limit": limit,
|
|
483
|
+
"league": league,
|
|
484
|
+
},
|
|
485
|
+
)
|
|
486
|
+
|
|
487
|
+
def get_winner_odds(
|
|
488
|
+
self,
|
|
489
|
+
*,
|
|
490
|
+
tournament_id: Optional[str] = None,
|
|
491
|
+
season: Optional[Union[str, int, float]] = None,
|
|
492
|
+
major: Optional[Literal["masters", "pga_championship", "us_open", "the_open"]] = None,
|
|
493
|
+
player_id: Optional[str] = None,
|
|
494
|
+
bookmaker: Optional[str] = None,
|
|
495
|
+
limit: Optional[int] = None,
|
|
496
|
+
) -> Any:
|
|
497
|
+
"""Winner odds.
|
|
498
|
+
|
|
499
|
+
Use for the pre-tournament winner prices of the four men's majors by sportsbook, and how they
|
|
500
|
+
moved: for each golfer and book the opening price (the first snapshot) and the latest price
|
|
501
|
+
before the first round, with the implied probability, from The Odds API (the archive for every
|
|
502
|
+
major from 2021 plus the 2020 U.S. Open, then refreshed daily while a major's market is open,
|
|
503
|
+
every six hours in tournament week). For past majors the latest price can be up to a week before
|
|
504
|
+
the first tee time; round, matchup and finishing-position markets are not held. Pass
|
|
505
|
+
tournament_id, or season (all four majors), or player_id (one golfer across the majors). Costs 5
|
|
506
|
+
credits.
|
|
507
|
+
|
|
508
|
+
Costs 5 credits per call.
|
|
509
|
+
|
|
510
|
+
Args:
|
|
511
|
+
tournament_id (optional): A major's tournament id from list_tournaments (major: true).
|
|
512
|
+
season (optional): ESPN's season year, e.g. 2025: all four majors of that season (the 2020
|
|
513
|
+
U.S. Open and the November 2020 Masters are season 2021).
|
|
514
|
+
major (optional): Only this major (with season, or alone for every year). One of: masters,
|
|
515
|
+
pga_championship, us_open, the_open.
|
|
516
|
+
player_id (optional): Player id from search_players: this golfer across the majors he was
|
|
517
|
+
priced for.
|
|
518
|
+
bookmaker (optional): Bookmaker key, e.g. draftkings, fanduel, betmgm, bovada, betonlineag,
|
|
519
|
+
espnbet, pinnacle, mybookieag.
|
|
520
|
+
limit (optional): Golfers per tournament, favourites first (default 20, max 200). Ignored
|
|
521
|
+
with player_id.
|
|
522
|
+
|
|
523
|
+
Returns:
|
|
524
|
+
The unwrapped ``data`` object from the response.
|
|
525
|
+
"""
|
|
526
|
+
return self._call(
|
|
527
|
+
"get_winner_odds",
|
|
528
|
+
{
|
|
529
|
+
"tournament_id": tournament_id,
|
|
530
|
+
"season": season,
|
|
531
|
+
"major": major,
|
|
532
|
+
"player_id": player_id,
|
|
533
|
+
"bookmaker": bookmaker,
|
|
534
|
+
"limit": limit,
|
|
535
|
+
},
|
|
536
|
+
)
|
birdieapi/py.typed
ADDED
|
File without changes
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: birdieapi
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Python client for the BirdieAPI golf data API. No dependencies.
|
|
5
|
+
Project-URL: Homepage, https://birdieapi.com
|
|
6
|
+
Project-URL: Documentation, https://birdieapi.com/docs
|
|
7
|
+
Project-URL: Repository, https://github.com/noahowsh/puckapi
|
|
8
|
+
Project-URL: Sign up, https://birdieapi.com/signup
|
|
9
|
+
Author-email: BirdieAPI <support@birdieapi.com>
|
|
10
|
+
License-Expression: MIT
|
|
11
|
+
License-File: LICENSE
|
|
12
|
+
Keywords: api-client,betting,birdieapi,golf,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
|
+
# BirdieAPI for Python
|
|
32
|
+
|
|
33
|
+
Python client for the [BirdieAPI](https://birdieapi.com) golf 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 birdieapi
|
|
39
|
+
pip install "birdieapi[pandas]" # only if you want DataFrames
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
## Set your key
|
|
43
|
+
|
|
44
|
+
Get a free key at https://birdieapi.com/signup. Free accounts get 500 credits a month.
|
|
45
|
+
|
|
46
|
+
```bash
|
|
47
|
+
export BIRDIEAPI_API_KEY=your_key_here
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
```python
|
|
51
|
+
from birdieapi import Client
|
|
52
|
+
|
|
53
|
+
client = Client() # reads BIRDIEAPI_API_KEY
|
|
54
|
+
client = Client(api_key="your_key") # or pass it in
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
## Examples
|
|
58
|
+
|
|
59
|
+
### 1. Recent tournaments
|
|
60
|
+
|
|
61
|
+
`list_tournaments` filters by season and status.
|
|
62
|
+
|
|
63
|
+
```python
|
|
64
|
+
from birdieapi import Client
|
|
65
|
+
|
|
66
|
+
client = Client()
|
|
67
|
+
|
|
68
|
+
events = client.list_tournaments(status="final", limit=3)["tournaments"]
|
|
69
|
+
for e in events:
|
|
70
|
+
print(e["tournament_id"], e["name"], e["winner_name"], e["winning_to_par"])
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
### 2. A leaderboard
|
|
74
|
+
|
|
75
|
+
Pass a tournament id from `list_tournaments`.
|
|
76
|
+
|
|
77
|
+
```python
|
|
78
|
+
from birdieapi import Client, to_dataframe
|
|
79
|
+
|
|
80
|
+
client = Client()
|
|
81
|
+
|
|
82
|
+
board = client.get_leaderboard("401850916", limit=10)
|
|
83
|
+
print(to_dataframe(board).head(10))
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
### 3. A player's results
|
|
87
|
+
|
|
88
|
+
Find the player id with `search_players`, then pull his results for a season.
|
|
89
|
+
|
|
90
|
+
```python
|
|
91
|
+
from birdieapi import Client
|
|
92
|
+
|
|
93
|
+
client = Client()
|
|
94
|
+
|
|
95
|
+
scheffler = client.search_players("Scheffler", limit=1)["players"][0]
|
|
96
|
+
results = client.get_player_results(scheffler["player_id"], season=2026, limit=5)
|
|
97
|
+
print(scheffler["name"], results)
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
## Credits
|
|
101
|
+
|
|
102
|
+
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.
|
|
103
|
+
|
|
104
|
+
## Pandas
|
|
105
|
+
|
|
106
|
+
```python
|
|
107
|
+
from birdieapi import to_dataframe
|
|
108
|
+
|
|
109
|
+
df = to_dataframe(data) # finds the main list in the response
|
|
110
|
+
df = to_dataframe(data, key="rows") # or name the list yourself
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
Nested fields become dotted columns. Install pandas separately or with `pip install "birdieapi[pandas]"`.
|
|
114
|
+
|
|
115
|
+
## Errors
|
|
116
|
+
|
|
117
|
+
Every error is an `APIError` subclass, with the server's message and the HTTP status.
|
|
118
|
+
|
|
119
|
+
| Exception | Status | Meaning |
|
|
120
|
+
| --- | --- | --- |
|
|
121
|
+
| `AuthError` | 401 | Missing, invalid or revoked key |
|
|
122
|
+
| `InsufficientCreditsError` | 402 | Not enough credits for the call |
|
|
123
|
+
| `NotFoundError` | 404 | The id or name does not exist |
|
|
124
|
+
| `RateLimitError` | 429 | Too many requests; has `.retry_after` when the server sends it |
|
|
125
|
+
| `APIError` | 400, 5xx | Bad arguments or a server problem |
|
|
126
|
+
| `APIConnectionError` | none | No response (network or timeout) |
|
|
127
|
+
|
|
128
|
+
```python
|
|
129
|
+
from birdieapi import Client, APIError, InsufficientCreditsError, RateLimitError
|
|
130
|
+
|
|
131
|
+
client = Client()
|
|
132
|
+
try:
|
|
133
|
+
data = client.list_tournaments()
|
|
134
|
+
except InsufficientCreditsError:
|
|
135
|
+
print("Out of credits, top up at https://birdieapi.com")
|
|
136
|
+
except RateLimitError as exc:
|
|
137
|
+
print("Slow down, retry after", exc.retry_after)
|
|
138
|
+
except APIError as exc:
|
|
139
|
+
print(exc.status_code, exc.message)
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
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.
|
|
143
|
+
|
|
144
|
+
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).
|
|
145
|
+
|
|
146
|
+
## Options
|
|
147
|
+
|
|
148
|
+
```python
|
|
149
|
+
Client(
|
|
150
|
+
api_key=None, # defaults to $BIRDIEAPI_API_KEY
|
|
151
|
+
base_url="https://mcp.birdieapi.com", # point at another host
|
|
152
|
+
timeout=60.0, # seconds per request
|
|
153
|
+
max_retries=3,
|
|
154
|
+
)
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
`client.call("tool_name", {"arg": "value"})` calls any tool by name, which helps when the API adds a tool before this package does.
|
|
158
|
+
|
|
159
|
+
## Tools
|
|
160
|
+
|
|
161
|
+
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_tournaments)`).
|
|
162
|
+
|
|
163
|
+
| Method | Credits | What it does |
|
|
164
|
+
| --- | --- | --- |
|
|
165
|
+
| `list_tournaments` | 1 | List tournaments |
|
|
166
|
+
| `get_schedule` | 1 | Schedule |
|
|
167
|
+
| `get_leaderboard` | 2 | Leaderboard |
|
|
168
|
+
| `get_course` | 2 | Course |
|
|
169
|
+
| `search_players` | 1 | Search players |
|
|
170
|
+
| `get_player` | 5 | Player |
|
|
171
|
+
| `get_player_results` | 5 | Player results |
|
|
172
|
+
| `get_player_stats` | 5 | Player stats |
|
|
173
|
+
| `get_scorecard` | 2 | Scorecard |
|
|
174
|
+
| `get_course_history` | 5 | Course history |
|
|
175
|
+
| `get_head_to_head` | 5 | Head to head |
|
|
176
|
+
| `get_ratings` | 2 | Ratings |
|
|
177
|
+
| `get_leaders` | 5 | Leaders |
|
|
178
|
+
| `get_winner_odds` | 5 | Winner odds |
|
|
179
|
+
|
|
180
|
+
Full API reference: https://birdieapi.com/docs
|
|
181
|
+
|
|
182
|
+
## License
|
|
183
|
+
|
|
184
|
+
MIT
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
birdieapi/__init__.py,sha256=NiicMIy_okR7a3JvZ5_Y9IR3oeEWA5gdRwNp2ywT2vg,510
|
|
2
|
+
birdieapi/_core.py,sha256=4zyKuW-zKf3LnxwXYRF4kNV2JiomWxhNzCjIXHSVRQ8,10604
|
|
3
|
+
birdieapi/_version.py,sha256=kUR5RAFc7HCeiqdlX36dZOHkUI5wI6V_43RpEcD8b-0,22
|
|
4
|
+
birdieapi/client.py,sha256=5OXc4bMy2iMOGaN_X9g93767RQc9nMGUcY0uVKPGOWc,19838
|
|
5
|
+
birdieapi/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
6
|
+
birdieapi-0.1.0.dist-info/METADATA,sha256=_AZuCwXF_iO10Sa6bBwVI_Rdl2aw8RLfweLOGyhRKzg,6038
|
|
7
|
+
birdieapi-0.1.0.dist-info/WHEEL,sha256=W3fkpkm7-wf9vBI5Z-7s0eWkeM-spu78I8Neb98DeEg,87
|
|
8
|
+
birdieapi-0.1.0.dist-info/licenses/LICENSE,sha256=jZbcZfjQOO4cnpy0N68uhPi2HgElF8sPB80CmySOqnM,1066
|
|
9
|
+
birdieapi-0.1.0.dist-info/RECORD,,
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 BirdieAPI
|
|
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.
|