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,291 @@
|
|
|
1
|
+
"""Validate responses from implemented CFBD Stats endpoints."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from pydantic import BaseModel, ConfigDict, Field, field_validator
|
|
6
|
+
|
|
7
|
+
from cfb_data.enums import SeasonType
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class _ResponseModel(BaseModel):
|
|
11
|
+
"""Apply the upstream closed-object contract to Stats responses."""
|
|
12
|
+
|
|
13
|
+
model_config = ConfigDict(populate_by_name=True, extra="forbid")
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class PlayerStat(_ResponseModel):
|
|
17
|
+
"""Represent one player statistic aggregated by season."""
|
|
18
|
+
|
|
19
|
+
season: int = Field(ge=1869)
|
|
20
|
+
player_id: str = Field(alias="playerId")
|
|
21
|
+
player: str
|
|
22
|
+
position: str
|
|
23
|
+
team: str
|
|
24
|
+
conference: str
|
|
25
|
+
category: str
|
|
26
|
+
stat_type: str = Field(alias="statType")
|
|
27
|
+
stat: str
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class PlayerSuccessRateSplit(_ResponseModel):
|
|
31
|
+
"""Represent passing or rushing success-rate totals."""
|
|
32
|
+
|
|
33
|
+
plays: int = Field(ge=0)
|
|
34
|
+
successes: int = Field(ge=0)
|
|
35
|
+
success_rate: float | None = Field(alias="successRate")
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
class PlayerSeasonSuccessRate(_ResponseModel):
|
|
39
|
+
"""Represent one player's passing and rushing season success rates."""
|
|
40
|
+
|
|
41
|
+
season: int = Field(ge=1869)
|
|
42
|
+
id: str
|
|
43
|
+
name: str
|
|
44
|
+
position: str
|
|
45
|
+
team: str
|
|
46
|
+
conference: str
|
|
47
|
+
passing: PlayerSuccessRateSplit
|
|
48
|
+
rushing: PlayerSuccessRateSplit
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
class PlayerGameSuccessRate(_ResponseModel):
|
|
52
|
+
"""Represent one player's passing and rushing success rates for a game."""
|
|
53
|
+
|
|
54
|
+
season: int = Field(ge=1869)
|
|
55
|
+
season_type: SeasonType = Field(alias="seasonType")
|
|
56
|
+
week: int = Field(ge=0)
|
|
57
|
+
game_id: int = Field(alias="gameId", gt=0)
|
|
58
|
+
id: str
|
|
59
|
+
name: str
|
|
60
|
+
position: str
|
|
61
|
+
team: str
|
|
62
|
+
conference: str
|
|
63
|
+
opponent: str
|
|
64
|
+
passing: PlayerSuccessRateSplit
|
|
65
|
+
rushing: PlayerSuccessRateSplit
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
class TeamStat(_ResponseModel):
|
|
69
|
+
"""Represent one season-level team statistic without scalar coercion."""
|
|
70
|
+
|
|
71
|
+
season: int = Field(ge=1869)
|
|
72
|
+
team: str
|
|
73
|
+
conference: str
|
|
74
|
+
stat_name: str = Field(alias="statName")
|
|
75
|
+
stat_value: str | int | float = Field(alias="statValue")
|
|
76
|
+
|
|
77
|
+
@field_validator("stat_value", mode="before")
|
|
78
|
+
@classmethod
|
|
79
|
+
def reject_non_stat_scalars(cls, value: object) -> object:
|
|
80
|
+
"""Accept only upstream string and numeric statistic values."""
|
|
81
|
+
if isinstance(value, bool) or not isinstance(value, str | int | float):
|
|
82
|
+
raise ValueError("statValue must be a string or number")
|
|
83
|
+
return value
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
class StatCategory(_ResponseModel):
|
|
87
|
+
"""Represent one team-stat category for tabular presentation."""
|
|
88
|
+
|
|
89
|
+
category: str
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
class FieldPosition(_ResponseModel):
|
|
93
|
+
"""Represent average starting field position and predicted points."""
|
|
94
|
+
|
|
95
|
+
average_start: float | None = Field(alias="averageStart")
|
|
96
|
+
average_predicted_points: float | None = Field(alias="averagePredictedPoints")
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
class Havoc(_ResponseModel):
|
|
100
|
+
"""Represent aggregate havoc rates by position group."""
|
|
101
|
+
|
|
102
|
+
total: float | None
|
|
103
|
+
front_seven: float | None = Field(alias="frontSeven")
|
|
104
|
+
db: float | None
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
class SeasonDownStats(_ResponseModel):
|
|
108
|
+
"""Represent advanced season metrics for one down classification."""
|
|
109
|
+
|
|
110
|
+
rate: float
|
|
111
|
+
ppa: float
|
|
112
|
+
success_rate: float = Field(alias="successRate")
|
|
113
|
+
explosiveness: float | None
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
class SeasonPassingDownStats(SeasonDownStats):
|
|
117
|
+
"""Represent defensive passing-down season metrics."""
|
|
118
|
+
|
|
119
|
+
total_ppa: float = Field(alias="totalPPA")
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
class SeasonPlayStats(_ResponseModel):
|
|
123
|
+
"""Represent advanced season metrics for one play classification."""
|
|
124
|
+
|
|
125
|
+
rate: float
|
|
126
|
+
ppa: float
|
|
127
|
+
total_ppa: float = Field(alias="totalPPA")
|
|
128
|
+
success_rate: float = Field(alias="successRate")
|
|
129
|
+
explosiveness: float | None
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
class AdvancedSeasonOffense(_ResponseModel):
|
|
133
|
+
"""Represent a team's advanced season offense metrics."""
|
|
134
|
+
|
|
135
|
+
plays: int = Field(ge=0)
|
|
136
|
+
drives: int = Field(ge=0)
|
|
137
|
+
ppa: float
|
|
138
|
+
total_ppa: float = Field(alias="totalPPA")
|
|
139
|
+
success_rate: float = Field(alias="successRate")
|
|
140
|
+
explosiveness: float | None
|
|
141
|
+
power_success: float | None = Field(alias="powerSuccess")
|
|
142
|
+
stuff_rate: float = Field(alias="stuffRate")
|
|
143
|
+
line_yards: float = Field(alias="lineYards")
|
|
144
|
+
line_yards_total: int = Field(alias="lineYardsTotal")
|
|
145
|
+
second_level_yards: float = Field(alias="secondLevelYards")
|
|
146
|
+
second_level_yards_total: int = Field(alias="secondLevelYardsTotal")
|
|
147
|
+
open_field_yards: float = Field(alias="openFieldYards")
|
|
148
|
+
open_field_yards_total: int = Field(alias="openFieldYardsTotal")
|
|
149
|
+
total_opportunities: int = Field(alias="totalOpportunies")
|
|
150
|
+
points_per_opportunity: float = Field(alias="pointsPerOpportunity")
|
|
151
|
+
field_position: FieldPosition = Field(alias="fieldPosition")
|
|
152
|
+
havoc: Havoc
|
|
153
|
+
standard_downs: SeasonDownStats = Field(alias="standardDowns")
|
|
154
|
+
passing_downs: SeasonDownStats = Field(alias="passingDowns")
|
|
155
|
+
rushing_plays: SeasonPlayStats = Field(alias="rushingPlays")
|
|
156
|
+
passing_plays: SeasonPlayStats = Field(alias="passingPlays")
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
class AdvancedSeasonDefense(AdvancedSeasonOffense):
|
|
160
|
+
"""Represent a team's advanced season defense metrics."""
|
|
161
|
+
|
|
162
|
+
passing_downs: SeasonPassingDownStats = Field(alias="passingDowns")
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
class AdvancedSeasonStat(_ResponseModel):
|
|
166
|
+
"""Represent advanced team metrics aggregated by season."""
|
|
167
|
+
|
|
168
|
+
season: int = Field(ge=1869)
|
|
169
|
+
team: str
|
|
170
|
+
conference: str
|
|
171
|
+
offense: AdvancedSeasonOffense
|
|
172
|
+
defense: AdvancedSeasonDefense
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
class GameDownStats(_ResponseModel):
|
|
176
|
+
"""Represent advanced game metrics for one down classification."""
|
|
177
|
+
|
|
178
|
+
ppa: float
|
|
179
|
+
success_rate: float = Field(alias="successRate")
|
|
180
|
+
explosiveness: float | None
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
class GamePlayStats(_ResponseModel):
|
|
184
|
+
"""Represent advanced game metrics for one play classification."""
|
|
185
|
+
|
|
186
|
+
ppa: float
|
|
187
|
+
total_ppa: float = Field(alias="totalPPA")
|
|
188
|
+
success_rate: float = Field(alias="successRate")
|
|
189
|
+
explosiveness: float | None
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
class _AdvancedGameUnit(_ResponseModel):
|
|
193
|
+
"""Represent fields shared by advanced game offense and defense."""
|
|
194
|
+
|
|
195
|
+
plays: int = Field(ge=0)
|
|
196
|
+
drives: int = Field(ge=0)
|
|
197
|
+
ppa: float
|
|
198
|
+
total_ppa: float = Field(alias="totalPPA")
|
|
199
|
+
success_rate: float = Field(alias="successRate")
|
|
200
|
+
explosiveness: float | None
|
|
201
|
+
power_success: float | None = Field(alias="powerSuccess")
|
|
202
|
+
stuff_rate: float = Field(alias="stuffRate")
|
|
203
|
+
line_yards: float = Field(alias="lineYards")
|
|
204
|
+
line_yards_total: int = Field(alias="lineYardsTotal")
|
|
205
|
+
second_level_yards: float = Field(alias="secondLevelYards")
|
|
206
|
+
second_level_yards_total: int = Field(alias="secondLevelYardsTotal")
|
|
207
|
+
open_field_yards: float | None = Field(alias="openFieldYards")
|
|
208
|
+
open_field_yards_total: int | None = Field(alias="openFieldYardsTotal")
|
|
209
|
+
standard_downs: GameDownStats = Field(alias="standardDowns")
|
|
210
|
+
passing_downs: GameDownStats = Field(alias="passingDowns")
|
|
211
|
+
rushing_plays: GamePlayStats = Field(alias="rushingPlays")
|
|
212
|
+
passing_plays: GamePlayStats = Field(alias="passingPlays")
|
|
213
|
+
|
|
214
|
+
|
|
215
|
+
class AdvancedGameOffense(_AdvancedGameUnit):
|
|
216
|
+
"""Represent a team's advanced offense metrics for one game."""
|
|
217
|
+
|
|
218
|
+
open_field_yards_total: int = Field(alias="openFieldYardsTotal")
|
|
219
|
+
|
|
220
|
+
|
|
221
|
+
class AdvancedGameDefense(_AdvancedGameUnit):
|
|
222
|
+
"""Represent a team's advanced defense metrics for one game."""
|
|
223
|
+
|
|
224
|
+
explosiveness: float
|
|
225
|
+
open_field_yards: float = Field(alias="openFieldYards")
|
|
226
|
+
open_field_yards_total: int | None = Field(alias="openFieldYardsTotal")
|
|
227
|
+
|
|
228
|
+
|
|
229
|
+
class AdvancedGameStat(_ResponseModel):
|
|
230
|
+
"""Represent advanced team metrics for one game."""
|
|
231
|
+
|
|
232
|
+
game_id: int = Field(alias="gameId", gt=0)
|
|
233
|
+
season: int = Field(ge=1869)
|
|
234
|
+
season_type: SeasonType = Field(alias="seasonType")
|
|
235
|
+
week: int = Field(ge=0)
|
|
236
|
+
team: str
|
|
237
|
+
opponent: str
|
|
238
|
+
offense: AdvancedGameOffense
|
|
239
|
+
defense: AdvancedGameDefense
|
|
240
|
+
|
|
241
|
+
|
|
242
|
+
class GameHavocUnit(_ResponseModel):
|
|
243
|
+
"""Represent havoc event counts and rates for one side of a game."""
|
|
244
|
+
|
|
245
|
+
total_plays: float = Field(alias="totalPlays", ge=0)
|
|
246
|
+
total_havoc_events: float = Field(alias="totalHavocEvents", ge=0)
|
|
247
|
+
front_seven_havoc_events: float = Field(alias="frontSevenHavocEvents", ge=0)
|
|
248
|
+
db_havoc_events: float = Field(alias="dbHavocEvents", ge=0)
|
|
249
|
+
havoc_rate: float = Field(alias="havocRate")
|
|
250
|
+
front_seven_havoc_rate: float = Field(alias="frontSevenHavocRate")
|
|
251
|
+
db_havoc_rate: float = Field(alias="dbHavocRate")
|
|
252
|
+
|
|
253
|
+
|
|
254
|
+
class GameHavocStats(_ResponseModel):
|
|
255
|
+
"""Represent team havoc statistics for one game."""
|
|
256
|
+
|
|
257
|
+
game_id: int = Field(alias="gameId", gt=0)
|
|
258
|
+
season: int = Field(ge=1869)
|
|
259
|
+
season_type: SeasonType = Field(alias="seasonType")
|
|
260
|
+
week: int = Field(ge=0)
|
|
261
|
+
team: str
|
|
262
|
+
conference: str | None
|
|
263
|
+
opponent: str
|
|
264
|
+
opponent_conference: str | None = Field(alias="opponentConference")
|
|
265
|
+
offense: GameHavocUnit
|
|
266
|
+
defense: GameHavocUnit
|
|
267
|
+
|
|
268
|
+
|
|
269
|
+
__all__ = [
|
|
270
|
+
"AdvancedGameDefense",
|
|
271
|
+
"AdvancedGameOffense",
|
|
272
|
+
"AdvancedGameStat",
|
|
273
|
+
"AdvancedSeasonDefense",
|
|
274
|
+
"AdvancedSeasonOffense",
|
|
275
|
+
"AdvancedSeasonStat",
|
|
276
|
+
"FieldPosition",
|
|
277
|
+
"GameDownStats",
|
|
278
|
+
"GameHavocStats",
|
|
279
|
+
"GameHavocUnit",
|
|
280
|
+
"GamePlayStats",
|
|
281
|
+
"Havoc",
|
|
282
|
+
"PlayerGameSuccessRate",
|
|
283
|
+
"PlayerSeasonSuccessRate",
|
|
284
|
+
"PlayerStat",
|
|
285
|
+
"PlayerSuccessRateSplit",
|
|
286
|
+
"SeasonDownStats",
|
|
287
|
+
"SeasonPassingDownStats",
|
|
288
|
+
"SeasonPlayStats",
|
|
289
|
+
"StatCategory",
|
|
290
|
+
"TeamStat",
|
|
291
|
+
]
|
|
@@ -0,0 +1,400 @@
|
|
|
1
|
+
"""Expose typed Stats endpoints through the primary client."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from collections.abc import Mapping
|
|
6
|
+
from typing import Literal, TypeVar, overload
|
|
7
|
+
|
|
8
|
+
from pydantic import BaseModel, ConfigDict, 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.enums import Classification, SeasonType
|
|
14
|
+
from cfb_data.stats.models.pydantic.requests import (
|
|
15
|
+
AdvancedGameStatsRequest,
|
|
16
|
+
AdvancedSeasonStatsRequest,
|
|
17
|
+
GameHavocRequest,
|
|
18
|
+
PlayerGameSuccessRequest,
|
|
19
|
+
PlayerSeasonStatsRequest,
|
|
20
|
+
PlayerSeasonSuccessRequest,
|
|
21
|
+
TeamSeasonStatsRequest,
|
|
22
|
+
)
|
|
23
|
+
from cfb_data.stats.models.pydantic.responses import (
|
|
24
|
+
AdvancedGameStat,
|
|
25
|
+
AdvancedSeasonStat,
|
|
26
|
+
GameHavocStats,
|
|
27
|
+
PlayerGameSuccessRate,
|
|
28
|
+
PlayerSeasonSuccessRate,
|
|
29
|
+
PlayerStat,
|
|
30
|
+
StatCategory,
|
|
31
|
+
TeamStat,
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
_RequestT = TypeVar("_RequestT", bound=BaseModel)
|
|
35
|
+
_RowT = TypeVar("_RowT", bound=BaseModel)
|
|
36
|
+
type _ClassificationArgument = Classification | Literal["fbs", "fcs", "ii", "iii"]
|
|
37
|
+
type _SeasonTypeArgument = (
|
|
38
|
+
SeasonType
|
|
39
|
+
| Literal[
|
|
40
|
+
"regular",
|
|
41
|
+
"postseason",
|
|
42
|
+
"both",
|
|
43
|
+
"allstar",
|
|
44
|
+
"spring_regular",
|
|
45
|
+
"spring_postseason",
|
|
46
|
+
]
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
_PLAYER_STAT_ROWS = TypeAdapter(list[PlayerStat])
|
|
50
|
+
_PLAYER_SEASON_SUCCESS_ROWS = TypeAdapter(list[PlayerSeasonSuccessRate])
|
|
51
|
+
_PLAYER_GAME_SUCCESS_ROWS = TypeAdapter(list[PlayerGameSuccessRate])
|
|
52
|
+
_TEAM_STAT_ROWS = TypeAdapter(list[TeamStat])
|
|
53
|
+
_CATEGORY_VALUES = TypeAdapter(list[str])
|
|
54
|
+
_ADVANCED_SEASON_ROWS = TypeAdapter(list[AdvancedSeasonStat])
|
|
55
|
+
_ADVANCED_GAME_ROWS = TypeAdapter(list[AdvancedGameStat])
|
|
56
|
+
_GAME_HAVOC_ROWS = TypeAdapter(list[GameHavocStats])
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
class _CategoriesRequest(BaseModel):
|
|
60
|
+
"""Represent the empty filter set accepted by ``GET /stats/categories``."""
|
|
61
|
+
|
|
62
|
+
model_config = ConfigDict(extra="forbid")
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
_CATEGORIES_REQUEST = _CategoriesRequest()
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
class StatsResource[FrameT]:
|
|
69
|
+
"""Provide validated Stats endpoints with backend-specific frame results."""
|
|
70
|
+
|
|
71
|
+
def __init__(
|
|
72
|
+
self,
|
|
73
|
+
executor: _EndpointExecutor,
|
|
74
|
+
dataframe_adapter: _DataFrameAdapter[FrameT],
|
|
75
|
+
) -> None:
|
|
76
|
+
"""Bind the namespace to shared execution and presentation services."""
|
|
77
|
+
self._executor = executor
|
|
78
|
+
self._dataframe_adapter = dataframe_adapter
|
|
79
|
+
|
|
80
|
+
@overload
|
|
81
|
+
async def player_season(self, request: PlayerSeasonStatsRequest, /) -> FrameT: ...
|
|
82
|
+
|
|
83
|
+
@overload
|
|
84
|
+
async def player_season(
|
|
85
|
+
self,
|
|
86
|
+
request: None = None,
|
|
87
|
+
/,
|
|
88
|
+
*,
|
|
89
|
+
year: int,
|
|
90
|
+
conference: str | None = None,
|
|
91
|
+
team: str | None = None,
|
|
92
|
+
start_week: int | None = None,
|
|
93
|
+
end_week: int | None = None,
|
|
94
|
+
season_type: _SeasonTypeArgument | None = None,
|
|
95
|
+
category: str | None = None,
|
|
96
|
+
) -> FrameT: ...
|
|
97
|
+
|
|
98
|
+
async def player_season(
|
|
99
|
+
self, request: PlayerSeasonStatsRequest | None = None, /, **filters: object
|
|
100
|
+
) -> FrameT:
|
|
101
|
+
"""Return player statistics aggregated by season.
|
|
102
|
+
|
|
103
|
+
:param request: Validated request model, mutually exclusive with filters.
|
|
104
|
+
:param filters: Explicit snake-case endpoint filters.
|
|
105
|
+
:return: Eager frame containing validated ``PlayerStat`` rows.
|
|
106
|
+
:raises TypeError: If request styles are mixed or the model type is wrong.
|
|
107
|
+
:raises CFBDError: If request, transport, response, or conversion fails.
|
|
108
|
+
"""
|
|
109
|
+
return await self._fetch_many(
|
|
110
|
+
endpoint="/stats/player/season",
|
|
111
|
+
request_type=PlayerSeasonStatsRequest,
|
|
112
|
+
request=request,
|
|
113
|
+
filters=filters,
|
|
114
|
+
response_adapter=_PLAYER_STAT_ROWS,
|
|
115
|
+
row_model=PlayerStat,
|
|
116
|
+
)
|
|
117
|
+
|
|
118
|
+
@overload
|
|
119
|
+
async def player_season_success(
|
|
120
|
+
self, request: PlayerSeasonSuccessRequest, /
|
|
121
|
+
) -> FrameT: ...
|
|
122
|
+
|
|
123
|
+
@overload
|
|
124
|
+
async def player_season_success(
|
|
125
|
+
self,
|
|
126
|
+
request: None = None,
|
|
127
|
+
/,
|
|
128
|
+
*,
|
|
129
|
+
year: int | None = None,
|
|
130
|
+
conference: str | None = None,
|
|
131
|
+
team: str | None = None,
|
|
132
|
+
player_id: int | None = None,
|
|
133
|
+
season_type: _SeasonTypeArgument | None = None,
|
|
134
|
+
start_week: int | None = None,
|
|
135
|
+
end_week: int | None = None,
|
|
136
|
+
threshold: int | None = None,
|
|
137
|
+
exclude_garbage_time: bool | None = None,
|
|
138
|
+
) -> FrameT: ...
|
|
139
|
+
|
|
140
|
+
async def player_season_success(
|
|
141
|
+
self, request: PlayerSeasonSuccessRequest | None = None, /, **filters: object
|
|
142
|
+
) -> FrameT:
|
|
143
|
+
"""Return player passing and rushing success rates by season.
|
|
144
|
+
|
|
145
|
+
:param request: Validated request model, mutually exclusive with filters.
|
|
146
|
+
:param filters: Explicit snake-case endpoint filters.
|
|
147
|
+
:return: Eager frame containing validated season success-rate rows.
|
|
148
|
+
:raises TypeError: If request styles are mixed or the model type is wrong.
|
|
149
|
+
:raises CFBDError: If request, transport, response, or conversion fails.
|
|
150
|
+
"""
|
|
151
|
+
return await self._fetch_many(
|
|
152
|
+
endpoint="/stats/player/success",
|
|
153
|
+
request_type=PlayerSeasonSuccessRequest,
|
|
154
|
+
request=request,
|
|
155
|
+
filters=filters,
|
|
156
|
+
response_adapter=_PLAYER_SEASON_SUCCESS_ROWS,
|
|
157
|
+
row_model=PlayerSeasonSuccessRate,
|
|
158
|
+
)
|
|
159
|
+
|
|
160
|
+
@overload
|
|
161
|
+
async def player_game_success(
|
|
162
|
+
self, request: PlayerGameSuccessRequest, /
|
|
163
|
+
) -> FrameT: ...
|
|
164
|
+
|
|
165
|
+
@overload
|
|
166
|
+
async def player_game_success(
|
|
167
|
+
self,
|
|
168
|
+
request: None = None,
|
|
169
|
+
/,
|
|
170
|
+
*,
|
|
171
|
+
year: int,
|
|
172
|
+
week: int | None = None,
|
|
173
|
+
season_type: _SeasonTypeArgument | None = None,
|
|
174
|
+
conference: str | None = None,
|
|
175
|
+
team: str | None = None,
|
|
176
|
+
player_id: int | None = None,
|
|
177
|
+
threshold: int | None = None,
|
|
178
|
+
exclude_garbage_time: bool | None = None,
|
|
179
|
+
) -> FrameT: ...
|
|
180
|
+
|
|
181
|
+
async def player_game_success(
|
|
182
|
+
self, request: PlayerGameSuccessRequest | None = None, /, **filters: object
|
|
183
|
+
) -> FrameT:
|
|
184
|
+
"""Return player passing and rushing success rates by game.
|
|
185
|
+
|
|
186
|
+
:param request: Validated request model, mutually exclusive with filters.
|
|
187
|
+
:param filters: Explicit snake-case endpoint filters.
|
|
188
|
+
:return: Eager frame containing validated game success-rate rows.
|
|
189
|
+
:raises TypeError: If request styles are mixed or the model type is wrong.
|
|
190
|
+
:raises CFBDError: If request, transport, response, or conversion fails.
|
|
191
|
+
"""
|
|
192
|
+
return await self._fetch_many(
|
|
193
|
+
endpoint="/stats/player/success/game",
|
|
194
|
+
request_type=PlayerGameSuccessRequest,
|
|
195
|
+
request=request,
|
|
196
|
+
filters=filters,
|
|
197
|
+
response_adapter=_PLAYER_GAME_SUCCESS_ROWS,
|
|
198
|
+
row_model=PlayerGameSuccessRate,
|
|
199
|
+
)
|
|
200
|
+
|
|
201
|
+
@overload
|
|
202
|
+
async def team_season(self, request: TeamSeasonStatsRequest, /) -> FrameT: ...
|
|
203
|
+
|
|
204
|
+
@overload
|
|
205
|
+
async def team_season(
|
|
206
|
+
self,
|
|
207
|
+
request: None = None,
|
|
208
|
+
/,
|
|
209
|
+
*,
|
|
210
|
+
year: int | None = None,
|
|
211
|
+
team: str | None = None,
|
|
212
|
+
conference: str | None = None,
|
|
213
|
+
start_week: int | None = None,
|
|
214
|
+
end_week: int | None = None,
|
|
215
|
+
classification: _ClassificationArgument | None = None,
|
|
216
|
+
) -> FrameT: ...
|
|
217
|
+
|
|
218
|
+
async def team_season(
|
|
219
|
+
self, request: TeamSeasonStatsRequest | None = None, /, **filters: object
|
|
220
|
+
) -> FrameT:
|
|
221
|
+
"""Return team statistics aggregated by season.
|
|
222
|
+
|
|
223
|
+
``stat_value`` preserves upstream strings and numbers in an object-typed
|
|
224
|
+
column for both supported DataFrame backends.
|
|
225
|
+
|
|
226
|
+
:param request: Validated request model, mutually exclusive with filters.
|
|
227
|
+
:param filters: Explicit snake-case endpoint filters.
|
|
228
|
+
:return: Eager frame containing validated ``TeamStat`` rows.
|
|
229
|
+
:raises TypeError: If request styles are mixed or the model type is wrong.
|
|
230
|
+
:raises CFBDError: If request, transport, response, or conversion fails.
|
|
231
|
+
"""
|
|
232
|
+
return await self._fetch_many(
|
|
233
|
+
endpoint="/stats/season",
|
|
234
|
+
request_type=TeamSeasonStatsRequest,
|
|
235
|
+
request=request,
|
|
236
|
+
filters=filters,
|
|
237
|
+
response_adapter=_TEAM_STAT_ROWS,
|
|
238
|
+
row_model=TeamStat,
|
|
239
|
+
)
|
|
240
|
+
|
|
241
|
+
async def categories(self) -> FrameT:
|
|
242
|
+
"""Return team-stat categories as a one-column selected frame.
|
|
243
|
+
|
|
244
|
+
:return: Eager frame with one ``category`` column in upstream order.
|
|
245
|
+
:raises CFBDError: If transport, response, or conversion fails.
|
|
246
|
+
"""
|
|
247
|
+
values = await self._executor.fetch_values(
|
|
248
|
+
endpoint="/stats/categories",
|
|
249
|
+
request=_CATEGORIES_REQUEST,
|
|
250
|
+
response_adapter=_CATEGORY_VALUES,
|
|
251
|
+
)
|
|
252
|
+
rows = [StatCategory(category=value) for value in values]
|
|
253
|
+
return self._dataframe_adapter.from_models(
|
|
254
|
+
endpoint="/stats/categories",
|
|
255
|
+
row_model=StatCategory,
|
|
256
|
+
models=rows,
|
|
257
|
+
)
|
|
258
|
+
|
|
259
|
+
@overload
|
|
260
|
+
async def advanced_season(
|
|
261
|
+
self, request: AdvancedSeasonStatsRequest, /
|
|
262
|
+
) -> FrameT: ...
|
|
263
|
+
|
|
264
|
+
@overload
|
|
265
|
+
async def advanced_season(
|
|
266
|
+
self,
|
|
267
|
+
request: None = None,
|
|
268
|
+
/,
|
|
269
|
+
*,
|
|
270
|
+
year: int | None = None,
|
|
271
|
+
team: str | None = None,
|
|
272
|
+
exclude_garbage_time: bool | None = None,
|
|
273
|
+
start_week: int | None = None,
|
|
274
|
+
end_week: int | None = None,
|
|
275
|
+
classification: _ClassificationArgument | None = None,
|
|
276
|
+
) -> FrameT: ...
|
|
277
|
+
|
|
278
|
+
async def advanced_season(
|
|
279
|
+
self, request: AdvancedSeasonStatsRequest | None = None, /, **filters: object
|
|
280
|
+
) -> FrameT:
|
|
281
|
+
"""Return advanced team statistics aggregated by season.
|
|
282
|
+
|
|
283
|
+
:param request: Validated request model, mutually exclusive with filters.
|
|
284
|
+
:param filters: Explicit snake-case endpoint filters.
|
|
285
|
+
:return: Eager frame containing validated advanced season rows.
|
|
286
|
+
:raises TypeError: If request styles are mixed or the model type is wrong.
|
|
287
|
+
:raises CFBDError: If request, transport, response, or conversion fails.
|
|
288
|
+
"""
|
|
289
|
+
return await self._fetch_many(
|
|
290
|
+
endpoint="/stats/season/advanced",
|
|
291
|
+
request_type=AdvancedSeasonStatsRequest,
|
|
292
|
+
request=request,
|
|
293
|
+
filters=filters,
|
|
294
|
+
response_adapter=_ADVANCED_SEASON_ROWS,
|
|
295
|
+
row_model=AdvancedSeasonStat,
|
|
296
|
+
)
|
|
297
|
+
|
|
298
|
+
@overload
|
|
299
|
+
async def advanced_game(self, request: AdvancedGameStatsRequest, /) -> FrameT: ...
|
|
300
|
+
|
|
301
|
+
@overload
|
|
302
|
+
async def advanced_game(
|
|
303
|
+
self,
|
|
304
|
+
request: None = None,
|
|
305
|
+
/,
|
|
306
|
+
*,
|
|
307
|
+
year: int | None = None,
|
|
308
|
+
team: str | None = None,
|
|
309
|
+
week: int | None = None,
|
|
310
|
+
opponent: str | None = None,
|
|
311
|
+
exclude_garbage_time: bool | None = None,
|
|
312
|
+
season_type: _SeasonTypeArgument | None = None,
|
|
313
|
+
) -> FrameT: ...
|
|
314
|
+
|
|
315
|
+
async def advanced_game(
|
|
316
|
+
self, request: AdvancedGameStatsRequest | None = None, /, **filters: object
|
|
317
|
+
) -> FrameT:
|
|
318
|
+
"""Return advanced team statistics by game.
|
|
319
|
+
|
|
320
|
+
:param request: Validated request model, mutually exclusive with filters.
|
|
321
|
+
:param filters: Explicit snake-case endpoint filters.
|
|
322
|
+
:return: Eager frame containing validated advanced game rows.
|
|
323
|
+
:raises TypeError: If request styles are mixed or the model type is wrong.
|
|
324
|
+
:raises CFBDError: If request, transport, response, or conversion fails.
|
|
325
|
+
"""
|
|
326
|
+
return await self._fetch_many(
|
|
327
|
+
endpoint="/stats/game/advanced",
|
|
328
|
+
request_type=AdvancedGameStatsRequest,
|
|
329
|
+
request=request,
|
|
330
|
+
filters=filters,
|
|
331
|
+
response_adapter=_ADVANCED_GAME_ROWS,
|
|
332
|
+
row_model=AdvancedGameStat,
|
|
333
|
+
)
|
|
334
|
+
|
|
335
|
+
@overload
|
|
336
|
+
async def game_havoc(self, request: GameHavocRequest, /) -> FrameT: ...
|
|
337
|
+
|
|
338
|
+
@overload
|
|
339
|
+
async def game_havoc(
|
|
340
|
+
self,
|
|
341
|
+
request: None = None,
|
|
342
|
+
/,
|
|
343
|
+
*,
|
|
344
|
+
year: int | None = None,
|
|
345
|
+
team: str | None = None,
|
|
346
|
+
week: int | None = None,
|
|
347
|
+
opponent: str | None = None,
|
|
348
|
+
season_type: _SeasonTypeArgument | None = None,
|
|
349
|
+
) -> FrameT: ...
|
|
350
|
+
|
|
351
|
+
async def game_havoc(
|
|
352
|
+
self, request: GameHavocRequest | None = None, /, **filters: object
|
|
353
|
+
) -> FrameT:
|
|
354
|
+
"""Return team havoc statistics by game.
|
|
355
|
+
|
|
356
|
+
:param request: Validated request model, mutually exclusive with filters.
|
|
357
|
+
:param filters: Explicit snake-case endpoint filters.
|
|
358
|
+
:return: Eager frame containing validated game havoc rows.
|
|
359
|
+
:raises TypeError: If request styles are mixed or the model type is wrong.
|
|
360
|
+
:raises CFBDError: If request, transport, response, or conversion fails.
|
|
361
|
+
"""
|
|
362
|
+
return await self._fetch_many(
|
|
363
|
+
endpoint="/stats/game/havoc",
|
|
364
|
+
request_type=GameHavocRequest,
|
|
365
|
+
request=request,
|
|
366
|
+
filters=filters,
|
|
367
|
+
response_adapter=_GAME_HAVOC_ROWS,
|
|
368
|
+
row_model=GameHavocStats,
|
|
369
|
+
)
|
|
370
|
+
|
|
371
|
+
async def _fetch_many(
|
|
372
|
+
self,
|
|
373
|
+
*,
|
|
374
|
+
endpoint: str,
|
|
375
|
+
request_type: type[_RequestT],
|
|
376
|
+
request: _RequestT | None,
|
|
377
|
+
filters: Mapping[str, object],
|
|
378
|
+
response_adapter: TypeAdapter[list[_RowT]],
|
|
379
|
+
row_model: type[_RowT],
|
|
380
|
+
) -> FrameT:
|
|
381
|
+
"""Validate, fetch, and convert one Stats list route."""
|
|
382
|
+
validated = _resolve_request(
|
|
383
|
+
endpoint=endpoint,
|
|
384
|
+
request_type=request_type,
|
|
385
|
+
request=request,
|
|
386
|
+
filters=filters,
|
|
387
|
+
)
|
|
388
|
+
rows = await self._executor.fetch_many(
|
|
389
|
+
endpoint=endpoint,
|
|
390
|
+
request=validated,
|
|
391
|
+
response_adapter=response_adapter,
|
|
392
|
+
)
|
|
393
|
+
return self._dataframe_adapter.from_models(
|
|
394
|
+
endpoint=endpoint,
|
|
395
|
+
row_model=row_model,
|
|
396
|
+
models=rows,
|
|
397
|
+
)
|
|
398
|
+
|
|
399
|
+
|
|
400
|
+
__all__ = ["StatsResource"]
|