nflmeta 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.
nflmeta/__init__.py ADDED
@@ -0,0 +1,82 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Any
4
+
5
+ from .client import NFLMetaClientCore
6
+ from .errors import (
7
+ NFLMetaBadRequestError,
8
+ NFLMetaError,
9
+ NFLMetaNotFoundError,
10
+ NFLMetaRateLimitError,
11
+ NFLMetaTimeoutError,
12
+ NFLMetaUnauthorizedError,
13
+ )
14
+ from .resources import (
15
+ AllStarGamesResource,
16
+ AssetsResource,
17
+ CoachesResource,
18
+ ContributorsResource,
19
+ ExecutivesResource,
20
+ GamesResource,
21
+ HallOfFameResource,
22
+ HealthResource,
23
+ HistoryResource,
24
+ MetadataResource,
25
+ PlayoffGamesResource,
26
+ PlayoffsResource,
27
+ PlayersResource,
28
+ ReferenceResource,
29
+ SeasonsResource,
30
+ StaffResource,
31
+ StandingsResource,
32
+ StatsResource,
33
+ SuperBowlsResource,
34
+ TeamsResource,
35
+ UsageResource,
36
+ )
37
+ from .types import APIResponse, FieldValue, RateLimitInfo
38
+
39
+
40
+ class NFLMetaClient(NFLMetaClientCore):
41
+ def __init__(self, **kwargs: Any) -> None:
42
+ super().__init__(**kwargs)
43
+ self.health = HealthResource(self)
44
+ self.usage = UsageResource(self)
45
+ self.metadata = MetadataResource(self)
46
+ self.seasons = SeasonsResource(self)
47
+ self.teams = TeamsResource(self)
48
+ self.players = PlayersResource(self)
49
+ self.games = GamesResource(self)
50
+ self.playoff_games = PlayoffGamesResource(self)
51
+ self.standings = StandingsResource(self)
52
+ self.playoffs = PlayoffsResource(self)
53
+ self.super_bowls = SuperBowlsResource(self)
54
+ self.history = HistoryResource(self)
55
+ self.hall_of_fame = HallOfFameResource(self)
56
+ self.contributors = ContributorsResource(self, "contributors")
57
+ self.executives = ExecutivesResource(self, "executives")
58
+ self.staff = StaffResource(self, "staff")
59
+ self.coaches = CoachesResource(self, "coaches")
60
+ self.assets = AssetsResource(self)
61
+ self.all_star_games = AllStarGamesResource(self)
62
+ self.stats = StatsResource(self)
63
+ self.reference = ReferenceResource(self)
64
+
65
+
66
+ def create_client(**kwargs: Any) -> NFLMetaClient:
67
+ return NFLMetaClient(**kwargs)
68
+
69
+
70
+ __all__ = [
71
+ "APIResponse",
72
+ "FieldValue",
73
+ "NFLMetaBadRequestError",
74
+ "NFLMetaClient",
75
+ "NFLMetaError",
76
+ "NFLMetaNotFoundError",
77
+ "NFLMetaRateLimitError",
78
+ "NFLMetaTimeoutError",
79
+ "NFLMetaUnauthorizedError",
80
+ "RateLimitInfo",
81
+ "create_client",
82
+ ]
nflmeta/client.py ADDED
@@ -0,0 +1,164 @@
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
10
+ from urllib.request import Request, urlopen
11
+
12
+ from .errors import NFLMetaTimeoutError, create_http_error
13
+ from .types import APIResponse, QueryParams, RateLimitInfo
14
+
15
+
16
+ def _normalize_base_url(base_url: str | None) -> str:
17
+ base = (base_url or "https://nflmeta.org").strip()
18
+ return base.rstrip("/")
19
+
20
+
21
+ def _is_absolute_url(value: str) -> bool:
22
+ return value.startswith("http://") or value.startswith("https://")
23
+
24
+
25
+ def _serialize_query_value(value: Any) -> str:
26
+ if isinstance(value, bool):
27
+ return "true" if value else "false"
28
+ if isinstance(value, datetime):
29
+ return value.isoformat()
30
+ if isinstance(value, date):
31
+ return value.isoformat()
32
+ return str(value)
33
+
34
+
35
+ def _build_query_string(query: QueryParams | None) -> str:
36
+ if not query:
37
+ return ""
38
+ pairs: list[tuple[str, str]] = []
39
+ for key, raw_value in query.items():
40
+ if raw_value is None:
41
+ continue
42
+ values = raw_value if isinstance(raw_value, (list, tuple)) else [raw_value]
43
+ for value in values:
44
+ if value is None:
45
+ continue
46
+ pairs.append((key, _serialize_query_value(value)))
47
+ return urlencode(pairs)
48
+
49
+
50
+ def _parse_int_header(headers: Mapping[str, str], name: str) -> Optional[int]:
51
+ value = headers.get(name)
52
+ if not value:
53
+ return None
54
+ try:
55
+ return int(value)
56
+ except (TypeError, ValueError):
57
+ return None
58
+
59
+
60
+ def _parse_rate_limit(headers: Mapping[str, str]) -> RateLimitInfo:
61
+ return RateLimitInfo(
62
+ limit=_parse_int_header(headers, "X-RateLimit-Limit"),
63
+ remaining=_parse_int_header(headers, "X-RateLimit-Remaining"),
64
+ reset=headers.get("X-RateLimit-Reset"),
65
+ policy=headers.get("X-RateLimit-Policy"),
66
+ )
67
+
68
+
69
+ def _decode_payload(raw: bytes, content_type: str | None) -> Any:
70
+ text = raw.decode("utf-8") if raw else ""
71
+ if not text:
72
+ return None
73
+ if content_type and "application/json" in content_type.lower():
74
+ try:
75
+ return json.loads(text)
76
+ except json.JSONDecodeError:
77
+ return {"message": text}
78
+ return {"message": text}
79
+
80
+
81
+ @dataclass(slots=True)
82
+ class TransportResponse:
83
+ status: int
84
+ headers: MutableMapping[str, str]
85
+ payload: Any
86
+
87
+
88
+ def _default_transport(url: str, headers: Mapping[str, str], timeout: float | None) -> TransportResponse:
89
+ request = Request(url, headers=dict(headers), method="GET")
90
+ try:
91
+ with urlopen(request, timeout=timeout) as response:
92
+ raw = response.read()
93
+ response_headers = {key: value for key, value in response.headers.items()}
94
+ payload = _decode_payload(raw, response.headers.get("Content-Type"))
95
+ return TransportResponse(status=response.status, headers=response_headers, payload=payload)
96
+ except HTTPError as error:
97
+ raw = error.read()
98
+ response_headers = {key: value for key, value in error.headers.items()}
99
+ payload = _decode_payload(raw, error.headers.get("Content-Type"))
100
+ return TransportResponse(status=error.code, headers=response_headers, payload=payload)
101
+ except TimeoutError as error:
102
+ raise NFLMetaTimeoutError(timeout or 0.0) from error
103
+ except socket.timeout as error:
104
+ raise NFLMetaTimeoutError(timeout or 0.0) from error
105
+ except URLError as error:
106
+ if isinstance(error.reason, socket.timeout):
107
+ raise NFLMetaTimeoutError(timeout or 0.0) from error
108
+ raise
109
+
110
+
111
+ Transport = Callable[[str, Mapping[str, str], float | None], TransportResponse]
112
+
113
+
114
+ class NFLMetaClientCore:
115
+ def __init__(
116
+ self,
117
+ api_key: str | None = None,
118
+ base_url: str | None = None,
119
+ timeout: float | None = None,
120
+ headers: Mapping[str, str] | None = None,
121
+ transport: Transport | None = None,
122
+ ) -> None:
123
+ self.api_key = api_key.strip() if api_key else None
124
+ self.base_url = _normalize_base_url(base_url)
125
+ self.timeout = timeout
126
+ self.default_headers = dict(headers or {})
127
+ self._transport = transport or _default_transport
128
+
129
+ def get(
130
+ self,
131
+ path: str,
132
+ query: QueryParams | None = None,
133
+ headers: Mapping[str, str] | None = None,
134
+ ) -> APIResponse:
135
+ url = path if _is_absolute_url(path) else urljoin(f"{self.base_url}/", path.lstrip("/"))
136
+ query_string = _build_query_string(query)
137
+ if query_string:
138
+ separator = "&" if "?" in url else "?"
139
+ url = f"{url}{separator}{query_string}"
140
+
141
+ request_headers: dict[str, str] = {"Accept": "application/json"}
142
+ request_headers.update(self.default_headers)
143
+ if headers:
144
+ request_headers.update(headers)
145
+ if self.api_key and "X-NFLMeta-Key" not in request_headers:
146
+ request_headers["X-NFLMeta-Key"] = self.api_key
147
+
148
+ transport_response = self._transport(url, request_headers, self.timeout)
149
+ rate_limit = _parse_rate_limit(transport_response.headers)
150
+
151
+ if transport_response.status < 200 or transport_response.status >= 300:
152
+ raise create_http_error(transport_response.status, transport_response.payload, rate_limit)
153
+
154
+ payload = transport_response.payload
155
+ if not isinstance(payload, dict) or "data" not in payload:
156
+ raise create_http_error(transport_response.status, payload, rate_limit)
157
+
158
+ return APIResponse(
159
+ data=payload.get("data"),
160
+ meta=payload.get("meta"),
161
+ status=transport_response.status,
162
+ rate_limit=rate_limit,
163
+ headers=transport_response.headers,
164
+ )
nflmeta/errors.py ADDED
@@ -0,0 +1,83 @@
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 NFLMetaBadRequestError(NFLMetaError):
27
+ pass
28
+
29
+
30
+ class NFLMetaUnauthorizedError(NFLMetaError):
31
+ pass
32
+
33
+
34
+ class NFLMetaNotFoundError(NFLMetaError):
35
+ pass
36
+
37
+
38
+ class NFLMetaRateLimitError(NFLMetaError):
39
+ pass
40
+
41
+
42
+ class NFLMetaTimeoutError(TimeoutError):
43
+ def __init__(self, timeout_seconds: float) -> None:
44
+ super().__init__(f"NFLMeta request timed out after {timeout_seconds:.3f}s")
45
+ self.timeout_seconds = timeout_seconds
46
+
47
+
48
+ def _error_message(payload: Any, status: int) -> str:
49
+ if isinstance(payload, dict):
50
+ error = payload.get("error")
51
+ if isinstance(error, dict):
52
+ message = error.get("message")
53
+ if isinstance(message, str) and message.strip():
54
+ return message
55
+ return f"NFLMeta request failed with status {status}"
56
+
57
+
58
+ def _error_code(payload: Any) -> Optional[str]:
59
+ if isinstance(payload, dict):
60
+ error = payload.get("error")
61
+ if isinstance(error, dict):
62
+ code = error.get("code")
63
+ if isinstance(code, str) and code.strip():
64
+ return code
65
+ return None
66
+
67
+
68
+ def create_http_error(status: int, payload: Any, rate_limit: RateLimitInfo | None = None) -> NFLMetaError:
69
+ details = NFLMetaErrorDetails(
70
+ code=_error_code(payload),
71
+ payload=payload,
72
+ rate_limit=rate_limit,
73
+ )
74
+ message = _error_message(payload, status)
75
+ if status == 400:
76
+ return NFLMetaBadRequestError(message, status, details)
77
+ if status == 401:
78
+ return NFLMetaUnauthorizedError(message, status, details)
79
+ if status == 404:
80
+ return NFLMetaNotFoundError(message, status, details)
81
+ if status == 429:
82
+ return NFLMetaRateLimitError(message, status, details)
83
+ return NFLMetaError(message, status, details)
nflmeta/py.typed ADDED
@@ -0,0 +1 @@
1
+
nflmeta/resources.py ADDED
@@ -0,0 +1,466 @@
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 PlayoffGamesResource(BaseResource):
209
+ def get(self, game_id: int) -> APIResponse:
210
+ return self._request(_api_path("playoff-games", game_id))
211
+
212
+ def field(self, game_id: int, field: str) -> APIResponse:
213
+ return self._field(_api_path("playoff-games", game_id, field))
214
+
215
+ def extras(self, game_id: int) -> APIResponse:
216
+ return self._request(_api_path("playoff-games", game_id, "extras"))
217
+
218
+ def supplemental(self, game_id: int) -> APIResponse:
219
+ return self._request(_api_path("playoff-games", game_id, "supplemental"))
220
+
221
+
222
+ class StandingsResource(BaseResource):
223
+ def get(self, **query: Any) -> APIResponse:
224
+ return self._request(_api_path("standings"), query=query or None)
225
+
226
+
227
+ class PlayoffsResource(BaseResource):
228
+ def picture(self, **query: Any) -> APIResponse:
229
+ return self._request(_api_path("playoffs", "picture"), query=query or None)
230
+
231
+
232
+ class SuperBowlsResource(BaseResource):
233
+ def list(self, **query: Any) -> APIResponse:
234
+ return self._request(_api_path("super-bowls"), query=query or None)
235
+
236
+ def by_season(self, season_year: int) -> APIResponse:
237
+ return self._request(_api_path("super-bowls", season_year))
238
+
239
+ def by_team(self, abbr: str) -> APIResponse:
240
+ return self._request(_api_path("super-bowls", "teams", abbr))
241
+
242
+
243
+ class HistoryResource(BaseResource):
244
+ def officials(self, **query: Any) -> APIResponse:
245
+ return self._request(_api_path("history", "officials"), query=query or None)
246
+
247
+ def stadiums(self, **query: Any) -> APIResponse:
248
+ return self._request(_api_path("history", "stadiums"), query=query or None)
249
+
250
+ def team_logos(self, **query: Any) -> APIResponse:
251
+ return self._request(_api_path("history", "team-logos"), query=query or None)
252
+
253
+ def venues(self, **query: Any) -> APIResponse:
254
+ return self._request(_api_path("history", "venues"), query=query or None)
255
+
256
+
257
+ class HallOfFameResource(BaseResource):
258
+ def list(self, **query: Any) -> APIResponse:
259
+ return self._request(_api_path("hall-of-fame"), query=query or None)
260
+
261
+ def election(self, year: int) -> APIResponse:
262
+ return self._request(_api_path("hall-of-fame", year))
263
+
264
+ def election_field(self, year: int, field: str) -> APIResponse:
265
+ return self._field(_api_path("hall-of-fame", year, field))
266
+
267
+
268
+ class _PeopleResource(BaseResource):
269
+ def __init__(self, client: NFLMetaClientCore, *base_segments: str) -> None:
270
+ super().__init__(client)
271
+ self.base_segments = base_segments
272
+
273
+ def _resource_path(self, key: str, *suffix: str | int) -> str:
274
+ return _api_path(*self.base_segments, key, *suffix)
275
+
276
+ def get(self, key: str) -> APIResponse:
277
+ return self._request(self._resource_path(key))
278
+
279
+ def identity(self, key: str) -> APIResponse:
280
+ return self._request(self._resource_path(key, "identity"))
281
+
282
+ def identity_field(self, key: str, field: str) -> APIResponse:
283
+ return self._field(self._resource_path(key, "identity", field))
284
+
285
+ def bio(self, key: str) -> APIResponse:
286
+ return self._request(self._resource_path(key, "bio"))
287
+
288
+ def bio_field(self, key: str, field: str) -> APIResponse:
289
+ return self._field(self._resource_path(key, "bio", field))
290
+
291
+ def history(self, key: str) -> APIResponse:
292
+ return self._request(self._resource_path(key, "history"))
293
+
294
+ def history_field(self, key: str, field: str) -> APIResponse:
295
+ return self._field(self._resource_path(key, "history", field))
296
+
297
+
298
+ class ContributorsResource(_PeopleResource):
299
+ def list(self, **query: Any) -> APIResponse:
300
+ return self._request(_api_path("contributors"), query=query or None)
301
+
302
+
303
+ class ExecutivesResource(_PeopleResource):
304
+ def list(self, **query: Any) -> APIResponse:
305
+ return self._request(_api_path("executives"), query=query or None)
306
+
307
+ def career(self, key: str) -> APIResponse:
308
+ return self._request(self._resource_path(key, "career"))
309
+
310
+ def career_field(self, key: str, field: str) -> APIResponse:
311
+ return self._field(self._resource_path(key, "career", field))
312
+
313
+
314
+ class StaffResource(_PeopleResource):
315
+ def list(self, **query: Any) -> APIResponse:
316
+ return self._request(_api_path("staff"), query=query or None)
317
+
318
+ def career(self, key: str) -> APIResponse:
319
+ return self._request(self._resource_path(key, "career"))
320
+
321
+ def career_field(self, key: str, field: str) -> APIResponse:
322
+ return self._field(self._resource_path(key, "career", field))
323
+
324
+
325
+ class CoachesResource(_PeopleResource):
326
+ def list(self, **query: Any) -> APIResponse:
327
+ return self._request(_api_path("coaches"), query=query or None)
328
+
329
+ def career(self, key: str) -> APIResponse:
330
+ return self._request(self._resource_path(key, "career"))
331
+
332
+ def career_field(self, key: str, field: str) -> APIResponse:
333
+ return self._field(self._resource_path(key, "career", field))
334
+
335
+
336
+ class AssetsResource(BaseResource):
337
+ def logos(self, **query: Any) -> APIResponse:
338
+ return self._request(_api_path("assets", "logos"), query=query or None)
339
+
340
+ def images(self, **query: Any) -> APIResponse:
341
+ return self._request(_api_path("assets", "images"), query=query or None)
342
+
343
+
344
+ class AllStarGamesResource(BaseResource):
345
+ def list(self, **query: Any) -> APIResponse:
346
+ return self._request(_api_path("all-star-games"), query=query or None)
347
+
348
+
349
+ class StatsPlayersResource(BaseResource):
350
+ def catalog(self, **query: Any) -> APIResponse:
351
+ return self._request(_api_path("stats", "players", "catalog"), query=query or None)
352
+
353
+ def search(self, **query: Any) -> APIResponse:
354
+ return self._request(_api_path("stats", "players", "search"), query=query or None)
355
+
356
+ def compare(self, **query: Any) -> APIResponse:
357
+ return self._request(_api_path("stats", "players", "compare"), query=query or None)
358
+
359
+ def seasons(self, **query: Any) -> APIResponse:
360
+ return self._request(_api_path("stats", "players", "seasons"), query=query or None)
361
+
362
+ def seasons_by_stat(self, season_year: int, stat: str, **query: Any) -> APIResponse:
363
+ return self._request(_api_path("stats", "players", "seasons", season_year, stat), query=query or None)
364
+
365
+ def careers(self, **query: Any) -> APIResponse:
366
+ return self._request(_api_path("stats", "players", "careers"), query=query or None)
367
+
368
+ def careers_by_stat(self, stat: str, **query: Any) -> APIResponse:
369
+ return self._request(_api_path("stats", "players", "careers", stat), query=query or None)
370
+
371
+ def games(self, **query: Any) -> APIResponse:
372
+ return self._request(_api_path("stats", "players", "games"), query=query or None)
373
+
374
+ def game_splits(self, **query: Any) -> APIResponse:
375
+ return self._request(_api_path("stats", "players", "games", "splits"), query=query or None)
376
+
377
+ def game_leaders_by_family(self, stat_family: str, **query: Any) -> APIResponse:
378
+ return self._request(_api_path("stats", "players", "games", "leaders", stat_family), query=query or None)
379
+
380
+ def teams(self, **query: Any) -> APIResponse:
381
+ return self._request(_api_path("stats", "players", "teams"), query=query or None)
382
+
383
+ def weeks(self, season_year: int, **query: Any) -> APIResponse:
384
+ return self._request(_api_path("stats", "players", "season", season_year, "weeks"), query=query or None)
385
+
386
+
387
+ class StatsResource:
388
+ def __init__(self, client: NFLMetaClientCore) -> None:
389
+ self.players = StatsPlayersResource(client)
390
+
391
+
392
+ class ReferenceResource(BaseResource):
393
+ def all_star_games(self, **query: Any) -> APIResponse:
394
+ return self._request(_api_path("reference", "all-star-games"), query=query or None)
395
+
396
+ def coaches(self, **query: Any) -> APIResponse:
397
+ return self._request(_api_path("reference", "coaches"), query=query or None)
398
+
399
+ def coach(self, key: str) -> APIResponse:
400
+ return self._request(_api_path("reference", "coaches", key))
401
+
402
+ def coach_field(self, key: str, field: str) -> APIResponse:
403
+ return self._field(_api_path("reference", "coaches", key, field))
404
+
405
+ def officials(self, **query: Any) -> APIResponse:
406
+ return self._request(_api_path("reference", "officials"), query=query or None)
407
+
408
+ def official(self, key: str) -> APIResponse:
409
+ return self._request(_api_path("reference", "officials", key))
410
+
411
+ def official_field(self, key: str, field: str) -> APIResponse:
412
+ return self._field(_api_path("reference", "officials", key, field))
413
+
414
+ def power_rankings(self, **query: Any) -> APIResponse:
415
+ return self._request(_api_path("reference", "power-rankings"), query=query or None)
416
+
417
+ def pro_bowl_player_bios(self, **query: Any) -> APIResponse:
418
+ return self._request(_api_path("reference", "pro-bowl-player-bios"), query=query or None)
419
+
420
+ def pro_bowl_player_stats(self, **query: Any) -> APIResponse:
421
+ return self._request(_api_path("reference", "pro-bowl-player-stats"), query=query or None)
422
+
423
+ def pro_bowl_selections(self, **query: Any) -> APIResponse:
424
+ return self._request(_api_path("reference", "pro-bowl-selections"), query=query or None)
425
+
426
+ def seasons(self, **query: Any) -> APIResponse:
427
+ return self._request(_api_path("reference", "seasons"), query=query or None)
428
+
429
+ def stadium_histories(self, **query: Any) -> APIResponse:
430
+ return self._request(_api_path("reference", "stadium-histories"), query=query or None)
431
+
432
+ def stadiums(self, **query: Any) -> APIResponse:
433
+ return self._request(_api_path("reference", "stadiums"), query=query or None)
434
+
435
+ def team_aliases(self, **query: Any) -> APIResponse:
436
+ return self._request(_api_path("reference", "team-aliases"), query=query or None)
437
+
438
+ def team_coaches(self, **query: Any) -> APIResponse:
439
+ return self._request(_api_path("reference", "team-coaches"), query=query or None)
440
+
441
+ def team_colors(self, **query: Any) -> APIResponse:
442
+ return self._request(_api_path("reference", "team-colors"), query=query or None)
443
+
444
+ def team_logo_histories(self, **query: Any) -> APIResponse:
445
+ return self._request(_api_path("reference", "team-logo-histories"), query=query or None)
446
+
447
+ def team_name_histories(self, **query: Any) -> APIResponse:
448
+ return self._request(_api_path("reference", "team-name-histories"), query=query or None)
449
+
450
+ def team_season_records(self, **query: Any) -> APIResponse:
451
+ return self._request(_api_path("reference", "team-season-records"), query=query or None)
452
+
453
+ def team_season_stats(self, **query: Any) -> APIResponse:
454
+ return self._request(_api_path("reference", "team-season-stats"), query=query or None)
455
+
456
+ def teams(self, **query: Any) -> APIResponse:
457
+ return self._request(_api_path("reference", "teams"), query=query or None)
458
+
459
+ def venues(self, **query: Any) -> APIResponse:
460
+ return self._request(_api_path("reference", "venues"), query=query or None)
461
+
462
+ def venue(self, key: str) -> APIResponse:
463
+ return self._request(_api_path("reference", "venues", key))
464
+
465
+ def venue_field(self, key: str, field: str) -> APIResponse:
466
+ 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,112 @@
1
+ Metadata-Version: 2.4
2
+ Name: nflmeta
3
+ Version: 0.1.0
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/pbourdon71/NFLMeta-public
10
+ Keywords: nfl,nflmeta,sports,api,sdk,python
11
+ Classifier: Development Status :: 3 - Alpha
12
+ Classifier: Intended Audience :: Developers
13
+ Classifier: License :: Other/Proprietary License
14
+ Classifier: Programming Language :: Python :: 3
15
+ Classifier: Programming Language :: Python :: 3 :: Only
16
+ Classifier: Programming Language :: Python :: 3.9
17
+ Classifier: Programming Language :: Python :: 3.10
18
+ Classifier: Programming Language :: Python :: 3.11
19
+ Classifier: Programming Language :: Python :: 3.12
20
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
21
+ Requires-Python: >=3.9
22
+ Description-Content-Type: text/markdown
23
+ License-File: LICENSE
24
+ Dynamic: license-file
25
+
26
+ # `nflmeta`
27
+
28
+ Official Python SDK for the NFLMeta API.
29
+
30
+ ## Install
31
+
32
+ ```bash
33
+ pip install nflmeta
34
+ ```
35
+
36
+ ## Quickstart
37
+
38
+ ```python
39
+ from nflmeta import NFLMetaClient
40
+
41
+ client = NFLMetaClient(api_key="YOUR_KEY")
42
+
43
+ team = client.teams.get("PIT", season=2025)
44
+ player = client.players.identity("josh-allen-1996")
45
+ usage = client.usage.get()
46
+
47
+ print(team.data["displayName"])
48
+ print(player.data["display_name"])
49
+ print(usage.data["quota_remaining"])
50
+ ```
51
+
52
+ ## What The SDK Does
53
+
54
+ - sends `X-NFLMeta-Key` automatically when you provide `api_key`
55
+ - preserves the API envelope shape: `data`, optional `meta`, plus parsed rate-limit headers
56
+ - throws typed HTTP errors for common failure cases
57
+ - exposes the main public API surfaces as resource clients instead of forcing raw URL construction
58
+ - still gives you a low-level `client.get(path, query={...})` escape hatch for any route not wrapped yet
59
+
60
+ ## Configuration
61
+
62
+ ```python
63
+ client = NFLMetaClient(
64
+ api_key="YOUR_KEY",
65
+ base_url="https://nflmeta.org",
66
+ timeout=10.0,
67
+ )
68
+ ```
69
+
70
+ 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.
71
+
72
+ ## Resource Coverage
73
+
74
+ The Python SDK wraps the same main surfaces as the TypeScript package:
75
+
76
+ - `health`
77
+ - `usage`
78
+ - `metadata`
79
+ - `seasons`
80
+ - `teams`
81
+ - `players`
82
+ - `games`
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
+ ## Errors
99
+
100
+ - `NFLMetaBadRequestError`
101
+ - `NFLMetaUnauthorizedError`
102
+ - `NFLMetaNotFoundError`
103
+ - `NFLMetaRateLimitError`
104
+ - `NFLMetaTimeoutError`
105
+ - `NFLMetaError`
106
+
107
+ ## Escape Hatch
108
+
109
+ ```python
110
+ raw = client.get("/api/v1/reference/team-colors", query={"limit": 10})
111
+ print(raw.data)
112
+ ```
@@ -0,0 +1,11 @@
1
+ nflmeta/__init__.py,sha256=hwpSvsMvDH6NQ55-riRHF9PbECfyi1fqX5xR9f_DZF0,2351
2
+ nflmeta/client.py,sha256=OFl_C0S0JUT722m6SFEJofMwkMhGFsJWRftP5MY7x3k,5891
3
+ nflmeta/errors.py,sha256=X5bM1Bwyjh7va47pUIc8qIn5mcmMY4rZDQdMmTZj3sE,2436
4
+ nflmeta/py.typed,sha256=AbpHGcgLb-kRsJGnwFEktk7uzpZOCcBY74-YBdrKVGs,1
5
+ nflmeta/resources.py,sha256=OJUziWPGBYxeAfAgsNYApoE8797BvmDBzq8_aVJSQA0,20022
6
+ nflmeta/types.py,sha256=-7or03gfPjUeXbrJ0fPwoQiJPmkj65OeiFb83feabDA,842
7
+ nflmeta-0.1.0.dist-info/licenses/LICENSE,sha256=CvieYTWEkYHB4hdt-3nbkw4-UsHNEDMFiCnHguw5f-A,1091
8
+ nflmeta-0.1.0.dist-info/METADATA,sha256=EQZyHX9xdJapNabSSUD-GPks4R17ekvJZii2QQy8ccM,2820
9
+ nflmeta-0.1.0.dist-info/WHEEL,sha256=YVMoNqKzERt-wjUZwJ33xBGAwnFl-4cqbYkTtWa4itE,91
10
+ nflmeta-0.1.0.dist-info/top_level.txt,sha256=LG6vsDGLjCPcaIhUopRKcxOgeWoXXTf841tQLIgOyxw,8
11
+ nflmeta-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (84.0.0)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -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