fighterapi 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.
- fighterapi/__init__.py +25 -0
- fighterapi/_core.py +307 -0
- fighterapi/_version.py +1 -0
- fighterapi/client.py +402 -0
- fighterapi/py.typed +0 -0
- fighterapi-0.1.0.dist-info/METADATA +181 -0
- fighterapi-0.1.0.dist-info/RECORD +9 -0
- fighterapi-0.1.0.dist-info/WHEEL +4 -0
- fighterapi-0.1.0.dist-info/licenses/LICENSE +21 -0
fighterapi/__init__.py
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
"""FighterAPI: Python client for the FighterAPI MMA 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
|
+
]
|
fighterapi/_core.py
ADDED
|
@@ -0,0 +1,307 @@
|
|
|
1
|
+
"""Shared runtime for the FighterAPI 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 = "FighterAPI"
|
|
20
|
+
ENV_VAR = "FIGHTERAPI_API_KEY"
|
|
21
|
+
DEFAULT_BASE_URL = "https://mcp.fighterapi.com"
|
|
22
|
+
SIGNUP_URL = "https://fighterapi.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)
|
fighterapi/_version.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "0.1.0"
|
fighterapi/client.py
ADDED
|
@@ -0,0 +1,402 @@
|
|
|
1
|
+
"""Generated by packages/python/generate.py from the FighterAPI 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_events": 1,
|
|
11
|
+
"get_event": 2,
|
|
12
|
+
"get_schedule": 2,
|
|
13
|
+
"get_fight": 5,
|
|
14
|
+
"search_fighters": 1,
|
|
15
|
+
"get_fighter": 5,
|
|
16
|
+
"get_fighter_fights": 5,
|
|
17
|
+
"get_leaders": 5,
|
|
18
|
+
"get_ratings": 2,
|
|
19
|
+
"get_odds": 5,
|
|
20
|
+
"get_line_movement": 10,
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class Client(BaseClient):
|
|
25
|
+
"""Client for the FighterAPI API (MMA).
|
|
26
|
+
|
|
27
|
+
Create one with ``Client()`` (reads FIGHTERAPI_API_KEY) or ``Client(api_key="...")``.
|
|
28
|
+
Every method returns the unwrapped ``data`` from the response.
|
|
29
|
+
``last_credits_used`` and ``last_credits_remaining`` hold the counters from
|
|
30
|
+
the most recent response.
|
|
31
|
+
"""
|
|
32
|
+
|
|
33
|
+
def list_events(
|
|
34
|
+
self,
|
|
35
|
+
*,
|
|
36
|
+
season: Optional[Union[str, int, float]] = None,
|
|
37
|
+
status: Optional[Literal["scheduled", "final"]] = None,
|
|
38
|
+
limit: Optional[int] = None,
|
|
39
|
+
offset: Optional[int] = None,
|
|
40
|
+
league: Optional[Literal["mma", "pfl", "bellator"]] = None,
|
|
41
|
+
) -> Any:
|
|
42
|
+
"""List events.
|
|
43
|
+
|
|
44
|
+
Use to find cards. Lists events newest first with the venue, status, number of fights and main
|
|
45
|
+
event. Filter by season (year) and status (scheduled or final). Costs 1 credit.
|
|
46
|
+
|
|
47
|
+
Costs 1 credit per call.
|
|
48
|
+
|
|
49
|
+
Args:
|
|
50
|
+
season (optional): Year, e.g. 2025.
|
|
51
|
+
status (optional): Only upcoming or only completed cards. One of: scheduled, final.
|
|
52
|
+
limit (optional): Max events (default 50).
|
|
53
|
+
offset (optional): Page offset; use next_offset.
|
|
54
|
+
league (optional): League. Defaults to mma. One of: mma, pfl, bellator.
|
|
55
|
+
|
|
56
|
+
Returns:
|
|
57
|
+
The unwrapped ``data`` object from the response.
|
|
58
|
+
"""
|
|
59
|
+
return self._call(
|
|
60
|
+
"list_events",
|
|
61
|
+
{
|
|
62
|
+
"season": season,
|
|
63
|
+
"status": status,
|
|
64
|
+
"limit": limit,
|
|
65
|
+
"offset": offset,
|
|
66
|
+
"league": league,
|
|
67
|
+
},
|
|
68
|
+
)
|
|
69
|
+
|
|
70
|
+
def get_event(
|
|
71
|
+
self,
|
|
72
|
+
event_id: str,
|
|
73
|
+
*,
|
|
74
|
+
league: Optional[Literal["mma", "pfl", "bellator"]] = None,
|
|
75
|
+
) -> Any:
|
|
76
|
+
"""Event.
|
|
77
|
+
|
|
78
|
+
Use for one card: every fight in order with the result, method, round and time, judges' scores
|
|
79
|
+
and referee. Costs 2 credits.
|
|
80
|
+
|
|
81
|
+
Costs 2 credits per call.
|
|
82
|
+
|
|
83
|
+
Args:
|
|
84
|
+
event_id: Event id from list_events or get_schedule.
|
|
85
|
+
league (optional): League. Defaults to mma. One of: mma, pfl, bellator.
|
|
86
|
+
|
|
87
|
+
Returns:
|
|
88
|
+
The unwrapped ``data`` object from the response.
|
|
89
|
+
"""
|
|
90
|
+
return self._call(
|
|
91
|
+
"get_event",
|
|
92
|
+
{
|
|
93
|
+
"event_id": event_id,
|
|
94
|
+
"league": league,
|
|
95
|
+
},
|
|
96
|
+
)
|
|
97
|
+
|
|
98
|
+
def get_schedule(
|
|
99
|
+
self,
|
|
100
|
+
*,
|
|
101
|
+
days: Optional[int] = None,
|
|
102
|
+
league: Optional[Literal["mma", "pfl", "bellator"]] = None,
|
|
103
|
+
) -> Any:
|
|
104
|
+
"""Schedule.
|
|
105
|
+
|
|
106
|
+
Use for upcoming fights: every scheduled bout in the next N days (default 30) with the current
|
|
107
|
+
moneyline from any book that has posted one. Costs 2 credits.
|
|
108
|
+
|
|
109
|
+
Costs 2 credits per call.
|
|
110
|
+
|
|
111
|
+
Args:
|
|
112
|
+
days (optional): Days ahead (default 30, max 180).
|
|
113
|
+
league (optional): League. Defaults to mma. One of: mma, pfl, bellator.
|
|
114
|
+
|
|
115
|
+
Returns:
|
|
116
|
+
The unwrapped ``data`` object from the response.
|
|
117
|
+
"""
|
|
118
|
+
return self._call(
|
|
119
|
+
"get_schedule",
|
|
120
|
+
{
|
|
121
|
+
"days": days,
|
|
122
|
+
"league": league,
|
|
123
|
+
},
|
|
124
|
+
)
|
|
125
|
+
|
|
126
|
+
def get_fight(
|
|
127
|
+
self,
|
|
128
|
+
bout_id: str,
|
|
129
|
+
*,
|
|
130
|
+
league: Optional[Literal["mma", "pfl", "bellator"]] = None,
|
|
131
|
+
) -> Any:
|
|
132
|
+
"""Fight.
|
|
133
|
+
|
|
134
|
+
Use for everything about one fight: the result, both fighters' full stat sheets (strikes by
|
|
135
|
+
target and position, takedowns, control time, guard passes, reversals, submission attempts; UFC
|
|
136
|
+
only, null elsewhere and where the source has no sheet), round-by-round lines (UFC only), each
|
|
137
|
+
fighter's rating before and after, and the opening and closing moneyline from each book that
|
|
138
|
+
posted one (2024 on). Costs 5 credits.
|
|
139
|
+
|
|
140
|
+
Costs 5 credits per call.
|
|
141
|
+
|
|
142
|
+
Args:
|
|
143
|
+
bout_id: Fight id (bout_id) from get_event or get_fighter_fights.
|
|
144
|
+
league (optional): League. Defaults to mma. One of: mma, pfl, bellator.
|
|
145
|
+
|
|
146
|
+
Returns:
|
|
147
|
+
The unwrapped ``data`` object from the response.
|
|
148
|
+
"""
|
|
149
|
+
return self._call(
|
|
150
|
+
"get_fight",
|
|
151
|
+
{
|
|
152
|
+
"bout_id": bout_id,
|
|
153
|
+
"league": league,
|
|
154
|
+
},
|
|
155
|
+
)
|
|
156
|
+
|
|
157
|
+
def search_fighters(
|
|
158
|
+
self,
|
|
159
|
+
query: str,
|
|
160
|
+
*,
|
|
161
|
+
weight_class: Optional[str] = None,
|
|
162
|
+
limit: Optional[int] = None,
|
|
163
|
+
league: Optional[Literal["mma", "pfl", "bellator"]] = None,
|
|
164
|
+
) -> Any:
|
|
165
|
+
"""Search fighters.
|
|
166
|
+
|
|
167
|
+
Use to find a fighter's id by name or nickname (accents optional). Returns weight class,
|
|
168
|
+
country, pro record, number of fights in the promotion (bouts; ufc_bouts for the UFC only) and
|
|
169
|
+
last fight date. Costs 1 credit.
|
|
170
|
+
|
|
171
|
+
Costs 1 credit per call.
|
|
172
|
+
|
|
173
|
+
Args:
|
|
174
|
+
query: Name or nickname, e.g. Topuria or 'El Matador'.
|
|
175
|
+
weight_class (optional): e.g. Lightweight, Women's Strawweight.
|
|
176
|
+
limit (optional): Max results (default 20).
|
|
177
|
+
league (optional): League. Defaults to mma. One of: mma, pfl, bellator.
|
|
178
|
+
|
|
179
|
+
Returns:
|
|
180
|
+
The unwrapped ``data`` object from the response.
|
|
181
|
+
"""
|
|
182
|
+
return self._call(
|
|
183
|
+
"search_fighters",
|
|
184
|
+
{
|
|
185
|
+
"query": query,
|
|
186
|
+
"weight_class": weight_class,
|
|
187
|
+
"limit": limit,
|
|
188
|
+
"league": league,
|
|
189
|
+
},
|
|
190
|
+
)
|
|
191
|
+
|
|
192
|
+
def get_fighter(
|
|
193
|
+
self,
|
|
194
|
+
fighter_id: str,
|
|
195
|
+
*,
|
|
196
|
+
league: Optional[Literal["mma", "pfl", "bellator"]] = None,
|
|
197
|
+
) -> Any:
|
|
198
|
+
"""Fighter.
|
|
199
|
+
|
|
200
|
+
Use for one fighter's profile: date of birth, height, reach, stance, gym, record in the
|
|
201
|
+
promotion by method (record; ufc_record is the same for the UFC and null for other promotions),
|
|
202
|
+
career rates per 15 minutes from the stat sheets (significant strikes landed and absorbed,
|
|
203
|
+
accuracy, defense, takedowns, submission attempts, control share; UFC only), current and peak
|
|
204
|
+
rating, and the last five fights. Costs 5 credits.
|
|
205
|
+
|
|
206
|
+
Costs 5 credits per call.
|
|
207
|
+
|
|
208
|
+
Args:
|
|
209
|
+
fighter_id: Fighter id from search_fighters.
|
|
210
|
+
league (optional): League. Defaults to mma. One of: mma, pfl, bellator.
|
|
211
|
+
|
|
212
|
+
Returns:
|
|
213
|
+
The unwrapped ``data`` object from the response.
|
|
214
|
+
"""
|
|
215
|
+
return self._call(
|
|
216
|
+
"get_fighter",
|
|
217
|
+
{
|
|
218
|
+
"fighter_id": fighter_id,
|
|
219
|
+
"league": league,
|
|
220
|
+
},
|
|
221
|
+
)
|
|
222
|
+
|
|
223
|
+
def get_fighter_fights(
|
|
224
|
+
self,
|
|
225
|
+
fighter_id: str,
|
|
226
|
+
*,
|
|
227
|
+
limit: Optional[int] = None,
|
|
228
|
+
offset: Optional[int] = None,
|
|
229
|
+
league: Optional[Literal["mma", "pfl", "bellator"]] = None,
|
|
230
|
+
) -> Any:
|
|
231
|
+
"""Fighter fights.
|
|
232
|
+
|
|
233
|
+
Use for a fighter's full history in the promotion: every fight with the opponent, result,
|
|
234
|
+
method, round, time, strikes landed and absorbed and takedowns (UFC only), rating before and
|
|
235
|
+
after, and the median closing moneyline across sportsbooks. Costs 5 credits.
|
|
236
|
+
|
|
237
|
+
Costs 5 credits per call.
|
|
238
|
+
|
|
239
|
+
Args:
|
|
240
|
+
fighter_id: Fighter id from search_fighters.
|
|
241
|
+
limit (optional): Max fights (default 50).
|
|
242
|
+
offset (optional): Page offset; use next_offset.
|
|
243
|
+
league (optional): League. Defaults to mma. One of: mma, pfl, bellator.
|
|
244
|
+
|
|
245
|
+
Returns:
|
|
246
|
+
The unwrapped ``data`` object from the response.
|
|
247
|
+
"""
|
|
248
|
+
return self._call(
|
|
249
|
+
"get_fighter_fights",
|
|
250
|
+
{
|
|
251
|
+
"fighter_id": fighter_id,
|
|
252
|
+
"limit": limit,
|
|
253
|
+
"offset": offset,
|
|
254
|
+
"league": league,
|
|
255
|
+
},
|
|
256
|
+
)
|
|
257
|
+
|
|
258
|
+
def get_leaders(
|
|
259
|
+
self,
|
|
260
|
+
stat: Literal["sig_landed_per_15", "sig_absorbed_per_15", "sig_accuracy", "sig_defense", "td_per_15", "td_accuracy", "td_defense", "knockdowns", "sub_attempts_per_15", "control_share", "finish_rate"],
|
|
261
|
+
*,
|
|
262
|
+
weight_class: Optional[str] = None,
|
|
263
|
+
since: Optional[Union[str, int, float]] = None,
|
|
264
|
+
min_fights: Optional[int] = None,
|
|
265
|
+
limit: Optional[int] = None,
|
|
266
|
+
) -> Any:
|
|
267
|
+
"""Leaders.
|
|
268
|
+
|
|
269
|
+
Use for career leaderboards computed from every fight's stat sheet: sig_landed_per_15
|
|
270
|
+
(significant strikes landed per 15 min), sig_absorbed_per_15 (significant strikes absorbed per
|
|
271
|
+
15 min), sig_accuracy (significant strike accuracy), sig_defense (significant strike defense),
|
|
272
|
+
td_per_15 (takedowns landed per 15 min), td_accuracy (takedown accuracy), td_defense (takedown
|
|
273
|
+
defense), knockdowns (knockdowns), sub_attempts_per_15 (submission attempts per 15 min),
|
|
274
|
+
control_share (share of fight time in control), finish_rate (wins by KO/TKO or submission, as a
|
|
275
|
+
share of wins). Filter by weight class, a starting year, and a minimum number of fights (default
|
|
276
|
+
5). Costs 5 credits.
|
|
277
|
+
|
|
278
|
+
Costs 5 credits per call.
|
|
279
|
+
|
|
280
|
+
Args:
|
|
281
|
+
stat: Which leaderboard. One of: sig_landed_per_15, sig_absorbed_per_15, sig_accuracy,
|
|
282
|
+
sig_defense, td_per_15, td_accuracy, td_defense, knockdowns, sub_attempts_per_15,
|
|
283
|
+
control_share, finish_rate.
|
|
284
|
+
weight_class (optional): Only fights in this weight class, e.g. Lightweight.
|
|
285
|
+
since (optional): Only fights from this year on, e.g. 2020.
|
|
286
|
+
min_fights (optional): Minimum fights to qualify (default 5).
|
|
287
|
+
limit (optional): Max fighters (default 20).
|
|
288
|
+
|
|
289
|
+
Returns:
|
|
290
|
+
The unwrapped ``data`` object from the response.
|
|
291
|
+
"""
|
|
292
|
+
return self._call(
|
|
293
|
+
"get_leaders",
|
|
294
|
+
{
|
|
295
|
+
"stat": stat,
|
|
296
|
+
"weight_class": weight_class,
|
|
297
|
+
"since": since,
|
|
298
|
+
"min_fights": min_fights,
|
|
299
|
+
"limit": limit,
|
|
300
|
+
},
|
|
301
|
+
)
|
|
302
|
+
|
|
303
|
+
def get_ratings(
|
|
304
|
+
self,
|
|
305
|
+
*,
|
|
306
|
+
weight_class: Optional[str] = None,
|
|
307
|
+
active_since: Optional[Union[str, int, float]] = None,
|
|
308
|
+
limit: Optional[int] = None,
|
|
309
|
+
league: Optional[Literal["mma", "pfl", "bellator"]] = None,
|
|
310
|
+
) -> Any:
|
|
311
|
+
"""Ratings.
|
|
312
|
+
|
|
313
|
+
Use for our fighter ratings: an Elo built from the promotion's own results (the UFC since 2000),
|
|
314
|
+
highest first, for fighters active since a given year (default last year). Filter by weight
|
|
315
|
+
class. Costs 2 credits.
|
|
316
|
+
|
|
317
|
+
Costs 2 credits per call.
|
|
318
|
+
|
|
319
|
+
Args:
|
|
320
|
+
weight_class (optional): e.g. Welterweight.
|
|
321
|
+
active_since (optional): Only fighters whose last fight was in this year or later.
|
|
322
|
+
limit (optional): Max fighters (default 25).
|
|
323
|
+
league (optional): League. Defaults to mma. One of: mma, pfl, bellator.
|
|
324
|
+
|
|
325
|
+
Returns:
|
|
326
|
+
The unwrapped ``data`` object from the response.
|
|
327
|
+
"""
|
|
328
|
+
return self._call(
|
|
329
|
+
"get_ratings",
|
|
330
|
+
{
|
|
331
|
+
"weight_class": weight_class,
|
|
332
|
+
"active_since": active_since,
|
|
333
|
+
"limit": limit,
|
|
334
|
+
"league": league,
|
|
335
|
+
},
|
|
336
|
+
)
|
|
337
|
+
|
|
338
|
+
def get_odds(
|
|
339
|
+
self,
|
|
340
|
+
*,
|
|
341
|
+
bout_id: Optional[str] = None,
|
|
342
|
+
event_id: Optional[str] = None,
|
|
343
|
+
league: Optional[Literal["mma", "pfl", "bellator"]] = None,
|
|
344
|
+
) -> Any:
|
|
345
|
+
"""Odds.
|
|
346
|
+
|
|
347
|
+
Use for betting lines: each posted sportsbook's opening, closing and current moneyline for both
|
|
348
|
+
fighters, with implied probabilities and the total-rounds line. Opening and closing lines start
|
|
349
|
+
with the UFC cards of June 2020; a median of 5 to 7 books per fight (ESPN's own feed is one book
|
|
350
|
+
per fight since mid-2024). Pass bout_id for one fight (5 credits) or event_id for a whole card
|
|
351
|
+
(10 credits).
|
|
352
|
+
|
|
353
|
+
Costs 5 credits per call.
|
|
354
|
+
|
|
355
|
+
Args:
|
|
356
|
+
bout_id (optional): One fight.
|
|
357
|
+
event_id (optional): A whole card.
|
|
358
|
+
league (optional): League. Defaults to mma. One of: mma, pfl, bellator.
|
|
359
|
+
|
|
360
|
+
Returns:
|
|
361
|
+
The unwrapped ``data`` object from the response.
|
|
362
|
+
"""
|
|
363
|
+
return self._call(
|
|
364
|
+
"get_odds",
|
|
365
|
+
{
|
|
366
|
+
"bout_id": bout_id,
|
|
367
|
+
"event_id": event_id,
|
|
368
|
+
"league": league,
|
|
369
|
+
},
|
|
370
|
+
)
|
|
371
|
+
|
|
372
|
+
def get_line_movement(
|
|
373
|
+
self,
|
|
374
|
+
bout_id: str,
|
|
375
|
+
*,
|
|
376
|
+
provider: Optional[str] = None,
|
|
377
|
+
league: Optional[Literal["mma", "pfl", "bellator"]] = None,
|
|
378
|
+
) -> Any:
|
|
379
|
+
"""Line movement.
|
|
380
|
+
|
|
381
|
+
Use to see how a fight's line moved before the bell: our snapshots of each posted moneyline,
|
|
382
|
+
from first capture to fight day (ESPN's once a day from October 2026, The Odds API's every hour
|
|
383
|
+
in the two days before). Costs 10 credits.
|
|
384
|
+
|
|
385
|
+
Costs 10 credits per call.
|
|
386
|
+
|
|
387
|
+
Args:
|
|
388
|
+
bout_id: Fight id.
|
|
389
|
+
provider (optional): One sportsbook, e.g. DraftKings.
|
|
390
|
+
league (optional): League. Defaults to mma. One of: mma, pfl, bellator.
|
|
391
|
+
|
|
392
|
+
Returns:
|
|
393
|
+
The unwrapped ``data`` object from the response.
|
|
394
|
+
"""
|
|
395
|
+
return self._call(
|
|
396
|
+
"get_line_movement",
|
|
397
|
+
{
|
|
398
|
+
"bout_id": bout_id,
|
|
399
|
+
"provider": provider,
|
|
400
|
+
"league": league,
|
|
401
|
+
},
|
|
402
|
+
)
|
fighterapi/py.typed
ADDED
|
File without changes
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: fighterapi
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Python client for the FighterAPI MMA data API. No dependencies.
|
|
5
|
+
Project-URL: Homepage, https://fighterapi.com
|
|
6
|
+
Project-URL: Documentation, https://fighterapi.com/docs
|
|
7
|
+
Project-URL: Repository, https://github.com/noahowsh/puckapi
|
|
8
|
+
Project-URL: Sign up, https://fighterapi.com/signup
|
|
9
|
+
Author-email: FighterAPI <support@fighterapi.com>
|
|
10
|
+
License-Expression: MIT
|
|
11
|
+
License-File: LICENSE
|
|
12
|
+
Keywords: api-client,betting,fighterapi,mma,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
|
+
# FighterAPI for Python
|
|
32
|
+
|
|
33
|
+
Python client for the [FighterAPI](https://fighterapi.com) MMA 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 fighterapi
|
|
39
|
+
pip install "fighterapi[pandas]" # only if you want DataFrames
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
## Set your key
|
|
43
|
+
|
|
44
|
+
Get a free key at https://fighterapi.com/signup. Free accounts get 500 credits a month.
|
|
45
|
+
|
|
46
|
+
```bash
|
|
47
|
+
export FIGHTERAPI_API_KEY=your_key_here
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
```python
|
|
51
|
+
from fighterapi import Client
|
|
52
|
+
|
|
53
|
+
client = Client() # reads FIGHTERAPI_API_KEY
|
|
54
|
+
client = Client(api_key="your_key") # or pass it in
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
## Examples
|
|
58
|
+
|
|
59
|
+
### 1. Recent events
|
|
60
|
+
|
|
61
|
+
`list_events` filters by season and status. Pass `league="pfl"` or `"bellator"` for other promotions.
|
|
62
|
+
|
|
63
|
+
```python
|
|
64
|
+
from fighterapi import Client
|
|
65
|
+
|
|
66
|
+
client = Client()
|
|
67
|
+
|
|
68
|
+
events = client.list_events(status="final", limit=3)["events"]
|
|
69
|
+
for e in events:
|
|
70
|
+
print(e["event_id"], e["event_date"], e["name"])
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
### 2. A fight card
|
|
74
|
+
|
|
75
|
+
`get_event` returns the bouts on one event.
|
|
76
|
+
|
|
77
|
+
```python
|
|
78
|
+
from fighterapi import Client
|
|
79
|
+
|
|
80
|
+
client = Client()
|
|
81
|
+
|
|
82
|
+
card = client.get_event("600061541")
|
|
83
|
+
print(card)
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
### 3. A fighter's record
|
|
87
|
+
|
|
88
|
+
Find the id with `search_fighters`, then pull his fight history.
|
|
89
|
+
|
|
90
|
+
```python
|
|
91
|
+
from fighterapi import Client
|
|
92
|
+
|
|
93
|
+
client = Client()
|
|
94
|
+
|
|
95
|
+
fighter = client.search_fighters("Makhachev", limit=1)["fighters"][0]
|
|
96
|
+
fights = client.get_fighter_fights(fighter["fighter_id"], limit=5)
|
|
97
|
+
print(fighter["name"], fights)
|
|
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 fighterapi 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 "fighterapi[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 fighterapi import Client, APIError, InsufficientCreditsError, RateLimitError
|
|
130
|
+
|
|
131
|
+
client = Client()
|
|
132
|
+
try:
|
|
133
|
+
data = client.list_events()
|
|
134
|
+
except InsufficientCreditsError:
|
|
135
|
+
print("Out of credits, top up at https://fighterapi.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 $FIGHTERAPI_API_KEY
|
|
151
|
+
base_url="https://mcp.fighterapi.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_events)`).
|
|
162
|
+
|
|
163
|
+
| Method | Credits | What it does |
|
|
164
|
+
| --- | --- | --- |
|
|
165
|
+
| `list_events` | 1 | List events |
|
|
166
|
+
| `get_event` | 2 | Event |
|
|
167
|
+
| `get_schedule` | 2 | Schedule |
|
|
168
|
+
| `get_fight` | 5 | Fight |
|
|
169
|
+
| `search_fighters` | 1 | Search fighters |
|
|
170
|
+
| `get_fighter` | 5 | Fighter |
|
|
171
|
+
| `get_fighter_fights` | 5 | Fighter fights |
|
|
172
|
+
| `get_leaders` | 5 | Leaders |
|
|
173
|
+
| `get_ratings` | 2 | Ratings |
|
|
174
|
+
| `get_odds` | 5 | Odds |
|
|
175
|
+
| `get_line_movement` | 10 | Line movement |
|
|
176
|
+
|
|
177
|
+
Full API reference: https://fighterapi.com/docs
|
|
178
|
+
|
|
179
|
+
## License
|
|
180
|
+
|
|
181
|
+
MIT
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
fighterapi/__init__.py,sha256=LJNQAaYcXr3e-wdAeNRu0SxI_F8YKYYETOlHjoJZ6RE,511
|
|
2
|
+
fighterapi/_core.py,sha256=1hlUDyXd2tJ9hsCdKphFaqp-lSfyzrRff7DEF5zKjFM,10609
|
|
3
|
+
fighterapi/_version.py,sha256=kUR5RAFc7HCeiqdlX36dZOHkUI5wI6V_43RpEcD8b-0,22
|
|
4
|
+
fighterapi/client.py,sha256=kTBWwdL98AcglMDEeFmfDJ1X7mKA7CQFqNBuZEnU02s,13845
|
|
5
|
+
fighterapi/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
6
|
+
fighterapi-0.1.0.dist-info/METADATA,sha256=mv6DfRk-RxhrMtZBHtqLZE2kqtq-T9eDvz8nkttFaYM,5819
|
|
7
|
+
fighterapi-0.1.0.dist-info/WHEEL,sha256=W3fkpkm7-wf9vBI5Z-7s0eWkeM-spu78I8Neb98DeEg,87
|
|
8
|
+
fighterapi-0.1.0.dist-info/licenses/LICENSE,sha256=IOWMvk6uaPzhTuyLCjGmy-Uwmuy0rzc7oGufSqpEqP4,1067
|
|
9
|
+
fighterapi-0.1.0.dist-info/RECORD,,
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 FighterAPI
|
|
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.
|