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,40 @@
1
+ """Export the supported Teams namespace and public contracts."""
2
+
3
+ from cfb_data.conferences import ConferenceClassification
4
+ from cfb_data.enums import Classification
5
+ from cfb_data.venues import Venue
6
+
7
+ from .models.pydantic import (
8
+ FBSTeamsRequest,
9
+ Matchup,
10
+ MatchupGame,
11
+ RosterPlayer,
12
+ RosterRequest,
13
+ TalentRequest,
14
+ Team,
15
+ TeamATS,
16
+ TeamATSRequest,
17
+ TeamMatchupRequest,
18
+ TeamsRequest,
19
+ TeamTalent,
20
+ )
21
+ from .resource import TeamsResource
22
+
23
+ __all__ = [
24
+ "Classification",
25
+ "ConferenceClassification",
26
+ "FBSTeamsRequest",
27
+ "Matchup",
28
+ "MatchupGame",
29
+ "RosterPlayer",
30
+ "RosterRequest",
31
+ "TalentRequest",
32
+ "Team",
33
+ "TeamATS",
34
+ "TeamATSRequest",
35
+ "TeamMatchupRequest",
36
+ "TeamsRequest",
37
+ "TeamsResource",
38
+ "TeamTalent",
39
+ "Venue",
40
+ ]
@@ -0,0 +1 @@
1
+ """Provide authoritative Pydantic models for Teams endpoints."""
@@ -0,0 +1,26 @@
1
+ """Export Pydantic models for Teams endpoints."""
2
+
3
+ from .requests import (
4
+ FBSTeamsRequest,
5
+ RosterRequest,
6
+ TalentRequest,
7
+ TeamATSRequest,
8
+ TeamMatchupRequest,
9
+ TeamsRequest,
10
+ )
11
+ from .responses import Matchup, MatchupGame, RosterPlayer, Team, TeamATS, TeamTalent
12
+
13
+ __all__ = [
14
+ "FBSTeamsRequest",
15
+ "Matchup",
16
+ "MatchupGame",
17
+ "RosterPlayer",
18
+ "RosterRequest",
19
+ "TalentRequest",
20
+ "Team",
21
+ "TeamATS",
22
+ "TeamATSRequest",
23
+ "TeamMatchupRequest",
24
+ "TeamsRequest",
25
+ "TeamTalent",
26
+ ]
@@ -0,0 +1,96 @@
1
+ """Validate request parameters for implemented Teams 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
10
+
11
+
12
+ class TeamsRequest(BaseModel):
13
+ """Validate filters accepted by ``GET /teams``.
14
+
15
+ :param conference: Conference abbreviation.
16
+ :param year: Season used for historical affiliation membership.
17
+ """
18
+
19
+ model_config = ConfigDict(populate_by_name=True, extra="forbid")
20
+ conference: str | None = None
21
+ year: int | None = Field(default=None, ge=1869)
22
+
23
+
24
+ class FBSTeamsRequest(BaseModel):
25
+ """Validate filters accepted by ``GET /teams/fbs``.
26
+
27
+ :param year: Season used for historical FBS membership.
28
+ """
29
+
30
+ model_config = ConfigDict(populate_by_name=True, extra="forbid")
31
+ year: int | None = Field(default=None, ge=1869)
32
+
33
+
34
+ class TeamMatchupRequest(BaseModel):
35
+ """Validate filters accepted by ``GET /teams/matchup``.
36
+
37
+ :param team1: First team name.
38
+ :param team2: Second team name.
39
+ :param min_year: Earliest season to include.
40
+ :param max_year: Latest season to include.
41
+ """
42
+
43
+ model_config = ConfigDict(populate_by_name=True, extra="forbid")
44
+ team1: str = Field(min_length=1)
45
+ team2: str = Field(min_length=1)
46
+ min_year: int | None = Field(default=None, ge=1869, alias="minYear")
47
+ max_year: int | None = Field(default=None, ge=1869, alias="maxYear")
48
+
49
+ @model_validator(mode="after")
50
+ def validate_year_range(self) -> Self:
51
+ """Reject a reversed matchup season range."""
52
+ if (
53
+ self.min_year is not None
54
+ and self.max_year is not None
55
+ and self.min_year > self.max_year
56
+ ):
57
+ raise ValueError("min_year cannot be greater than max_year")
58
+ return self
59
+
60
+
61
+ class TeamATSRequest(BaseModel):
62
+ """Validate filters accepted by ``GET /teams/ats``.
63
+
64
+ :param year: Required season year.
65
+ :param conference: Conference name or abbreviation.
66
+ :param team: Team name.
67
+ """
68
+
69
+ model_config = ConfigDict(populate_by_name=True, extra="forbid")
70
+ year: int = Field(ge=1869)
71
+ conference: str | None = None
72
+ team: str | None = None
73
+
74
+
75
+ class RosterRequest(BaseModel):
76
+ """Validate filters accepted by ``GET /roster``.
77
+
78
+ :param team: Team name.
79
+ :param year: Roster season, or ``None`` to retain the upstream default.
80
+ :param classification: Division classification selector.
81
+ """
82
+
83
+ model_config = ConfigDict(populate_by_name=True, extra="forbid")
84
+ team: str | None = None
85
+ year: int | None = Field(default=None, ge=1869)
86
+ classification: Classification | None = None
87
+
88
+
89
+ class TalentRequest(BaseModel):
90
+ """Validate filters accepted by ``GET /talent``.
91
+
92
+ :param year: Required talent-composite season.
93
+ """
94
+
95
+ model_config = ConfigDict(populate_by_name=True, extra="forbid")
96
+ year: int = Field(ge=1869)
@@ -0,0 +1,116 @@
1
+ """Validate responses from implemented CFBD Teams endpoints."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from datetime import UTC, datetime
6
+
7
+ from pydantic import BaseModel, ConfigDict, Field, field_validator
8
+
9
+ from cfb_data.conferences.models.pydantic.responses import ConferenceClassification
10
+ from cfb_data.venues.models.pydantic.responses import Venue
11
+
12
+
13
+ class _ResponseModel(BaseModel):
14
+ """Apply the upstream closed-object contract to response models."""
15
+
16
+ model_config = ConfigDict(populate_by_name=True, extra="forbid")
17
+
18
+ @field_validator("*", mode="after", check_fields=False)
19
+ @classmethod
20
+ def require_utc_datetimes(cls, value: object) -> object:
21
+ """Require aware response timestamps and normalize them to UTC."""
22
+ if not isinstance(value, datetime):
23
+ return value
24
+ if value.tzinfo is None or value.utcoffset() is None:
25
+ raise ValueError("Response timestamps must be timezone-aware")
26
+ return value.astimezone(UTC)
27
+
28
+
29
+ class Team(_ResponseModel):
30
+ """Represent one team and its current or historical affiliation."""
31
+
32
+ id: int = Field(gt=0)
33
+ school: str
34
+ mascot: str | None
35
+ abbreviation: str | None
36
+ alternate_names: list[str] | None = Field(alias="alternateNames")
37
+ conference: str | None
38
+ division: str | None
39
+ classification: ConferenceClassification | None
40
+ color: str | None
41
+ alternate_color: str | None = Field(alias="alternateColor")
42
+ logos: list[str] | None
43
+ twitter: str | None
44
+ location: Venue | None
45
+
46
+
47
+ class MatchupGame(_ResponseModel):
48
+ """Represent one completed game in a historical matchup."""
49
+
50
+ season: int = Field(ge=1869)
51
+ week: int = Field(ge=0)
52
+ season_type: str = Field(alias="seasonType")
53
+ date: datetime
54
+ neutral_site: bool = Field(alias="neutralSite")
55
+ venue: str | None
56
+ home_team: str = Field(alias="homeTeam")
57
+ home_score: int | None = Field(alias="homeScore", ge=0)
58
+ away_team: str = Field(alias="awayTeam")
59
+ away_score: int | None = Field(alias="awayScore", ge=0)
60
+ winner: str | None
61
+
62
+
63
+ class Matchup(_ResponseModel):
64
+ """Represent a matchup summary and its nested completed games."""
65
+
66
+ team1: str
67
+ team2: str
68
+ start_year: int | None = Field(default=None, alias="startYear", ge=1869)
69
+ end_year: int | None = Field(default=None, alias="endYear", ge=1869)
70
+ team1_wins: int = Field(alias="team1Wins", ge=0)
71
+ team2_wins: int = Field(alias="team2Wins", ge=0)
72
+ ties: int = Field(ge=0)
73
+ games: list[MatchupGame]
74
+
75
+
76
+ class RosterPlayer(_ResponseModel):
77
+ """Represent one player on a historical team roster."""
78
+
79
+ id: str
80
+ first_name: str = Field(alias="firstName")
81
+ last_name: str = Field(alias="lastName")
82
+ team: str
83
+ height: float | None
84
+ weight: int | None = Field(ge=0)
85
+ jersey: int | None = Field(ge=0)
86
+ year: int = Field(ge=0)
87
+ position: str | None
88
+ home_city: str | None = Field(alias="homeCity")
89
+ home_state: str | None = Field(alias="homeState")
90
+ home_country: str | None = Field(alias="homeCountry")
91
+ home_latitude: float | None = Field(alias="homeLatitude")
92
+ home_longitude: float | None = Field(alias="homeLongitude")
93
+ home_county_fips: str | None = Field(alias="homeCountyFIPS")
94
+ recruit_ids: list[str] | None = Field(alias="recruitIds")
95
+
96
+
97
+ class TeamTalent(_ResponseModel):
98
+ """Represent one team's 247Sports Team Talent Composite rating."""
99
+
100
+ year: int = Field(ge=1869)
101
+ team: str
102
+ talent: float
103
+
104
+
105
+ class TeamATS(_ResponseModel):
106
+ """Represent one team's against-the-spread season record."""
107
+
108
+ year: int = Field(ge=1869)
109
+ team_id: int = Field(alias="teamId", gt=0)
110
+ team: str
111
+ conference: str | None
112
+ games: int = Field(ge=0)
113
+ ats_wins: int = Field(alias="atsWins", ge=0)
114
+ ats_losses: int = Field(alias="atsLosses", ge=0)
115
+ ats_pushes: int = Field(alias="atsPushes", ge=0)
116
+ avg_cover_margin: float | None = Field(alias="avgCoverMargin")
@@ -0,0 +1,270 @@
1
+ """Expose typed Teams endpoints through the primary client."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Literal, overload
6
+
7
+ from pydantic import TypeAdapter
8
+
9
+ from cfb_data._dataframes import _DataFrameAdapter
10
+ from cfb_data._executor import _EndpointExecutor
11
+ from cfb_data._requests import _resolve_request
12
+ from cfb_data.enums import Classification
13
+ from cfb_data.teams.models.pydantic.requests import (
14
+ FBSTeamsRequest,
15
+ RosterRequest,
16
+ TalentRequest,
17
+ TeamATSRequest,
18
+ TeamMatchupRequest,
19
+ TeamsRequest,
20
+ )
21
+ from cfb_data.teams.models.pydantic.responses import (
22
+ Matchup,
23
+ RosterPlayer,
24
+ Team,
25
+ TeamATS,
26
+ TeamTalent,
27
+ )
28
+
29
+ type _ClassificationArgument = Classification | Literal["fbs", "fcs", "ii", "iii"]
30
+ _TEAM_ROWS = TypeAdapter(list[Team])
31
+ _MATCHUP = TypeAdapter(Matchup)
32
+ _ATS_ROWS = TypeAdapter(list[TeamATS])
33
+ _ROSTER_ROWS = TypeAdapter(list[RosterPlayer])
34
+ _TALENT_ROWS = TypeAdapter(list[TeamTalent])
35
+
36
+
37
+ class TeamsResource[FrameT]:
38
+ """Provide validated Teams endpoints with backend-specific frame results."""
39
+
40
+ def __init__(
41
+ self,
42
+ executor: _EndpointExecutor,
43
+ dataframe_adapter: _DataFrameAdapter[FrameT],
44
+ ) -> None:
45
+ """Bind the namespace to shared execution and presentation services."""
46
+ self._executor = executor
47
+ self._dataframe_adapter = dataframe_adapter
48
+
49
+ @overload
50
+ async def list(self, request: TeamsRequest, /) -> FrameT: ...
51
+
52
+ @overload
53
+ async def list(
54
+ self,
55
+ request: None = None,
56
+ /,
57
+ *,
58
+ conference: str | None = None,
59
+ year: int | None = None,
60
+ ) -> FrameT: ...
61
+
62
+ async def list(
63
+ self, request: TeamsRequest | None = None, /, **filters: object
64
+ ) -> FrameT:
65
+ """Return teams as the selected DataFrame type.
66
+
67
+ :param request: Validated request model, mutually exclusive with filters.
68
+ :param filters: Explicit snake-case endpoint filters.
69
+ :return: Eager frame containing validated ``Team`` rows.
70
+ :raises TypeError: If request styles are mixed or the model type is wrong.
71
+ :raises CFBDError: If request, transport, response, or conversion fails.
72
+ """
73
+ endpoint = "/teams"
74
+ validated = _resolve_request(
75
+ endpoint=endpoint,
76
+ request_type=TeamsRequest,
77
+ request=request,
78
+ filters=filters,
79
+ )
80
+ rows = await self._executor.fetch_many(
81
+ endpoint=endpoint, request=validated, response_adapter=_TEAM_ROWS
82
+ )
83
+ return self._dataframe_adapter.from_models(
84
+ endpoint=endpoint, row_model=Team, models=rows
85
+ )
86
+
87
+ @overload
88
+ async def fbs(self, request: FBSTeamsRequest, /) -> FrameT: ...
89
+
90
+ @overload
91
+ async def fbs(
92
+ self, request: None = None, /, *, year: int | None = None
93
+ ) -> FrameT: ...
94
+
95
+ async def fbs(
96
+ self, request: FBSTeamsRequest | None = None, /, **filters: object
97
+ ) -> FrameT:
98
+ """Return FBS teams as the selected DataFrame type.
99
+
100
+ :param request: Validated request model, mutually exclusive with filters.
101
+ :param filters: Explicit snake-case endpoint filters.
102
+ :return: Eager frame containing validated ``Team`` rows.
103
+ :raises TypeError: If request styles are mixed or the model type is wrong.
104
+ :raises CFBDError: If request, transport, response, or conversion fails.
105
+ """
106
+ endpoint = "/teams/fbs"
107
+ validated = _resolve_request(
108
+ endpoint=endpoint,
109
+ request_type=FBSTeamsRequest,
110
+ request=request,
111
+ filters=filters,
112
+ )
113
+ rows = await self._executor.fetch_many(
114
+ endpoint=endpoint, request=validated, response_adapter=_TEAM_ROWS
115
+ )
116
+ return self._dataframe_adapter.from_models(
117
+ endpoint=endpoint, row_model=Team, models=rows
118
+ )
119
+
120
+ @overload
121
+ async def matchup(self, request: TeamMatchupRequest, /) -> FrameT: ...
122
+
123
+ @overload
124
+ async def matchup(
125
+ self,
126
+ request: None = None,
127
+ /,
128
+ *,
129
+ team1: str,
130
+ team2: str,
131
+ min_year: int | None = None,
132
+ max_year: int | None = None,
133
+ ) -> FrameT: ...
134
+
135
+ async def matchup(
136
+ self, request: TeamMatchupRequest | None = None, /, **filters: object
137
+ ) -> FrameT:
138
+ """Return one matchup summary with nested games as a selected frame.
139
+
140
+ pandas stores ``games`` in an ``object`` column. Polars stores the same
141
+ logical values in a native ``List[Struct]`` column.
142
+
143
+ :param request: Validated request model, mutually exclusive with filters.
144
+ :param filters: Explicit snake-case endpoint filters.
145
+ :return: One-row eager frame containing the validated matchup summary.
146
+ :raises TypeError: If request styles are mixed or the model type is wrong.
147
+ :raises CFBDError: If request, transport, response, or conversion fails.
148
+ """
149
+ endpoint = "/teams/matchup"
150
+ validated = _resolve_request(
151
+ endpoint=endpoint,
152
+ request_type=TeamMatchupRequest,
153
+ request=request,
154
+ filters=filters,
155
+ )
156
+ matchup = await self._executor.fetch_one(
157
+ endpoint=endpoint, request=validated, response_adapter=_MATCHUP
158
+ )
159
+ return self._dataframe_adapter.from_models(
160
+ endpoint=endpoint, row_model=Matchup, models=[matchup]
161
+ )
162
+
163
+ @overload
164
+ async def ats(self, request: TeamATSRequest, /) -> FrameT: ...
165
+
166
+ @overload
167
+ async def ats(
168
+ self,
169
+ request: None = None,
170
+ /,
171
+ *,
172
+ year: int,
173
+ conference: str | None = None,
174
+ team: str | None = None,
175
+ ) -> FrameT: ...
176
+
177
+ async def ats(
178
+ self, request: TeamATSRequest | None = None, /, **filters: object
179
+ ) -> FrameT:
180
+ """Return team against-the-spread records as the selected frame.
181
+
182
+ :param request: Validated request model, mutually exclusive with filters.
183
+ :param filters: Explicit snake-case endpoint filters.
184
+ :return: Eager frame containing validated ``TeamATS`` rows.
185
+ :raises TypeError: If request styles are mixed or the model type is wrong.
186
+ :raises CFBDError: If request, transport, response, or conversion fails.
187
+ """
188
+ endpoint = "/teams/ats"
189
+ validated = _resolve_request(
190
+ endpoint=endpoint,
191
+ request_type=TeamATSRequest,
192
+ request=request,
193
+ filters=filters,
194
+ )
195
+ rows = await self._executor.fetch_many(
196
+ endpoint=endpoint, request=validated, response_adapter=_ATS_ROWS
197
+ )
198
+ return self._dataframe_adapter.from_models(
199
+ endpoint=endpoint, row_model=TeamATS, models=rows
200
+ )
201
+
202
+ @overload
203
+ async def roster(self, request: RosterRequest, /) -> FrameT: ...
204
+
205
+ @overload
206
+ async def roster(
207
+ self,
208
+ request: None = None,
209
+ /,
210
+ *,
211
+ team: str | None = None,
212
+ year: int | None = None,
213
+ classification: _ClassificationArgument | None = None,
214
+ ) -> FrameT: ...
215
+
216
+ async def roster(
217
+ self, request: RosterRequest | None = None, /, **filters: object
218
+ ) -> FrameT:
219
+ """Return historical roster players as the selected frame.
220
+
221
+ :param request: Validated request model, mutually exclusive with filters.
222
+ :param filters: Explicit snake-case endpoint filters.
223
+ :return: Eager frame containing validated ``RosterPlayer`` rows.
224
+ :raises TypeError: If request styles are mixed or the model type is wrong.
225
+ :raises CFBDError: If request, transport, response, or conversion fails.
226
+ """
227
+ endpoint = "/roster"
228
+ validated = _resolve_request(
229
+ endpoint=endpoint,
230
+ request_type=RosterRequest,
231
+ request=request,
232
+ filters=filters,
233
+ )
234
+ rows = await self._executor.fetch_many(
235
+ endpoint=endpoint, request=validated, response_adapter=_ROSTER_ROWS
236
+ )
237
+ return self._dataframe_adapter.from_models(
238
+ endpoint=endpoint, row_model=RosterPlayer, models=rows
239
+ )
240
+
241
+ @overload
242
+ async def talent(self, request: TalentRequest, /) -> FrameT: ...
243
+
244
+ @overload
245
+ async def talent(self, request: None = None, /, *, year: int) -> FrameT: ...
246
+
247
+ async def talent(
248
+ self, request: TalentRequest | None = None, /, **filters: object
249
+ ) -> FrameT:
250
+ """Return team talent ratings as the selected frame.
251
+
252
+ :param request: Validated request model, mutually exclusive with filters.
253
+ :param filters: Explicit snake-case endpoint filters.
254
+ :return: Eager frame containing validated ``TeamTalent`` rows.
255
+ :raises TypeError: If request styles are mixed or the model type is wrong.
256
+ :raises CFBDError: If request, transport, response, or conversion fails.
257
+ """
258
+ endpoint = "/talent"
259
+ validated = _resolve_request(
260
+ endpoint=endpoint,
261
+ request_type=TalentRequest,
262
+ request=request,
263
+ filters=filters,
264
+ )
265
+ rows = await self._executor.fetch_many(
266
+ endpoint=endpoint, request=validated, response_adapter=_TALENT_ROWS
267
+ )
268
+ return self._dataframe_adapter.from_models(
269
+ endpoint=endpoint, row_model=TeamTalent, models=rows
270
+ )
@@ -0,0 +1,6 @@
1
+ """Export the supported Venues namespace and public contracts."""
2
+
3
+ from .models.pydantic import Venue
4
+ from .resource import VenuesResource
5
+
6
+ __all__ = ["Venue", "VenuesResource"]
@@ -0,0 +1 @@
1
+ """Provide authoritative Pydantic models for Venues endpoints."""
@@ -0,0 +1,5 @@
1
+ """Export Pydantic models for Venues endpoints."""
2
+
3
+ from .responses import Venue
4
+
5
+ __all__ = ["Venue"]
@@ -0,0 +1,24 @@
1
+ """Validate responses from implemented CFBD Venues endpoints."""
2
+
3
+ from pydantic import BaseModel, ConfigDict, Field
4
+
5
+
6
+ class Venue(BaseModel):
7
+ """Represent one venue or a Team's nested home location."""
8
+
9
+ model_config = ConfigDict(populate_by_name=True, extra="forbid")
10
+
11
+ id: int | None = Field(ge=0)
12
+ name: str | None
13
+ city: str | None
14
+ state: str | None
15
+ zip: str | None
16
+ country_code: str | None = Field(alias="countryCode")
17
+ timezone: str | None
18
+ latitude: float | None
19
+ longitude: float | None
20
+ elevation: str | None
21
+ capacity: int | None = Field(ge=0)
22
+ construction_year: int | None = Field(default=None, alias="constructionYear")
23
+ grass: bool | None = None
24
+ dome: bool | None = None
@@ -0,0 +1,51 @@
1
+ """Expose typed Venues endpoints through the primary client."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from pydantic import BaseModel, ConfigDict, TypeAdapter
6
+
7
+ from cfb_data._dataframes import _DataFrameAdapter
8
+ from cfb_data._executor import _EndpointExecutor
9
+ from cfb_data.venues.models.pydantic.responses import Venue
10
+
11
+ _VENUE_ROWS = TypeAdapter(list[Venue])
12
+
13
+
14
+ class _VenuesRequest(BaseModel):
15
+ """Represent the empty filter set accepted by ``GET /venues``."""
16
+
17
+ model_config = ConfigDict(extra="forbid")
18
+
19
+
20
+ _VENUES_REQUEST = _VenuesRequest()
21
+
22
+
23
+ class VenuesResource[FrameT]:
24
+ """Provide validated Venues endpoints with backend-specific frame results."""
25
+
26
+ def __init__(
27
+ self,
28
+ executor: _EndpointExecutor,
29
+ dataframe_adapter: _DataFrameAdapter[FrameT],
30
+ ) -> None:
31
+ """Bind the namespace to shared execution and presentation services."""
32
+ self._executor = executor
33
+ self._dataframe_adapter = dataframe_adapter
34
+
35
+ async def list(self) -> FrameT:
36
+ """Return venues in upstream order as the selected DataFrame type.
37
+
38
+ :return: Eager frame containing validated ``Venue`` rows.
39
+ :raises CFBDError: If transport, response, or conversion fails.
40
+ """
41
+ endpoint = "/venues"
42
+ rows = await self._executor.fetch_many(
43
+ endpoint=endpoint,
44
+ request=_VENUES_REQUEST,
45
+ response_adapter=_VENUE_ROWS,
46
+ )
47
+ return self._dataframe_adapter.from_models(
48
+ endpoint=endpoint,
49
+ row_model=Venue,
50
+ models=rows,
51
+ )