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,342 @@
1
+ """Expose typed Ratings 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 Classification, SeasonType
14
+ from cfb_data.ratings.models.pydantic.requests import (
15
+ ConferenceSPRatingsRequest,
16
+ CoreRatingsRequest,
17
+ EloRatingsRequest,
18
+ ExpandedSRSRatingsRequest,
19
+ FPIRatingsRequest,
20
+ SPRatingsRequest,
21
+ SRSRatingsRequest,
22
+ )
23
+ from cfb_data.ratings.models.pydantic.responses import (
24
+ ConferenceSP,
25
+ ExpandedTeamSRS,
26
+ TeamCoreRating,
27
+ TeamElo,
28
+ TeamFPI,
29
+ TeamSP,
30
+ TeamSRS,
31
+ )
32
+
33
+ _RequestT = TypeVar("_RequestT", bound=BaseModel)
34
+ _RowT = TypeVar("_RowT", bound=BaseModel)
35
+ type _ClassificationArgument = Classification | Literal["fbs", "fcs", "ii", "iii"]
36
+ type _SeasonTypeArgument = (
37
+ SeasonType
38
+ | Literal[
39
+ "regular",
40
+ "postseason",
41
+ "both",
42
+ "allstar",
43
+ "spring_regular",
44
+ "spring_postseason",
45
+ ]
46
+ )
47
+
48
+ _CORE_ROWS = TypeAdapter(list[TeamCoreRating])
49
+ _SP_ROWS = TypeAdapter(list[TeamSP])
50
+ _CONFERENCE_SP_ROWS = TypeAdapter(list[ConferenceSP])
51
+ _SRS_ROWS = TypeAdapter(list[TeamSRS])
52
+ _EXPANDED_SRS_ROWS = TypeAdapter(list[ExpandedTeamSRS])
53
+ _ELO_ROWS = TypeAdapter(list[TeamElo])
54
+ _FPI_ROWS = TypeAdapter(list[TeamFPI])
55
+
56
+
57
+ class RatingsResource[FrameT]:
58
+ """Provide validated Ratings endpoints with selected frame results."""
59
+
60
+ def __init__(
61
+ self,
62
+ executor: _EndpointExecutor,
63
+ dataframe_adapter: _DataFrameAdapter[FrameT],
64
+ ) -> None:
65
+ """Bind the namespace to shared execution and presentation services."""
66
+ self._executor = executor
67
+ self._dataframe_adapter = dataframe_adapter
68
+
69
+ @overload
70
+ async def core(self, request: CoreRatingsRequest, /) -> FrameT: ...
71
+
72
+ @overload
73
+ async def core(
74
+ self,
75
+ request: None = None,
76
+ /,
77
+ *,
78
+ year: int | None = None,
79
+ team: str | None = None,
80
+ conference: str | None = None,
81
+ ) -> FrameT: ...
82
+
83
+ async def core(
84
+ self, request: CoreRatingsRequest | None = None, /, **filters: object
85
+ ) -> FrameT:
86
+ """Return Context and Opponent-Relative Efficiency ratings.
87
+
88
+ :param request: Validated request model, mutually exclusive with filters.
89
+ :param filters: Explicit snake-case endpoint filters.
90
+ :return: Eager frame containing validated CORE rows.
91
+ :raises TypeError: If request styles are mixed or the model type is wrong.
92
+ :raises CFBDError: If request, transport, response, or conversion fails.
93
+ """
94
+ return await self._fetch_many(
95
+ endpoint="/ratings/core",
96
+ request_type=CoreRatingsRequest,
97
+ request=request,
98
+ filters=filters,
99
+ response_adapter=_CORE_ROWS,
100
+ row_model=TeamCoreRating,
101
+ )
102
+
103
+ @overload
104
+ async def sp(self, request: SPRatingsRequest, /) -> FrameT: ...
105
+
106
+ @overload
107
+ async def sp(
108
+ self,
109
+ request: None = None,
110
+ /,
111
+ *,
112
+ year: int | None = None,
113
+ team: str | None = None,
114
+ ) -> FrameT: ...
115
+
116
+ async def sp(
117
+ self, request: SPRatingsRequest | None = None, /, **filters: object
118
+ ) -> FrameT:
119
+ """Return team SP+ ratings by season.
120
+
121
+ :param request: Validated request model, mutually exclusive with filters.
122
+ :param filters: Explicit snake-case endpoint filters.
123
+ :return: Eager frame containing validated team SP+ rows.
124
+ :raises TypeError: If request styles are mixed or the model type is wrong.
125
+ :raises CFBDError: If request, transport, response, or conversion fails.
126
+ """
127
+ return await self._fetch_many(
128
+ endpoint="/ratings/sp",
129
+ request_type=SPRatingsRequest,
130
+ request=request,
131
+ filters=filters,
132
+ response_adapter=_SP_ROWS,
133
+ row_model=TeamSP,
134
+ )
135
+
136
+ @overload
137
+ async def conference_sp(self, request: ConferenceSPRatingsRequest, /) -> FrameT: ...
138
+
139
+ @overload
140
+ async def conference_sp(
141
+ self,
142
+ request: None = None,
143
+ /,
144
+ *,
145
+ year: int | None = None,
146
+ conference: str | None = None,
147
+ classification: _ClassificationArgument | None = None,
148
+ ) -> FrameT: ...
149
+
150
+ async def conference_sp(
151
+ self,
152
+ request: ConferenceSPRatingsRequest | None = None,
153
+ /,
154
+ **filters: object,
155
+ ) -> FrameT:
156
+ """Return conference-level SP+ ratings by season.
157
+
158
+ :param request: Validated request model, mutually exclusive with filters.
159
+ :param filters: Explicit snake-case endpoint filters.
160
+ :return: Eager frame containing validated conference SP+ rows.
161
+ :raises TypeError: If request styles are mixed or the model type is wrong.
162
+ :raises CFBDError: If request, transport, response, or conversion fails.
163
+ """
164
+ return await self._fetch_many(
165
+ endpoint="/ratings/sp/conferences",
166
+ request_type=ConferenceSPRatingsRequest,
167
+ request=request,
168
+ filters=filters,
169
+ response_adapter=_CONFERENCE_SP_ROWS,
170
+ row_model=ConferenceSP,
171
+ )
172
+
173
+ @overload
174
+ async def srs(self, request: SRSRatingsRequest, /) -> FrameT: ...
175
+
176
+ @overload
177
+ async def srs(
178
+ self,
179
+ request: None = None,
180
+ /,
181
+ *,
182
+ year: int | None = None,
183
+ team: str | None = None,
184
+ conference: str | None = None,
185
+ ) -> FrameT: ...
186
+
187
+ async def srs(
188
+ self, request: SRSRatingsRequest | None = None, /, **filters: object
189
+ ) -> FrameT:
190
+ """Return Simple Rating System ratings by team and season.
191
+
192
+ :param request: Validated request model, mutually exclusive with filters.
193
+ :param filters: Explicit snake-case endpoint filters.
194
+ :return: Eager frame containing validated SRS rows.
195
+ :raises TypeError: If request styles are mixed or the model type is wrong.
196
+ :raises CFBDError: If request, transport, response, or conversion fails.
197
+ """
198
+ return await self._fetch_many(
199
+ endpoint="/ratings/srs",
200
+ request_type=SRSRatingsRequest,
201
+ request=request,
202
+ filters=filters,
203
+ response_adapter=_SRS_ROWS,
204
+ row_model=TeamSRS,
205
+ )
206
+
207
+ @overload
208
+ async def expanded_srs(self, request: ExpandedSRSRatingsRequest, /) -> FrameT: ...
209
+
210
+ @overload
211
+ async def expanded_srs(
212
+ self,
213
+ request: None = None,
214
+ /,
215
+ *,
216
+ year: int | None = None,
217
+ team: str | None = None,
218
+ conference: str | None = None,
219
+ classification: _ClassificationArgument | None = None,
220
+ ) -> FrameT: ...
221
+
222
+ async def expanded_srs(
223
+ self,
224
+ request: ExpandedSRSRatingsRequest | None = None,
225
+ /,
226
+ **filters: object,
227
+ ) -> FrameT:
228
+ """Return expanded SRS ratings including lower classifications.
229
+
230
+ :param request: Validated request model, mutually exclusive with filters.
231
+ :param filters: Explicit snake-case endpoint filters.
232
+ :return: Eager frame containing validated expanded SRS rows.
233
+ :raises TypeError: If request styles are mixed or the model type is wrong.
234
+ :raises CFBDError: If request, transport, response, or conversion fails.
235
+ """
236
+ return await self._fetch_many(
237
+ endpoint="/ratings/srs/expanded",
238
+ request_type=ExpandedSRSRatingsRequest,
239
+ request=request,
240
+ filters=filters,
241
+ response_adapter=_EXPANDED_SRS_ROWS,
242
+ row_model=ExpandedTeamSRS,
243
+ )
244
+
245
+ @overload
246
+ async def elo(self, request: EloRatingsRequest, /) -> FrameT: ...
247
+
248
+ @overload
249
+ async def elo(
250
+ self,
251
+ request: None = None,
252
+ /,
253
+ *,
254
+ year: int | None = None,
255
+ week: int | None = None,
256
+ season_type: _SeasonTypeArgument | None = None,
257
+ team: str | None = None,
258
+ conference: str | None = None,
259
+ ) -> FrameT: ...
260
+
261
+ async def elo(
262
+ self, request: EloRatingsRequest | None = None, /, **filters: object
263
+ ) -> FrameT:
264
+ """Return historical Elo ratings for the selected period.
265
+
266
+ :param request: Validated request model, mutually exclusive with filters.
267
+ :param filters: Explicit snake-case endpoint filters.
268
+ :return: Eager frame containing validated Elo rows.
269
+ :raises TypeError: If request styles are mixed or the model type is wrong.
270
+ :raises CFBDError: If request, transport, response, or conversion fails.
271
+ """
272
+ return await self._fetch_many(
273
+ endpoint="/ratings/elo",
274
+ request_type=EloRatingsRequest,
275
+ request=request,
276
+ filters=filters,
277
+ response_adapter=_ELO_ROWS,
278
+ row_model=TeamElo,
279
+ )
280
+
281
+ @overload
282
+ async def fpi(self, request: FPIRatingsRequest, /) -> FrameT: ...
283
+
284
+ @overload
285
+ async def fpi(
286
+ self,
287
+ request: None = None,
288
+ /,
289
+ *,
290
+ year: int | None = None,
291
+ team: str | None = None,
292
+ conference: str | None = None,
293
+ ) -> FrameT: ...
294
+
295
+ async def fpi(
296
+ self, request: FPIRatingsRequest | None = None, /, **filters: object
297
+ ) -> FrameT:
298
+ """Return Football Power Index ratings by team and season.
299
+
300
+ :param request: Validated request model, mutually exclusive with filters.
301
+ :param filters: Explicit snake-case endpoint filters.
302
+ :return: Eager frame containing validated FPI rows.
303
+ :raises TypeError: If request styles are mixed or the model type is wrong.
304
+ :raises CFBDError: If request, transport, response, or conversion fails.
305
+ """
306
+ return await self._fetch_many(
307
+ endpoint="/ratings/fpi",
308
+ request_type=FPIRatingsRequest,
309
+ request=request,
310
+ filters=filters,
311
+ response_adapter=_FPI_ROWS,
312
+ row_model=TeamFPI,
313
+ )
314
+
315
+ async def _fetch_many(
316
+ self,
317
+ *,
318
+ endpoint: str,
319
+ request_type: type[_RequestT],
320
+ request: _RequestT | None,
321
+ filters: Mapping[str, object],
322
+ response_adapter: TypeAdapter[list[_RowT]],
323
+ row_model: type[_RowT],
324
+ ) -> FrameT:
325
+ """Resolve, validate, fetch, and tabularize one list endpoint."""
326
+ validated = _resolve_request(
327
+ endpoint=endpoint,
328
+ request_type=request_type,
329
+ request=request,
330
+ filters=filters,
331
+ )
332
+ rows = await self._executor.fetch_many(
333
+ endpoint=endpoint,
334
+ request=validated,
335
+ response_adapter=response_adapter,
336
+ )
337
+ return self._dataframe_adapter.from_models(
338
+ endpoint=endpoint, row_model=row_model, models=rows
339
+ )
340
+
341
+
342
+ __all__ = ["RatingsResource"]
@@ -0,0 +1,26 @@
1
+ """Export the supported Recruiting namespace and public contracts."""
2
+
3
+ from cfb_data.enums import RecruitClassification
4
+
5
+ from .models.pydantic import (
6
+ AggregatedTeamRecruiting,
7
+ Recruit,
8
+ RecruitHometown,
9
+ RecruitingGroupsRequest,
10
+ RecruitingPlayersRequest,
11
+ RecruitingTeamsRequest,
12
+ TeamRecruitingRanking,
13
+ )
14
+ from .resource import RecruitingResource
15
+
16
+ __all__ = [
17
+ "AggregatedTeamRecruiting",
18
+ "Recruit",
19
+ "RecruitClassification",
20
+ "RecruitHometown",
21
+ "RecruitingGroupsRequest",
22
+ "RecruitingPlayersRequest",
23
+ "RecruitingResource",
24
+ "RecruitingTeamsRequest",
25
+ "TeamRecruitingRanking",
26
+ ]
@@ -0,0 +1 @@
1
+ """Export Recruiting model families."""
@@ -0,0 +1,23 @@
1
+ """Export validated Recruiting request and response models."""
2
+
3
+ from .requests import (
4
+ RecruitingGroupsRequest,
5
+ RecruitingPlayersRequest,
6
+ RecruitingTeamsRequest,
7
+ )
8
+ from .responses import (
9
+ AggregatedTeamRecruiting,
10
+ Recruit,
11
+ RecruitHometown,
12
+ TeamRecruitingRanking,
13
+ )
14
+
15
+ __all__ = [
16
+ "AggregatedTeamRecruiting",
17
+ "Recruit",
18
+ "RecruitHometown",
19
+ "RecruitingGroupsRequest",
20
+ "RecruitingPlayersRequest",
21
+ "RecruitingTeamsRequest",
22
+ "TeamRecruitingRanking",
23
+ ]
@@ -0,0 +1,69 @@
1
+ """Validate request parameters for implemented Recruiting 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 RecruitClassification
10
+
11
+
12
+ class _RecruitingRequest(BaseModel):
13
+ """Apply the closed-object contract shared by Recruiting requests."""
14
+
15
+ model_config = ConfigDict(populate_by_name=True, extra="forbid")
16
+
17
+
18
+ class RecruitingPlayersRequest(_RecruitingRequest):
19
+ """Validate filters accepted by ``GET /recruiting/players``."""
20
+
21
+ year: int | None = Field(default=None, ge=1869)
22
+ team: str | None = Field(default=None, min_length=1)
23
+ position: str | None = Field(default=None, min_length=1)
24
+ state: str | None = Field(default=None, min_length=1)
25
+ classification: RecruitClassification | None = None
26
+
27
+ @model_validator(mode="after")
28
+ def validate_selectors(self) -> Self:
29
+ """Require a recruiting year or committed team."""
30
+ if self.year is None and self.team is None:
31
+ raise ValueError("year is required when team is not specified")
32
+ return self
33
+
34
+
35
+ class RecruitingTeamsRequest(_RecruitingRequest):
36
+ """Validate filters accepted by ``GET /recruiting/teams``."""
37
+
38
+ year: int | None = Field(default=None, ge=1869)
39
+ team: str | None = Field(default=None, min_length=1)
40
+
41
+
42
+ class RecruitingGroupsRequest(_RecruitingRequest):
43
+ """Validate filters accepted by ``GET /recruiting/groups``."""
44
+
45
+ team: str | None = Field(default=None, min_length=1)
46
+ conference: str | None = Field(default=None, min_length=1)
47
+ recruit_type: RecruitClassification | None = Field(
48
+ default=None, alias="recruitType"
49
+ )
50
+ start_year: int | None = Field(default=None, alias="startYear", ge=1869)
51
+ end_year: int | None = Field(default=None, alias="endYear", ge=1869)
52
+
53
+ @model_validator(mode="after")
54
+ def validate_year_range(self) -> Self:
55
+ """Require the recruiting year range to be ascending."""
56
+ if (
57
+ self.start_year is not None
58
+ and self.end_year is not None
59
+ and self.start_year > self.end_year
60
+ ):
61
+ raise ValueError("start_year must be less than or equal to end_year")
62
+ return self
63
+
64
+
65
+ __all__ = [
66
+ "RecruitingGroupsRequest",
67
+ "RecruitingPlayersRequest",
68
+ "RecruitingTeamsRequest",
69
+ ]
@@ -0,0 +1,70 @@
1
+ """Validate responses from implemented CFBD Recruiting endpoints."""
2
+
3
+ from pydantic import BaseModel, ConfigDict, Field
4
+
5
+ from cfb_data.enums import RecruitClassification
6
+
7
+
8
+ class _ResponseModel(BaseModel):
9
+ """Apply the upstream closed-object contract to Recruiting responses."""
10
+
11
+ model_config = ConfigDict(populate_by_name=True, extra="forbid")
12
+
13
+
14
+ class RecruitHometown(_ResponseModel):
15
+ """Represent normalized geographic details for a recruit's hometown."""
16
+
17
+ latitude: float | None
18
+ longitude: float | None
19
+ fips_code: str | None = Field(alias="fipsCode")
20
+
21
+
22
+ class Recruit(_ResponseModel):
23
+ """Represent one ranked recruiting prospect."""
24
+
25
+ id: str
26
+ athlete_id: str | None = Field(alias="athleteId")
27
+ recruit_type: RecruitClassification = Field(alias="recruitType")
28
+ year: int = Field(ge=1869)
29
+ ranking: int | None = Field(ge=1)
30
+ name: str
31
+ school: str | None
32
+ committed_to: str | None = Field(alias="committedTo")
33
+ position: str | None
34
+ height: float | None = Field(ge=0)
35
+ weight: int | None = Field(ge=0)
36
+ stars: int | None = Field(ge=0, le=5)
37
+ rating: float | None = Field(ge=0)
38
+ city: str | None
39
+ state_province: str | None = Field(alias="stateProvince")
40
+ country: str | None
41
+ hometown_info: RecruitHometown = Field(alias="hometownInfo")
42
+
43
+
44
+ class TeamRecruitingRanking(_ResponseModel):
45
+ """Represent one team's recruiting class ranking."""
46
+
47
+ year: int = Field(ge=1869)
48
+ rank: int = Field(ge=1)
49
+ team: str
50
+ points: float
51
+
52
+
53
+ class AggregatedTeamRecruiting(_ResponseModel):
54
+ """Represent recruiting quality aggregated by team and position group."""
55
+
56
+ team: str
57
+ conference: str
58
+ position_group: str | None = Field(alias="positionGroup")
59
+ average_rating: float = Field(alias="averageRating")
60
+ total_rating: float = Field(alias="totalRating")
61
+ commits: int = Field(ge=0)
62
+ average_stars: float = Field(alias="averageStars", ge=0, le=5)
63
+
64
+
65
+ __all__ = [
66
+ "AggregatedTeamRecruiting",
67
+ "Recruit",
68
+ "RecruitHometown",
69
+ "TeamRecruitingRanking",
70
+ ]