nflmeta 0.1.1__tar.gz
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-0.1.1/LICENSE +21 -0
- nflmeta-0.1.1/PKG-INFO +112 -0
- nflmeta-0.1.1/README.md +87 -0
- nflmeta-0.1.1/pyproject.toml +42 -0
- nflmeta-0.1.1/setup.cfg +4 -0
- nflmeta-0.1.1/src/nflmeta/__init__.py +82 -0
- nflmeta-0.1.1/src/nflmeta/client.py +164 -0
- nflmeta-0.1.1/src/nflmeta/errors.py +83 -0
- nflmeta-0.1.1/src/nflmeta/py.typed +1 -0
- nflmeta-0.1.1/src/nflmeta/resources.py +466 -0
- nflmeta-0.1.1/src/nflmeta/types.py +32 -0
- nflmeta-0.1.1/src/nflmeta.egg-info/PKG-INFO +112 -0
- nflmeta-0.1.1/src/nflmeta.egg-info/SOURCES.txt +14 -0
- nflmeta-0.1.1/src/nflmeta.egg-info/dependency_links.txt +1 -0
- nflmeta-0.1.1/src/nflmeta.egg-info/top_level.txt +1 -0
- nflmeta-0.1.1/tests/test_client.py +103 -0
nflmeta-0.1.1/LICENSE
ADDED
|
@@ -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.
|
nflmeta-0.1.1/PKG-INFO
ADDED
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: nflmeta
|
|
3
|
+
Version: 0.1.1
|
|
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 :: 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
|
+
```
|
nflmeta-0.1.1/README.md
ADDED
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
# `nflmeta`
|
|
2
|
+
|
|
3
|
+
Official Python SDK for the NFLMeta API.
|
|
4
|
+
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
pip install nflmeta
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Quickstart
|
|
12
|
+
|
|
13
|
+
```python
|
|
14
|
+
from nflmeta import NFLMetaClient
|
|
15
|
+
|
|
16
|
+
client = NFLMetaClient(api_key="YOUR_KEY")
|
|
17
|
+
|
|
18
|
+
team = client.teams.get("PIT", season=2025)
|
|
19
|
+
player = client.players.identity("josh-allen-1996")
|
|
20
|
+
usage = client.usage.get()
|
|
21
|
+
|
|
22
|
+
print(team.data["displayName"])
|
|
23
|
+
print(player.data["display_name"])
|
|
24
|
+
print(usage.data["quota_remaining"])
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
## What The SDK Does
|
|
28
|
+
|
|
29
|
+
- sends `X-NFLMeta-Key` automatically when you provide `api_key`
|
|
30
|
+
- preserves the API envelope shape: `data`, optional `meta`, plus parsed rate-limit headers
|
|
31
|
+
- throws typed HTTP errors for common failure cases
|
|
32
|
+
- exposes the main public API surfaces as resource clients instead of forcing raw URL construction
|
|
33
|
+
- still gives you a low-level `client.get(path, query={...})` escape hatch for any route not wrapped yet
|
|
34
|
+
|
|
35
|
+
## Configuration
|
|
36
|
+
|
|
37
|
+
```python
|
|
38
|
+
client = NFLMetaClient(
|
|
39
|
+
api_key="YOUR_KEY",
|
|
40
|
+
base_url="https://nflmeta.org",
|
|
41
|
+
timeout=10.0,
|
|
42
|
+
)
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
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.
|
|
46
|
+
|
|
47
|
+
## Resource Coverage
|
|
48
|
+
|
|
49
|
+
The Python SDK wraps the same main surfaces as the TypeScript package:
|
|
50
|
+
|
|
51
|
+
- `health`
|
|
52
|
+
- `usage`
|
|
53
|
+
- `metadata`
|
|
54
|
+
- `seasons`
|
|
55
|
+
- `teams`
|
|
56
|
+
- `players`
|
|
57
|
+
- `games`
|
|
58
|
+
- `playoff_games`
|
|
59
|
+
- `standings`
|
|
60
|
+
- `playoffs`
|
|
61
|
+
- `super_bowls`
|
|
62
|
+
- `history`
|
|
63
|
+
- `hall_of_fame`
|
|
64
|
+
- `contributors`
|
|
65
|
+
- `executives`
|
|
66
|
+
- `staff`
|
|
67
|
+
- `coaches`
|
|
68
|
+
- `assets`
|
|
69
|
+
- `all_star_games`
|
|
70
|
+
- `stats.players`
|
|
71
|
+
- `reference`
|
|
72
|
+
|
|
73
|
+
## Errors
|
|
74
|
+
|
|
75
|
+
- `NFLMetaBadRequestError`
|
|
76
|
+
- `NFLMetaUnauthorizedError`
|
|
77
|
+
- `NFLMetaNotFoundError`
|
|
78
|
+
- `NFLMetaRateLimitError`
|
|
79
|
+
- `NFLMetaTimeoutError`
|
|
80
|
+
- `NFLMetaError`
|
|
81
|
+
|
|
82
|
+
## Escape Hatch
|
|
83
|
+
|
|
84
|
+
```python
|
|
85
|
+
raw = client.get("/api/v1/reference/team-colors", query={"limit": 10})
|
|
86
|
+
print(raw.data)
|
|
87
|
+
```
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=68"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "nflmeta"
|
|
7
|
+
version = "0.1.1"
|
|
8
|
+
description = "Official Python SDK for the NFLMeta API"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.9"
|
|
11
|
+
license = { text = "MIT" }
|
|
12
|
+
authors = [
|
|
13
|
+
{ name = "NFLMeta" }
|
|
14
|
+
]
|
|
15
|
+
keywords = ["nfl", "nflmeta", "sports", "api", "sdk", "python"]
|
|
16
|
+
classifiers = [
|
|
17
|
+
"Development Status :: 3 - Alpha",
|
|
18
|
+
"Intended Audience :: Developers",
|
|
19
|
+
"License :: Other/Proprietary License",
|
|
20
|
+
"Programming Language :: Python :: 3",
|
|
21
|
+
"Programming Language :: Python :: 3 :: Only",
|
|
22
|
+
"Programming Language :: Python :: 3.9",
|
|
23
|
+
"Programming Language :: Python :: 3.10",
|
|
24
|
+
"Programming Language :: Python :: 3.11",
|
|
25
|
+
"Programming Language :: Python :: 3.12",
|
|
26
|
+
"Topic :: Software Development :: Libraries :: Python Modules"
|
|
27
|
+
]
|
|
28
|
+
|
|
29
|
+
[tool.setuptools]
|
|
30
|
+
package-dir = {"" = "src"}
|
|
31
|
+
include-package-data = true
|
|
32
|
+
|
|
33
|
+
[tool.setuptools.packages.find]
|
|
34
|
+
where = ["src"]
|
|
35
|
+
|
|
36
|
+
[tool.setuptools.package-data]
|
|
37
|
+
nflmeta = ["py.typed"]
|
|
38
|
+
|
|
39
|
+
[project.urls]
|
|
40
|
+
Homepage = "https://nflmeta.org/api-docs/sdk"
|
|
41
|
+
Documentation = "https://nflmeta.org/api-docs"
|
|
42
|
+
Source = "https://github.com/philippebourdon/NFLMeta"
|
nflmeta-0.1.1/setup.cfg
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
|
+
]
|
|
@@ -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
|
+
)
|
|
@@ -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)
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -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))
|
|
@@ -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.1
|
|
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 :: 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,14 @@
|
|
|
1
|
+
LICENSE
|
|
2
|
+
README.md
|
|
3
|
+
pyproject.toml
|
|
4
|
+
src/nflmeta/__init__.py
|
|
5
|
+
src/nflmeta/client.py
|
|
6
|
+
src/nflmeta/errors.py
|
|
7
|
+
src/nflmeta/py.typed
|
|
8
|
+
src/nflmeta/resources.py
|
|
9
|
+
src/nflmeta/types.py
|
|
10
|
+
src/nflmeta.egg-info/PKG-INFO
|
|
11
|
+
src/nflmeta.egg-info/SOURCES.txt
|
|
12
|
+
src/nflmeta.egg-info/dependency_links.txt
|
|
13
|
+
src/nflmeta.egg-info/top_level.txt
|
|
14
|
+
tests/test_client.py
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
nflmeta
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import unittest
|
|
4
|
+
from io import BytesIO
|
|
5
|
+
from urllib.error import HTTPError
|
|
6
|
+
from urllib.request import Request
|
|
7
|
+
|
|
8
|
+
from nflmeta import NFLMetaClient, NFLMetaUnauthorizedError
|
|
9
|
+
from nflmeta.client import TransportResponse, _default_transport
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class NFLMetaClientTests(unittest.TestCase):
|
|
13
|
+
def test_adds_api_key_and_parses_rate_limits(self) -> None:
|
|
14
|
+
captured: dict[str, object] = {}
|
|
15
|
+
|
|
16
|
+
def transport(url: str, headers, timeout):
|
|
17
|
+
captured["url"] = url
|
|
18
|
+
captured["headers"] = dict(headers)
|
|
19
|
+
captured["timeout"] = timeout
|
|
20
|
+
return TransportResponse(
|
|
21
|
+
status=200,
|
|
22
|
+
headers={
|
|
23
|
+
"X-RateLimit-Limit": "1000",
|
|
24
|
+
"X-RateLimit-Remaining": "999",
|
|
25
|
+
"X-RateLimit-Reset": "2026-04-01T00:00:00.000Z",
|
|
26
|
+
"X-RateLimit-Policy": "monthly",
|
|
27
|
+
},
|
|
28
|
+
payload={
|
|
29
|
+
"data": [{"year": 2025, "id": 57}],
|
|
30
|
+
"meta": {"total": 1, "returned": 1},
|
|
31
|
+
},
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
client = NFLMetaClient(api_key="test-key", transport=transport)
|
|
35
|
+
result = client.seasons.list(year_from=2025, limit=1)
|
|
36
|
+
|
|
37
|
+
self.assertEqual(captured["headers"]["X-NFLMeta-Key"], "test-key")
|
|
38
|
+
self.assertTrue(str(captured["url"]).endswith("/api/v1/seasons?year_from=2025&limit=1"))
|
|
39
|
+
self.assertEqual(result.rate_limit.limit, 1000)
|
|
40
|
+
self.assertEqual(result.rate_limit.remaining, 999)
|
|
41
|
+
self.assertEqual(result.data[0]["year"], 2025)
|
|
42
|
+
|
|
43
|
+
def test_raises_typed_unauthorized_errors(self) -> None:
|
|
44
|
+
def transport(_url: str, _headers, _timeout):
|
|
45
|
+
return TransportResponse(
|
|
46
|
+
status=401,
|
|
47
|
+
headers={},
|
|
48
|
+
payload={"error": {"code": "unauthorized", "message": "invalid api key"}},
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
client = NFLMetaClient(transport=transport)
|
|
52
|
+
|
|
53
|
+
with self.assertRaises(NFLMetaUnauthorizedError) as ctx:
|
|
54
|
+
client.usage.get()
|
|
55
|
+
|
|
56
|
+
self.assertEqual(ctx.exception.status, 401)
|
|
57
|
+
self.assertEqual(ctx.exception.code, "unauthorized")
|
|
58
|
+
|
|
59
|
+
def test_builds_nested_resource_paths(self) -> None:
|
|
60
|
+
captured: dict[str, object] = {}
|
|
61
|
+
|
|
62
|
+
def transport(url: str, _headers, _timeout):
|
|
63
|
+
captured["url"] = url
|
|
64
|
+
return TransportResponse(
|
|
65
|
+
status=200,
|
|
66
|
+
headers={},
|
|
67
|
+
payload={"data": {"field": "pass_passer_rating", "value": 99.4}},
|
|
68
|
+
)
|
|
69
|
+
|
|
70
|
+
client = NFLMetaClient(transport=transport)
|
|
71
|
+
result = client.players.career_field("josh-allen-1996", "pass_passer_rating")
|
|
72
|
+
|
|
73
|
+
self.assertTrue(str(captured["url"]).endswith("/api/v1/players/josh-allen-1996/career/pass_passer_rating"))
|
|
74
|
+
self.assertEqual(result.data["value"], 99.4)
|
|
75
|
+
|
|
76
|
+
def test_default_transport_normalizes_http_errors(self) -> None:
|
|
77
|
+
request = Request("https://nflmeta.org/api/v1/usage", method="GET")
|
|
78
|
+
http_error = HTTPError(
|
|
79
|
+
request.full_url,
|
|
80
|
+
401,
|
|
81
|
+
"Unauthorized",
|
|
82
|
+
hdrs={"Content-Type": "application/json"},
|
|
83
|
+
fp=BytesIO(b'{"error":{"code":"unauthorized","message":"invalid api key"}}'),
|
|
84
|
+
)
|
|
85
|
+
|
|
86
|
+
def fake_urlopen(_request, timeout=None):
|
|
87
|
+
raise http_error
|
|
88
|
+
|
|
89
|
+
from nflmeta import client as client_module
|
|
90
|
+
|
|
91
|
+
original_urlopen = client_module.urlopen
|
|
92
|
+
client_module.urlopen = fake_urlopen
|
|
93
|
+
try:
|
|
94
|
+
response = _default_transport(request.full_url, {}, 10.0)
|
|
95
|
+
finally:
|
|
96
|
+
client_module.urlopen = original_urlopen
|
|
97
|
+
|
|
98
|
+
self.assertEqual(response.status, 401)
|
|
99
|
+
self.assertEqual(response.payload["error"]["code"], "unauthorized")
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
if __name__ == "__main__":
|
|
103
|
+
unittest.main()
|