cfb-data 0.4.1__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.
- cfb_data/__init__.py +234 -0
- cfb_data/_dataframes.py +279 -0
- cfb_data/_executor.py +129 -0
- cfb_data/_parquet.py +197 -0
- cfb_data/_request_rules.py +41 -0
- cfb_data/_requests.py +36 -0
- cfb_data/_tabular.py +676 -0
- cfb_data/_transport.py +452 -0
- cfb_data/adjusted_metrics/__init__.py +29 -0
- cfb_data/adjusted_metrics/models/__init__.py +1 -0
- cfb_data/adjusted_metrics/models/pydantic/__init__.py +29 -0
- cfb_data/adjusted_metrics/models/pydantic/requests.py +45 -0
- cfb_data/adjusted_metrics/models/pydantic/responses.py +88 -0
- cfb_data/adjusted_metrics/resource.py +220 -0
- cfb_data/base/__init__.py +6 -0
- cfb_data/base/types.py +112 -0
- cfb_data/betting/__init__.py +15 -0
- cfb_data/betting/models/__init__.py +1 -0
- cfb_data/betting/models/pydantic/__init__.py +6 -0
- cfb_data/betting/models/pydantic/requests.py +35 -0
- cfb_data/betting/models/pydantic/responses.py +62 -0
- cfb_data/betting/resource.py +91 -0
- cfb_data/client.py +305 -0
- cfb_data/coaches/__init__.py +57 -0
- cfb_data/coaches/models/__init__.py +1 -0
- cfb_data/coaches/models/pydantic/__init__.py +57 -0
- cfb_data/coaches/models/pydantic/requests.py +75 -0
- cfb_data/coaches/models/pydantic/responses.py +261 -0
- cfb_data/coaches/resource.py +216 -0
- cfb_data/conferences/__init__.py +23 -0
- cfb_data/conferences/models/__init__.py +1 -0
- cfb_data/conferences/models/pydantic/__init__.py +23 -0
- cfb_data/conferences/models/pydantic/requests.py +69 -0
- cfb_data/conferences/models/pydantic/responses.py +64 -0
- cfb_data/conferences/resource.py +175 -0
- cfb_data/draft/__init__.py +19 -0
- cfb_data/draft/models/__init__.py +1 -0
- cfb_data/draft/models/pydantic/__init__.py +12 -0
- cfb_data/draft/models/pydantic/requests.py +18 -0
- cfb_data/draft/models/pydantic/responses.py +65 -0
- cfb_data/draft/resource.py +132 -0
- cfb_data/drives/__init__.py +13 -0
- cfb_data/drives/models/__init__.py +1 -0
- cfb_data/drives/models/pydantic/__init__.py +17 -0
- cfb_data/drives/models/pydantic/requests.py +42 -0
- cfb_data/drives/models/pydantic/responses.py +46 -0
- cfb_data/drives/resource.py +94 -0
- cfb_data/enums.py +93 -0
- cfb_data/errors.py +234 -0
- cfb_data/games/__init__.py +42 -0
- cfb_data/games/models/__init__.py +1 -0
- cfb_data/games/models/pydantic/__init__.py +106 -0
- cfb_data/games/models/pydantic/requests.py +266 -0
- cfb_data/games/models/pydantic/responses.py +495 -0
- cfb_data/games/resource.py +486 -0
- cfb_data/info/__init__.py +25 -0
- cfb_data/info/models/__init__.py +1 -0
- cfb_data/info/models/pydantic/__init__.py +23 -0
- cfb_data/info/models/pydantic/requests.py +18 -0
- cfb_data/info/models/pydantic/responses.py +103 -0
- cfb_data/info/resource.py +88 -0
- cfb_data/metrics/__init__.py +53 -0
- cfb_data/metrics/models/__init__.py +1 -0
- cfb_data/metrics/models/pydantic/__init__.py +49 -0
- cfb_data/metrics/models/pydantic/requests.py +121 -0
- cfb_data/metrics/models/pydantic/responses.py +182 -0
- cfb_data/metrics/resource.py +371 -0
- cfb_data/players/__init__.py +44 -0
- cfb_data/players/models/__init__.py +1 -0
- cfb_data/players/models/pydantic/__init__.py +41 -0
- cfb_data/players/models/pydantic/requests.py +70 -0
- cfb_data/players/models/pydantic/responses.py +171 -0
- cfb_data/players/resource.py +258 -0
- cfb_data/playoffs/__init__.py +41 -0
- cfb_data/playoffs/models/__init__.py +1 -0
- cfb_data/playoffs/models/pydantic/__init__.py +37 -0
- cfb_data/playoffs/models/pydantic/requests.py +30 -0
- cfb_data/playoffs/models/pydantic/responses.py +173 -0
- cfb_data/playoffs/resource.py +149 -0
- cfb_data/plays/__init__.py +43 -0
- cfb_data/plays/models/__init__.py +1 -0
- cfb_data/plays/models/pydantic/__init__.py +35 -0
- cfb_data/plays/models/pydantic/requests.py +85 -0
- cfb_data/plays/models/pydantic/responses.py +231 -0
- cfb_data/plays/resource.py +249 -0
- cfb_data/py.typed +0 -0
- cfb_data/rankings/__init__.py +16 -0
- cfb_data/rankings/models/__init__.py +1 -0
- cfb_data/rankings/models/pydantic/__init__.py +6 -0
- cfb_data/rankings/models/pydantic/requests.py +36 -0
- cfb_data/rankings/models/pydantic/responses.py +42 -0
- cfb_data/rankings/resource.py +89 -0
- cfb_data/ratings/__init__.py +59 -0
- cfb_data/ratings/models/__init__.py +1 -0
- cfb_data/ratings/models/pydantic/__init__.py +55 -0
- cfb_data/ratings/models/pydantic/requests.py +87 -0
- cfb_data/ratings/models/pydantic/responses.py +215 -0
- cfb_data/ratings/resource.py +342 -0
- cfb_data/recruiting/__init__.py +26 -0
- cfb_data/recruiting/models/__init__.py +1 -0
- cfb_data/recruiting/models/pydantic/__init__.py +23 -0
- cfb_data/recruiting/models/pydantic/requests.py +69 -0
- cfb_data/recruiting/models/pydantic/responses.py +70 -0
- cfb_data/recruiting/resource.py +180 -0
- cfb_data/retry.py +49 -0
- cfb_data/stats/__init__.py +69 -0
- cfb_data/stats/models/__init__.py +1 -0
- cfb_data/stats/models/pydantic/__init__.py +65 -0
- cfb_data/stats/models/pydantic/requests.py +167 -0
- cfb_data/stats/models/pydantic/responses.py +291 -0
- cfb_data/stats/resource.py +400 -0
- cfb_data/teams/__init__.py +40 -0
- cfb_data/teams/models/__init__.py +1 -0
- cfb_data/teams/models/pydantic/__init__.py +26 -0
- cfb_data/teams/models/pydantic/requests.py +96 -0
- cfb_data/teams/models/pydantic/responses.py +116 -0
- cfb_data/teams/resource.py +270 -0
- cfb_data/venues/__init__.py +6 -0
- cfb_data/venues/models/__init__.py +1 -0
- cfb_data/venues/models/pydantic/__init__.py +5 -0
- cfb_data/venues/models/pydantic/responses.py +24 -0
- cfb_data/venues/resource.py +51 -0
- cfb_data-0.4.1.dist-info/METADATA +414 -0
- cfb_data-0.4.1.dist-info/RECORD +127 -0
- cfb_data-0.4.1.dist-info/WHEEL +5 -0
- cfb_data-0.4.1.dist-info/licenses/LICENSE +21 -0
- cfb_data-0.4.1.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
"""Expose typed Adjusted Metrics endpoints through the primary client."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from collections.abc import Mapping
|
|
6
|
+
from typing import TypeVar, overload
|
|
7
|
+
|
|
8
|
+
from pydantic import BaseModel, TypeAdapter
|
|
9
|
+
|
|
10
|
+
from cfb_data._dataframes import _DataFrameAdapter
|
|
11
|
+
from cfb_data._executor import _EndpointExecutor
|
|
12
|
+
from cfb_data._requests import _resolve_request
|
|
13
|
+
from cfb_data.adjusted_metrics.models.pydantic.requests import (
|
|
14
|
+
AdjustedPlayerPassingRequest,
|
|
15
|
+
AdjustedPlayerRushingRequest,
|
|
16
|
+
AdjustedTeamMetricsRequest,
|
|
17
|
+
KickerPAARRequest,
|
|
18
|
+
)
|
|
19
|
+
from cfb_data.adjusted_metrics.models.pydantic.responses import (
|
|
20
|
+
AdjustedTeamMetrics,
|
|
21
|
+
KickerPAAR,
|
|
22
|
+
PlayerWeightedEPA,
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
_RequestT = TypeVar("_RequestT", bound=BaseModel)
|
|
26
|
+
_RowT = TypeVar("_RowT", bound=BaseModel)
|
|
27
|
+
|
|
28
|
+
_ADJUSTED_TEAM_ROWS = TypeAdapter(list[AdjustedTeamMetrics])
|
|
29
|
+
_PLAYER_WEIGHTED_EPA_ROWS = TypeAdapter(list[PlayerWeightedEPA])
|
|
30
|
+
_KICKER_PAAR_ROWS = TypeAdapter(list[KickerPAAR])
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class AdjustedMetricsResource[FrameT]:
|
|
34
|
+
"""Provide validated opponent-adjusted metrics as selected frames."""
|
|
35
|
+
|
|
36
|
+
def __init__(
|
|
37
|
+
self,
|
|
38
|
+
executor: _EndpointExecutor,
|
|
39
|
+
dataframe_adapter: _DataFrameAdapter[FrameT],
|
|
40
|
+
) -> None:
|
|
41
|
+
"""Bind the namespace to shared execution and presentation services."""
|
|
42
|
+
self._executor = executor
|
|
43
|
+
self._dataframe_adapter = dataframe_adapter
|
|
44
|
+
|
|
45
|
+
@overload
|
|
46
|
+
async def team_season(self, request: AdjustedTeamMetricsRequest, /) -> FrameT: ...
|
|
47
|
+
|
|
48
|
+
@overload
|
|
49
|
+
async def team_season(
|
|
50
|
+
self,
|
|
51
|
+
request: None = None,
|
|
52
|
+
/,
|
|
53
|
+
*,
|
|
54
|
+
year: int | None = None,
|
|
55
|
+
team: str | None = None,
|
|
56
|
+
conference: str | None = None,
|
|
57
|
+
) -> FrameT: ...
|
|
58
|
+
|
|
59
|
+
async def team_season(
|
|
60
|
+
self, request: AdjustedTeamMetricsRequest | None = None, /, **filters: object
|
|
61
|
+
) -> FrameT:
|
|
62
|
+
"""Return opponent-adjusted team metrics by season.
|
|
63
|
+
|
|
64
|
+
:param request: Validated request model, mutually exclusive with filters.
|
|
65
|
+
:param filters: Explicit snake-case endpoint filters.
|
|
66
|
+
:return: Eager frame containing validated adjusted-team rows.
|
|
67
|
+
:raises TypeError: If request styles are mixed or the model type is wrong.
|
|
68
|
+
:raises CFBDError: If request, transport, response, or conversion fails.
|
|
69
|
+
"""
|
|
70
|
+
return await self._fetch_many(
|
|
71
|
+
endpoint="/wepa/team/season",
|
|
72
|
+
request_type=AdjustedTeamMetricsRequest,
|
|
73
|
+
request=request,
|
|
74
|
+
filters=filters,
|
|
75
|
+
response_adapter=_ADJUSTED_TEAM_ROWS,
|
|
76
|
+
row_model=AdjustedTeamMetrics,
|
|
77
|
+
)
|
|
78
|
+
|
|
79
|
+
@overload
|
|
80
|
+
async def player_passing(
|
|
81
|
+
self, request: AdjustedPlayerPassingRequest, /
|
|
82
|
+
) -> FrameT: ...
|
|
83
|
+
|
|
84
|
+
@overload
|
|
85
|
+
async def player_passing(
|
|
86
|
+
self,
|
|
87
|
+
request: None = None,
|
|
88
|
+
/,
|
|
89
|
+
*,
|
|
90
|
+
year: int | None = None,
|
|
91
|
+
team: str | None = None,
|
|
92
|
+
conference: str | None = None,
|
|
93
|
+
position: str | None = None,
|
|
94
|
+
) -> FrameT: ...
|
|
95
|
+
|
|
96
|
+
async def player_passing(
|
|
97
|
+
self,
|
|
98
|
+
request: AdjustedPlayerPassingRequest | None = None,
|
|
99
|
+
/,
|
|
100
|
+
**filters: object,
|
|
101
|
+
) -> FrameT:
|
|
102
|
+
"""Return opponent-adjusted player passing EPA.
|
|
103
|
+
|
|
104
|
+
:param request: Validated request model, mutually exclusive with filters.
|
|
105
|
+
:param filters: Explicit snake-case endpoint filters.
|
|
106
|
+
:return: Eager frame containing validated player WEPA rows.
|
|
107
|
+
:raises TypeError: If request styles are mixed or the model type is wrong.
|
|
108
|
+
:raises CFBDError: If request, transport, response, or conversion fails.
|
|
109
|
+
"""
|
|
110
|
+
return await self._fetch_many(
|
|
111
|
+
endpoint="/wepa/players/passing",
|
|
112
|
+
request_type=AdjustedPlayerPassingRequest,
|
|
113
|
+
request=request,
|
|
114
|
+
filters=filters,
|
|
115
|
+
response_adapter=_PLAYER_WEIGHTED_EPA_ROWS,
|
|
116
|
+
row_model=PlayerWeightedEPA,
|
|
117
|
+
)
|
|
118
|
+
|
|
119
|
+
@overload
|
|
120
|
+
async def player_rushing(
|
|
121
|
+
self, request: AdjustedPlayerRushingRequest, /
|
|
122
|
+
) -> FrameT: ...
|
|
123
|
+
|
|
124
|
+
@overload
|
|
125
|
+
async def player_rushing(
|
|
126
|
+
self,
|
|
127
|
+
request: None = None,
|
|
128
|
+
/,
|
|
129
|
+
*,
|
|
130
|
+
year: int | None = None,
|
|
131
|
+
team: str | None = None,
|
|
132
|
+
conference: str | None = None,
|
|
133
|
+
position: str | None = None,
|
|
134
|
+
) -> FrameT: ...
|
|
135
|
+
|
|
136
|
+
async def player_rushing(
|
|
137
|
+
self,
|
|
138
|
+
request: AdjustedPlayerRushingRequest | None = None,
|
|
139
|
+
/,
|
|
140
|
+
**filters: object,
|
|
141
|
+
) -> FrameT:
|
|
142
|
+
"""Return opponent-adjusted player rushing EPA.
|
|
143
|
+
|
|
144
|
+
:param request: Validated request model, mutually exclusive with filters.
|
|
145
|
+
:param filters: Explicit snake-case endpoint filters.
|
|
146
|
+
:return: Eager frame containing validated player WEPA rows.
|
|
147
|
+
:raises TypeError: If request styles are mixed or the model type is wrong.
|
|
148
|
+
:raises CFBDError: If request, transport, response, or conversion fails.
|
|
149
|
+
"""
|
|
150
|
+
return await self._fetch_many(
|
|
151
|
+
endpoint="/wepa/players/rushing",
|
|
152
|
+
request_type=AdjustedPlayerRushingRequest,
|
|
153
|
+
request=request,
|
|
154
|
+
filters=filters,
|
|
155
|
+
response_adapter=_PLAYER_WEIGHTED_EPA_ROWS,
|
|
156
|
+
row_model=PlayerWeightedEPA,
|
|
157
|
+
)
|
|
158
|
+
|
|
159
|
+
@overload
|
|
160
|
+
async def kicker_paar(self, request: KickerPAARRequest, /) -> FrameT: ...
|
|
161
|
+
|
|
162
|
+
@overload
|
|
163
|
+
async def kicker_paar(
|
|
164
|
+
self,
|
|
165
|
+
request: None = None,
|
|
166
|
+
/,
|
|
167
|
+
*,
|
|
168
|
+
year: int | None = None,
|
|
169
|
+
team: str | None = None,
|
|
170
|
+
conference: str | None = None,
|
|
171
|
+
) -> FrameT: ...
|
|
172
|
+
|
|
173
|
+
async def kicker_paar(
|
|
174
|
+
self, request: KickerPAARRequest | None = None, /, **filters: object
|
|
175
|
+
) -> FrameT:
|
|
176
|
+
"""Return Points Added Above Replacement ratings for kickers.
|
|
177
|
+
|
|
178
|
+
:param request: Validated request model, mutually exclusive with filters.
|
|
179
|
+
:param filters: Explicit snake-case endpoint filters.
|
|
180
|
+
:return: Eager frame containing validated kicker PAAR rows.
|
|
181
|
+
:raises TypeError: If request styles are mixed or the model type is wrong.
|
|
182
|
+
:raises CFBDError: If request, transport, response, or conversion fails.
|
|
183
|
+
"""
|
|
184
|
+
return await self._fetch_many(
|
|
185
|
+
endpoint="/wepa/players/kicking",
|
|
186
|
+
request_type=KickerPAARRequest,
|
|
187
|
+
request=request,
|
|
188
|
+
filters=filters,
|
|
189
|
+
response_adapter=_KICKER_PAAR_ROWS,
|
|
190
|
+
row_model=KickerPAAR,
|
|
191
|
+
)
|
|
192
|
+
|
|
193
|
+
async def _fetch_many(
|
|
194
|
+
self,
|
|
195
|
+
*,
|
|
196
|
+
endpoint: str,
|
|
197
|
+
request_type: type[_RequestT],
|
|
198
|
+
request: _RequestT | None,
|
|
199
|
+
filters: Mapping[str, object],
|
|
200
|
+
response_adapter: TypeAdapter[list[_RowT]],
|
|
201
|
+
row_model: type[_RowT],
|
|
202
|
+
) -> FrameT:
|
|
203
|
+
"""Resolve, validate, fetch, and tabularize one list endpoint."""
|
|
204
|
+
validated = _resolve_request(
|
|
205
|
+
endpoint=endpoint,
|
|
206
|
+
request_type=request_type,
|
|
207
|
+
request=request,
|
|
208
|
+
filters=filters,
|
|
209
|
+
)
|
|
210
|
+
rows = await self._executor.fetch_many(
|
|
211
|
+
endpoint=endpoint,
|
|
212
|
+
request=validated,
|
|
213
|
+
response_adapter=response_adapter,
|
|
214
|
+
)
|
|
215
|
+
return self._dataframe_adapter.from_models(
|
|
216
|
+
endpoint=endpoint, row_model=row_model, models=rows
|
|
217
|
+
)
|
|
218
|
+
|
|
219
|
+
|
|
220
|
+
__all__ = ["AdjustedMetricsResource"]
|
cfb_data/base/types.py
ADDED
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
"""Define and validate values crossing API serialization boundaries."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from collections.abc import Mapping
|
|
6
|
+
|
|
7
|
+
type JSONScalar = str | int | float | bool | None
|
|
8
|
+
type JSONValue = JSONScalar | list["JSONValue"] | dict[str, "JSONValue"]
|
|
9
|
+
type JSONObject = dict[str, JSONValue]
|
|
10
|
+
type JSONResponse = JSONObject | list[JSONObject]
|
|
11
|
+
type QueryValue = str | int | float | bool
|
|
12
|
+
type QueryParameters = dict[str, QueryValue]
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def json_value(value: object) -> JSONValue:
|
|
16
|
+
"""Validate and return a recursively JSON-compatible value.
|
|
17
|
+
|
|
18
|
+
:param value: Untrusted value from a JSON decoder or serializer.
|
|
19
|
+
:return: A value containing only JSON-compatible primitives and containers.
|
|
20
|
+
:raises TypeError: If the value is not JSON-compatible or has non-string keys.
|
|
21
|
+
"""
|
|
22
|
+
if value is None or isinstance(value, str | int | float | bool):
|
|
23
|
+
return value
|
|
24
|
+
if isinstance(value, list):
|
|
25
|
+
return [json_value(item) for item in value]
|
|
26
|
+
if isinstance(value, dict):
|
|
27
|
+
result: JSONObject = {}
|
|
28
|
+
for key, item in value.items():
|
|
29
|
+
if not isinstance(key, str):
|
|
30
|
+
raise TypeError("JSON object keys must be strings")
|
|
31
|
+
result[key] = json_value(item)
|
|
32
|
+
return result
|
|
33
|
+
raise TypeError(f"Unsupported JSON value type: {type(value).__name__}")
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def json_response(value: object) -> JSONResponse:
|
|
37
|
+
"""Validate and return the supported top-level API response shape.
|
|
38
|
+
|
|
39
|
+
:param value: Untrusted value returned by the HTTP JSON decoder.
|
|
40
|
+
:return: A JSON object or a list of JSON objects.
|
|
41
|
+
:raises TypeError: If the value is not a supported API response.
|
|
42
|
+
"""
|
|
43
|
+
parsed = json_value(value)
|
|
44
|
+
if isinstance(parsed, dict):
|
|
45
|
+
return parsed
|
|
46
|
+
if isinstance(parsed, list):
|
|
47
|
+
objects: list[JSONObject] = []
|
|
48
|
+
for item in parsed:
|
|
49
|
+
if not isinstance(item, dict):
|
|
50
|
+
raise TypeError("API response lists must contain only JSON objects")
|
|
51
|
+
objects.append(item)
|
|
52
|
+
return objects
|
|
53
|
+
raise TypeError("API response must be a JSON object or a list of JSON objects")
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def json_object(value: object) -> JSONObject:
|
|
57
|
+
"""Validate and return a JSON object.
|
|
58
|
+
|
|
59
|
+
:param value: Untrusted value expected to contain a JSON object.
|
|
60
|
+
:return: A recursively validated JSON object.
|
|
61
|
+
:raises TypeError: If the value is not a JSON object.
|
|
62
|
+
"""
|
|
63
|
+
parsed = json_value(value)
|
|
64
|
+
if isinstance(parsed, dict):
|
|
65
|
+
return parsed
|
|
66
|
+
raise TypeError("Expected a JSON object")
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def json_object_list(value: object) -> list[JSONObject]:
|
|
70
|
+
"""Validate and return a list of JSON objects.
|
|
71
|
+
|
|
72
|
+
:param value: Untrusted value expected to contain JSON objects.
|
|
73
|
+
:return: A recursively validated list of JSON objects.
|
|
74
|
+
:raises TypeError: If the value is not a list of JSON objects.
|
|
75
|
+
"""
|
|
76
|
+
parsed = json_response(value)
|
|
77
|
+
if isinstance(parsed, list):
|
|
78
|
+
return parsed
|
|
79
|
+
raise TypeError("Expected a list of JSON objects")
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def json_list(value: object) -> list[JSONValue]:
|
|
83
|
+
"""Validate and return a list of recursively JSON-compatible values.
|
|
84
|
+
|
|
85
|
+
:param value: Untrusted value expected to contain a JSON array.
|
|
86
|
+
:return: Recursively validated JSON values in their original order.
|
|
87
|
+
:raises TypeError: If the value is not a JSON array.
|
|
88
|
+
"""
|
|
89
|
+
parsed = json_value(value)
|
|
90
|
+
if isinstance(parsed, list):
|
|
91
|
+
return parsed
|
|
92
|
+
raise TypeError("Expected a JSON array")
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def query_parameters(value: object) -> QueryParameters:
|
|
96
|
+
"""Validate and return URL query parameters.
|
|
97
|
+
|
|
98
|
+
:param value: Serialized request-model data.
|
|
99
|
+
:return: Query parameters containing scalar URL values.
|
|
100
|
+
:raises TypeError: If a key or value cannot be represented in a query string.
|
|
101
|
+
"""
|
|
102
|
+
if not isinstance(value, Mapping):
|
|
103
|
+
raise TypeError("Query parameters must be a mapping")
|
|
104
|
+
|
|
105
|
+
result: QueryParameters = {}
|
|
106
|
+
for key, item in value.items():
|
|
107
|
+
if not isinstance(key, str):
|
|
108
|
+
raise TypeError("Query parameter keys must be strings")
|
|
109
|
+
if not isinstance(item, str | int | float | bool):
|
|
110
|
+
raise TypeError(f"Unsupported query parameter type for {key!r}")
|
|
111
|
+
result[key] = item
|
|
112
|
+
return result
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
"""Export the supported Betting namespace and public contracts."""
|
|
2
|
+
|
|
3
|
+
from cfb_data.enums import Classification, SeasonType
|
|
4
|
+
|
|
5
|
+
from .models.pydantic import BettingGame, BettingLinesRequest, GameLine
|
|
6
|
+
from .resource import BettingResource
|
|
7
|
+
|
|
8
|
+
__all__ = [
|
|
9
|
+
"BettingGame",
|
|
10
|
+
"BettingLinesRequest",
|
|
11
|
+
"BettingResource",
|
|
12
|
+
"Classification",
|
|
13
|
+
"GameLine",
|
|
14
|
+
"SeasonType",
|
|
15
|
+
]
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Export Betting model families."""
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
"""Validate request parameters for implemented Betting endpoints."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Self
|
|
6
|
+
|
|
7
|
+
from pydantic import BaseModel, ConfigDict, Field, model_validator
|
|
8
|
+
|
|
9
|
+
from cfb_data._request_rules import _validate_year_or_game_id
|
|
10
|
+
from cfb_data.enums import SeasonType
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class BettingLinesRequest(BaseModel):
|
|
14
|
+
"""Validate filters accepted by ``GET /lines``."""
|
|
15
|
+
|
|
16
|
+
model_config = ConfigDict(populate_by_name=True, extra="forbid")
|
|
17
|
+
|
|
18
|
+
game_id: int | None = Field(default=None, alias="gameId", gt=0)
|
|
19
|
+
year: int | None = Field(default=None, ge=1869)
|
|
20
|
+
season_type: SeasonType | None = Field(default=None, alias="seasonType")
|
|
21
|
+
week: int | None = Field(default=None, ge=0)
|
|
22
|
+
team: str | None = Field(default=None, min_length=1)
|
|
23
|
+
home: str | None = Field(default=None, min_length=1)
|
|
24
|
+
away: str | None = Field(default=None, min_length=1)
|
|
25
|
+
conference: str | None = Field(default=None, min_length=1)
|
|
26
|
+
provider: str | None = Field(default=None, min_length=1)
|
|
27
|
+
|
|
28
|
+
@model_validator(mode="after")
|
|
29
|
+
def validate_selectors(self) -> Self:
|
|
30
|
+
"""Require a season or game identifier."""
|
|
31
|
+
_validate_year_or_game_id(self.year, self.game_id)
|
|
32
|
+
return self
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
__all__ = ["BettingLinesRequest"]
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
"""Validate responses from implemented CFBD Betting endpoints."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from datetime import UTC, datetime
|
|
6
|
+
|
|
7
|
+
from pydantic import BaseModel, ConfigDict, Field, field_validator
|
|
8
|
+
|
|
9
|
+
from cfb_data.enums import Classification, SeasonType
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class _ResponseModel(BaseModel):
|
|
13
|
+
"""Apply the upstream closed-object contract to Betting responses."""
|
|
14
|
+
|
|
15
|
+
model_config = ConfigDict(populate_by_name=True, extra="forbid")
|
|
16
|
+
|
|
17
|
+
@field_validator("*", mode="after", check_fields=False)
|
|
18
|
+
@classmethod
|
|
19
|
+
def require_utc_datetimes(cls, value: object) -> object:
|
|
20
|
+
"""Require aware response timestamps and normalize them to UTC."""
|
|
21
|
+
if not isinstance(value, datetime):
|
|
22
|
+
return value
|
|
23
|
+
if value.tzinfo is None or value.utcoffset() is None:
|
|
24
|
+
raise ValueError("Response timestamps must be timezone-aware")
|
|
25
|
+
return value.astimezone(UTC)
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class GameLine(_ResponseModel):
|
|
29
|
+
"""Represent one provider's line for a game."""
|
|
30
|
+
|
|
31
|
+
provider: str
|
|
32
|
+
spread: float | None
|
|
33
|
+
formatted_spread: str = Field(alias="formattedSpread")
|
|
34
|
+
spread_open: float | None = Field(alias="spreadOpen")
|
|
35
|
+
over_under: float | None = Field(alias="overUnder")
|
|
36
|
+
over_under_open: float | None = Field(alias="overUnderOpen")
|
|
37
|
+
home_moneyline: int | None = Field(alias="homeMoneyline")
|
|
38
|
+
away_moneyline: int | None = Field(alias="awayMoneyline")
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
class BettingGame(_ResponseModel):
|
|
42
|
+
"""Represent one game and all returned provider lines."""
|
|
43
|
+
|
|
44
|
+
id: int = Field(gt=0)
|
|
45
|
+
season: int = Field(ge=1869)
|
|
46
|
+
season_type: SeasonType = Field(alias="seasonType")
|
|
47
|
+
week: int = Field(ge=0)
|
|
48
|
+
start_date: datetime = Field(alias="startDate")
|
|
49
|
+
home_team_id: int = Field(alias="homeTeamId", gt=0)
|
|
50
|
+
home_team: str = Field(alias="homeTeam")
|
|
51
|
+
home_conference: str | None = Field(alias="homeConference")
|
|
52
|
+
home_classification: Classification | None = Field(alias="homeClassification")
|
|
53
|
+
home_score: int | None = Field(alias="homeScore", ge=0)
|
|
54
|
+
away_team_id: int = Field(alias="awayTeamId", gt=0)
|
|
55
|
+
away_team: str = Field(alias="awayTeam")
|
|
56
|
+
away_conference: str | None = Field(alias="awayConference")
|
|
57
|
+
away_classification: Classification | None = Field(alias="awayClassification")
|
|
58
|
+
away_score: int | None = Field(alias="awayScore", ge=0)
|
|
59
|
+
lines: list[GameLine]
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
__all__ = ["BettingGame", "GameLine"]
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
"""Expose typed Betting endpoints through the primary client."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Literal, overload
|
|
6
|
+
|
|
7
|
+
from pydantic import TypeAdapter
|
|
8
|
+
|
|
9
|
+
from cfb_data._dataframes import _DataFrameAdapter
|
|
10
|
+
from cfb_data._executor import _EndpointExecutor
|
|
11
|
+
from cfb_data._requests import _resolve_request
|
|
12
|
+
from cfb_data.betting.models.pydantic.requests import BettingLinesRequest
|
|
13
|
+
from cfb_data.betting.models.pydantic.responses import BettingGame
|
|
14
|
+
from cfb_data.enums import SeasonType
|
|
15
|
+
|
|
16
|
+
type _SeasonTypeArgument = (
|
|
17
|
+
SeasonType
|
|
18
|
+
| Literal[
|
|
19
|
+
"regular",
|
|
20
|
+
"postseason",
|
|
21
|
+
"both",
|
|
22
|
+
"allstar",
|
|
23
|
+
"spring_regular",
|
|
24
|
+
"spring_postseason",
|
|
25
|
+
]
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
_BETTING_GAME_ROWS = TypeAdapter(list[BettingGame])
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class BettingResource[FrameT]:
|
|
32
|
+
"""Provide validated Betting endpoints with selected frame results."""
|
|
33
|
+
|
|
34
|
+
def __init__(
|
|
35
|
+
self,
|
|
36
|
+
executor: _EndpointExecutor,
|
|
37
|
+
dataframe_adapter: _DataFrameAdapter[FrameT],
|
|
38
|
+
) -> None:
|
|
39
|
+
"""Bind the namespace to shared execution and presentation services."""
|
|
40
|
+
self._executor = executor
|
|
41
|
+
self._dataframe_adapter = dataframe_adapter
|
|
42
|
+
|
|
43
|
+
@overload
|
|
44
|
+
async def lines(self, request: BettingLinesRequest, /) -> FrameT: ...
|
|
45
|
+
|
|
46
|
+
@overload
|
|
47
|
+
async def lines(
|
|
48
|
+
self,
|
|
49
|
+
request: None = None,
|
|
50
|
+
/,
|
|
51
|
+
*,
|
|
52
|
+
game_id: int | None = None,
|
|
53
|
+
year: int | None = None,
|
|
54
|
+
season_type: _SeasonTypeArgument | None = None,
|
|
55
|
+
week: int | None = None,
|
|
56
|
+
team: str | None = None,
|
|
57
|
+
home: str | None = None,
|
|
58
|
+
away: str | None = None,
|
|
59
|
+
conference: str | None = None,
|
|
60
|
+
provider: str | None = None,
|
|
61
|
+
) -> FrameT: ...
|
|
62
|
+
|
|
63
|
+
async def lines(
|
|
64
|
+
self, request: BettingLinesRequest | None = None, /, **filters: object
|
|
65
|
+
) -> FrameT:
|
|
66
|
+
"""Return games with nested historical provider lines.
|
|
67
|
+
|
|
68
|
+
:param request: Validated request model, mutually exclusive with filters.
|
|
69
|
+
:param filters: Explicit snake-case endpoint filters.
|
|
70
|
+
:return: Eager frame containing validated betting-game rows.
|
|
71
|
+
:raises TypeError: If request styles are mixed or the model type is wrong.
|
|
72
|
+
:raises CFBDError: If request, transport, response, or conversion fails.
|
|
73
|
+
"""
|
|
74
|
+
endpoint = "/lines"
|
|
75
|
+
validated = _resolve_request(
|
|
76
|
+
endpoint=endpoint,
|
|
77
|
+
request_type=BettingLinesRequest,
|
|
78
|
+
request=request,
|
|
79
|
+
filters=filters,
|
|
80
|
+
)
|
|
81
|
+
rows = await self._executor.fetch_many(
|
|
82
|
+
endpoint=endpoint,
|
|
83
|
+
request=validated,
|
|
84
|
+
response_adapter=_BETTING_GAME_ROWS,
|
|
85
|
+
)
|
|
86
|
+
return self._dataframe_adapter.from_models(
|
|
87
|
+
endpoint=endpoint, row_model=BettingGame, models=rows
|
|
88
|
+
)
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
__all__ = ["BettingResource"]
|