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,180 @@
|
|
|
1
|
+
"""Expose typed Recruiting 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, 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 RecruitClassification
|
|
14
|
+
from cfb_data.recruiting.models.pydantic.requests import (
|
|
15
|
+
RecruitingGroupsRequest,
|
|
16
|
+
RecruitingPlayersRequest,
|
|
17
|
+
RecruitingTeamsRequest,
|
|
18
|
+
)
|
|
19
|
+
from cfb_data.recruiting.models.pydantic.responses import (
|
|
20
|
+
AggregatedTeamRecruiting,
|
|
21
|
+
Recruit,
|
|
22
|
+
TeamRecruitingRanking,
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
_RequestT = TypeVar("_RequestT", bound=BaseModel)
|
|
26
|
+
_RowT = TypeVar("_RowT", bound=BaseModel)
|
|
27
|
+
type _RecruitClassificationArgument = (
|
|
28
|
+
RecruitClassification | Literal["JUCO", "PrepSchool", "HighSchool"]
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
_RECRUIT_ROWS = TypeAdapter(list[Recruit])
|
|
32
|
+
_TEAM_RANKING_ROWS = TypeAdapter(list[TeamRecruitingRanking])
|
|
33
|
+
_GROUP_ROWS = TypeAdapter(list[AggregatedTeamRecruiting])
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class RecruitingResource[FrameT]:
|
|
37
|
+
"""Provide validated Recruiting endpoints with selected frame results."""
|
|
38
|
+
|
|
39
|
+
def __init__(
|
|
40
|
+
self,
|
|
41
|
+
executor: _EndpointExecutor,
|
|
42
|
+
dataframe_adapter: _DataFrameAdapter[FrameT],
|
|
43
|
+
) -> None:
|
|
44
|
+
"""Bind the namespace to shared execution and presentation services."""
|
|
45
|
+
self._executor = executor
|
|
46
|
+
self._dataframe_adapter = dataframe_adapter
|
|
47
|
+
|
|
48
|
+
@overload
|
|
49
|
+
async def players(self, request: RecruitingPlayersRequest, /) -> FrameT: ...
|
|
50
|
+
|
|
51
|
+
@overload
|
|
52
|
+
async def players(
|
|
53
|
+
self,
|
|
54
|
+
request: None = None,
|
|
55
|
+
/,
|
|
56
|
+
*,
|
|
57
|
+
year: int | None = None,
|
|
58
|
+
team: str | None = None,
|
|
59
|
+
position: str | None = None,
|
|
60
|
+
state: str | None = None,
|
|
61
|
+
classification: _RecruitClassificationArgument | None = None,
|
|
62
|
+
) -> FrameT: ...
|
|
63
|
+
|
|
64
|
+
async def players(
|
|
65
|
+
self, request: RecruitingPlayersRequest | None = None, /, **filters: object
|
|
66
|
+
) -> FrameT:
|
|
67
|
+
"""Return ranked recruiting prospects.
|
|
68
|
+
|
|
69
|
+
:param request: Validated request model, mutually exclusive with filters.
|
|
70
|
+
:param filters: Explicit snake-case endpoint filters.
|
|
71
|
+
:return: Eager frame containing validated recruit rows.
|
|
72
|
+
:raises TypeError: If request styles are mixed or the model type is wrong.
|
|
73
|
+
:raises CFBDError: If request, transport, response, or conversion fails.
|
|
74
|
+
"""
|
|
75
|
+
return await self._fetch_many(
|
|
76
|
+
endpoint="/recruiting/players",
|
|
77
|
+
request_type=RecruitingPlayersRequest,
|
|
78
|
+
request=request,
|
|
79
|
+
filters=filters,
|
|
80
|
+
response_adapter=_RECRUIT_ROWS,
|
|
81
|
+
row_model=Recruit,
|
|
82
|
+
)
|
|
83
|
+
|
|
84
|
+
@overload
|
|
85
|
+
async def teams(self, request: RecruitingTeamsRequest, /) -> FrameT: ...
|
|
86
|
+
|
|
87
|
+
@overload
|
|
88
|
+
async def teams(
|
|
89
|
+
self,
|
|
90
|
+
request: None = None,
|
|
91
|
+
/,
|
|
92
|
+
*,
|
|
93
|
+
year: int | None = None,
|
|
94
|
+
team: str | None = None,
|
|
95
|
+
) -> FrameT: ...
|
|
96
|
+
|
|
97
|
+
async def teams(
|
|
98
|
+
self, request: RecruitingTeamsRequest | None = None, /, **filters: object
|
|
99
|
+
) -> FrameT:
|
|
100
|
+
"""Return team recruiting class rankings.
|
|
101
|
+
|
|
102
|
+
:param request: Validated request model, mutually exclusive with filters.
|
|
103
|
+
:param filters: Explicit snake-case endpoint filters.
|
|
104
|
+
:return: Eager frame containing validated team ranking rows.
|
|
105
|
+
:raises TypeError: If request styles are mixed or the model type is wrong.
|
|
106
|
+
:raises CFBDError: If request, transport, response, or conversion fails.
|
|
107
|
+
"""
|
|
108
|
+
return await self._fetch_many(
|
|
109
|
+
endpoint="/recruiting/teams",
|
|
110
|
+
request_type=RecruitingTeamsRequest,
|
|
111
|
+
request=request,
|
|
112
|
+
filters=filters,
|
|
113
|
+
response_adapter=_TEAM_RANKING_ROWS,
|
|
114
|
+
row_model=TeamRecruitingRanking,
|
|
115
|
+
)
|
|
116
|
+
|
|
117
|
+
@overload
|
|
118
|
+
async def groups(self, request: RecruitingGroupsRequest, /) -> FrameT: ...
|
|
119
|
+
|
|
120
|
+
@overload
|
|
121
|
+
async def groups(
|
|
122
|
+
self,
|
|
123
|
+
request: None = None,
|
|
124
|
+
/,
|
|
125
|
+
*,
|
|
126
|
+
team: str | None = None,
|
|
127
|
+
conference: str | None = None,
|
|
128
|
+
recruit_type: _RecruitClassificationArgument | None = None,
|
|
129
|
+
start_year: int | None = None,
|
|
130
|
+
end_year: int | None = None,
|
|
131
|
+
) -> FrameT: ...
|
|
132
|
+
|
|
133
|
+
async def groups(
|
|
134
|
+
self, request: RecruitingGroupsRequest | None = None, /, **filters: object
|
|
135
|
+
) -> FrameT:
|
|
136
|
+
"""Return recruiting ratings aggregated by team and position group.
|
|
137
|
+
|
|
138
|
+
:param request: Validated request model, mutually exclusive with filters.
|
|
139
|
+
:param filters: Explicit snake-case endpoint filters.
|
|
140
|
+
:return: Eager frame containing validated recruiting aggregates.
|
|
141
|
+
:raises TypeError: If request styles are mixed or the model type is wrong.
|
|
142
|
+
:raises CFBDError: If request, transport, response, or conversion fails.
|
|
143
|
+
"""
|
|
144
|
+
return await self._fetch_many(
|
|
145
|
+
endpoint="/recruiting/groups",
|
|
146
|
+
request_type=RecruitingGroupsRequest,
|
|
147
|
+
request=request,
|
|
148
|
+
filters=filters,
|
|
149
|
+
response_adapter=_GROUP_ROWS,
|
|
150
|
+
row_model=AggregatedTeamRecruiting,
|
|
151
|
+
)
|
|
152
|
+
|
|
153
|
+
async def _fetch_many(
|
|
154
|
+
self,
|
|
155
|
+
*,
|
|
156
|
+
endpoint: str,
|
|
157
|
+
request_type: type[_RequestT],
|
|
158
|
+
request: _RequestT | None,
|
|
159
|
+
filters: Mapping[str, object],
|
|
160
|
+
response_adapter: TypeAdapter[list[_RowT]],
|
|
161
|
+
row_model: type[_RowT],
|
|
162
|
+
) -> FrameT:
|
|
163
|
+
"""Resolve, validate, fetch, and tabularize one list endpoint."""
|
|
164
|
+
validated = _resolve_request(
|
|
165
|
+
endpoint=endpoint,
|
|
166
|
+
request_type=request_type,
|
|
167
|
+
request=request,
|
|
168
|
+
filters=filters,
|
|
169
|
+
)
|
|
170
|
+
rows = await self._executor.fetch_many(
|
|
171
|
+
endpoint=endpoint,
|
|
172
|
+
request=validated,
|
|
173
|
+
response_adapter=response_adapter,
|
|
174
|
+
)
|
|
175
|
+
return self._dataframe_adapter.from_models(
|
|
176
|
+
endpoint=endpoint, row_model=row_model, models=rows
|
|
177
|
+
)
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
__all__ = ["RecruitingResource"]
|
cfb_data/retry.py
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
"""Configure retry behavior for safe CFBD GET requests."""
|
|
2
|
+
|
|
3
|
+
import math
|
|
4
|
+
from dataclasses import dataclass
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
@dataclass(frozen=True, slots=True)
|
|
8
|
+
class RetryPolicy:
|
|
9
|
+
"""Configure bounded retries and backoff.
|
|
10
|
+
|
|
11
|
+
``max_attempts`` counts the initial request. Set it to ``1`` to disable
|
|
12
|
+
retries.
|
|
13
|
+
|
|
14
|
+
:param max_attempts: Maximum total attempts for one endpoint call.
|
|
15
|
+
:param base_delay_seconds: Initial exponential-backoff ceiling.
|
|
16
|
+
:param max_backoff_seconds: Maximum client-selected backoff ceiling.
|
|
17
|
+
:param max_retry_after_seconds: Largest server delay the client will honor.
|
|
18
|
+
:raises ValueError: If an option is outside its valid range.
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
max_attempts: int = 3
|
|
22
|
+
base_delay_seconds: float = 0.5
|
|
23
|
+
max_backoff_seconds: float = 8.0
|
|
24
|
+
max_retry_after_seconds: float = 90.0
|
|
25
|
+
|
|
26
|
+
def __post_init__(self) -> None:
|
|
27
|
+
"""Validate retry-policy invariants."""
|
|
28
|
+
if isinstance(self.max_attempts, bool) or not isinstance(
|
|
29
|
+
self.max_attempts, int
|
|
30
|
+
):
|
|
31
|
+
raise ValueError("max_attempts must be an integer")
|
|
32
|
+
if self.max_attempts < 1:
|
|
33
|
+
raise ValueError("max_attempts must be at least 1")
|
|
34
|
+
if not math.isfinite(self.base_delay_seconds) or self.base_delay_seconds < 0:
|
|
35
|
+
raise ValueError("base_delay_seconds must be finite and non-negative")
|
|
36
|
+
if not math.isfinite(self.max_backoff_seconds):
|
|
37
|
+
raise ValueError("max_backoff_seconds must be finite")
|
|
38
|
+
if self.max_backoff_seconds < self.base_delay_seconds:
|
|
39
|
+
raise ValueError(
|
|
40
|
+
"max_backoff_seconds cannot be less than base_delay_seconds"
|
|
41
|
+
)
|
|
42
|
+
if (
|
|
43
|
+
not math.isfinite(self.max_retry_after_seconds)
|
|
44
|
+
or self.max_retry_after_seconds < 0
|
|
45
|
+
):
|
|
46
|
+
raise ValueError("max_retry_after_seconds must be finite and non-negative")
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
__all__ = ["RetryPolicy"]
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
"""Export the supported Stats namespace and public contracts."""
|
|
2
|
+
|
|
3
|
+
from cfb_data.enums import Classification, SeasonType
|
|
4
|
+
|
|
5
|
+
from .models.pydantic import (
|
|
6
|
+
AdvancedGameDefense,
|
|
7
|
+
AdvancedGameOffense,
|
|
8
|
+
AdvancedGameStat,
|
|
9
|
+
AdvancedGameStatsRequest,
|
|
10
|
+
AdvancedSeasonDefense,
|
|
11
|
+
AdvancedSeasonOffense,
|
|
12
|
+
AdvancedSeasonStat,
|
|
13
|
+
AdvancedSeasonStatsRequest,
|
|
14
|
+
FieldPosition,
|
|
15
|
+
GameDownStats,
|
|
16
|
+
GameHavocRequest,
|
|
17
|
+
GameHavocStats,
|
|
18
|
+
GameHavocUnit,
|
|
19
|
+
GamePlayStats,
|
|
20
|
+
Havoc,
|
|
21
|
+
PlayerGameSuccessRate,
|
|
22
|
+
PlayerGameSuccessRequest,
|
|
23
|
+
PlayerSeasonStatsRequest,
|
|
24
|
+
PlayerSeasonSuccessRate,
|
|
25
|
+
PlayerSeasonSuccessRequest,
|
|
26
|
+
PlayerStat,
|
|
27
|
+
PlayerSuccessRateSplit,
|
|
28
|
+
SeasonDownStats,
|
|
29
|
+
SeasonPassingDownStats,
|
|
30
|
+
SeasonPlayStats,
|
|
31
|
+
StatCategory,
|
|
32
|
+
TeamSeasonStatsRequest,
|
|
33
|
+
TeamStat,
|
|
34
|
+
)
|
|
35
|
+
from .resource import StatsResource
|
|
36
|
+
|
|
37
|
+
__all__ = [
|
|
38
|
+
"AdvancedGameDefense",
|
|
39
|
+
"AdvancedGameOffense",
|
|
40
|
+
"AdvancedGameStat",
|
|
41
|
+
"AdvancedGameStatsRequest",
|
|
42
|
+
"AdvancedSeasonDefense",
|
|
43
|
+
"AdvancedSeasonOffense",
|
|
44
|
+
"AdvancedSeasonStat",
|
|
45
|
+
"AdvancedSeasonStatsRequest",
|
|
46
|
+
"Classification",
|
|
47
|
+
"FieldPosition",
|
|
48
|
+
"GameDownStats",
|
|
49
|
+
"GameHavocRequest",
|
|
50
|
+
"GameHavocStats",
|
|
51
|
+
"GameHavocUnit",
|
|
52
|
+
"GamePlayStats",
|
|
53
|
+
"Havoc",
|
|
54
|
+
"PlayerGameSuccessRate",
|
|
55
|
+
"PlayerGameSuccessRequest",
|
|
56
|
+
"PlayerSeasonStatsRequest",
|
|
57
|
+
"PlayerSeasonSuccessRate",
|
|
58
|
+
"PlayerSeasonSuccessRequest",
|
|
59
|
+
"PlayerStat",
|
|
60
|
+
"PlayerSuccessRateSplit",
|
|
61
|
+
"SeasonDownStats",
|
|
62
|
+
"SeasonPassingDownStats",
|
|
63
|
+
"SeasonPlayStats",
|
|
64
|
+
"SeasonType",
|
|
65
|
+
"StatCategory",
|
|
66
|
+
"StatsResource",
|
|
67
|
+
"TeamSeasonStatsRequest",
|
|
68
|
+
"TeamStat",
|
|
69
|
+
]
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Provide authoritative Pydantic models for Stats endpoints."""
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
"""Export validated Stats request and response models."""
|
|
2
|
+
|
|
3
|
+
from .requests import (
|
|
4
|
+
AdvancedGameStatsRequest,
|
|
5
|
+
AdvancedSeasonStatsRequest,
|
|
6
|
+
GameHavocRequest,
|
|
7
|
+
PlayerGameSuccessRequest,
|
|
8
|
+
PlayerSeasonStatsRequest,
|
|
9
|
+
PlayerSeasonSuccessRequest,
|
|
10
|
+
TeamSeasonStatsRequest,
|
|
11
|
+
)
|
|
12
|
+
from .responses import (
|
|
13
|
+
AdvancedGameDefense,
|
|
14
|
+
AdvancedGameOffense,
|
|
15
|
+
AdvancedGameStat,
|
|
16
|
+
AdvancedSeasonDefense,
|
|
17
|
+
AdvancedSeasonOffense,
|
|
18
|
+
AdvancedSeasonStat,
|
|
19
|
+
FieldPosition,
|
|
20
|
+
GameDownStats,
|
|
21
|
+
GameHavocStats,
|
|
22
|
+
GameHavocUnit,
|
|
23
|
+
GamePlayStats,
|
|
24
|
+
Havoc,
|
|
25
|
+
PlayerGameSuccessRate,
|
|
26
|
+
PlayerSeasonSuccessRate,
|
|
27
|
+
PlayerStat,
|
|
28
|
+
PlayerSuccessRateSplit,
|
|
29
|
+
SeasonDownStats,
|
|
30
|
+
SeasonPassingDownStats,
|
|
31
|
+
SeasonPlayStats,
|
|
32
|
+
StatCategory,
|
|
33
|
+
TeamStat,
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
__all__ = [
|
|
37
|
+
"AdvancedGameDefense",
|
|
38
|
+
"AdvancedGameOffense",
|
|
39
|
+
"AdvancedGameStat",
|
|
40
|
+
"AdvancedGameStatsRequest",
|
|
41
|
+
"AdvancedSeasonDefense",
|
|
42
|
+
"AdvancedSeasonOffense",
|
|
43
|
+
"AdvancedSeasonStat",
|
|
44
|
+
"AdvancedSeasonStatsRequest",
|
|
45
|
+
"FieldPosition",
|
|
46
|
+
"GameDownStats",
|
|
47
|
+
"GameHavocRequest",
|
|
48
|
+
"GameHavocStats",
|
|
49
|
+
"GameHavocUnit",
|
|
50
|
+
"GamePlayStats",
|
|
51
|
+
"Havoc",
|
|
52
|
+
"PlayerGameSuccessRate",
|
|
53
|
+
"PlayerGameSuccessRequest",
|
|
54
|
+
"PlayerSeasonStatsRequest",
|
|
55
|
+
"PlayerSeasonSuccessRate",
|
|
56
|
+
"PlayerSeasonSuccessRequest",
|
|
57
|
+
"PlayerStat",
|
|
58
|
+
"PlayerSuccessRateSplit",
|
|
59
|
+
"SeasonDownStats",
|
|
60
|
+
"SeasonPassingDownStats",
|
|
61
|
+
"SeasonPlayStats",
|
|
62
|
+
"StatCategory",
|
|
63
|
+
"TeamSeasonStatsRequest",
|
|
64
|
+
"TeamStat",
|
|
65
|
+
]
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
"""Validate request parameters for implemented Stats 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.enums import Classification, SeasonType
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class _StatsRequest(BaseModel):
|
|
13
|
+
"""Apply the closed-object contract shared by Stats requests."""
|
|
14
|
+
|
|
15
|
+
model_config = ConfigDict(populate_by_name=True, extra="forbid")
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def _validate_week_range(start_week: int | None, end_week: int | None) -> None:
|
|
19
|
+
"""Reject a reversed inclusive week range."""
|
|
20
|
+
if start_week is not None and end_week is not None and start_week > end_week:
|
|
21
|
+
raise ValueError("start_week cannot be greater than end_week")
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def _validate_year_or_team(year: int | None, team: str | None) -> None:
|
|
25
|
+
"""Require a season or team selector."""
|
|
26
|
+
if year is None and team is None:
|
|
27
|
+
raise ValueError("year is required when team is not specified")
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class PlayerSeasonStatsRequest(_StatsRequest):
|
|
31
|
+
"""Validate filters accepted by ``GET /stats/player/season``."""
|
|
32
|
+
|
|
33
|
+
year: int = Field(ge=1869)
|
|
34
|
+
conference: str | None = Field(default=None, min_length=1)
|
|
35
|
+
team: str | None = Field(default=None, min_length=1)
|
|
36
|
+
start_week: int | None = Field(default=None, alias="startWeek", ge=0)
|
|
37
|
+
end_week: int | None = Field(default=None, alias="endWeek", ge=0)
|
|
38
|
+
season_type: SeasonType | None = Field(default=None, alias="seasonType")
|
|
39
|
+
category: str | None = Field(default=None, min_length=1)
|
|
40
|
+
|
|
41
|
+
@model_validator(mode="after")
|
|
42
|
+
def validate_week_range(self) -> Self:
|
|
43
|
+
"""Reject a reversed week range."""
|
|
44
|
+
_validate_week_range(self.start_week, self.end_week)
|
|
45
|
+
return self
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
class PlayerSeasonSuccessRequest(_StatsRequest):
|
|
49
|
+
"""Validate filters accepted by ``GET /stats/player/success``."""
|
|
50
|
+
|
|
51
|
+
year: int | None = Field(default=None, ge=1869)
|
|
52
|
+
conference: str | None = Field(default=None, min_length=1)
|
|
53
|
+
team: str | None = Field(default=None, min_length=1)
|
|
54
|
+
player_id: int | None = Field(default=None, alias="playerId", gt=0)
|
|
55
|
+
season_type: SeasonType | None = Field(default=None, alias="seasonType")
|
|
56
|
+
start_week: int | None = Field(default=None, alias="startWeek", ge=0)
|
|
57
|
+
end_week: int | None = Field(default=None, alias="endWeek", ge=0)
|
|
58
|
+
threshold: int | None = Field(default=None, ge=0)
|
|
59
|
+
exclude_garbage_time: bool | None = Field(default=None, alias="excludeGarbageTime")
|
|
60
|
+
|
|
61
|
+
@model_validator(mode="after")
|
|
62
|
+
def validate_selectors(self) -> Self:
|
|
63
|
+
"""Require a season or player and reject a reversed week range."""
|
|
64
|
+
if self.year is None and self.player_id is None:
|
|
65
|
+
raise ValueError("year is required when player_id is not specified")
|
|
66
|
+
_validate_week_range(self.start_week, self.end_week)
|
|
67
|
+
return self
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
class PlayerGameSuccessRequest(_StatsRequest):
|
|
71
|
+
"""Validate filters accepted by ``GET /stats/player/success/game``."""
|
|
72
|
+
|
|
73
|
+
year: int = Field(ge=1869)
|
|
74
|
+
week: int | None = Field(default=None, ge=0)
|
|
75
|
+
season_type: SeasonType | None = Field(default=None, alias="seasonType")
|
|
76
|
+
conference: str | None = Field(default=None, min_length=1)
|
|
77
|
+
team: str | None = Field(default=None, min_length=1)
|
|
78
|
+
player_id: int | None = Field(default=None, alias="playerId", gt=0)
|
|
79
|
+
threshold: int | None = Field(default=None, ge=0)
|
|
80
|
+
exclude_garbage_time: bool | None = Field(default=None, alias="excludeGarbageTime")
|
|
81
|
+
|
|
82
|
+
@model_validator(mode="after")
|
|
83
|
+
def validate_selectors(self) -> Self:
|
|
84
|
+
"""Require a week, team, or player selector."""
|
|
85
|
+
if self.week is None and self.team is None and self.player_id is None:
|
|
86
|
+
raise ValueError("At least one of week, team, or player_id is required")
|
|
87
|
+
return self
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
class TeamSeasonStatsRequest(_StatsRequest):
|
|
91
|
+
"""Validate filters accepted by ``GET /stats/season``."""
|
|
92
|
+
|
|
93
|
+
year: int | None = Field(default=None, ge=1869)
|
|
94
|
+
team: str | None = Field(default=None, min_length=1)
|
|
95
|
+
conference: str | None = Field(default=None, min_length=1)
|
|
96
|
+
start_week: int | None = Field(default=None, alias="startWeek", ge=0)
|
|
97
|
+
end_week: int | None = Field(default=None, alias="endWeek", ge=0)
|
|
98
|
+
classification: Classification | None = None
|
|
99
|
+
|
|
100
|
+
@model_validator(mode="after")
|
|
101
|
+
def validate_selectors(self) -> Self:
|
|
102
|
+
"""Require a season or team and reject a reversed week range."""
|
|
103
|
+
_validate_year_or_team(self.year, self.team)
|
|
104
|
+
_validate_week_range(self.start_week, self.end_week)
|
|
105
|
+
return self
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
class AdvancedSeasonStatsRequest(_StatsRequest):
|
|
109
|
+
"""Validate filters accepted by ``GET /stats/season/advanced``."""
|
|
110
|
+
|
|
111
|
+
year: int | None = Field(default=None, ge=1869)
|
|
112
|
+
team: str | None = Field(default=None, min_length=1)
|
|
113
|
+
exclude_garbage_time: bool | None = Field(default=None, alias="excludeGarbageTime")
|
|
114
|
+
start_week: int | None = Field(default=None, alias="startWeek", ge=0)
|
|
115
|
+
end_week: int | None = Field(default=None, alias="endWeek", ge=0)
|
|
116
|
+
classification: Classification | None = None
|
|
117
|
+
|
|
118
|
+
@model_validator(mode="after")
|
|
119
|
+
def validate_selectors(self) -> Self:
|
|
120
|
+
"""Require a season or team and reject a reversed week range."""
|
|
121
|
+
_validate_year_or_team(self.year, self.team)
|
|
122
|
+
_validate_week_range(self.start_week, self.end_week)
|
|
123
|
+
return self
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
class AdvancedGameStatsRequest(_StatsRequest):
|
|
127
|
+
"""Validate filters accepted by ``GET /stats/game/advanced``."""
|
|
128
|
+
|
|
129
|
+
year: int | None = Field(default=None, ge=1869)
|
|
130
|
+
team: str | None = Field(default=None, min_length=1)
|
|
131
|
+
week: int | None = Field(default=None, ge=0)
|
|
132
|
+
opponent: str | None = Field(default=None, min_length=1)
|
|
133
|
+
exclude_garbage_time: bool | None = Field(default=None, alias="excludeGarbageTime")
|
|
134
|
+
season_type: SeasonType | None = Field(default=None, alias="seasonType")
|
|
135
|
+
|
|
136
|
+
@model_validator(mode="after")
|
|
137
|
+
def validate_selectors(self) -> Self:
|
|
138
|
+
"""Require a season or team selector."""
|
|
139
|
+
_validate_year_or_team(self.year, self.team)
|
|
140
|
+
return self
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
class GameHavocRequest(_StatsRequest):
|
|
144
|
+
"""Validate filters accepted by ``GET /stats/game/havoc``."""
|
|
145
|
+
|
|
146
|
+
year: int | None = Field(default=None, ge=1869)
|
|
147
|
+
team: str | None = Field(default=None, min_length=1)
|
|
148
|
+
week: int | None = Field(default=None, ge=0)
|
|
149
|
+
opponent: str | None = Field(default=None, min_length=1)
|
|
150
|
+
season_type: SeasonType | None = Field(default=None, alias="seasonType")
|
|
151
|
+
|
|
152
|
+
@model_validator(mode="after")
|
|
153
|
+
def validate_selectors(self) -> Self:
|
|
154
|
+
"""Require a season or team selector."""
|
|
155
|
+
_validate_year_or_team(self.year, self.team)
|
|
156
|
+
return self
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
__all__ = [
|
|
160
|
+
"AdvancedGameStatsRequest",
|
|
161
|
+
"AdvancedSeasonStatsRequest",
|
|
162
|
+
"GameHavocRequest",
|
|
163
|
+
"PlayerGameSuccessRequest",
|
|
164
|
+
"PlayerSeasonStatsRequest",
|
|
165
|
+
"PlayerSeasonSuccessRequest",
|
|
166
|
+
"TeamSeasonStatsRequest",
|
|
167
|
+
]
|