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.
Files changed (127) hide show
  1. cfb_data/__init__.py +234 -0
  2. cfb_data/_dataframes.py +279 -0
  3. cfb_data/_executor.py +129 -0
  4. cfb_data/_parquet.py +197 -0
  5. cfb_data/_request_rules.py +41 -0
  6. cfb_data/_requests.py +36 -0
  7. cfb_data/_tabular.py +676 -0
  8. cfb_data/_transport.py +452 -0
  9. cfb_data/adjusted_metrics/__init__.py +29 -0
  10. cfb_data/adjusted_metrics/models/__init__.py +1 -0
  11. cfb_data/adjusted_metrics/models/pydantic/__init__.py +29 -0
  12. cfb_data/adjusted_metrics/models/pydantic/requests.py +45 -0
  13. cfb_data/adjusted_metrics/models/pydantic/responses.py +88 -0
  14. cfb_data/adjusted_metrics/resource.py +220 -0
  15. cfb_data/base/__init__.py +6 -0
  16. cfb_data/base/types.py +112 -0
  17. cfb_data/betting/__init__.py +15 -0
  18. cfb_data/betting/models/__init__.py +1 -0
  19. cfb_data/betting/models/pydantic/__init__.py +6 -0
  20. cfb_data/betting/models/pydantic/requests.py +35 -0
  21. cfb_data/betting/models/pydantic/responses.py +62 -0
  22. cfb_data/betting/resource.py +91 -0
  23. cfb_data/client.py +305 -0
  24. cfb_data/coaches/__init__.py +57 -0
  25. cfb_data/coaches/models/__init__.py +1 -0
  26. cfb_data/coaches/models/pydantic/__init__.py +57 -0
  27. cfb_data/coaches/models/pydantic/requests.py +75 -0
  28. cfb_data/coaches/models/pydantic/responses.py +261 -0
  29. cfb_data/coaches/resource.py +216 -0
  30. cfb_data/conferences/__init__.py +23 -0
  31. cfb_data/conferences/models/__init__.py +1 -0
  32. cfb_data/conferences/models/pydantic/__init__.py +23 -0
  33. cfb_data/conferences/models/pydantic/requests.py +69 -0
  34. cfb_data/conferences/models/pydantic/responses.py +64 -0
  35. cfb_data/conferences/resource.py +175 -0
  36. cfb_data/draft/__init__.py +19 -0
  37. cfb_data/draft/models/__init__.py +1 -0
  38. cfb_data/draft/models/pydantic/__init__.py +12 -0
  39. cfb_data/draft/models/pydantic/requests.py +18 -0
  40. cfb_data/draft/models/pydantic/responses.py +65 -0
  41. cfb_data/draft/resource.py +132 -0
  42. cfb_data/drives/__init__.py +13 -0
  43. cfb_data/drives/models/__init__.py +1 -0
  44. cfb_data/drives/models/pydantic/__init__.py +17 -0
  45. cfb_data/drives/models/pydantic/requests.py +42 -0
  46. cfb_data/drives/models/pydantic/responses.py +46 -0
  47. cfb_data/drives/resource.py +94 -0
  48. cfb_data/enums.py +93 -0
  49. cfb_data/errors.py +234 -0
  50. cfb_data/games/__init__.py +42 -0
  51. cfb_data/games/models/__init__.py +1 -0
  52. cfb_data/games/models/pydantic/__init__.py +106 -0
  53. cfb_data/games/models/pydantic/requests.py +266 -0
  54. cfb_data/games/models/pydantic/responses.py +495 -0
  55. cfb_data/games/resource.py +486 -0
  56. cfb_data/info/__init__.py +25 -0
  57. cfb_data/info/models/__init__.py +1 -0
  58. cfb_data/info/models/pydantic/__init__.py +23 -0
  59. cfb_data/info/models/pydantic/requests.py +18 -0
  60. cfb_data/info/models/pydantic/responses.py +103 -0
  61. cfb_data/info/resource.py +88 -0
  62. cfb_data/metrics/__init__.py +53 -0
  63. cfb_data/metrics/models/__init__.py +1 -0
  64. cfb_data/metrics/models/pydantic/__init__.py +49 -0
  65. cfb_data/metrics/models/pydantic/requests.py +121 -0
  66. cfb_data/metrics/models/pydantic/responses.py +182 -0
  67. cfb_data/metrics/resource.py +371 -0
  68. cfb_data/players/__init__.py +44 -0
  69. cfb_data/players/models/__init__.py +1 -0
  70. cfb_data/players/models/pydantic/__init__.py +41 -0
  71. cfb_data/players/models/pydantic/requests.py +70 -0
  72. cfb_data/players/models/pydantic/responses.py +171 -0
  73. cfb_data/players/resource.py +258 -0
  74. cfb_data/playoffs/__init__.py +41 -0
  75. cfb_data/playoffs/models/__init__.py +1 -0
  76. cfb_data/playoffs/models/pydantic/__init__.py +37 -0
  77. cfb_data/playoffs/models/pydantic/requests.py +30 -0
  78. cfb_data/playoffs/models/pydantic/responses.py +173 -0
  79. cfb_data/playoffs/resource.py +149 -0
  80. cfb_data/plays/__init__.py +43 -0
  81. cfb_data/plays/models/__init__.py +1 -0
  82. cfb_data/plays/models/pydantic/__init__.py +35 -0
  83. cfb_data/plays/models/pydantic/requests.py +85 -0
  84. cfb_data/plays/models/pydantic/responses.py +231 -0
  85. cfb_data/plays/resource.py +249 -0
  86. cfb_data/py.typed +0 -0
  87. cfb_data/rankings/__init__.py +16 -0
  88. cfb_data/rankings/models/__init__.py +1 -0
  89. cfb_data/rankings/models/pydantic/__init__.py +6 -0
  90. cfb_data/rankings/models/pydantic/requests.py +36 -0
  91. cfb_data/rankings/models/pydantic/responses.py +42 -0
  92. cfb_data/rankings/resource.py +89 -0
  93. cfb_data/ratings/__init__.py +59 -0
  94. cfb_data/ratings/models/__init__.py +1 -0
  95. cfb_data/ratings/models/pydantic/__init__.py +55 -0
  96. cfb_data/ratings/models/pydantic/requests.py +87 -0
  97. cfb_data/ratings/models/pydantic/responses.py +215 -0
  98. cfb_data/ratings/resource.py +342 -0
  99. cfb_data/recruiting/__init__.py +26 -0
  100. cfb_data/recruiting/models/__init__.py +1 -0
  101. cfb_data/recruiting/models/pydantic/__init__.py +23 -0
  102. cfb_data/recruiting/models/pydantic/requests.py +69 -0
  103. cfb_data/recruiting/models/pydantic/responses.py +70 -0
  104. cfb_data/recruiting/resource.py +180 -0
  105. cfb_data/retry.py +49 -0
  106. cfb_data/stats/__init__.py +69 -0
  107. cfb_data/stats/models/__init__.py +1 -0
  108. cfb_data/stats/models/pydantic/__init__.py +65 -0
  109. cfb_data/stats/models/pydantic/requests.py +167 -0
  110. cfb_data/stats/models/pydantic/responses.py +291 -0
  111. cfb_data/stats/resource.py +400 -0
  112. cfb_data/teams/__init__.py +40 -0
  113. cfb_data/teams/models/__init__.py +1 -0
  114. cfb_data/teams/models/pydantic/__init__.py +26 -0
  115. cfb_data/teams/models/pydantic/requests.py +96 -0
  116. cfb_data/teams/models/pydantic/responses.py +116 -0
  117. cfb_data/teams/resource.py +270 -0
  118. cfb_data/venues/__init__.py +6 -0
  119. cfb_data/venues/models/__init__.py +1 -0
  120. cfb_data/venues/models/pydantic/__init__.py +5 -0
  121. cfb_data/venues/models/pydantic/responses.py +24 -0
  122. cfb_data/venues/resource.py +51 -0
  123. cfb_data-0.4.1.dist-info/METADATA +414 -0
  124. cfb_data-0.4.1.dist-info/RECORD +127 -0
  125. cfb_data-0.4.1.dist-info/WHEEL +5 -0
  126. cfb_data-0.4.1.dist-info/licenses/LICENSE +21 -0
  127. 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
+ ]