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,261 @@
1
+ """Validate responses from implemented CFBD Coaches endpoints."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from datetime import UTC, datetime
6
+ from enum import StrEnum
7
+
8
+ from pydantic import BaseModel, ConfigDict, Field, field_validator
9
+
10
+
11
+ class _ResponseModel(BaseModel):
12
+ """Apply the upstream closed-object contract to Coaches responses."""
13
+
14
+ model_config = ConfigDict(populate_by_name=True, extra="forbid")
15
+
16
+ @field_validator("*", mode="after", check_fields=False)
17
+ @classmethod
18
+ def require_utc_datetimes(cls, value: object) -> object:
19
+ """Require aware response timestamps and normalize them to UTC."""
20
+ if not isinstance(value, datetime):
21
+ return value
22
+ if value.tzinfo is None or value.utcoffset() is None:
23
+ raise ValueError("Response timestamps must be timezone-aware")
24
+ return value.astimezone(UTC)
25
+
26
+
27
+ class CoachSeason(_ResponseModel):
28
+ """Represent one season in the historical coach summary route."""
29
+
30
+ team_id: int = Field(alias="teamId", gt=0)
31
+ school: str
32
+ conference: str | None
33
+ year: int = Field(ge=1869)
34
+ games: int = Field(ge=0)
35
+ wins: int = Field(ge=0)
36
+ losses: int = Field(ge=0)
37
+ ties: int = Field(ge=0)
38
+ win_percentage: float | None = Field(alias="winPercentage", ge=0, le=1)
39
+ preseason_rank: int | None = Field(alias="preseasonRank", ge=1)
40
+ postseason_rank: int | None = Field(alias="postseasonRank", ge=1)
41
+ srs: float | None
42
+ sp_overall: float | None = Field(alias="spOverall")
43
+ sp_offense: float | None = Field(alias="spOffense")
44
+ sp_defense: float | None = Field(alias="spDefense")
45
+
46
+
47
+ class Coach(_ResponseModel):
48
+ """Represent one historical head coach and selected seasons."""
49
+
50
+ id: int = Field(gt=0)
51
+ first_name: str = Field(alias="firstName")
52
+ last_name: str = Field(alias="lastName")
53
+ hire_date: datetime | None = Field(alias="hireDate")
54
+ seasons: list[CoachSeason]
55
+
56
+
57
+ class CoachRecord(_ResponseModel):
58
+ """Represent an attributed coaching win-loss record."""
59
+
60
+ games: int = Field(ge=0)
61
+ wins: int = Field(ge=0)
62
+ losses: int = Field(ge=0)
63
+ ties: int = Field(ge=0)
64
+ win_percentage: float | None = Field(alias="winPercentage", ge=0, le=1)
65
+
66
+
67
+ class CoachReference(_ResponseModel):
68
+ """Represent the canonical identity of a coach."""
69
+
70
+ id: int = Field(gt=0)
71
+ first_name: str = Field(alias="firstName")
72
+ last_name: str = Field(alias="lastName")
73
+
74
+
75
+ class CoachTeamReference(_ResponseModel):
76
+ """Represent a team associated with a coach."""
77
+
78
+ id: int = Field(gt=0)
79
+ school: str
80
+
81
+
82
+ class CoachSeasonTeamReference(CoachTeamReference):
83
+ """Represent a team and conference within one coaching season."""
84
+
85
+ conference: str | None
86
+
87
+
88
+ class CoachCareer(CoachRecord):
89
+ """Represent career totals in a coach profile."""
90
+
91
+ seasons: int = Field(ge=0)
92
+ teams: int = Field(ge=0)
93
+ first_year: int = Field(alias="firstYear", ge=1869)
94
+ last_year: int = Field(alias="lastYear", ge=1869)
95
+
96
+
97
+ class CoachAlmaMater(_ResponseModel):
98
+ """Represent a coach's alma mater."""
99
+
100
+ id: int = Field(gt=0)
101
+ school: str
102
+
103
+
104
+ class CoachProfile(_ResponseModel):
105
+ """Represent canonical identity and career totals for one coach."""
106
+
107
+ id: int = Field(gt=0)
108
+ first_name: str = Field(alias="firstName")
109
+ last_name: str = Field(alias="lastName")
110
+ display_name: str | None = Field(alias="displayName")
111
+ current_team: CoachSeasonTeamReference | None = Field(alias="currentTeam")
112
+ career: CoachCareer
113
+ birth_date: str | None = Field(alias="birthDate")
114
+ alma_mater: CoachAlmaMater | None = Field(alias="almaMater")
115
+ graduation_year: int | None = Field(alias="graduationYear", ge=1869)
116
+ wikidata_id: str | None = Field(alias="wikidataId")
117
+ hall_of_fame_year: int | None = Field(alias="hallOfFameYear", ge=1869)
118
+
119
+
120
+ class CoachTenure(_ResponseModel):
121
+ """Represent one continuous head-coaching tenure."""
122
+
123
+ id: int = Field(gt=0)
124
+ coach: CoachReference
125
+ team: CoachTeamReference
126
+ hire_date: str | None = Field(alias="hireDate")
127
+ start_year: int = Field(alias="startYear", ge=1869)
128
+ end_year: int | None = Field(alias="endYear", ge=1869)
129
+ effective_start: datetime | None = Field(alias="effectiveStart")
130
+ effective_end: datetime | None = Field(alias="effectiveEnd")
131
+ is_interim: bool = Field(alias="isInterim")
132
+ active: bool
133
+ seasons: int = Field(ge=0)
134
+ record: CoachRecord
135
+ attribution_complete: bool = Field(alias="attributionComplete")
136
+
137
+
138
+ class CoachYearOverYear(_ResponseModel):
139
+ """Represent change from the preceding team season."""
140
+
141
+ wins: int | None
142
+ srs: float | None
143
+ sp_overall: float | None = Field(alias="spOverall")
144
+
145
+
146
+ class CoachRatingContext(_ResponseModel):
147
+ """Represent team rating context for a coaching season."""
148
+
149
+ sp_special_teams: float | None = Field(alias="spSpecialTeams")
150
+ strength_of_schedule: float | None = Field(alias="strengthOfSchedule")
151
+ second_order_wins: float | None = Field(alias="secondOrderWins")
152
+ fpi: float | None
153
+ year_over_year: CoachYearOverYear = Field(alias="yearOverYear")
154
+
155
+
156
+ class CoachRecruitingContext(_ResponseModel):
157
+ """Represent recruiting context for a coaching season."""
158
+
159
+ rank: int | None = Field(ge=1)
160
+ points: float | None
161
+ talent: float | None
162
+
163
+
164
+ class CoachPollResume(_ResponseModel):
165
+ """Represent poll résumé totals for a coaching season."""
166
+
167
+ preseason_rank: int | None = Field(alias="preseasonRank", ge=1)
168
+ postseason_rank: int | None = Field(alias="postseasonRank", ge=1)
169
+ best_rank: int | None = Field(alias="bestRank", ge=1)
170
+ weeks_ranked: int = Field(alias="weeksRanked", ge=0)
171
+ weeks_top_ten: int = Field(alias="weeksTopTen", ge=0)
172
+
173
+
174
+ class CoachRecordSplits(_ResponseModel):
175
+ """Represent coaching records split by game context."""
176
+
177
+ conference: CoachRecord
178
+ postseason: CoachRecord
179
+ home: CoachRecord
180
+ away: CoachRecord
181
+ neutral: CoachRecord
182
+
183
+
184
+ class CoachScoring(_ResponseModel):
185
+ """Represent aggregate scoring under a coach for one season."""
186
+
187
+ points_for: int = Field(alias="pointsFor", ge=0)
188
+ points_against: int = Field(alias="pointsAgainst", ge=0)
189
+ average_point_differential: float | None = Field(alias="averagePointDifferential")
190
+
191
+
192
+ class CoachCfpOutcome(StrEnum):
193
+ """Identify a coach's College Football Playoff season outcome."""
194
+
195
+ active = "active"
196
+ eliminated = "eliminated"
197
+ champion = "champion"
198
+
199
+
200
+ class CoachCfpContext(_ResponseModel):
201
+ """Represent College Football Playoff context for a coaching season."""
202
+
203
+ appeared: bool
204
+ seed: int | None = Field(ge=1)
205
+ outcome: CoachCfpOutcome | None
206
+
207
+
208
+ class CoachDraftContext(_ResponseModel):
209
+ """Represent draft outcomes following a coaching season."""
210
+
211
+ year: int = Field(ge=1936)
212
+ total_picks: int = Field(alias="totalPicks", ge=0)
213
+ first_round_picks: int = Field(alias="firstRoundPicks", ge=0)
214
+
215
+
216
+ class DetailedCoachSeason(CoachRecord):
217
+ """Represent one coach-season with results and team context."""
218
+
219
+ coach: CoachReference
220
+ team: CoachSeasonTeamReference
221
+ year: int = Field(ge=1869)
222
+ preseason_rank: int | None = Field(alias="preseasonRank", ge=1)
223
+ postseason_rank: int | None = Field(alias="postseasonRank", ge=1)
224
+ srs: float | None
225
+ sp_overall: float | None = Field(alias="spOverall")
226
+ sp_offense: float | None = Field(alias="spOffense")
227
+ sp_defense: float | None = Field(alias="spDefense")
228
+ team_metrics: CoachRatingContext = Field(alias="teamMetrics")
229
+ recruiting: CoachRecruitingContext
230
+ poll_resume: CoachPollResume | None = Field(alias="pollResume")
231
+ attribution_complete: bool = Field(alias="attributionComplete")
232
+ record_splits: CoachRecordSplits | None = Field(alias="recordSplits")
233
+ scoring: CoachScoring | None
234
+ cfp: CoachCfpContext
235
+ draft_following_season: CoachDraftContext | None = Field(
236
+ alias="draftFollowingSeason"
237
+ )
238
+
239
+
240
+ __all__ = [
241
+ "Coach",
242
+ "CoachAlmaMater",
243
+ "CoachCareer",
244
+ "CoachCfpContext",
245
+ "CoachCfpOutcome",
246
+ "CoachDraftContext",
247
+ "CoachPollResume",
248
+ "CoachProfile",
249
+ "CoachRatingContext",
250
+ "CoachRecord",
251
+ "CoachRecordSplits",
252
+ "CoachRecruitingContext",
253
+ "CoachReference",
254
+ "CoachScoring",
255
+ "CoachSeason",
256
+ "CoachSeasonTeamReference",
257
+ "CoachTeamReference",
258
+ "CoachTenure",
259
+ "CoachYearOverYear",
260
+ "DetailedCoachSeason",
261
+ ]
@@ -0,0 +1,216 @@
1
+ """Expose typed Coaches endpoints through the primary client."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import builtins
6
+ from collections.abc import Mapping
7
+ from typing import TypeVar, overload
8
+
9
+ from pydantic import BaseModel, TypeAdapter
10
+
11
+ from cfb_data._dataframes import _DataFrameAdapter
12
+ from cfb_data._executor import _EndpointExecutor
13
+ from cfb_data._requests import _resolve_request
14
+ from cfb_data.coaches.models.pydantic.requests import (
15
+ CoachesRequest,
16
+ CoachProfileRequest,
17
+ CoachSeasonsRequest,
18
+ CoachTenuresRequest,
19
+ )
20
+ from cfb_data.coaches.models.pydantic.responses import (
21
+ Coach,
22
+ CoachProfile,
23
+ CoachTenure,
24
+ DetailedCoachSeason,
25
+ )
26
+
27
+ _RequestT = TypeVar("_RequestT", bound=BaseModel)
28
+ _RowT = TypeVar("_RowT", bound=BaseModel)
29
+
30
+ _COACH_ROWS = TypeAdapter(list[Coach])
31
+ _COACH_PROFILE = TypeAdapter(CoachProfile)
32
+ _COACH_SEASON_ROWS = TypeAdapter(list[DetailedCoachSeason])
33
+ _COACH_TENURE_ROWS = TypeAdapter(list[CoachTenure])
34
+
35
+
36
+ class CoachesResource[FrameT]:
37
+ """Provide validated Coaches 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 list(self, request: CoachesRequest, /) -> FrameT: ...
50
+
51
+ @overload
52
+ async def list(
53
+ self,
54
+ request: None = None,
55
+ /,
56
+ *,
57
+ first_name: str | None = None,
58
+ last_name: str | None = None,
59
+ team: str | None = None,
60
+ year: int | None = None,
61
+ min_year: int | None = None,
62
+ max_year: int | None = None,
63
+ ) -> FrameT: ...
64
+
65
+ async def list(
66
+ self, request: CoachesRequest | None = None, /, **filters: object
67
+ ) -> FrameT:
68
+ """Return historical head coaches with nested season summaries.
69
+
70
+ :param request: Validated request model, mutually exclusive with filters.
71
+ :param filters: Explicit snake-case endpoint filters.
72
+ :return: Eager frame containing validated coach rows.
73
+ :raises TypeError: If request styles are mixed or the model type is wrong.
74
+ :raises CFBDError: If request, transport, response, or conversion fails.
75
+ """
76
+ return await self._fetch_many(
77
+ endpoint="/coaches",
78
+ request_type=CoachesRequest,
79
+ request=request,
80
+ filters=filters,
81
+ response_adapter=_COACH_ROWS,
82
+ row_model=Coach,
83
+ )
84
+
85
+ @overload
86
+ async def profile(self, request: CoachProfileRequest, /) -> FrameT: ...
87
+
88
+ @overload
89
+ async def profile(self, request: None = None, /, *, coach_id: int) -> FrameT: ...
90
+
91
+ async def profile(
92
+ self, request: CoachProfileRequest | None = None, /, **filters: object
93
+ ) -> FrameT:
94
+ """Return one canonical coach profile as a one-row frame.
95
+
96
+ :param request: Validated request model, mutually exclusive with filters.
97
+ :param filters: Explicit snake-case endpoint filters.
98
+ :return: One-row eager frame containing the validated coach profile.
99
+ :raises TypeError: If request styles are mixed or the model type is wrong.
100
+ :raises CFBDError: If request, transport, response, or conversion fails.
101
+ """
102
+ endpoint = "/coaches/profile"
103
+ validated = _resolve_request(
104
+ endpoint=endpoint,
105
+ request_type=CoachProfileRequest,
106
+ request=request,
107
+ filters=filters,
108
+ )
109
+ profile = await self._executor.fetch_one(
110
+ endpoint=endpoint,
111
+ request=validated,
112
+ response_adapter=_COACH_PROFILE,
113
+ )
114
+ return self._dataframe_adapter.from_models(
115
+ endpoint=endpoint, row_model=CoachProfile, models=[profile]
116
+ )
117
+
118
+ @overload
119
+ async def seasons(self, request: CoachSeasonsRequest, /) -> FrameT: ...
120
+
121
+ @overload
122
+ async def seasons(
123
+ self,
124
+ request: None = None,
125
+ /,
126
+ *,
127
+ coach_id: int | None = None,
128
+ team: str | None = None,
129
+ year: int | None = None,
130
+ min_year: int | None = None,
131
+ max_year: int | None = None,
132
+ ) -> FrameT: ...
133
+
134
+ async def seasons(
135
+ self, request: CoachSeasonsRequest | None = None, /, **filters: object
136
+ ) -> FrameT:
137
+ """Return coach-season records with attributed results and context.
138
+
139
+ :param request: Validated request model, mutually exclusive with filters.
140
+ :param filters: Explicit snake-case endpoint filters.
141
+ :return: Eager frame containing validated detailed coach seasons.
142
+ :raises TypeError: If request styles are mixed or the model type is wrong.
143
+ :raises CFBDError: If request, transport, response, or conversion fails.
144
+ """
145
+ return await self._fetch_many(
146
+ endpoint="/coaches/seasons",
147
+ request_type=CoachSeasonsRequest,
148
+ request=request,
149
+ filters=filters,
150
+ response_adapter=_COACH_SEASON_ROWS,
151
+ row_model=DetailedCoachSeason,
152
+ )
153
+
154
+ @overload
155
+ async def tenures(self, request: CoachTenuresRequest, /) -> FrameT: ...
156
+
157
+ @overload
158
+ async def tenures(
159
+ self,
160
+ request: None = None,
161
+ /,
162
+ *,
163
+ coach_id: int | None = None,
164
+ team: str | None = None,
165
+ year: int | None = None,
166
+ active: bool | None = None,
167
+ ) -> FrameT: ...
168
+
169
+ async def tenures(
170
+ self, request: CoachTenuresRequest | None = None, /, **filters: object
171
+ ) -> FrameT:
172
+ """Return continuous head-coaching tenures and attributed records.
173
+
174
+ :param request: Validated request model, mutually exclusive with filters.
175
+ :param filters: Explicit snake-case endpoint filters.
176
+ :return: Eager frame containing validated coaching tenure rows.
177
+ :raises TypeError: If request styles are mixed or the model type is wrong.
178
+ :raises CFBDError: If request, transport, response, or conversion fails.
179
+ """
180
+ return await self._fetch_many(
181
+ endpoint="/coaches/tenures",
182
+ request_type=CoachTenuresRequest,
183
+ request=request,
184
+ filters=filters,
185
+ response_adapter=_COACH_TENURE_ROWS,
186
+ row_model=CoachTenure,
187
+ )
188
+
189
+ async def _fetch_many(
190
+ self,
191
+ *,
192
+ endpoint: str,
193
+ request_type: type[_RequestT],
194
+ request: _RequestT | None,
195
+ filters: Mapping[str, object],
196
+ response_adapter: TypeAdapter[builtins.list[_RowT]],
197
+ row_model: type[_RowT],
198
+ ) -> FrameT:
199
+ """Resolve, validate, fetch, and tabularize one list endpoint."""
200
+ validated = _resolve_request(
201
+ endpoint=endpoint,
202
+ request_type=request_type,
203
+ request=request,
204
+ filters=filters,
205
+ )
206
+ rows: builtins.list[_RowT] = await self._executor.fetch_many(
207
+ endpoint=endpoint,
208
+ request=validated,
209
+ response_adapter=response_adapter,
210
+ )
211
+ return self._dataframe_adapter.from_models(
212
+ endpoint=endpoint, row_model=row_model, models=rows
213
+ )
214
+
215
+
216
+ __all__ = ["CoachesResource"]
@@ -0,0 +1,23 @@
1
+ """Export the supported Conferences namespace and public contracts."""
2
+
3
+ from .models.pydantic import (
4
+ Conference,
5
+ ConferenceAffiliationsRequest,
6
+ ConferenceChangesRequest,
7
+ ConferenceClassification,
8
+ ConferencesRequest,
9
+ TeamConferenceAffiliation,
10
+ TeamConferenceChange,
11
+ )
12
+ from .resource import ConferencesResource
13
+
14
+ __all__ = [
15
+ "Conference",
16
+ "ConferenceAffiliationsRequest",
17
+ "ConferenceChangesRequest",
18
+ "ConferenceClassification",
19
+ "ConferencesRequest",
20
+ "ConferencesResource",
21
+ "TeamConferenceAffiliation",
22
+ "TeamConferenceChange",
23
+ ]
@@ -0,0 +1 @@
1
+ """Provide authoritative Pydantic models for Conferences endpoints."""
@@ -0,0 +1,23 @@
1
+ """Export Pydantic models for Conferences endpoints."""
2
+
3
+ from .requests import (
4
+ ConferenceAffiliationsRequest,
5
+ ConferenceChangesRequest,
6
+ ConferencesRequest,
7
+ )
8
+ from .responses import (
9
+ Conference,
10
+ ConferenceClassification,
11
+ TeamConferenceAffiliation,
12
+ TeamConferenceChange,
13
+ )
14
+
15
+ __all__ = [
16
+ "Conference",
17
+ "ConferenceAffiliationsRequest",
18
+ "ConferenceChangesRequest",
19
+ "ConferenceClassification",
20
+ "ConferencesRequest",
21
+ "TeamConferenceAffiliation",
22
+ "TeamConferenceChange",
23
+ ]
@@ -0,0 +1,69 @@
1
+ """Validate request parameters for implemented Conferences 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 .responses import ConferenceClassification
10
+
11
+
12
+ class ConferencesRequest(BaseModel):
13
+ """Validate filters accepted by ``GET /conferences``.
14
+
15
+ :param year: Season used to calculate membership.
16
+ :param classification: Conference division classification.
17
+ """
18
+
19
+ model_config = ConfigDict(populate_by_name=True, extra="forbid")
20
+
21
+ year: int | None = Field(default=None, ge=1869)
22
+ classification: ConferenceClassification | None = None
23
+
24
+
25
+ class ConferenceChangesRequest(BaseModel):
26
+ """Validate filters accepted by ``GET /conferences/changes``.
27
+
28
+ :param year: Season whose effective conference changes are returned.
29
+ """
30
+
31
+ model_config = ConfigDict(populate_by_name=True, extra="forbid")
32
+
33
+ year: int = Field(ge=1869)
34
+
35
+
36
+ class ConferenceAffiliationsRequest(BaseModel):
37
+ """Validate filters accepted by ``GET /conferences/affiliations``.
38
+
39
+ :param team: Team school name or abbreviation.
40
+ :param conference: Conference name or abbreviation.
41
+ :param year: Exact active-affiliation season.
42
+ :param min_year: Earliest overlapping affiliation season.
43
+ :param max_year: Latest overlapping affiliation season.
44
+ :param classification: Conference division classification.
45
+ """
46
+
47
+ model_config = ConfigDict(populate_by_name=True, extra="forbid")
48
+
49
+ team: str | None = None
50
+ conference: str | None = None
51
+ year: int | None = Field(default=None, ge=1869)
52
+ min_year: int | None = Field(default=None, ge=1869, alias="minYear")
53
+ max_year: int | None = Field(default=None, ge=1869, alias="maxYear")
54
+ classification: ConferenceClassification | None = None
55
+
56
+ @model_validator(mode="after")
57
+ def validate_year_filters(self) -> Self:
58
+ """Reject mutually exclusive and reversed season filters."""
59
+ if self.year is not None and (
60
+ self.min_year is not None or self.max_year is not None
61
+ ):
62
+ raise ValueError("year cannot be combined with min_year or max_year")
63
+ if (
64
+ self.min_year is not None
65
+ and self.max_year is not None
66
+ and self.min_year > self.max_year
67
+ ):
68
+ raise ValueError("min_year cannot be greater than max_year")
69
+ return self
@@ -0,0 +1,64 @@
1
+ """Validate responses from implemented CFBD Conferences endpoints."""
2
+
3
+ from enum import StrEnum
4
+
5
+ from pydantic import BaseModel, ConfigDict, Field
6
+
7
+
8
+ class ConferenceClassification(StrEnum):
9
+ """Identify an official conference division classification."""
10
+
11
+ fbs = "fbs"
12
+ fcs = "fcs"
13
+ ii = "ii"
14
+ ii_or_iii = "ii/iii"
15
+ iii = "iii"
16
+
17
+
18
+ class _ResponseModel(BaseModel):
19
+ """Apply the upstream closed-object contract to response models."""
20
+
21
+ model_config = ConfigDict(populate_by_name=True, extra="forbid")
22
+
23
+
24
+ class Conference(_ResponseModel):
25
+ """Represent a conference and its membership count."""
26
+
27
+ id: int = Field(gt=0)
28
+ name: str
29
+ short_name: str | None = Field(alias="shortName")
30
+ abbreviation: str | None
31
+ classification: ConferenceClassification | None
32
+ member_count: int = Field(alias="memberCount", ge=0)
33
+
34
+
35
+ class TeamConferenceAffiliation(_ResponseModel):
36
+ """Represent one historical team-to-conference affiliation."""
37
+
38
+ team_id: int = Field(alias="teamId", gt=0)
39
+ team: str
40
+ conference_id: int = Field(alias="conferenceId", gt=0)
41
+ conference: str
42
+ conference_abbreviation: str | None = Field(alias="conferenceAbbreviation")
43
+ classification: ConferenceClassification | None
44
+ conference_division: str | None = Field(alias="conferenceDivision")
45
+ start_year: int = Field(alias="startYear", ge=1869)
46
+ end_year: int | None = Field(alias="endYear", ge=1869)
47
+
48
+
49
+ class TeamConferenceChange(_ResponseModel):
50
+ """Represent one team's conference change for a season."""
51
+
52
+ team_id: int = Field(alias="teamId", gt=0)
53
+ team: str
54
+ from_conference_id: int = Field(alias="fromConferenceId", gt=0)
55
+ from_conference: str = Field(alias="fromConference")
56
+ from_conference_abbreviation: str | None = Field(alias="fromConferenceAbbreviation")
57
+ from_classification: ConferenceClassification | None = Field(
58
+ alias="fromClassification"
59
+ )
60
+ to_conference_id: int = Field(alias="toConferenceId", gt=0)
61
+ to_conference: str = Field(alias="toConference")
62
+ to_conference_abbreviation: str | None = Field(alias="toConferenceAbbreviation")
63
+ to_classification: ConferenceClassification | None = Field(alias="toClassification")
64
+ effective_year: int = Field(alias="effectiveYear", ge=1869)