nflmeta 0.1.3__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.
- nflmeta/__init__.py +87 -0
- nflmeta/client.py +249 -0
- nflmeta/errors.py +98 -0
- nflmeta/py.typed +1 -0
- nflmeta/resources.py +486 -0
- nflmeta/types.py +32 -0
- nflmeta-0.1.3.dist-info/METADATA +130 -0
- nflmeta-0.1.3.dist-info/RECORD +11 -0
- nflmeta-0.1.3.dist-info/WHEEL +5 -0
- nflmeta-0.1.3.dist-info/licenses/LICENSE +21 -0
- nflmeta-0.1.3.dist-info/top_level.txt +1 -0
nflmeta/__init__.py
ADDED
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import Any
|
|
4
|
+
|
|
5
|
+
from .client import NFLMetaClientCore, __version__
|
|
6
|
+
from .errors import (
|
|
7
|
+
NFLMetaBadRequestError,
|
|
8
|
+
NFLMetaError,
|
|
9
|
+
NFLMetaInvalidUrlError,
|
|
10
|
+
NFLMetaNotFoundError,
|
|
11
|
+
NFLMetaRateLimitError,
|
|
12
|
+
NFLMetaTimeoutError,
|
|
13
|
+
NFLMetaUnauthorizedError,
|
|
14
|
+
)
|
|
15
|
+
from .resources import (
|
|
16
|
+
AllStarGamesResource,
|
|
17
|
+
AssetsResource,
|
|
18
|
+
CoachesResource,
|
|
19
|
+
ContributorsResource,
|
|
20
|
+
ExecutivesResource,
|
|
21
|
+
GamesResource,
|
|
22
|
+
HallOfFameResource,
|
|
23
|
+
HealthResource,
|
|
24
|
+
HistoryResource,
|
|
25
|
+
MetadataResource,
|
|
26
|
+
PlayersResource,
|
|
27
|
+
PlayoffGamesResource,
|
|
28
|
+
PlayoffsResource,
|
|
29
|
+
PlaysResource,
|
|
30
|
+
ReferenceResource,
|
|
31
|
+
SeasonsResource,
|
|
32
|
+
StaffResource,
|
|
33
|
+
StandingsResource,
|
|
34
|
+
StatsResource,
|
|
35
|
+
SuperBowlsResource,
|
|
36
|
+
TeamsResource,
|
|
37
|
+
UsageResource,
|
|
38
|
+
)
|
|
39
|
+
from .types import APIResponse, FieldValue, RateLimitInfo
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
class NFLMetaClient(NFLMetaClientCore):
|
|
43
|
+
def __init__(self, **kwargs: Any) -> None:
|
|
44
|
+
super().__init__(**kwargs)
|
|
45
|
+
self.health = HealthResource(self)
|
|
46
|
+
self.usage = UsageResource(self)
|
|
47
|
+
self.metadata = MetadataResource(self)
|
|
48
|
+
self.seasons = SeasonsResource(self)
|
|
49
|
+
self.teams = TeamsResource(self)
|
|
50
|
+
self.players = PlayersResource(self)
|
|
51
|
+
self.games = GamesResource(self)
|
|
52
|
+
self.plays = PlaysResource(self)
|
|
53
|
+
self.playoff_games = PlayoffGamesResource(self)
|
|
54
|
+
self.standings = StandingsResource(self)
|
|
55
|
+
self.playoffs = PlayoffsResource(self)
|
|
56
|
+
self.super_bowls = SuperBowlsResource(self)
|
|
57
|
+
self.history = HistoryResource(self)
|
|
58
|
+
self.hall_of_fame = HallOfFameResource(self)
|
|
59
|
+
self.contributors = ContributorsResource(self, "contributors")
|
|
60
|
+
self.executives = ExecutivesResource(self, "executives")
|
|
61
|
+
self.staff = StaffResource(self, "staff")
|
|
62
|
+
self.coaches = CoachesResource(self, "coaches")
|
|
63
|
+
self.assets = AssetsResource(self)
|
|
64
|
+
self.all_star_games = AllStarGamesResource(self)
|
|
65
|
+
self.stats = StatsResource(self)
|
|
66
|
+
self.reference = ReferenceResource(self)
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def create_client(**kwargs: Any) -> NFLMetaClient:
|
|
70
|
+
return NFLMetaClient(**kwargs)
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
__all__ = [
|
|
74
|
+
"APIResponse",
|
|
75
|
+
"__version__",
|
|
76
|
+
"FieldValue",
|
|
77
|
+
"NFLMetaBadRequestError",
|
|
78
|
+
"NFLMetaClient",
|
|
79
|
+
"NFLMetaError",
|
|
80
|
+
"NFLMetaInvalidUrlError",
|
|
81
|
+
"NFLMetaNotFoundError",
|
|
82
|
+
"NFLMetaRateLimitError",
|
|
83
|
+
"NFLMetaTimeoutError",
|
|
84
|
+
"NFLMetaUnauthorizedError",
|
|
85
|
+
"RateLimitInfo",
|
|
86
|
+
"create_client",
|
|
87
|
+
]
|
nflmeta/client.py
ADDED
|
@@ -0,0 +1,249 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import socket
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
from datetime import date, datetime
|
|
7
|
+
from typing import Any, Callable, Mapping, MutableMapping, Optional
|
|
8
|
+
from urllib.error import HTTPError, URLError
|
|
9
|
+
from urllib.parse import urlencode, urljoin, urlsplit
|
|
10
|
+
from urllib.request import HTTPRedirectHandler, Request, build_opener
|
|
11
|
+
|
|
12
|
+
from .errors import NFLMetaInvalidUrlError, NFLMetaTimeoutError, create_http_error
|
|
13
|
+
from .types import APIResponse, QueryParams, RateLimitInfo
|
|
14
|
+
|
|
15
|
+
__version__ = "0.1.3"
|
|
16
|
+
USER_AGENT = f"nflmeta-python/{__version__}"
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def _normalize_base_url(base_url: str | None) -> str:
|
|
20
|
+
base = (base_url or "https://nflmeta.org").strip()
|
|
21
|
+
return base.rstrip("/")
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def _origin(url: str) -> tuple[str, str]:
|
|
25
|
+
parts = urlsplit(url)
|
|
26
|
+
return (parts.scheme.lower(), parts.netloc.lower())
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def _resolve_request_url(base_url: str, path: str) -> str:
|
|
30
|
+
"""Resolve a caller-supplied path against the base, refusing anything that leaves it.
|
|
31
|
+
|
|
32
|
+
This used to accept any absolute http(s) URL and then attach X-NFLMeta-Key to
|
|
33
|
+
it unconditionally, so an application passing a user-controlled or mistakenly
|
|
34
|
+
absolute URL handed its API key to whatever host was named.
|
|
35
|
+
|
|
36
|
+
The boundary is the one mcp-server/src/api-client.mts already enforces: stay
|
|
37
|
+
on the configured origin, and keep the path inside /api/v1. Every endpoint
|
|
38
|
+
this client can reach lives there. Same-origin absolute URLs are still
|
|
39
|
+
accepted -- they are not a leak.
|
|
40
|
+
"""
|
|
41
|
+
base = f"{base_url}/"
|
|
42
|
+
resolved = urljoin(base, path)
|
|
43
|
+
if _origin(resolved) != _origin(base):
|
|
44
|
+
raise NFLMetaInvalidUrlError(
|
|
45
|
+
f"Refusing to send the API key to {urlsplit(resolved).netloc}. "
|
|
46
|
+
f"Requests must stay on {urlsplit(base).netloc}."
|
|
47
|
+
)
|
|
48
|
+
if not urlsplit(resolved).path.startswith("/api/v1/"):
|
|
49
|
+
raise NFLMetaInvalidUrlError(
|
|
50
|
+
f"The request path resolved outside /api/v1: {urlsplit(resolved).path}"
|
|
51
|
+
)
|
|
52
|
+
return resolved
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
class _SameOriginRedirectHandler(HTTPRedirectHandler):
|
|
56
|
+
"""Follow same-origin redirects; refuse to carry the key across an origin.
|
|
57
|
+
|
|
58
|
+
Redirects are a second way the key escapes, and one the request-URL check does
|
|
59
|
+
not cover. Measured on 2026-08-27 against a local server: urllib forwards
|
|
60
|
+
every request header across a cross-origin redirect -- X-NFLMeta-Key AND
|
|
61
|
+
Authorization, which even Node's fetch strips.
|
|
62
|
+
|
|
63
|
+
Same-origin redirects are left alone: the API normalises trailing slashes with
|
|
64
|
+
a 308 and that has to keep working.
|
|
65
|
+
"""
|
|
66
|
+
|
|
67
|
+
def redirect_request(self, req, fp, code, msg, headers, newurl): # type: ignore[no-untyped-def]
|
|
68
|
+
if _origin(newurl) != _origin(req.full_url):
|
|
69
|
+
raise NFLMetaInvalidUrlError(
|
|
70
|
+
f"Refusing to follow a redirect to {urlsplit(newurl).netloc}, which would send "
|
|
71
|
+
f"the API key off {urlsplit(req.full_url).netloc}."
|
|
72
|
+
)
|
|
73
|
+
return super().redirect_request(req, fp, code, msg, headers, newurl)
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
_OPENER = build_opener(_SameOriginRedirectHandler())
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def _serialize_query_value(value: Any) -> str:
|
|
80
|
+
if isinstance(value, bool):
|
|
81
|
+
return "true" if value else "false"
|
|
82
|
+
if isinstance(value, datetime):
|
|
83
|
+
return value.isoformat()
|
|
84
|
+
if isinstance(value, date):
|
|
85
|
+
return value.isoformat()
|
|
86
|
+
return str(value)
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def _build_query_string(query: QueryParams | None) -> str:
|
|
90
|
+
if not query:
|
|
91
|
+
return ""
|
|
92
|
+
pairs: list[tuple[str, str]] = []
|
|
93
|
+
for key, raw_value in query.items():
|
|
94
|
+
if raw_value is None:
|
|
95
|
+
continue
|
|
96
|
+
values = raw_value if isinstance(raw_value, (list, tuple)) else [raw_value]
|
|
97
|
+
for value in values:
|
|
98
|
+
if value is None:
|
|
99
|
+
continue
|
|
100
|
+
pairs.append((key, _serialize_query_value(value)))
|
|
101
|
+
return urlencode(pairs)
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def _lookup_header(headers: Mapping[str, str], name: str) -> Optional[str]:
|
|
105
|
+
"""Case-insensitive header lookup.
|
|
106
|
+
|
|
107
|
+
HTTP header names are case-insensitive, but the transport hands back a plain
|
|
108
|
+
dict built from whatever casing the wire used. Looking these up by the
|
|
109
|
+
canonical spelling returned None for every response, so the documented
|
|
110
|
+
``response.rate_limit.remaining`` was always None in Python while the
|
|
111
|
+
TypeScript SDK reported the real number.
|
|
112
|
+
"""
|
|
113
|
+
value = headers.get(name)
|
|
114
|
+
if value is not None:
|
|
115
|
+
return value
|
|
116
|
+
target = name.lower()
|
|
117
|
+
for key, candidate in headers.items():
|
|
118
|
+
if key.lower() == target:
|
|
119
|
+
return candidate
|
|
120
|
+
return None
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def _parse_int_header(headers: Mapping[str, str], name: str) -> Optional[int]:
|
|
124
|
+
value = _lookup_header(headers, name)
|
|
125
|
+
if not value:
|
|
126
|
+
return None
|
|
127
|
+
try:
|
|
128
|
+
return int(value)
|
|
129
|
+
except (TypeError, ValueError):
|
|
130
|
+
return None
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
def _parse_rate_limit(headers: Mapping[str, str]) -> RateLimitInfo:
|
|
134
|
+
return RateLimitInfo(
|
|
135
|
+
limit=_parse_int_header(headers, "X-RateLimit-Limit"),
|
|
136
|
+
remaining=_parse_int_header(headers, "X-RateLimit-Remaining"),
|
|
137
|
+
reset=_lookup_header(headers, "X-RateLimit-Reset"),
|
|
138
|
+
policy=_lookup_header(headers, "X-RateLimit-Policy"),
|
|
139
|
+
)
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
def _decode_payload(raw: bytes, content_type: str | None) -> Any:
|
|
143
|
+
text = raw.decode("utf-8") if raw else ""
|
|
144
|
+
if not text:
|
|
145
|
+
return None
|
|
146
|
+
if content_type and "application/json" in content_type.lower():
|
|
147
|
+
try:
|
|
148
|
+
return json.loads(text)
|
|
149
|
+
except json.JSONDecodeError:
|
|
150
|
+
return {"message": text}
|
|
151
|
+
return {"message": text}
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
@dataclass(slots=True)
|
|
155
|
+
class TransportResponse:
|
|
156
|
+
status: int
|
|
157
|
+
headers: MutableMapping[str, str]
|
|
158
|
+
payload: Any
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
def _default_transport(url: str, headers: Mapping[str, str], timeout: float | None) -> TransportResponse:
|
|
162
|
+
request_headers = dict(headers)
|
|
163
|
+
# urllib supplies its own "Python-urllib/x.y" User-Agent when none is set,
|
|
164
|
+
# and the edge in front of the API rejects that string outright: every call
|
|
165
|
+
# from the published package came back 403 (Cloudflare error 1010) before
|
|
166
|
+
# reaching the application. Note the fix is to send a real User-Agent, not
|
|
167
|
+
# to send none -- omitting it is what produces the failing default.
|
|
168
|
+
if not _lookup_header(request_headers, "User-Agent"):
|
|
169
|
+
request_headers["User-Agent"] = USER_AGENT
|
|
170
|
+
request = Request(url, headers=request_headers, method="GET")
|
|
171
|
+
try:
|
|
172
|
+
# _OPENER, not urlopen: the default opener follows a cross-origin
|
|
173
|
+
# redirect and re-sends every header, key included.
|
|
174
|
+
with _OPENER.open(request, timeout=timeout) as response:
|
|
175
|
+
raw = response.read()
|
|
176
|
+
response_headers = {key: value for key, value in response.headers.items()}
|
|
177
|
+
payload = _decode_payload(raw, response.headers.get("Content-Type"))
|
|
178
|
+
return TransportResponse(status=response.status, headers=response_headers, payload=payload)
|
|
179
|
+
except HTTPError as error:
|
|
180
|
+
raw = error.read()
|
|
181
|
+
response_headers = {key: value for key, value in error.headers.items()}
|
|
182
|
+
payload = _decode_payload(raw, error.headers.get("Content-Type"))
|
|
183
|
+
return TransportResponse(status=error.code, headers=response_headers, payload=payload)
|
|
184
|
+
except TimeoutError as error:
|
|
185
|
+
raise NFLMetaTimeoutError(timeout or 0.0) from error
|
|
186
|
+
except socket.timeout as error:
|
|
187
|
+
raise NFLMetaTimeoutError(timeout or 0.0) from error
|
|
188
|
+
except URLError as error:
|
|
189
|
+
if isinstance(error.reason, socket.timeout):
|
|
190
|
+
raise NFLMetaTimeoutError(timeout or 0.0) from error
|
|
191
|
+
raise
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
Transport = Callable[[str, Mapping[str, str], float | None], TransportResponse]
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
class NFLMetaClientCore:
|
|
198
|
+
def __init__(
|
|
199
|
+
self,
|
|
200
|
+
api_key: str | None = None,
|
|
201
|
+
base_url: str | None = None,
|
|
202
|
+
timeout: float | None = None,
|
|
203
|
+
headers: Mapping[str, str] | None = None,
|
|
204
|
+
transport: Transport | None = None,
|
|
205
|
+
) -> None:
|
|
206
|
+
self.api_key = api_key.strip() if api_key else None
|
|
207
|
+
self.base_url = _normalize_base_url(base_url)
|
|
208
|
+
self.timeout = timeout
|
|
209
|
+
self.default_headers = dict(headers or {})
|
|
210
|
+
self._transport = transport or _default_transport
|
|
211
|
+
|
|
212
|
+
def get(
|
|
213
|
+
self,
|
|
214
|
+
path: str,
|
|
215
|
+
query: QueryParams | None = None,
|
|
216
|
+
headers: Mapping[str, str] | None = None,
|
|
217
|
+
) -> APIResponse:
|
|
218
|
+
# Before any header is built: a URL that leaves the configured origin must
|
|
219
|
+
# never reach the point where the key is attached.
|
|
220
|
+
url = _resolve_request_url(self.base_url, path)
|
|
221
|
+
query_string = _build_query_string(query)
|
|
222
|
+
if query_string:
|
|
223
|
+
separator = "&" if "?" in url else "?"
|
|
224
|
+
url = f"{url}{separator}{query_string}"
|
|
225
|
+
|
|
226
|
+
request_headers: dict[str, str] = {"Accept": "application/json"}
|
|
227
|
+
request_headers.update(self.default_headers)
|
|
228
|
+
if headers:
|
|
229
|
+
request_headers.update(headers)
|
|
230
|
+
if self.api_key and "X-NFLMeta-Key" not in request_headers:
|
|
231
|
+
request_headers["X-NFLMeta-Key"] = self.api_key
|
|
232
|
+
|
|
233
|
+
transport_response = self._transport(url, request_headers, self.timeout)
|
|
234
|
+
rate_limit = _parse_rate_limit(transport_response.headers)
|
|
235
|
+
|
|
236
|
+
if transport_response.status < 200 or transport_response.status >= 300:
|
|
237
|
+
raise create_http_error(transport_response.status, transport_response.payload, rate_limit)
|
|
238
|
+
|
|
239
|
+
payload = transport_response.payload
|
|
240
|
+
if not isinstance(payload, dict) or "data" not in payload:
|
|
241
|
+
raise create_http_error(transport_response.status, payload, rate_limit)
|
|
242
|
+
|
|
243
|
+
return APIResponse(
|
|
244
|
+
data=payload.get("data"),
|
|
245
|
+
meta=payload.get("meta"),
|
|
246
|
+
status=transport_response.status,
|
|
247
|
+
rate_limit=rate_limit,
|
|
248
|
+
headers=transport_response.headers,
|
|
249
|
+
)
|
nflmeta/errors.py
ADDED
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
from typing import Any, Optional
|
|
5
|
+
|
|
6
|
+
from .types import RateLimitInfo
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
@dataclass(slots=True)
|
|
10
|
+
class NFLMetaErrorDetails:
|
|
11
|
+
code: Optional[str] = None
|
|
12
|
+
payload: Any = None
|
|
13
|
+
rate_limit: Optional[RateLimitInfo] = None
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class NFLMetaError(Exception):
|
|
17
|
+
def __init__(self, message: str, status: int, details: NFLMetaErrorDetails | None = None) -> None:
|
|
18
|
+
super().__init__(message)
|
|
19
|
+
details = details or NFLMetaErrorDetails()
|
|
20
|
+
self.status = status
|
|
21
|
+
self.code = details.code
|
|
22
|
+
self.payload = details.payload
|
|
23
|
+
self.rate_limit = details.rate_limit
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class NFLMetaInvalidUrlError(NFLMetaError):
|
|
27
|
+
"""A request was refused before it was sent, because its URL left the API origin.
|
|
28
|
+
|
|
29
|
+
``status`` is 0 precisely because nothing was transmitted: the client attaches
|
|
30
|
+
X-NFLMeta-Key to every request, so a URL pointing elsewhere has to be stopped
|
|
31
|
+
before any header is built, not reported after the fact.
|
|
32
|
+
"""
|
|
33
|
+
|
|
34
|
+
def __init__(self, message: str, details: NFLMetaErrorDetails | None = None) -> None:
|
|
35
|
+
details = details or NFLMetaErrorDetails()
|
|
36
|
+
if details.code is None:
|
|
37
|
+
details.code = "invalid_url"
|
|
38
|
+
super().__init__(message, 0, details)
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
class NFLMetaBadRequestError(NFLMetaError):
|
|
42
|
+
pass
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
class NFLMetaUnauthorizedError(NFLMetaError):
|
|
46
|
+
pass
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
class NFLMetaNotFoundError(NFLMetaError):
|
|
50
|
+
pass
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
class NFLMetaRateLimitError(NFLMetaError):
|
|
54
|
+
pass
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
class NFLMetaTimeoutError(TimeoutError):
|
|
58
|
+
def __init__(self, timeout_seconds: float) -> None:
|
|
59
|
+
super().__init__(f"NFLMeta request timed out after {timeout_seconds:.3f}s")
|
|
60
|
+
self.timeout_seconds = timeout_seconds
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def _error_message(payload: Any, status: int) -> str:
|
|
64
|
+
if isinstance(payload, dict):
|
|
65
|
+
error = payload.get("error")
|
|
66
|
+
if isinstance(error, dict):
|
|
67
|
+
message = error.get("message")
|
|
68
|
+
if isinstance(message, str) and message.strip():
|
|
69
|
+
return message
|
|
70
|
+
return f"NFLMeta request failed with status {status}"
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def _error_code(payload: Any) -> Optional[str]:
|
|
74
|
+
if isinstance(payload, dict):
|
|
75
|
+
error = payload.get("error")
|
|
76
|
+
if isinstance(error, dict):
|
|
77
|
+
code = error.get("code")
|
|
78
|
+
if isinstance(code, str) and code.strip():
|
|
79
|
+
return code
|
|
80
|
+
return None
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def create_http_error(status: int, payload: Any, rate_limit: RateLimitInfo | None = None) -> NFLMetaError:
|
|
84
|
+
details = NFLMetaErrorDetails(
|
|
85
|
+
code=_error_code(payload),
|
|
86
|
+
payload=payload,
|
|
87
|
+
rate_limit=rate_limit,
|
|
88
|
+
)
|
|
89
|
+
message = _error_message(payload, status)
|
|
90
|
+
if status == 400:
|
|
91
|
+
return NFLMetaBadRequestError(message, status, details)
|
|
92
|
+
if status == 401:
|
|
93
|
+
return NFLMetaUnauthorizedError(message, status, details)
|
|
94
|
+
if status == 404:
|
|
95
|
+
return NFLMetaNotFoundError(message, status, details)
|
|
96
|
+
if status == 429:
|
|
97
|
+
return NFLMetaRateLimitError(message, status, details)
|
|
98
|
+
return NFLMetaError(message, status, details)
|
nflmeta/py.typed
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
nflmeta/resources.py
ADDED
|
@@ -0,0 +1,486 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import Any
|
|
4
|
+
from urllib.parse import quote
|
|
5
|
+
|
|
6
|
+
from .client import NFLMetaClientCore
|
|
7
|
+
from .types import APIResponse, QueryParams
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def _segment(value: str | int) -> str:
|
|
11
|
+
return quote(str(value), safe="")
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def _api_path(*parts: str | int) -> str:
|
|
15
|
+
return "/api/v1/" + "/".join(_segment(part) for part in parts)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class BaseResource:
|
|
19
|
+
def __init__(self, client: NFLMetaClientCore) -> None:
|
|
20
|
+
self.client = client
|
|
21
|
+
|
|
22
|
+
def _request(self, path: str, query: QueryParams | None = None) -> APIResponse:
|
|
23
|
+
return self.client.get(path, query=query)
|
|
24
|
+
|
|
25
|
+
def _field(self, path: str, query: QueryParams | None = None) -> APIResponse:
|
|
26
|
+
return self.client.get(path, query=query)
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class HealthResource(BaseResource):
|
|
30
|
+
def get(self) -> APIResponse:
|
|
31
|
+
return self._request(_api_path("health"))
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class UsageResource(BaseResource):
|
|
35
|
+
def get(self) -> APIResponse:
|
|
36
|
+
return self._request(_api_path("usage"))
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
class MetadataResource(BaseResource):
|
|
40
|
+
def show(self) -> APIResponse:
|
|
41
|
+
return self._request(_api_path("show"))
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
class SeasonsResource(BaseResource):
|
|
45
|
+
def list(self, **query: Any) -> APIResponse:
|
|
46
|
+
return self._request(_api_path("seasons"), query=query or None)
|
|
47
|
+
|
|
48
|
+
def summary(self, year: int) -> APIResponse:
|
|
49
|
+
return self._request(_api_path("season", year))
|
|
50
|
+
|
|
51
|
+
def summary_field(self, year: int, field: str) -> APIResponse:
|
|
52
|
+
return self._field(_api_path("season", year, field))
|
|
53
|
+
|
|
54
|
+
def byes(self, year: int) -> APIResponse:
|
|
55
|
+
return self._request(_api_path("byes", year))
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
class TeamsResource(BaseResource):
|
|
59
|
+
def list(self, **query: Any) -> APIResponse:
|
|
60
|
+
return self._request(_api_path("teams"), query=query or None)
|
|
61
|
+
|
|
62
|
+
def get(self, abbr: str, **query: Any) -> APIResponse:
|
|
63
|
+
return self._request(_api_path("teams", abbr), query=query or None)
|
|
64
|
+
|
|
65
|
+
def recent_games(self, abbr: str, **query: Any) -> APIResponse:
|
|
66
|
+
return self._request(_api_path("teams", abbr, "games"), query=query or None)
|
|
67
|
+
|
|
68
|
+
def profile(self, abbr: str, **query: Any) -> APIResponse:
|
|
69
|
+
return self._request(_api_path("teams", abbr, "profile"), query=query or None)
|
|
70
|
+
|
|
71
|
+
def profile_field(self, abbr: str, field: str, **query: Any) -> APIResponse:
|
|
72
|
+
return self._field(_api_path("teams", abbr, "profile", field), query=query or None)
|
|
73
|
+
|
|
74
|
+
def identity(self, abbr: str) -> APIResponse:
|
|
75
|
+
return self._request(_api_path("teams", abbr, "identity"))
|
|
76
|
+
|
|
77
|
+
def identity_field(self, abbr: str, field: str) -> APIResponse:
|
|
78
|
+
return self._field(_api_path("teams", abbr, "identity", field))
|
|
79
|
+
|
|
80
|
+
def season(self, abbr: str, **query: Any) -> APIResponse:
|
|
81
|
+
return self._request(_api_path("teams", abbr, "season"), query=query or None)
|
|
82
|
+
|
|
83
|
+
def season_field(self, abbr: str, field: str, **query: Any) -> APIResponse:
|
|
84
|
+
return self._field(_api_path("teams", abbr, "season", field), query=query or None)
|
|
85
|
+
|
|
86
|
+
def stats(self, abbr: str, **query: Any) -> APIResponse:
|
|
87
|
+
return self._request(_api_path("teams", abbr, "stats"), query=query or None)
|
|
88
|
+
|
|
89
|
+
def stats_field(self, abbr: str, field: str, **query: Any) -> APIResponse:
|
|
90
|
+
return self._field(_api_path("teams", abbr, "stats", field), query=query or None)
|
|
91
|
+
|
|
92
|
+
def power_rankings(self, abbr: str, **query: Any) -> APIResponse:
|
|
93
|
+
return self._request(_api_path("teams", abbr, "power-rankings"), query=query or None)
|
|
94
|
+
|
|
95
|
+
def power_rankings_field(self, abbr: str, field: str, **query: Any) -> APIResponse:
|
|
96
|
+
return self._field(_api_path("teams", abbr, "power-rankings", field), query=query or None)
|
|
97
|
+
|
|
98
|
+
def stadium(self, abbr: str, **query: Any) -> APIResponse:
|
|
99
|
+
return self._request(_api_path("teams", abbr, "stadium"), query=query or None)
|
|
100
|
+
|
|
101
|
+
def stadium_field(self, abbr: str, field: str, **query: Any) -> APIResponse:
|
|
102
|
+
return self._field(_api_path("teams", abbr, "stadium", field), query=query or None)
|
|
103
|
+
|
|
104
|
+
def pro_bowl(self, abbr: str, **query: Any) -> APIResponse:
|
|
105
|
+
return self._request(_api_path("teams", abbr, "pro-bowl"), query=query or None)
|
|
106
|
+
|
|
107
|
+
def pro_bowl_field(self, abbr: str, field: str, **query: Any) -> APIResponse:
|
|
108
|
+
return self._field(_api_path("teams", abbr, "pro-bowl", field), query=query or None)
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
class PlayersResource(BaseResource):
|
|
112
|
+
def list(self, **query: Any) -> APIResponse:
|
|
113
|
+
return self._request(_api_path("players"), query=query or None)
|
|
114
|
+
|
|
115
|
+
def get(self, player_key: str) -> APIResponse:
|
|
116
|
+
return self._request(_api_path("players", player_key))
|
|
117
|
+
|
|
118
|
+
def identity(self, player_key: str) -> APIResponse:
|
|
119
|
+
return self._request(_api_path("players", player_key, "identity"))
|
|
120
|
+
|
|
121
|
+
def identity_field(self, player_key: str, field: str) -> APIResponse:
|
|
122
|
+
return self._field(_api_path("players", player_key, "identity", field))
|
|
123
|
+
|
|
124
|
+
def bio(self, player_key: str) -> APIResponse:
|
|
125
|
+
return self._request(_api_path("players", player_key, "bio"))
|
|
126
|
+
|
|
127
|
+
def bio_field(self, player_key: str, field: str) -> APIResponse:
|
|
128
|
+
return self._field(_api_path("players", player_key, "bio", field))
|
|
129
|
+
|
|
130
|
+
def ids(self, player_key: str) -> APIResponse:
|
|
131
|
+
return self._request(_api_path("players", player_key, "ids"))
|
|
132
|
+
|
|
133
|
+
def ids_field(self, player_key: str, field: str) -> APIResponse:
|
|
134
|
+
return self._field(_api_path("players", player_key, "ids", field))
|
|
135
|
+
|
|
136
|
+
def history(self, player_key: str) -> APIResponse:
|
|
137
|
+
return self._request(_api_path("players", player_key, "history"))
|
|
138
|
+
|
|
139
|
+
def history_field(self, player_key: str, field: str) -> APIResponse:
|
|
140
|
+
return self._field(_api_path("players", player_key, "history", field))
|
|
141
|
+
|
|
142
|
+
def honors(self, player_key: str) -> APIResponse:
|
|
143
|
+
return self._request(_api_path("players", player_key, "honors"))
|
|
144
|
+
|
|
145
|
+
def honors_field(self, player_key: str, field: str) -> APIResponse:
|
|
146
|
+
return self._field(_api_path("players", player_key, "honors", field))
|
|
147
|
+
|
|
148
|
+
def roster(self, player_key: str) -> APIResponse:
|
|
149
|
+
return self._request(_api_path("players", player_key, "roster"))
|
|
150
|
+
|
|
151
|
+
def games(self, player_key: str, **query: Any) -> APIResponse:
|
|
152
|
+
return self._request(_api_path("players", player_key, "games"), query=query or None)
|
|
153
|
+
|
|
154
|
+
def career(self, player_key: str) -> APIResponse:
|
|
155
|
+
return self._request(_api_path("players", player_key, "career"))
|
|
156
|
+
|
|
157
|
+
def career_field(self, player_key: str, field: str) -> APIResponse:
|
|
158
|
+
return self._field(_api_path("players", player_key, "career", field))
|
|
159
|
+
|
|
160
|
+
def career_seasons(self, player_key: str, **query: Any) -> APIResponse:
|
|
161
|
+
return self._request(_api_path("players", player_key, "career", "seasons"), query=query or None)
|
|
162
|
+
|
|
163
|
+
def splits(self, player_key: str, **query: Any) -> APIResponse:
|
|
164
|
+
return self._request(_api_path("players", player_key, "splits"), query=query or None)
|
|
165
|
+
|
|
166
|
+
def splits_field(self, player_key: str, field: str, **query: Any) -> APIResponse:
|
|
167
|
+
return self._field(_api_path("players", player_key, "splits", field), query=query or None)
|
|
168
|
+
|
|
169
|
+
def records(self, player_key: str, **query: Any) -> APIResponse:
|
|
170
|
+
return self._request(_api_path("players", player_key, "records"), query=query or None)
|
|
171
|
+
|
|
172
|
+
def records_field(self, player_key: str, field: str, **query: Any) -> APIResponse:
|
|
173
|
+
return self._field(_api_path("players", player_key, "records", field), query=query or None)
|
|
174
|
+
|
|
175
|
+
def kicking(self, player_key: str) -> APIResponse:
|
|
176
|
+
return self._request(_api_path("players", player_key, "kicking"))
|
|
177
|
+
|
|
178
|
+
def kicking_season(self, player_key: str, season_year: int) -> APIResponse:
|
|
179
|
+
return self._request(_api_path("players", player_key, "kicking", season_year))
|
|
180
|
+
|
|
181
|
+
def kicking_season_field(self, player_key: str, season_year: int, field: str) -> APIResponse:
|
|
182
|
+
return self._field(_api_path("players", player_key, "kicking", season_year, field))
|
|
183
|
+
|
|
184
|
+
def punting(self, player_key: str) -> APIResponse:
|
|
185
|
+
return self._request(_api_path("players", player_key, "punting"))
|
|
186
|
+
|
|
187
|
+
def punting_season(self, player_key: str, season_year: int) -> APIResponse:
|
|
188
|
+
return self._request(_api_path("players", player_key, "punting", season_year))
|
|
189
|
+
|
|
190
|
+
def punting_season_field(self, player_key: str, season_year: int, field: str) -> APIResponse:
|
|
191
|
+
return self._field(_api_path("players", player_key, "punting", season_year, field))
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
class GamesResource(BaseResource):
|
|
195
|
+
def list(self, **query: Any) -> APIResponse:
|
|
196
|
+
return self._request(_api_path("games"), query=query or None)
|
|
197
|
+
|
|
198
|
+
def get(self, game_id: int) -> APIResponse:
|
|
199
|
+
return self._request(_api_path("games", game_id))
|
|
200
|
+
|
|
201
|
+
def field(self, game_id: int, field: str) -> APIResponse:
|
|
202
|
+
return self._field(_api_path("games", game_id, field))
|
|
203
|
+
|
|
204
|
+
def extras(self, game_id: int) -> APIResponse:
|
|
205
|
+
return self._request(_api_path("games", game_id, "extras"))
|
|
206
|
+
|
|
207
|
+
|
|
208
|
+
class PlaysResource(BaseResource):
|
|
209
|
+
"""Play-by-play.
|
|
210
|
+
|
|
211
|
+
``list`` is metered per play and the API refuses an unnarrowed request with
|
|
212
|
+
400: pass ``game_id``, or ``player``, or ``season`` together with one of
|
|
213
|
+
``week``, ``team`` or ``opponent``. ``summary`` and ``leaders`` answer
|
|
214
|
+
season-wide questions server-side in tens of rows, which is what most
|
|
215
|
+
callers actually want.
|
|
216
|
+
"""
|
|
217
|
+
|
|
218
|
+
def list(self, **query: Any) -> APIResponse:
|
|
219
|
+
return self._request(_api_path("plays"), query=query or None)
|
|
220
|
+
|
|
221
|
+
def summary(self, **query: Any) -> APIResponse:
|
|
222
|
+
return self._request(_api_path("plays", "summary"), query=query or None)
|
|
223
|
+
|
|
224
|
+
def leaders(self, **query: Any) -> APIResponse:
|
|
225
|
+
return self._request(_api_path("plays", "leaders"), query=query or None)
|
|
226
|
+
|
|
227
|
+
|
|
228
|
+
class PlayoffGamesResource(BaseResource):
|
|
229
|
+
def get(self, game_id: int) -> APIResponse:
|
|
230
|
+
return self._request(_api_path("playoff-games", game_id))
|
|
231
|
+
|
|
232
|
+
def field(self, game_id: int, field: str) -> APIResponse:
|
|
233
|
+
return self._field(_api_path("playoff-games", game_id, field))
|
|
234
|
+
|
|
235
|
+
def extras(self, game_id: int) -> APIResponse:
|
|
236
|
+
return self._request(_api_path("playoff-games", game_id, "extras"))
|
|
237
|
+
|
|
238
|
+
def supplemental(self, game_id: int) -> APIResponse:
|
|
239
|
+
return self._request(_api_path("playoff-games", game_id, "supplemental"))
|
|
240
|
+
|
|
241
|
+
|
|
242
|
+
class StandingsResource(BaseResource):
|
|
243
|
+
def get(self, **query: Any) -> APIResponse:
|
|
244
|
+
return self._request(_api_path("standings"), query=query or None)
|
|
245
|
+
|
|
246
|
+
|
|
247
|
+
class PlayoffsResource(BaseResource):
|
|
248
|
+
def picture(self, **query: Any) -> APIResponse:
|
|
249
|
+
return self._request(_api_path("playoffs", "picture"), query=query or None)
|
|
250
|
+
|
|
251
|
+
|
|
252
|
+
class SuperBowlsResource(BaseResource):
|
|
253
|
+
def list(self, **query: Any) -> APIResponse:
|
|
254
|
+
return self._request(_api_path("super-bowls"), query=query or None)
|
|
255
|
+
|
|
256
|
+
def by_season(self, season_year: int) -> APIResponse:
|
|
257
|
+
return self._request(_api_path("super-bowls", season_year))
|
|
258
|
+
|
|
259
|
+
def by_team(self, abbr: str) -> APIResponse:
|
|
260
|
+
return self._request(_api_path("super-bowls", "teams", abbr))
|
|
261
|
+
|
|
262
|
+
|
|
263
|
+
class HistoryResource(BaseResource):
|
|
264
|
+
def officials(self, **query: Any) -> APIResponse:
|
|
265
|
+
return self._request(_api_path("history", "officials"), query=query or None)
|
|
266
|
+
|
|
267
|
+
def stadiums(self, **query: Any) -> APIResponse:
|
|
268
|
+
return self._request(_api_path("history", "stadiums"), query=query or None)
|
|
269
|
+
|
|
270
|
+
def team_logos(self, **query: Any) -> APIResponse:
|
|
271
|
+
return self._request(_api_path("history", "team-logos"), query=query or None)
|
|
272
|
+
|
|
273
|
+
def venues(self, **query: Any) -> APIResponse:
|
|
274
|
+
return self._request(_api_path("history", "venues"), query=query or None)
|
|
275
|
+
|
|
276
|
+
|
|
277
|
+
class HallOfFameResource(BaseResource):
|
|
278
|
+
def list(self, **query: Any) -> APIResponse:
|
|
279
|
+
return self._request(_api_path("hall-of-fame"), query=query or None)
|
|
280
|
+
|
|
281
|
+
def election(self, year: int) -> APIResponse:
|
|
282
|
+
return self._request(_api_path("hall-of-fame", year))
|
|
283
|
+
|
|
284
|
+
def election_field(self, year: int, field: str) -> APIResponse:
|
|
285
|
+
return self._field(_api_path("hall-of-fame", year, field))
|
|
286
|
+
|
|
287
|
+
|
|
288
|
+
class _PeopleResource(BaseResource):
|
|
289
|
+
def __init__(self, client: NFLMetaClientCore, *base_segments: str) -> None:
|
|
290
|
+
super().__init__(client)
|
|
291
|
+
self.base_segments = base_segments
|
|
292
|
+
|
|
293
|
+
def _resource_path(self, key: str, *suffix: str | int) -> str:
|
|
294
|
+
return _api_path(*self.base_segments, key, *suffix)
|
|
295
|
+
|
|
296
|
+
def get(self, key: str) -> APIResponse:
|
|
297
|
+
return self._request(self._resource_path(key))
|
|
298
|
+
|
|
299
|
+
def identity(self, key: str) -> APIResponse:
|
|
300
|
+
return self._request(self._resource_path(key, "identity"))
|
|
301
|
+
|
|
302
|
+
def identity_field(self, key: str, field: str) -> APIResponse:
|
|
303
|
+
return self._field(self._resource_path(key, "identity", field))
|
|
304
|
+
|
|
305
|
+
def bio(self, key: str) -> APIResponse:
|
|
306
|
+
return self._request(self._resource_path(key, "bio"))
|
|
307
|
+
|
|
308
|
+
def bio_field(self, key: str, field: str) -> APIResponse:
|
|
309
|
+
return self._field(self._resource_path(key, "bio", field))
|
|
310
|
+
|
|
311
|
+
def history(self, key: str) -> APIResponse:
|
|
312
|
+
return self._request(self._resource_path(key, "history"))
|
|
313
|
+
|
|
314
|
+
def history_field(self, key: str, field: str) -> APIResponse:
|
|
315
|
+
return self._field(self._resource_path(key, "history", field))
|
|
316
|
+
|
|
317
|
+
|
|
318
|
+
class ContributorsResource(_PeopleResource):
|
|
319
|
+
def list(self, **query: Any) -> APIResponse:
|
|
320
|
+
return self._request(_api_path("contributors"), query=query or None)
|
|
321
|
+
|
|
322
|
+
|
|
323
|
+
class ExecutivesResource(_PeopleResource):
|
|
324
|
+
def list(self, **query: Any) -> APIResponse:
|
|
325
|
+
return self._request(_api_path("executives"), query=query or None)
|
|
326
|
+
|
|
327
|
+
def career(self, key: str) -> APIResponse:
|
|
328
|
+
return self._request(self._resource_path(key, "career"))
|
|
329
|
+
|
|
330
|
+
def career_field(self, key: str, field: str) -> APIResponse:
|
|
331
|
+
return self._field(self._resource_path(key, "career", field))
|
|
332
|
+
|
|
333
|
+
|
|
334
|
+
class StaffResource(_PeopleResource):
|
|
335
|
+
def list(self, **query: Any) -> APIResponse:
|
|
336
|
+
return self._request(_api_path("staff"), query=query or None)
|
|
337
|
+
|
|
338
|
+
def career(self, key: str) -> APIResponse:
|
|
339
|
+
return self._request(self._resource_path(key, "career"))
|
|
340
|
+
|
|
341
|
+
def career_field(self, key: str, field: str) -> APIResponse:
|
|
342
|
+
return self._field(self._resource_path(key, "career", field))
|
|
343
|
+
|
|
344
|
+
|
|
345
|
+
class CoachesResource(_PeopleResource):
|
|
346
|
+
def list(self, **query: Any) -> APIResponse:
|
|
347
|
+
return self._request(_api_path("coaches"), query=query or None)
|
|
348
|
+
|
|
349
|
+
def career(self, key: str) -> APIResponse:
|
|
350
|
+
return self._request(self._resource_path(key, "career"))
|
|
351
|
+
|
|
352
|
+
def career_field(self, key: str, field: str) -> APIResponse:
|
|
353
|
+
return self._field(self._resource_path(key, "career", field))
|
|
354
|
+
|
|
355
|
+
|
|
356
|
+
class AssetsResource(BaseResource):
|
|
357
|
+
def logos(self, **query: Any) -> APIResponse:
|
|
358
|
+
return self._request(_api_path("assets", "logos"), query=query or None)
|
|
359
|
+
|
|
360
|
+
def images(self, **query: Any) -> APIResponse:
|
|
361
|
+
return self._request(_api_path("assets", "images"), query=query or None)
|
|
362
|
+
|
|
363
|
+
|
|
364
|
+
class AllStarGamesResource(BaseResource):
|
|
365
|
+
def list(self, **query: Any) -> APIResponse:
|
|
366
|
+
return self._request(_api_path("all-star-games"), query=query or None)
|
|
367
|
+
|
|
368
|
+
|
|
369
|
+
class StatsPlayersResource(BaseResource):
|
|
370
|
+
def catalog(self, **query: Any) -> APIResponse:
|
|
371
|
+
return self._request(_api_path("stats", "players", "catalog"), query=query or None)
|
|
372
|
+
|
|
373
|
+
def search(self, **query: Any) -> APIResponse:
|
|
374
|
+
return self._request(_api_path("stats", "players", "search"), query=query or None)
|
|
375
|
+
|
|
376
|
+
def compare(self, **query: Any) -> APIResponse:
|
|
377
|
+
return self._request(_api_path("stats", "players", "compare"), query=query or None)
|
|
378
|
+
|
|
379
|
+
def seasons(self, **query: Any) -> APIResponse:
|
|
380
|
+
return self._request(_api_path("stats", "players", "seasons"), query=query or None)
|
|
381
|
+
|
|
382
|
+
def seasons_by_stat(self, season_year: int, stat: str, **query: Any) -> APIResponse:
|
|
383
|
+
return self._request(_api_path("stats", "players", "seasons", season_year, stat), query=query or None)
|
|
384
|
+
|
|
385
|
+
def careers(self, **query: Any) -> APIResponse:
|
|
386
|
+
return self._request(_api_path("stats", "players", "careers"), query=query or None)
|
|
387
|
+
|
|
388
|
+
def careers_by_stat(self, stat: str, **query: Any) -> APIResponse:
|
|
389
|
+
return self._request(_api_path("stats", "players", "careers", stat), query=query or None)
|
|
390
|
+
|
|
391
|
+
def games(self, **query: Any) -> APIResponse:
|
|
392
|
+
return self._request(_api_path("stats", "players", "games"), query=query or None)
|
|
393
|
+
|
|
394
|
+
def game_splits(self, **query: Any) -> APIResponse:
|
|
395
|
+
return self._request(_api_path("stats", "players", "games", "splits"), query=query or None)
|
|
396
|
+
|
|
397
|
+
def game_leaders_by_family(self, stat_family: str, **query: Any) -> APIResponse:
|
|
398
|
+
return self._request(_api_path("stats", "players", "games", "leaders", stat_family), query=query or None)
|
|
399
|
+
|
|
400
|
+
def teams(self, **query: Any) -> APIResponse:
|
|
401
|
+
return self._request(_api_path("stats", "players", "teams"), query=query or None)
|
|
402
|
+
|
|
403
|
+
def weeks(self, season_year: int, **query: Any) -> APIResponse:
|
|
404
|
+
return self._request(_api_path("stats", "players", "season", season_year, "weeks"), query=query or None)
|
|
405
|
+
|
|
406
|
+
|
|
407
|
+
class StatsResource:
|
|
408
|
+
def __init__(self, client: NFLMetaClientCore) -> None:
|
|
409
|
+
self.players = StatsPlayersResource(client)
|
|
410
|
+
|
|
411
|
+
|
|
412
|
+
class ReferenceResource(BaseResource):
|
|
413
|
+
def all_star_games(self, **query: Any) -> APIResponse:
|
|
414
|
+
return self._request(_api_path("reference", "all-star-games"), query=query or None)
|
|
415
|
+
|
|
416
|
+
def coaches(self, **query: Any) -> APIResponse:
|
|
417
|
+
return self._request(_api_path("reference", "coaches"), query=query or None)
|
|
418
|
+
|
|
419
|
+
def coach(self, key: str) -> APIResponse:
|
|
420
|
+
return self._request(_api_path("reference", "coaches", key))
|
|
421
|
+
|
|
422
|
+
def coach_field(self, key: str, field: str) -> APIResponse:
|
|
423
|
+
return self._field(_api_path("reference", "coaches", key, field))
|
|
424
|
+
|
|
425
|
+
def officials(self, **query: Any) -> APIResponse:
|
|
426
|
+
return self._request(_api_path("reference", "officials"), query=query or None)
|
|
427
|
+
|
|
428
|
+
def official(self, key: str) -> APIResponse:
|
|
429
|
+
return self._request(_api_path("reference", "officials", key))
|
|
430
|
+
|
|
431
|
+
def official_field(self, key: str, field: str) -> APIResponse:
|
|
432
|
+
return self._field(_api_path("reference", "officials", key, field))
|
|
433
|
+
|
|
434
|
+
def power_rankings(self, **query: Any) -> APIResponse:
|
|
435
|
+
return self._request(_api_path("reference", "power-rankings"), query=query or None)
|
|
436
|
+
|
|
437
|
+
def pro_bowl_player_bios(self, **query: Any) -> APIResponse:
|
|
438
|
+
return self._request(_api_path("reference", "pro-bowl-player-bios"), query=query or None)
|
|
439
|
+
|
|
440
|
+
def pro_bowl_player_stats(self, **query: Any) -> APIResponse:
|
|
441
|
+
return self._request(_api_path("reference", "pro-bowl-player-stats"), query=query or None)
|
|
442
|
+
|
|
443
|
+
def pro_bowl_selections(self, **query: Any) -> APIResponse:
|
|
444
|
+
return self._request(_api_path("reference", "pro-bowl-selections"), query=query or None)
|
|
445
|
+
|
|
446
|
+
def seasons(self, **query: Any) -> APIResponse:
|
|
447
|
+
return self._request(_api_path("reference", "seasons"), query=query or None)
|
|
448
|
+
|
|
449
|
+
def stadium_histories(self, **query: Any) -> APIResponse:
|
|
450
|
+
return self._request(_api_path("reference", "stadium-histories"), query=query or None)
|
|
451
|
+
|
|
452
|
+
def stadiums(self, **query: Any) -> APIResponse:
|
|
453
|
+
return self._request(_api_path("reference", "stadiums"), query=query or None)
|
|
454
|
+
|
|
455
|
+
def team_aliases(self, **query: Any) -> APIResponse:
|
|
456
|
+
return self._request(_api_path("reference", "team-aliases"), query=query or None)
|
|
457
|
+
|
|
458
|
+
def team_coaches(self, **query: Any) -> APIResponse:
|
|
459
|
+
return self._request(_api_path("reference", "team-coaches"), query=query or None)
|
|
460
|
+
|
|
461
|
+
def team_colors(self, **query: Any) -> APIResponse:
|
|
462
|
+
return self._request(_api_path("reference", "team-colors"), query=query or None)
|
|
463
|
+
|
|
464
|
+
def team_logo_histories(self, **query: Any) -> APIResponse:
|
|
465
|
+
return self._request(_api_path("reference", "team-logo-histories"), query=query or None)
|
|
466
|
+
|
|
467
|
+
def team_name_histories(self, **query: Any) -> APIResponse:
|
|
468
|
+
return self._request(_api_path("reference", "team-name-histories"), query=query or None)
|
|
469
|
+
|
|
470
|
+
def team_season_records(self, **query: Any) -> APIResponse:
|
|
471
|
+
return self._request(_api_path("reference", "team-season-records"), query=query or None)
|
|
472
|
+
|
|
473
|
+
def team_season_stats(self, **query: Any) -> APIResponse:
|
|
474
|
+
return self._request(_api_path("reference", "team-season-stats"), query=query or None)
|
|
475
|
+
|
|
476
|
+
def teams(self, **query: Any) -> APIResponse:
|
|
477
|
+
return self._request(_api_path("reference", "teams"), query=query or None)
|
|
478
|
+
|
|
479
|
+
def venues(self, **query: Any) -> APIResponse:
|
|
480
|
+
return self._request(_api_path("reference", "venues"), query=query or None)
|
|
481
|
+
|
|
482
|
+
def venue(self, key: str) -> APIResponse:
|
|
483
|
+
return self._request(_api_path("reference", "venues", key))
|
|
484
|
+
|
|
485
|
+
def venue_field(self, key: str, field: str) -> APIResponse:
|
|
486
|
+
return self._field(_api_path("reference", "venues", key, field))
|
nflmeta/types.py
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass, field
|
|
4
|
+
from datetime import date, datetime
|
|
5
|
+
from typing import Any, Mapping, MutableMapping, Optional, Sequence, Union
|
|
6
|
+
|
|
7
|
+
QueryScalar = Union[str, int, float, bool, date, datetime]
|
|
8
|
+
QueryValue = Union[QueryScalar, Sequence[QueryScalar], None]
|
|
9
|
+
QueryParams = Mapping[str, QueryValue]
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
@dataclass(slots=True)
|
|
13
|
+
class RateLimitInfo:
|
|
14
|
+
limit: Optional[int] = None
|
|
15
|
+
remaining: Optional[int] = None
|
|
16
|
+
reset: Optional[str] = None
|
|
17
|
+
policy: Optional[str] = None
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
@dataclass(slots=True)
|
|
21
|
+
class APIResponse:
|
|
22
|
+
data: Any
|
|
23
|
+
meta: Any = None
|
|
24
|
+
status: int = 200
|
|
25
|
+
rate_limit: RateLimitInfo = field(default_factory=RateLimitInfo)
|
|
26
|
+
headers: Mapping[str, str] | MutableMapping[str, str] | None = None
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
@dataclass(slots=True)
|
|
30
|
+
class FieldValue:
|
|
31
|
+
field: str
|
|
32
|
+
value: Any
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: nflmeta
|
|
3
|
+
Version: 0.1.3
|
|
4
|
+
Summary: Official Python SDK for the NFLMeta API
|
|
5
|
+
Author: NFLMeta
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Homepage, https://nflmeta.org/api-docs/sdk
|
|
8
|
+
Project-URL: Documentation, https://nflmeta.org/api-docs
|
|
9
|
+
Project-URL: Source, https://github.com/philippebourdon/NFLMeta
|
|
10
|
+
Keywords: nfl,nflmeta,sports,api,sdk,python
|
|
11
|
+
Classifier: Development Status :: 3 - Alpha
|
|
12
|
+
Classifier: Intended Audience :: Developers
|
|
13
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
14
|
+
Classifier: Programming Language :: Python :: 3
|
|
15
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
19
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
20
|
+
Requires-Python: >=3.10
|
|
21
|
+
Description-Content-Type: text/markdown
|
|
22
|
+
License-File: LICENSE
|
|
23
|
+
Dynamic: license-file
|
|
24
|
+
|
|
25
|
+
# `nflmeta`
|
|
26
|
+
|
|
27
|
+
Official Python SDK for the NFLMeta API.
|
|
28
|
+
|
|
29
|
+
## Install
|
|
30
|
+
|
|
31
|
+
```bash
|
|
32
|
+
pip install nflmeta
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
## Quickstart
|
|
36
|
+
|
|
37
|
+
```python
|
|
38
|
+
from nflmeta import NFLMetaClient
|
|
39
|
+
|
|
40
|
+
client = NFLMetaClient(api_key="YOUR_KEY")
|
|
41
|
+
|
|
42
|
+
team = client.teams.get("PIT", season=2025)
|
|
43
|
+
player = client.players.identity("josh-allen-1996")
|
|
44
|
+
usage = client.usage.get()
|
|
45
|
+
|
|
46
|
+
print(team.data["displayName"])
|
|
47
|
+
print(player.data["display_name"])
|
|
48
|
+
print(usage.data["quota_remaining"])
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
## What The SDK Does
|
|
52
|
+
|
|
53
|
+
- sends `X-NFLMeta-Key` automatically when you provide `api_key`
|
|
54
|
+
- preserves the API envelope shape: `data`, optional `meta`, plus parsed rate-limit headers
|
|
55
|
+
- throws typed HTTP errors for common failure cases
|
|
56
|
+
- exposes the main public API surfaces as resource clients instead of forcing raw URL construction
|
|
57
|
+
- still gives you a low-level `client.get(path, query={...})` escape hatch for any route not wrapped yet
|
|
58
|
+
|
|
59
|
+
## Configuration
|
|
60
|
+
|
|
61
|
+
```python
|
|
62
|
+
client = NFLMetaClient(
|
|
63
|
+
api_key="YOUR_KEY",
|
|
64
|
+
base_url="https://nflmeta.org",
|
|
65
|
+
timeout=10.0,
|
|
66
|
+
)
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
You can also pass default headers or a custom `transport(url, headers, timeout)` callable if you want the SDK to run through your own HTTP layer in tests or internal tooling.
|
|
70
|
+
|
|
71
|
+
## Resource Coverage
|
|
72
|
+
|
|
73
|
+
The Python SDK wraps the same main surfaces as the TypeScript package:
|
|
74
|
+
|
|
75
|
+
- `health`
|
|
76
|
+
- `usage`
|
|
77
|
+
- `metadata`
|
|
78
|
+
- `seasons`
|
|
79
|
+
- `teams`
|
|
80
|
+
- `players`
|
|
81
|
+
- `games`
|
|
82
|
+
- `plays`
|
|
83
|
+
- `playoff_games`
|
|
84
|
+
- `standings`
|
|
85
|
+
- `playoffs`
|
|
86
|
+
- `super_bowls`
|
|
87
|
+
- `history`
|
|
88
|
+
- `hall_of_fame`
|
|
89
|
+
- `contributors`
|
|
90
|
+
- `executives`
|
|
91
|
+
- `staff`
|
|
92
|
+
- `coaches`
|
|
93
|
+
- `assets`
|
|
94
|
+
- `all_star_games`
|
|
95
|
+
- `stats.players`
|
|
96
|
+
- `reference`
|
|
97
|
+
|
|
98
|
+
```python
|
|
99
|
+
client.plays.summary(season=2024, group_by="team")
|
|
100
|
+
client.plays.leaders(season=2024, role="passer")
|
|
101
|
+
client.plays.list(game_id=21390, skip_markers=True)
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
`plays.list` is metered per play and the API refuses an unnarrowed request with
|
|
105
|
+
`400 invalid_request`: pass `game_id`, or `player`, or `season` together with one
|
|
106
|
+
of `week`, `team` or `opponent`. `plays.summary` and `plays.leaders` answer
|
|
107
|
+
season-wide questions server-side in tens of rows.
|
|
108
|
+
|
|
109
|
+
## Errors
|
|
110
|
+
|
|
111
|
+
- `NFLMetaBadRequestError`
|
|
112
|
+
- `NFLMetaUnauthorizedError`
|
|
113
|
+
- `NFLMetaNotFoundError`
|
|
114
|
+
- `NFLMetaRateLimitError`
|
|
115
|
+
- `NFLMetaTimeoutError`
|
|
116
|
+
- `NFLMetaError`
|
|
117
|
+
|
|
118
|
+
## Escape Hatch
|
|
119
|
+
|
|
120
|
+
```python
|
|
121
|
+
raw = client.get("/api/v1/reference/team-colors", query={"limit": 10})
|
|
122
|
+
print(raw.data)
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
The path is resolved against `base_url` and has to stay there. A URL on another
|
|
126
|
+
origin is refused with `NFLMetaInvalidUrlError` **before** any header is built,
|
|
127
|
+
and a redirect that would leave the origin is refused rather than followed —
|
|
128
|
+
your API key travels in the `X-NFLMeta-Key` header, and `urllib` forwards every
|
|
129
|
+
request header across a cross-origin redirect. Same-origin absolute URLs still
|
|
130
|
+
work.
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
nflmeta/__init__.py,sha256=32i_AyKiksBAXjoKXX2qr4rt9szNQydlM_OKB-BVYEk,2501
|
|
2
|
+
nflmeta/client.py,sha256=3NIkPrtnFAnfqNXGmfRwbQI3M_vW4WxEa-TelnrQONg,9771
|
|
3
|
+
nflmeta/errors.py,sha256=wsARGmwA27V9MCG9KR3pRct5E9M3uyD9IhDyhCeZ6yg,3065
|
|
4
|
+
nflmeta/py.typed,sha256=AbpHGcgLb-kRsJGnwFEktk7uzpZOCcBY74-YBdrKVGs,1
|
|
5
|
+
nflmeta/resources.py,sha256=1FzMW3muWx-Yjxtvv7ge3bpz_Nk8DsQt09noOvsDc5I,20810
|
|
6
|
+
nflmeta/types.py,sha256=-7or03gfPjUeXbrJ0fPwoQiJPmkj65OeiFb83feabDA,842
|
|
7
|
+
nflmeta-0.1.3.dist-info/licenses/LICENSE,sha256=CvieYTWEkYHB4hdt-3nbkw4-UsHNEDMFiCnHguw5f-A,1091
|
|
8
|
+
nflmeta-0.1.3.dist-info/METADATA,sha256=7oVWihmc1sG9X-WuEERrh2BPYoXWCVfI2suV9PLwX2U,3643
|
|
9
|
+
nflmeta-0.1.3.dist-info/WHEEL,sha256=YVMoNqKzERt-wjUZwJ33xBGAwnFl-4cqbYkTtWa4itE,91
|
|
10
|
+
nflmeta-0.1.3.dist-info/top_level.txt,sha256=LG6vsDGLjCPcaIhUopRKcxOgeWoXXTf841tQLIgOyxw,8
|
|
11
|
+
nflmeta-0.1.3.dist-info/RECORD,,
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Philippe Bourdon / ClearOak Studio
|
|
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.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
nflmeta
|