acesapi 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.
- acesapi/__init__.py +25 -0
- acesapi/_core.py +307 -0
- acesapi/_version.py +1 -0
- acesapi/client.py +518 -0
- acesapi/py.typed +0 -0
- acesapi-0.1.0.dist-info/METADATA +188 -0
- acesapi-0.1.0.dist-info/RECORD +9 -0
- acesapi-0.1.0.dist-info/WHEEL +4 -0
- acesapi-0.1.0.dist-info/licenses/LICENSE +21 -0
acesapi/__init__.py
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
"""AcesAPI: Python client for the AcesAPI tennis 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
|
+
]
|
acesapi/_core.py
ADDED
|
@@ -0,0 +1,307 @@
|
|
|
1
|
+
"""Shared runtime for the AcesAPI 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 = "AcesAPI"
|
|
20
|
+
ENV_VAR = "ACESAPI_API_KEY"
|
|
21
|
+
DEFAULT_BASE_URL = "https://mcp.acesapi.com"
|
|
22
|
+
SIGNUP_URL = "https://acesapi.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)
|
acesapi/_version.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "0.1.0"
|
acesapi/client.py
ADDED
|
@@ -0,0 +1,518 @@
|
|
|
1
|
+
"""Generated by packages/python/generate.py from the AcesAPI 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_tournament": 2,
|
|
12
|
+
"get_matches": 2,
|
|
13
|
+
"get_schedule": 2,
|
|
14
|
+
"get_match": 2,
|
|
15
|
+
"search_players": 1,
|
|
16
|
+
"get_player": 5,
|
|
17
|
+
"get_player_matches": 5,
|
|
18
|
+
"get_head_to_head": 3,
|
|
19
|
+
"get_surface_splits": 3,
|
|
20
|
+
"get_player_stats": 3,
|
|
21
|
+
"get_rankings": 2,
|
|
22
|
+
"get_ratings": 2,
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class Client(BaseClient):
|
|
27
|
+
"""Client for the AcesAPI API (tennis).
|
|
28
|
+
|
|
29
|
+
Create one with ``Client()`` (reads ACESAPI_API_KEY) or ``Client(api_key="...")``.
|
|
30
|
+
Every method returns the unwrapped ``data`` from the response.
|
|
31
|
+
``last_credits_used`` and ``last_credits_remaining`` hold the counters from
|
|
32
|
+
the most recent response.
|
|
33
|
+
"""
|
|
34
|
+
|
|
35
|
+
def list_tournaments(
|
|
36
|
+
self,
|
|
37
|
+
*,
|
|
38
|
+
season: Optional[Union[str, int, float]] = None,
|
|
39
|
+
surface: Optional[Literal["hard", "clay", "grass", "carpet"]] = None,
|
|
40
|
+
status: Optional[Literal["scheduled", "live", "final"]] = None,
|
|
41
|
+
limit: Optional[int] = None,
|
|
42
|
+
offset: Optional[int] = None,
|
|
43
|
+
league: Optional[Literal["tennis", "wta"]] = None,
|
|
44
|
+
) -> Any:
|
|
45
|
+
"""List tournaments.
|
|
46
|
+
|
|
47
|
+
Use to find tournaments. Lists a season's tournaments in calendar order with dates, city,
|
|
48
|
+
surface, whether it is a Grand Slam, status, main-draw match count and the champion. Filter by
|
|
49
|
+
surface (hard, clay, grass, carpet) and status. Tour: atp (default) or wta; on the shared
|
|
50
|
+
endpoint pass league: "wta" for the WTA. Costs 1 credit.
|
|
51
|
+
|
|
52
|
+
Costs 1 credit per call.
|
|
53
|
+
|
|
54
|
+
Args:
|
|
55
|
+
season (optional): Year, e.g. 2025 (default this year).
|
|
56
|
+
surface (optional): Only tournaments on this surface. One of: hard, clay, grass, carpet.
|
|
57
|
+
status (optional): Only upcoming, in-progress or completed tournaments. One of: scheduled,
|
|
58
|
+
live, final.
|
|
59
|
+
limit (optional): Max tournaments (default 100).
|
|
60
|
+
offset (optional): Page offset; use next_offset.
|
|
61
|
+
league (optional): League. Defaults to tennis. One of: tennis, wta.
|
|
62
|
+
|
|
63
|
+
Returns:
|
|
64
|
+
The unwrapped ``data`` object from the response.
|
|
65
|
+
"""
|
|
66
|
+
return self._call(
|
|
67
|
+
"list_tournaments",
|
|
68
|
+
{
|
|
69
|
+
"season": season,
|
|
70
|
+
"surface": surface,
|
|
71
|
+
"status": status,
|
|
72
|
+
"limit": limit,
|
|
73
|
+
"offset": offset,
|
|
74
|
+
"league": league,
|
|
75
|
+
},
|
|
76
|
+
)
|
|
77
|
+
|
|
78
|
+
def get_tournament(
|
|
79
|
+
self,
|
|
80
|
+
tournament_id: str,
|
|
81
|
+
*,
|
|
82
|
+
draw: Optional[Literal["main", "qualifying", "all"]] = None,
|
|
83
|
+
league: Optional[Literal["tennis", "wta"]] = None,
|
|
84
|
+
) -> Any:
|
|
85
|
+
"""Tournament.
|
|
86
|
+
|
|
87
|
+
Use for a tournament's draw and results: every singles match of one edition grouped by round in
|
|
88
|
+
draw order (qualifying, then the main draw to the final), each with the score by set and
|
|
89
|
+
tiebreak points, seeds, and the champion and runner-up. draw: main (default), qualifying or all.
|
|
90
|
+
Tour: atp (default) or wta; on the shared endpoint pass league: "wta" for the WTA. Costs 2
|
|
91
|
+
credits.
|
|
92
|
+
|
|
93
|
+
Costs 2 credits per call.
|
|
94
|
+
|
|
95
|
+
Args:
|
|
96
|
+
tournament_id: Tournament edition id from list_tournaments, e.g. "188-2025".
|
|
97
|
+
draw (optional): Which draw (default main). One of: main, qualifying, all.
|
|
98
|
+
league (optional): League. Defaults to tennis. One of: tennis, wta.
|
|
99
|
+
|
|
100
|
+
Returns:
|
|
101
|
+
The unwrapped ``data`` object from the response.
|
|
102
|
+
"""
|
|
103
|
+
return self._call(
|
|
104
|
+
"get_tournament",
|
|
105
|
+
{
|
|
106
|
+
"tournament_id": tournament_id,
|
|
107
|
+
"draw": draw,
|
|
108
|
+
"league": league,
|
|
109
|
+
},
|
|
110
|
+
)
|
|
111
|
+
|
|
112
|
+
def get_matches(
|
|
113
|
+
self,
|
|
114
|
+
*,
|
|
115
|
+
date: Optional[str] = None,
|
|
116
|
+
tournament_id: Optional[str] = None,
|
|
117
|
+
season: Optional[Union[str, int, float]] = None,
|
|
118
|
+
round: Optional[str] = None,
|
|
119
|
+
status: Optional[Literal["scheduled", "live", "final"]] = None,
|
|
120
|
+
limit: Optional[int] = None,
|
|
121
|
+
offset: Optional[int] = None,
|
|
122
|
+
league: Optional[Literal["tennis", "wta"]] = None,
|
|
123
|
+
) -> Any:
|
|
124
|
+
"""Matches.
|
|
125
|
+
|
|
126
|
+
Use for match results by day, tournament or season: each match with both players, seeds, round,
|
|
127
|
+
status, score and sets (games and tiebreak points). Pass a date (YYYY-MM-DD, UTC), a
|
|
128
|
+
tournament_id or a season; filter by round (e.g. "Final") and status. Tour: atp (default) or
|
|
129
|
+
wta; on the shared endpoint pass league: "wta" for the WTA. Costs 2 credits.
|
|
130
|
+
|
|
131
|
+
Costs 2 credits per call.
|
|
132
|
+
|
|
133
|
+
Args:
|
|
134
|
+
date (optional): UTC date, YYYY-MM-DD.
|
|
135
|
+
tournament_id (optional): Tournament edition id, e.g. "188-2025".
|
|
136
|
+
season (optional): Year.
|
|
137
|
+
round (optional): Round name as listed, e.g. "Final", "Semifinal", "Round 1".
|
|
138
|
+
status (optional): One of: scheduled, live, final.
|
|
139
|
+
limit (optional): Max matches (default 100).
|
|
140
|
+
offset (optional): offset
|
|
141
|
+
league (optional): League. Defaults to tennis. One of: tennis, wta.
|
|
142
|
+
|
|
143
|
+
Returns:
|
|
144
|
+
The unwrapped ``data`` object from the response.
|
|
145
|
+
"""
|
|
146
|
+
return self._call(
|
|
147
|
+
"get_matches",
|
|
148
|
+
{
|
|
149
|
+
"date": date,
|
|
150
|
+
"tournament_id": tournament_id,
|
|
151
|
+
"season": season,
|
|
152
|
+
"round": round,
|
|
153
|
+
"status": status,
|
|
154
|
+
"limit": limit,
|
|
155
|
+
"offset": offset,
|
|
156
|
+
"league": league,
|
|
157
|
+
},
|
|
158
|
+
)
|
|
159
|
+
|
|
160
|
+
def get_schedule(
|
|
161
|
+
self,
|
|
162
|
+
*,
|
|
163
|
+
days: Optional[int] = None,
|
|
164
|
+
league: Optional[Literal["tennis", "wta"]] = None,
|
|
165
|
+
) -> Any:
|
|
166
|
+
"""Schedule.
|
|
167
|
+
|
|
168
|
+
Use for upcoming matches: every scheduled or in-progress singles match in the next N days
|
|
169
|
+
(default 7, max 30) with players, seeds, round and start time where ESPN has set one. Tour: atp
|
|
170
|
+
(default) or wta; on the shared endpoint pass league: "wta" for the WTA. Costs 2 credits.
|
|
171
|
+
|
|
172
|
+
Costs 2 credits per call.
|
|
173
|
+
|
|
174
|
+
Args:
|
|
175
|
+
days (optional): Days ahead (default 7).
|
|
176
|
+
league (optional): League. Defaults to tennis. One of: tennis, wta.
|
|
177
|
+
|
|
178
|
+
Returns:
|
|
179
|
+
The unwrapped ``data`` object from the response.
|
|
180
|
+
"""
|
|
181
|
+
return self._call(
|
|
182
|
+
"get_schedule",
|
|
183
|
+
{
|
|
184
|
+
"days": days,
|
|
185
|
+
"league": league,
|
|
186
|
+
},
|
|
187
|
+
)
|
|
188
|
+
|
|
189
|
+
def get_match(
|
|
190
|
+
self,
|
|
191
|
+
match_id: str,
|
|
192
|
+
*,
|
|
193
|
+
league: Optional[Literal["tennis", "wta"]] = None,
|
|
194
|
+
) -> Any:
|
|
195
|
+
"""Match.
|
|
196
|
+
|
|
197
|
+
Use for everything about one match: the score by set with tiebreak points, seeds, each player's
|
|
198
|
+
sets, games and tiebreaks won, both players' ratings before and after (overall and on the
|
|
199
|
+
surface), and what the score says (total games, tiebreaks, whether it went the distance,
|
|
200
|
+
comebacks). No serve statistics: ESPN does not publish them. Tour: atp (default) or wta; on the
|
|
201
|
+
shared endpoint pass league: "wta" for the WTA. Costs 2 credits.
|
|
202
|
+
|
|
203
|
+
Costs 2 credits per call.
|
|
204
|
+
|
|
205
|
+
Args:
|
|
206
|
+
match_id: Match id from get_tournament, get_matches or get_player_matches.
|
|
207
|
+
league (optional): League. Defaults to tennis. One of: tennis, wta.
|
|
208
|
+
|
|
209
|
+
Returns:
|
|
210
|
+
The unwrapped ``data`` object from the response.
|
|
211
|
+
"""
|
|
212
|
+
return self._call(
|
|
213
|
+
"get_match",
|
|
214
|
+
{
|
|
215
|
+
"match_id": match_id,
|
|
216
|
+
"league": league,
|
|
217
|
+
},
|
|
218
|
+
)
|
|
219
|
+
|
|
220
|
+
def search_players(
|
|
221
|
+
self,
|
|
222
|
+
*,
|
|
223
|
+
name: Optional[str] = None,
|
|
224
|
+
country: Optional[str] = None,
|
|
225
|
+
limit: Optional[int] = None,
|
|
226
|
+
league: Optional[Literal["tennis", "wta"]] = None,
|
|
227
|
+
) -> Any:
|
|
228
|
+
"""Search players.
|
|
229
|
+
|
|
230
|
+
Use to find a player's id. Search by name (accents optional) and/or country code (e.g. ITA);
|
|
231
|
+
most-played first, with matches, last match and rank on the current list (null outside ESPN's
|
|
232
|
+
top 100 to 150). Tour: atp (default) or wta; on the shared endpoint pass league: "wta" for the
|
|
233
|
+
WTA. Costs 1 credit.
|
|
234
|
+
|
|
235
|
+
Costs 1 credit per call.
|
|
236
|
+
|
|
237
|
+
Args:
|
|
238
|
+
name (optional): Part of the player's name, e.g. "sinner".
|
|
239
|
+
country (optional): Three-letter country code, e.g. ESP.
|
|
240
|
+
limit (optional): Max players (default 20).
|
|
241
|
+
league (optional): League. Defaults to tennis. One of: tennis, wta.
|
|
242
|
+
|
|
243
|
+
Returns:
|
|
244
|
+
The unwrapped ``data`` object from the response.
|
|
245
|
+
"""
|
|
246
|
+
return self._call(
|
|
247
|
+
"search_players",
|
|
248
|
+
{
|
|
249
|
+
"name": name,
|
|
250
|
+
"country": country,
|
|
251
|
+
"limit": limit,
|
|
252
|
+
"league": league,
|
|
253
|
+
},
|
|
254
|
+
)
|
|
255
|
+
|
|
256
|
+
def get_player(
|
|
257
|
+
self,
|
|
258
|
+
player_id: str,
|
|
259
|
+
*,
|
|
260
|
+
league: Optional[Literal["tennis", "wta"]] = None,
|
|
261
|
+
) -> Any:
|
|
262
|
+
"""Player.
|
|
263
|
+
|
|
264
|
+
Use for a player profile: date of birth, country, height, playing hand; latest ranking; our
|
|
265
|
+
rating overall and per surface with peak; career record (wins, losses, sets, games, tiebreaks,
|
|
266
|
+
deciding sets), titles, finals and Grand Slam titles; record by surface; and the last 10
|
|
267
|
+
matches. Tour: atp (default) or wta; on the shared endpoint pass league: "wta" for the WTA.
|
|
268
|
+
Costs 5 credits.
|
|
269
|
+
|
|
270
|
+
Costs 5 credits per call.
|
|
271
|
+
|
|
272
|
+
Args:
|
|
273
|
+
player_id: Player id from search_players.
|
|
274
|
+
league (optional): League. Defaults to tennis. One of: tennis, wta.
|
|
275
|
+
|
|
276
|
+
Returns:
|
|
277
|
+
The unwrapped ``data`` object from the response.
|
|
278
|
+
"""
|
|
279
|
+
return self._call(
|
|
280
|
+
"get_player",
|
|
281
|
+
{
|
|
282
|
+
"player_id": player_id,
|
|
283
|
+
"league": league,
|
|
284
|
+
},
|
|
285
|
+
)
|
|
286
|
+
|
|
287
|
+
def get_player_matches(
|
|
288
|
+
self,
|
|
289
|
+
player_id: str,
|
|
290
|
+
*,
|
|
291
|
+
season: Optional[Union[str, int, float]] = None,
|
|
292
|
+
surface: Optional[Literal["hard", "clay", "grass", "carpet"]] = None,
|
|
293
|
+
opponent_id: Optional[str] = None,
|
|
294
|
+
tournament_id: Optional[str] = None,
|
|
295
|
+
draw: Optional[Literal["main", "qualifying"]] = None,
|
|
296
|
+
limit: Optional[int] = None,
|
|
297
|
+
offset: Optional[int] = None,
|
|
298
|
+
league: Optional[Literal["tennis", "wta"]] = None,
|
|
299
|
+
) -> Any:
|
|
300
|
+
"""Player matches.
|
|
301
|
+
|
|
302
|
+
Use for a player's matches, newest first: tournament, surface, round, opponent, result, score by
|
|
303
|
+
set and the player's rating before and after. Filter by season, surface, opponent_id,
|
|
304
|
+
tournament_id or draw. Tour: atp (default) or wta; on the shared endpoint pass league: "wta" for
|
|
305
|
+
the WTA. Costs 5 credits.
|
|
306
|
+
|
|
307
|
+
Costs 5 credits per call.
|
|
308
|
+
|
|
309
|
+
Args:
|
|
310
|
+
player_id: Player id from search_players.
|
|
311
|
+
season (optional): Year.
|
|
312
|
+
surface (optional): One of: hard, clay, grass, carpet.
|
|
313
|
+
opponent_id (optional): Only matches against this player.
|
|
314
|
+
tournament_id (optional): Only this tournament edition, e.g. "188-2025".
|
|
315
|
+
draw (optional): One of: main, qualifying.
|
|
316
|
+
limit (optional): Max matches (default 50).
|
|
317
|
+
offset (optional): offset
|
|
318
|
+
league (optional): League. Defaults to tennis. One of: tennis, wta.
|
|
319
|
+
|
|
320
|
+
Returns:
|
|
321
|
+
The unwrapped ``data`` object from the response.
|
|
322
|
+
"""
|
|
323
|
+
return self._call(
|
|
324
|
+
"get_player_matches",
|
|
325
|
+
{
|
|
326
|
+
"player_id": player_id,
|
|
327
|
+
"season": season,
|
|
328
|
+
"surface": surface,
|
|
329
|
+
"opponent_id": opponent_id,
|
|
330
|
+
"tournament_id": tournament_id,
|
|
331
|
+
"draw": draw,
|
|
332
|
+
"limit": limit,
|
|
333
|
+
"offset": offset,
|
|
334
|
+
"league": league,
|
|
335
|
+
},
|
|
336
|
+
)
|
|
337
|
+
|
|
338
|
+
def get_head_to_head(
|
|
339
|
+
self,
|
|
340
|
+
player_id: str,
|
|
341
|
+
opponent_id: str,
|
|
342
|
+
*,
|
|
343
|
+
league: Optional[Literal["tennis", "wta"]] = None,
|
|
344
|
+
) -> Any:
|
|
345
|
+
"""Head to head.
|
|
346
|
+
|
|
347
|
+
Use for two players' head to head: wins each, sets won, wins by surface and in finals, and every
|
|
348
|
+
meeting with its score. Walkovers are listed but not counted as wins; incomplete records are not
|
|
349
|
+
counted. Tour: atp (default) or wta; on the shared endpoint pass league: "wta" for the WTA.
|
|
350
|
+
Costs 3 credits.
|
|
351
|
+
|
|
352
|
+
Costs 3 credits per call.
|
|
353
|
+
|
|
354
|
+
Args:
|
|
355
|
+
player_id: First player id.
|
|
356
|
+
opponent_id: Second player id.
|
|
357
|
+
league (optional): League. Defaults to tennis. One of: tennis, wta.
|
|
358
|
+
|
|
359
|
+
Returns:
|
|
360
|
+
The unwrapped ``data`` object from the response.
|
|
361
|
+
"""
|
|
362
|
+
return self._call(
|
|
363
|
+
"get_head_to_head",
|
|
364
|
+
{
|
|
365
|
+
"player_id": player_id,
|
|
366
|
+
"opponent_id": opponent_id,
|
|
367
|
+
"league": league,
|
|
368
|
+
},
|
|
369
|
+
)
|
|
370
|
+
|
|
371
|
+
def get_surface_splits(
|
|
372
|
+
self,
|
|
373
|
+
player_id: str,
|
|
374
|
+
*,
|
|
375
|
+
season: Optional[Union[str, int, float]] = None,
|
|
376
|
+
by_season: Optional[bool] = None,
|
|
377
|
+
league: Optional[Literal["tennis", "wta"]] = None,
|
|
378
|
+
) -> Any:
|
|
379
|
+
"""Surface splits.
|
|
380
|
+
|
|
381
|
+
Use for a player's record by surface (hard, clay, grass, carpet): wins, losses, win rate, sets,
|
|
382
|
+
share of games won, tiebreaks, deciding sets and 6-0 sets, plus the current surface ratings. For
|
|
383
|
+
one season or the whole career; by_season splits each surface by year. Tour: atp (default) or
|
|
384
|
+
wta; on the shared endpoint pass league: "wta" for the WTA. Costs 3 credits.
|
|
385
|
+
|
|
386
|
+
Costs 3 credits per call.
|
|
387
|
+
|
|
388
|
+
Args:
|
|
389
|
+
player_id: Player id from search_players.
|
|
390
|
+
season (optional): Year (default the whole career).
|
|
391
|
+
by_season (optional): Split each surface by season.
|
|
392
|
+
league (optional): League. Defaults to tennis. One of: tennis, wta.
|
|
393
|
+
|
|
394
|
+
Returns:
|
|
395
|
+
The unwrapped ``data`` object from the response.
|
|
396
|
+
"""
|
|
397
|
+
return self._call(
|
|
398
|
+
"get_surface_splits",
|
|
399
|
+
{
|
|
400
|
+
"player_id": player_id,
|
|
401
|
+
"season": season,
|
|
402
|
+
"by_season": by_season,
|
|
403
|
+
"league": league,
|
|
404
|
+
},
|
|
405
|
+
)
|
|
406
|
+
|
|
407
|
+
def get_player_stats(
|
|
408
|
+
self,
|
|
409
|
+
player_id: str,
|
|
410
|
+
*,
|
|
411
|
+
season: Optional[Union[str, int, float]] = None,
|
|
412
|
+
league: Optional[Literal["tennis", "wta"]] = None,
|
|
413
|
+
) -> Any:
|
|
414
|
+
"""Player stats.
|
|
415
|
+
|
|
416
|
+
Use for a player's match statistics by season: wins, losses, sets and games won, tiebreaks won
|
|
417
|
+
and lost, deciding sets, 6-0 sets and retirements, all counted from scores; plus the record by
|
|
418
|
+
round and by the opponent's ranking (top 10, 11-50, 51-100). No serve statistics: ESPN does not
|
|
419
|
+
publish them. Tour: atp (default) or wta; on the shared endpoint pass league: "wta" for the WTA.
|
|
420
|
+
Costs 3 credits.
|
|
421
|
+
|
|
422
|
+
Costs 3 credits per call.
|
|
423
|
+
|
|
424
|
+
Args:
|
|
425
|
+
player_id: Player id from search_players.
|
|
426
|
+
season (optional): Year (default every season).
|
|
427
|
+
league (optional): League. Defaults to tennis. One of: tennis, wta.
|
|
428
|
+
|
|
429
|
+
Returns:
|
|
430
|
+
The unwrapped ``data`` object from the response.
|
|
431
|
+
"""
|
|
432
|
+
return self._call(
|
|
433
|
+
"get_player_stats",
|
|
434
|
+
{
|
|
435
|
+
"player_id": player_id,
|
|
436
|
+
"season": season,
|
|
437
|
+
"league": league,
|
|
438
|
+
},
|
|
439
|
+
)
|
|
440
|
+
|
|
441
|
+
def get_rankings(
|
|
442
|
+
self,
|
|
443
|
+
*,
|
|
444
|
+
season: Optional[Union[str, int, float]] = None,
|
|
445
|
+
week: Optional[int] = None,
|
|
446
|
+
date: Optional[str] = None,
|
|
447
|
+
player_id: Optional[str] = None,
|
|
448
|
+
limit: Optional[int] = None,
|
|
449
|
+
league: Optional[Literal["tennis", "wta"]] = None,
|
|
450
|
+
) -> Any:
|
|
451
|
+
"""Rankings.
|
|
452
|
+
|
|
453
|
+
Use for the ranking list: the latest (default), or a season and week, or the list in force on a
|
|
454
|
+
date, with rank, previous rank, points and player. Pass player_id instead for one player's
|
|
455
|
+
ranking history. ESPN keeps the top 100 to 150 for most weeks since 2007. Tour: atp (default) or
|
|
456
|
+
wta; on the shared endpoint pass league: "wta" for the WTA. Costs 2 credits.
|
|
457
|
+
|
|
458
|
+
Costs 2 credits per call.
|
|
459
|
+
|
|
460
|
+
Args:
|
|
461
|
+
season (optional): Year.
|
|
462
|
+
week (optional): Week number of the season.
|
|
463
|
+
date (optional): The list in force on this date, YYYY-MM-DD.
|
|
464
|
+
player_id (optional): One player's ranking history instead of a list.
|
|
465
|
+
limit (optional): Max rows of a list (default 100).
|
|
466
|
+
league (optional): League. Defaults to tennis. One of: tennis, wta.
|
|
467
|
+
|
|
468
|
+
Returns:
|
|
469
|
+
The unwrapped ``data`` object from the response.
|
|
470
|
+
"""
|
|
471
|
+
return self._call(
|
|
472
|
+
"get_rankings",
|
|
473
|
+
{
|
|
474
|
+
"season": season,
|
|
475
|
+
"week": week,
|
|
476
|
+
"date": date,
|
|
477
|
+
"player_id": player_id,
|
|
478
|
+
"limit": limit,
|
|
479
|
+
"league": league,
|
|
480
|
+
},
|
|
481
|
+
)
|
|
482
|
+
|
|
483
|
+
def get_ratings(
|
|
484
|
+
self,
|
|
485
|
+
*,
|
|
486
|
+
surface: Optional[Literal["hard", "clay", "grass", "carpet"]] = None,
|
|
487
|
+
active_since: Optional[str] = None,
|
|
488
|
+
limit: Optional[int] = None,
|
|
489
|
+
league: Optional[Literal["tennis", "wta"]] = None,
|
|
490
|
+
) -> Any:
|
|
491
|
+
"""Ratings.
|
|
492
|
+
|
|
493
|
+
Use for our Elo ratings, highest first: overall, or on one surface (hard, clay, grass). Players
|
|
494
|
+
with a rated match since active_since (default a year ago) and at least 10 rated matches. Tour:
|
|
495
|
+
atp (default) or wta; on the shared endpoint pass league: "wta" for the WTA. Costs 2 credits.
|
|
496
|
+
|
|
497
|
+
Costs 2 credits per call.
|
|
498
|
+
|
|
499
|
+
Args:
|
|
500
|
+
surface (optional): Rank by the surface rating instead of overall. One of: hard, clay,
|
|
501
|
+
grass, carpet.
|
|
502
|
+
active_since (optional): Only players with a rated match since this date, YYYY-MM-DD
|
|
503
|
+
(default one year ago).
|
|
504
|
+
limit (optional): Max players (default 50).
|
|
505
|
+
league (optional): League. Defaults to tennis. One of: tennis, wta.
|
|
506
|
+
|
|
507
|
+
Returns:
|
|
508
|
+
The unwrapped ``data`` object from the response.
|
|
509
|
+
"""
|
|
510
|
+
return self._call(
|
|
511
|
+
"get_ratings",
|
|
512
|
+
{
|
|
513
|
+
"surface": surface,
|
|
514
|
+
"active_since": active_since,
|
|
515
|
+
"limit": limit,
|
|
516
|
+
"league": league,
|
|
517
|
+
},
|
|
518
|
+
)
|
acesapi/py.typed
ADDED
|
File without changes
|
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: acesapi
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Python client for the AcesAPI tennis data API. No dependencies.
|
|
5
|
+
Project-URL: Homepage, https://acesapi.com
|
|
6
|
+
Project-URL: Documentation, https://acesapi.com/docs
|
|
7
|
+
Project-URL: Repository, https://github.com/noahowsh/puckapi
|
|
8
|
+
Project-URL: Sign up, https://acesapi.com/signup
|
|
9
|
+
Author-email: AcesAPI <support@acesapi.com>
|
|
10
|
+
License-Expression: MIT
|
|
11
|
+
License-File: LICENSE
|
|
12
|
+
Keywords: acesapi,api-client,betting,odds,player-props,sports-data,tennis
|
|
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
|
+
# AcesAPI for Python
|
|
32
|
+
|
|
33
|
+
Python client for the [AcesAPI](https://acesapi.com) tennis 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 acesapi
|
|
39
|
+
pip install "acesapi[pandas]" # only if you want DataFrames
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
## Set your key
|
|
43
|
+
|
|
44
|
+
Get a free key at https://acesapi.com/signup. Free accounts get 500 credits a month.
|
|
45
|
+
|
|
46
|
+
```bash
|
|
47
|
+
export ACESAPI_API_KEY=your_key_here
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
```python
|
|
51
|
+
from acesapi import Client
|
|
52
|
+
|
|
53
|
+
client = Client() # reads ACESAPI_API_KEY
|
|
54
|
+
client = Client(api_key="your_key") # or pass it in
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
## Examples
|
|
58
|
+
|
|
59
|
+
### 1. Current rankings
|
|
60
|
+
|
|
61
|
+
ATP is the default tour. Pass `league="wta"` for the women's tour.
|
|
62
|
+
|
|
63
|
+
```python
|
|
64
|
+
from acesapi import Client
|
|
65
|
+
|
|
66
|
+
client = Client()
|
|
67
|
+
|
|
68
|
+
ranks = client.get_rankings(limit=5)
|
|
69
|
+
for row in ranks["rankings"]:
|
|
70
|
+
print(row["rank"], row["name"], row["points"])
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
### 2. Head to head
|
|
74
|
+
|
|
75
|
+
Look up two player ids with `search_players`, then compare them.
|
|
76
|
+
|
|
77
|
+
```python
|
|
78
|
+
from acesapi import Client
|
|
79
|
+
|
|
80
|
+
client = Client()
|
|
81
|
+
|
|
82
|
+
sinner = client.search_players(name="Sinner", limit=1)["players"][0]
|
|
83
|
+
alcaraz = client.search_players(name="Alcaraz", limit=1)["players"][0]
|
|
84
|
+
h2h = client.get_head_to_head(sinner["player_id"], alcaraz["player_id"])
|
|
85
|
+
print(sinner["name"], "vs", alcaraz["name"])
|
|
86
|
+
print("matches:", h2h["summary"]["matches"])
|
|
87
|
+
print("wins:", h2h["summary"]["wins"])
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
### 3. Results by surface
|
|
91
|
+
|
|
92
|
+
`get_surface_splits` shows how a player does on hard, clay and grass.
|
|
93
|
+
|
|
94
|
+
```python
|
|
95
|
+
from acesapi import Client
|
|
96
|
+
|
|
97
|
+
client = Client()
|
|
98
|
+
|
|
99
|
+
sinner = client.search_players(name="Sinner", limit=1)["players"][0]
|
|
100
|
+
splits = client.get_surface_splits(sinner["player_id"])
|
|
101
|
+
for row in splits["splits"]:
|
|
102
|
+
print(row["surface"], row["wins"], "-", row["losses"], row["win_pct"])
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
## Credits
|
|
106
|
+
|
|
107
|
+
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.
|
|
108
|
+
|
|
109
|
+
## Pandas
|
|
110
|
+
|
|
111
|
+
```python
|
|
112
|
+
from acesapi import to_dataframe
|
|
113
|
+
|
|
114
|
+
df = to_dataframe(data) # finds the main list in the response
|
|
115
|
+
df = to_dataframe(data, key="rows") # or name the list yourself
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
Nested fields become dotted columns. Install pandas separately or with `pip install "acesapi[pandas]"`.
|
|
119
|
+
|
|
120
|
+
## Errors
|
|
121
|
+
|
|
122
|
+
Every error is an `APIError` subclass, with the server's message and the HTTP status.
|
|
123
|
+
|
|
124
|
+
| Exception | Status | Meaning |
|
|
125
|
+
| --- | --- | --- |
|
|
126
|
+
| `AuthError` | 401 | Missing, invalid or revoked key |
|
|
127
|
+
| `InsufficientCreditsError` | 402 | Not enough credits for the call |
|
|
128
|
+
| `NotFoundError` | 404 | The id or name does not exist |
|
|
129
|
+
| `RateLimitError` | 429 | Too many requests; has `.retry_after` when the server sends it |
|
|
130
|
+
| `APIError` | 400, 5xx | Bad arguments or a server problem |
|
|
131
|
+
| `APIConnectionError` | none | No response (network or timeout) |
|
|
132
|
+
|
|
133
|
+
```python
|
|
134
|
+
from acesapi import Client, APIError, InsufficientCreditsError, RateLimitError
|
|
135
|
+
|
|
136
|
+
client = Client()
|
|
137
|
+
try:
|
|
138
|
+
data = client.list_tournaments()
|
|
139
|
+
except InsufficientCreditsError:
|
|
140
|
+
print("Out of credits, top up at https://acesapi.com")
|
|
141
|
+
except RateLimitError as exc:
|
|
142
|
+
print("Slow down, retry after", exc.retry_after)
|
|
143
|
+
except APIError as exc:
|
|
144
|
+
print(exc.status_code, exc.message)
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
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.
|
|
148
|
+
|
|
149
|
+
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).
|
|
150
|
+
|
|
151
|
+
## Options
|
|
152
|
+
|
|
153
|
+
```python
|
|
154
|
+
Client(
|
|
155
|
+
api_key=None, # defaults to $ACESAPI_API_KEY
|
|
156
|
+
base_url="https://mcp.acesapi.com", # point at another host
|
|
157
|
+
timeout=60.0, # seconds per request
|
|
158
|
+
max_retries=3,
|
|
159
|
+
)
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
`client.call("tool_name", {"arg": "value"})` calls any tool by name, which helps when the API adds a tool before this package does.
|
|
163
|
+
|
|
164
|
+
## Tools
|
|
165
|
+
|
|
166
|
+
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)`).
|
|
167
|
+
|
|
168
|
+
| Method | Credits | What it does |
|
|
169
|
+
| --- | --- | --- |
|
|
170
|
+
| `list_tournaments` | 1 | List tournaments |
|
|
171
|
+
| `get_tournament` | 2 | Tournament |
|
|
172
|
+
| `get_matches` | 2 | Matches |
|
|
173
|
+
| `get_schedule` | 2 | Schedule |
|
|
174
|
+
| `get_match` | 2 | Match |
|
|
175
|
+
| `search_players` | 1 | Search players |
|
|
176
|
+
| `get_player` | 5 | Player |
|
|
177
|
+
| `get_player_matches` | 5 | Player matches |
|
|
178
|
+
| `get_head_to_head` | 3 | Head to head |
|
|
179
|
+
| `get_surface_splits` | 3 | Surface splits |
|
|
180
|
+
| `get_player_stats` | 3 | Player stats |
|
|
181
|
+
| `get_rankings` | 2 | Rankings |
|
|
182
|
+
| `get_ratings` | 2 | Ratings |
|
|
183
|
+
|
|
184
|
+
Full API reference: https://acesapi.com/docs
|
|
185
|
+
|
|
186
|
+
## License
|
|
187
|
+
|
|
188
|
+
MIT
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
acesapi/__init__.py,sha256=QP2-5MzYJSBnmubzaGvbYZTpfC4A5qSseio3Df19t0c,508
|
|
2
|
+
acesapi/_core.py,sha256=Q-Ks1j9kBI7MJlzYktUm_-MrgJSBkFj6l6ztAWzT13E,10594
|
|
3
|
+
acesapi/_version.py,sha256=kUR5RAFc7HCeiqdlX36dZOHkUI5wI6V_43RpEcD8b-0,22
|
|
4
|
+
acesapi/client.py,sha256=zTpezgIAEUZ4WvkQEpp6IIGQ-GLjAwbKooDgnBa_Rxs,18673
|
|
5
|
+
acesapi/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
6
|
+
acesapi-0.1.0.dist-info/METADATA,sha256=GGaxFkb_f4CsatXVTu3bt9QrCewxuL-Av4C46Xd-5vg,6199
|
|
7
|
+
acesapi-0.1.0.dist-info/WHEEL,sha256=W3fkpkm7-wf9vBI5Z-7s0eWkeM-spu78I8Neb98DeEg,87
|
|
8
|
+
acesapi-0.1.0.dist-info/licenses/LICENSE,sha256=8DCMeriFDveJPVNKBZ_wIStWeud4JxPLa_-T3GVio8A,1064
|
|
9
|
+
acesapi-0.1.0.dist-info/RECORD,,
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 AcesAPI
|
|
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.
|