nflmeta 0.1.3__py3-none-any.whl

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