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
cfb_data/client.py ADDED
@@ -0,0 +1,305 @@
1
+ """Provide the primary context-managed CollegeFootballData client."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from types import TracebackType
6
+ from typing import (
7
+ TYPE_CHECKING,
8
+ Literal,
9
+ Self,
10
+ cast,
11
+ overload,
12
+ )
13
+
14
+ import pandas as pd
15
+
16
+ from cfb_data._dataframes import _DataFrameAdapter, _PandasAdapter, _PolarsAdapter
17
+ from cfb_data._executor import _EndpointExecutor
18
+ from cfb_data._transport import (
19
+ _HTTPTransport,
20
+ _resolve_api_key,
21
+ _validate_base_url,
22
+ _validate_timeout,
23
+ )
24
+ from cfb_data.adjusted_metrics.resource import AdjustedMetricsResource
25
+ from cfb_data.betting.resource import BettingResource
26
+ from cfb_data.coaches.resource import CoachesResource
27
+ from cfb_data.conferences.resource import ConferencesResource
28
+ from cfb_data.draft.resource import DraftResource
29
+ from cfb_data.drives.resource import DrivesResource
30
+ from cfb_data.errors import CFBDConfigurationError
31
+ from cfb_data.games.resource import GamesResource
32
+ from cfb_data.info.resource import InfoResource
33
+ from cfb_data.metrics.resource import MetricsResource
34
+ from cfb_data.players.resource import PlayersResource
35
+ from cfb_data.playoffs.resource import PlayoffsResource
36
+ from cfb_data.plays.resource import PlaysResource
37
+ from cfb_data.rankings.resource import RankingsResource
38
+ from cfb_data.ratings.resource import RatingsResource
39
+ from cfb_data.recruiting.resource import RecruitingResource
40
+ from cfb_data.retry import RetryPolicy
41
+ from cfb_data.stats.resource import StatsResource
42
+ from cfb_data.teams.resource import TeamsResource
43
+ from cfb_data.venues.resource import VenuesResource
44
+
45
+ if TYPE_CHECKING:
46
+ import polars as pl
47
+
48
+ type DataFrameBackend = Literal["pandas", "polars"]
49
+
50
+
51
+ class CFBDClient[FrameT]:
52
+ """Access validated CFBD endpoint namespaces through one pooled session.
53
+
54
+ The client is one-shot and must be used with ``async with``. Endpoint calls
55
+ return eager pandas DataFrames by default or eager Polars DataFrames when
56
+ ``dataframe_backend="polars"`` is selected.
57
+ """
58
+
59
+ @overload
60
+ def __init__(
61
+ self: CFBDClient[pd.DataFrame],
62
+ api_key: str | None = None,
63
+ *,
64
+ dataframe_backend: Literal["pandas"] = "pandas",
65
+ base_url: str = "https://api.collegefootballdata.com",
66
+ timeout_seconds: float = 30.0,
67
+ retry_policy: RetryPolicy | None = None,
68
+ ) -> None: ...
69
+
70
+ @overload
71
+ def __init__(
72
+ self: CFBDClient[pl.DataFrame],
73
+ api_key: str | None = None,
74
+ *,
75
+ dataframe_backend: Literal["polars"],
76
+ base_url: str = "https://api.collegefootballdata.com",
77
+ timeout_seconds: float = 30.0,
78
+ retry_policy: RetryPolicy | None = None,
79
+ ) -> None: ...
80
+
81
+ @overload
82
+ def __init__(
83
+ self: CFBDClient[pd.DataFrame | pl.DataFrame],
84
+ api_key: str | None = None,
85
+ *,
86
+ dataframe_backend: DataFrameBackend,
87
+ base_url: str = "https://api.collegefootballdata.com",
88
+ timeout_seconds: float = 30.0,
89
+ retry_policy: RetryPolicy | None = None,
90
+ ) -> None: ...
91
+
92
+ def __init__(
93
+ self,
94
+ api_key: str | None = None,
95
+ *,
96
+ dataframe_backend: DataFrameBackend = "pandas",
97
+ base_url: str = "https://api.collegefootballdata.com",
98
+ timeout_seconds: float = 30.0,
99
+ retry_policy: RetryPolicy | None = None,
100
+ ) -> None:
101
+ """Initialize a one-shot client without opening its HTTP session.
102
+
103
+ :param api_key: Explicit API key, or ``None`` to use ``CFBD_API_KEY``.
104
+ :param dataframe_backend: Eager DataFrame implementation to return.
105
+ :param base_url: API origin and optional base path.
106
+ :param timeout_seconds: Finite total timeout for each HTTP attempt.
107
+ :param retry_policy: Custom retry behavior, or defaults when omitted.
108
+ :raises CFBDConfigurationError: If client configuration is invalid.
109
+ """
110
+ if dataframe_backend not in {"pandas", "polars"}:
111
+ raise CFBDConfigurationError(
112
+ "dataframe_backend must be either 'pandas' or 'polars'"
113
+ )
114
+
115
+ transport = _HTTPTransport(
116
+ api_key=_resolve_api_key(api_key),
117
+ base_url=_validate_base_url(base_url),
118
+ timeout_seconds=_validate_timeout(timeout_seconds),
119
+ retry_policy=retry_policy or RetryPolicy(),
120
+ )
121
+ executor = _EndpointExecutor(transport)
122
+ concrete_adapter = (
123
+ _PandasAdapter() if dataframe_backend == "pandas" else _PolarsAdapter()
124
+ )
125
+ # The constructor overload ties this runtime literal branch to FrameT.
126
+ adapter = cast(_DataFrameAdapter[FrameT], concrete_adapter)
127
+
128
+ self._transport = transport
129
+ self._games = GamesResource(executor, adapter)
130
+ self._drives = DrivesResource(executor, adapter)
131
+ self._plays = PlaysResource(executor, adapter)
132
+ self._venues = VenuesResource(executor, adapter)
133
+ self._conferences = ConferencesResource(executor, adapter)
134
+ self._teams = TeamsResource(executor, adapter)
135
+ self._stats = StatsResource(executor, adapter)
136
+ self._metrics = MetricsResource(executor, adapter)
137
+ self._ratings = RatingsResource(executor, adapter)
138
+ self._players = PlayersResource(executor, adapter)
139
+ self._rankings = RankingsResource(executor, adapter)
140
+ self._betting = BettingResource(executor, adapter)
141
+ self._recruiting = RecruitingResource(executor, adapter)
142
+ self._coaches = CoachesResource(executor, adapter)
143
+ self._draft = DraftResource(executor, adapter)
144
+ self._playoffs = PlayoffsResource(executor, adapter)
145
+ self._adjusted_metrics = AdjustedMetricsResource(executor, adapter)
146
+ self._info = InfoResource(executor)
147
+
148
+ @property
149
+ def games(self) -> GamesResource[FrameT]:
150
+ """Return the typed Games endpoint namespace.
151
+
152
+ :return: Resource bound to this client's session and DataFrame backend.
153
+ """
154
+ return self._games
155
+
156
+ @property
157
+ def drives(self) -> DrivesResource[FrameT]:
158
+ """Return the typed Drives endpoint namespace.
159
+
160
+ :return: Resource bound to this client's session and DataFrame backend.
161
+ """
162
+ return self._drives
163
+
164
+ @property
165
+ def plays(self) -> PlaysResource[FrameT]:
166
+ """Return the typed Plays endpoint namespace.
167
+
168
+ :return: Resource bound to this client's session and DataFrame backend.
169
+ """
170
+ return self._plays
171
+
172
+ @property
173
+ def venues(self) -> VenuesResource[FrameT]:
174
+ """Return the typed Venues endpoint namespace.
175
+
176
+ :return: Resource bound to this client's session and DataFrame backend.
177
+ """
178
+ return self._venues
179
+
180
+ @property
181
+ def conferences(self) -> ConferencesResource[FrameT]:
182
+ """Return the typed Conferences endpoint namespace.
183
+
184
+ :return: Resource bound to this client's session and DataFrame backend.
185
+ """
186
+ return self._conferences
187
+
188
+ @property
189
+ def teams(self) -> TeamsResource[FrameT]:
190
+ """Return the typed Teams endpoint namespace.
191
+
192
+ :return: Resource bound to this client's session and DataFrame backend.
193
+ """
194
+ return self._teams
195
+
196
+ @property
197
+ def stats(self) -> StatsResource[FrameT]:
198
+ """Return the typed Stats endpoint namespace.
199
+
200
+ :return: Resource bound to this client's session and DataFrame backend.
201
+ """
202
+ return self._stats
203
+
204
+ @property
205
+ def metrics(self) -> MetricsResource[FrameT]:
206
+ """Return the typed Metrics endpoint namespace.
207
+
208
+ :return: Resource bound to this client's session and DataFrame backend.
209
+ """
210
+ return self._metrics
211
+
212
+ @property
213
+ def ratings(self) -> RatingsResource[FrameT]:
214
+ """Return the typed Ratings endpoint namespace.
215
+
216
+ :return: Resource bound to this client's session and DataFrame backend.
217
+ """
218
+ return self._ratings
219
+
220
+ @property
221
+ def players(self) -> PlayersResource[FrameT]:
222
+ """Return the typed Players endpoint namespace.
223
+
224
+ :return: Resource bound to this client's session and DataFrame backend.
225
+ """
226
+ return self._players
227
+
228
+ @property
229
+ def rankings(self) -> RankingsResource[FrameT]:
230
+ """Return the typed Rankings endpoint namespace.
231
+
232
+ :return: Resource bound to this client's session and DataFrame backend.
233
+ """
234
+ return self._rankings
235
+
236
+ @property
237
+ def betting(self) -> BettingResource[FrameT]:
238
+ """Return the typed Betting endpoint namespace.
239
+
240
+ :return: Resource bound to this client's session and DataFrame backend.
241
+ """
242
+ return self._betting
243
+
244
+ @property
245
+ def recruiting(self) -> RecruitingResource[FrameT]:
246
+ """Return the typed Recruiting endpoint namespace.
247
+
248
+ :return: Resource bound to this client's session and DataFrame backend.
249
+ """
250
+ return self._recruiting
251
+
252
+ @property
253
+ def coaches(self) -> CoachesResource[FrameT]:
254
+ """Return the typed Coaches endpoint namespace.
255
+
256
+ :return: Resource bound to this client's session and DataFrame backend.
257
+ """
258
+ return self._coaches
259
+
260
+ @property
261
+ def draft(self) -> DraftResource[FrameT]:
262
+ """Return the typed Draft endpoint namespace.
263
+
264
+ :return: Resource bound to this client's session and DataFrame backend.
265
+ """
266
+ return self._draft
267
+
268
+ @property
269
+ def playoffs(self) -> PlayoffsResource[FrameT]:
270
+ """Return the typed Playoffs endpoint namespace.
271
+
272
+ :return: Resource bound to this client's session and DataFrame backend.
273
+ """
274
+ return self._playoffs
275
+
276
+ @property
277
+ def adjusted_metrics(self) -> AdjustedMetricsResource[FrameT]:
278
+ """Return the typed Adjusted Metrics endpoint namespace.
279
+
280
+ :return: Resource bound to this client's session and DataFrame backend.
281
+ """
282
+ return self._adjusted_metrics
283
+
284
+ @property
285
+ def info(self) -> InfoResource:
286
+ """Return the typed operational Info endpoint namespace.
287
+
288
+ :return: Resource bound to this client's session.
289
+ """
290
+ return self._info
291
+
292
+ async def __aenter__(self) -> Self:
293
+ await self._transport.open()
294
+ return self
295
+
296
+ async def __aexit__(
297
+ self,
298
+ exc_type: type[BaseException] | None,
299
+ exc_value: BaseException | None,
300
+ traceback: TracebackType | None,
301
+ ) -> None:
302
+ await self._transport.close()
303
+
304
+
305
+ __all__ = ["CFBDClient", "DataFrameBackend"]
@@ -0,0 +1,57 @@
1
+ """Export the supported Coaches namespace and public contracts."""
2
+
3
+ from .models.pydantic import (
4
+ Coach,
5
+ CoachAlmaMater,
6
+ CoachCareer,
7
+ CoachCfpContext,
8
+ CoachCfpOutcome,
9
+ CoachDraftContext,
10
+ CoachesRequest,
11
+ CoachPollResume,
12
+ CoachProfile,
13
+ CoachProfileRequest,
14
+ CoachRatingContext,
15
+ CoachRecord,
16
+ CoachRecordSplits,
17
+ CoachRecruitingContext,
18
+ CoachReference,
19
+ CoachScoring,
20
+ CoachSeason,
21
+ CoachSeasonsRequest,
22
+ CoachSeasonTeamReference,
23
+ CoachTeamReference,
24
+ CoachTenure,
25
+ CoachTenuresRequest,
26
+ CoachYearOverYear,
27
+ DetailedCoachSeason,
28
+ )
29
+ from .resource import CoachesResource
30
+
31
+ __all__ = [
32
+ "Coach",
33
+ "CoachAlmaMater",
34
+ "CoachCareer",
35
+ "CoachCfpContext",
36
+ "CoachCfpOutcome",
37
+ "CoachDraftContext",
38
+ "CoachesRequest",
39
+ "CoachesResource",
40
+ "CoachPollResume",
41
+ "CoachProfile",
42
+ "CoachProfileRequest",
43
+ "CoachRatingContext",
44
+ "CoachRecord",
45
+ "CoachRecordSplits",
46
+ "CoachRecruitingContext",
47
+ "CoachReference",
48
+ "CoachScoring",
49
+ "CoachSeason",
50
+ "CoachSeasonsRequest",
51
+ "CoachSeasonTeamReference",
52
+ "CoachTeamReference",
53
+ "CoachTenure",
54
+ "CoachTenuresRequest",
55
+ "CoachYearOverYear",
56
+ "DetailedCoachSeason",
57
+ ]
@@ -0,0 +1 @@
1
+ """Export Coaches model families."""
@@ -0,0 +1,57 @@
1
+ """Export validated Coaches request and response models."""
2
+
3
+ from .requests import (
4
+ CoachesRequest,
5
+ CoachProfileRequest,
6
+ CoachSeasonsRequest,
7
+ CoachTenuresRequest,
8
+ )
9
+ from .responses import (
10
+ Coach,
11
+ CoachAlmaMater,
12
+ CoachCareer,
13
+ CoachCfpContext,
14
+ CoachCfpOutcome,
15
+ CoachDraftContext,
16
+ CoachPollResume,
17
+ CoachProfile,
18
+ CoachRatingContext,
19
+ CoachRecord,
20
+ CoachRecordSplits,
21
+ CoachRecruitingContext,
22
+ CoachReference,
23
+ CoachScoring,
24
+ CoachSeason,
25
+ CoachSeasonTeamReference,
26
+ CoachTeamReference,
27
+ CoachTenure,
28
+ CoachYearOverYear,
29
+ DetailedCoachSeason,
30
+ )
31
+
32
+ __all__ = [
33
+ "Coach",
34
+ "CoachAlmaMater",
35
+ "CoachCareer",
36
+ "CoachCfpContext",
37
+ "CoachCfpOutcome",
38
+ "CoachDraftContext",
39
+ "CoachesRequest",
40
+ "CoachPollResume",
41
+ "CoachProfile",
42
+ "CoachProfileRequest",
43
+ "CoachRatingContext",
44
+ "CoachRecord",
45
+ "CoachRecordSplits",
46
+ "CoachRecruitingContext",
47
+ "CoachReference",
48
+ "CoachScoring",
49
+ "CoachSeason",
50
+ "CoachSeasonsRequest",
51
+ "CoachSeasonTeamReference",
52
+ "CoachTeamReference",
53
+ "CoachTenure",
54
+ "CoachTenuresRequest",
55
+ "CoachYearOverYear",
56
+ "DetailedCoachSeason",
57
+ ]
@@ -0,0 +1,75 @@
1
+ """Validate request parameters for implemented Coaches 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
+
10
+ def _require_ascending_years(min_year: int | None, max_year: int | None) -> None:
11
+ """Require an optional year range to be ascending."""
12
+ if min_year is not None and max_year is not None and min_year > max_year:
13
+ raise ValueError("min_year must be less than or equal to max_year")
14
+
15
+
16
+ class _CoachesRequest(BaseModel):
17
+ """Apply the closed-object contract shared by Coaches requests."""
18
+
19
+ model_config = ConfigDict(populate_by_name=True, extra="forbid")
20
+
21
+
22
+ class CoachesRequest(_CoachesRequest):
23
+ """Validate filters accepted by ``GET /coaches``."""
24
+
25
+ first_name: str | None = Field(default=None, alias="firstName", min_length=1)
26
+ last_name: str | None = Field(default=None, alias="lastName", min_length=1)
27
+ team: str | None = Field(default=None, min_length=1)
28
+ year: int | None = Field(default=None, ge=1869)
29
+ min_year: int | None = Field(default=None, alias="minYear", ge=1869)
30
+ max_year: int | None = Field(default=None, alias="maxYear", ge=1869)
31
+
32
+ @model_validator(mode="after")
33
+ def validate_year_range(self) -> Self:
34
+ """Require the season range to be ascending."""
35
+ _require_ascending_years(self.min_year, self.max_year)
36
+ return self
37
+
38
+
39
+ class CoachProfileRequest(_CoachesRequest):
40
+ """Validate filters accepted by ``GET /coaches/profile``."""
41
+
42
+ coach_id: int = Field(alias="coachId", gt=0)
43
+
44
+
45
+ class CoachSeasonsRequest(_CoachesRequest):
46
+ """Validate filters accepted by ``GET /coaches/seasons``."""
47
+
48
+ coach_id: int | None = Field(default=None, alias="coachId", gt=0)
49
+ team: str | None = Field(default=None, min_length=1)
50
+ year: int | None = Field(default=None, ge=1869)
51
+ min_year: int | None = Field(default=None, alias="minYear", ge=1869)
52
+ max_year: int | None = Field(default=None, alias="maxYear", ge=1869)
53
+
54
+ @model_validator(mode="after")
55
+ def validate_year_range(self) -> Self:
56
+ """Require the season range to be ascending."""
57
+ _require_ascending_years(self.min_year, self.max_year)
58
+ return self
59
+
60
+
61
+ class CoachTenuresRequest(_CoachesRequest):
62
+ """Validate filters accepted by ``GET /coaches/tenures``."""
63
+
64
+ coach_id: int | None = Field(default=None, alias="coachId", gt=0)
65
+ team: str | None = Field(default=None, min_length=1)
66
+ year: int | None = Field(default=None, ge=1869)
67
+ active: bool | None = None
68
+
69
+
70
+ __all__ = [
71
+ "CoachesRequest",
72
+ "CoachProfileRequest",
73
+ "CoachSeasonsRequest",
74
+ "CoachTenuresRequest",
75
+ ]