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,175 @@
1
+ """Expose typed Conferences 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 Literal, 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.conferences.models.pydantic.requests import (
15
+ ConferenceAffiliationsRequest,
16
+ ConferenceChangesRequest,
17
+ ConferencesRequest,
18
+ )
19
+ from cfb_data.conferences.models.pydantic.responses import (
20
+ Conference,
21
+ ConferenceClassification,
22
+ TeamConferenceAffiliation,
23
+ TeamConferenceChange,
24
+ )
25
+
26
+ _RequestT = TypeVar("_RequestT", bound=BaseModel)
27
+ _RowT = TypeVar("_RowT", bound=BaseModel)
28
+ type _ClassificationArgument = (
29
+ ConferenceClassification | Literal["fbs", "fcs", "ii", "ii/iii", "iii"]
30
+ )
31
+ _CONFERENCE_ROWS = TypeAdapter(list[Conference])
32
+ _CHANGE_ROWS = TypeAdapter(list[TeamConferenceChange])
33
+ _AFFILIATION_ROWS = TypeAdapter(list[TeamConferenceAffiliation])
34
+
35
+
36
+ class ConferencesResource[FrameT]:
37
+ """Provide Conferences endpoints with backend-specific 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: ConferencesRequest, /) -> FrameT: ...
50
+
51
+ @overload
52
+ async def list(
53
+ self,
54
+ request: None = None,
55
+ /,
56
+ *,
57
+ year: int | None = None,
58
+ classification: _ClassificationArgument | None = None,
59
+ ) -> FrameT: ...
60
+
61
+ async def list(
62
+ self, request: ConferencesRequest | None = None, /, **filters: object
63
+ ) -> FrameT:
64
+ """Return conferences as the selected DataFrame type.
65
+
66
+ :param request: Validated request model, mutually exclusive with filters.
67
+ :param filters: Explicit snake-case endpoint filters.
68
+ :return: Eager frame containing validated ``Conference`` rows.
69
+ :raises TypeError: If request styles are mixed or the model type is wrong.
70
+ :raises CFBDError: If request, transport, response, or conversion fails.
71
+ """
72
+ return await self._fetch_many(
73
+ endpoint="/conferences",
74
+ request_type=ConferencesRequest,
75
+ request=request,
76
+ filters=filters,
77
+ response_adapter=_CONFERENCE_ROWS,
78
+ row_model=Conference,
79
+ )
80
+
81
+ @overload
82
+ async def changes(self, request: ConferenceChangesRequest, /) -> FrameT: ...
83
+
84
+ @overload
85
+ async def changes(self, request: None = None, /, *, year: int) -> FrameT: ...
86
+
87
+ async def changes(
88
+ self, request: ConferenceChangesRequest | None = None, /, **filters: object
89
+ ) -> FrameT:
90
+ """Return team conference changes as the selected DataFrame type.
91
+
92
+ :param request: Validated request model, mutually exclusive with filters.
93
+ :param filters: Explicit snake-case endpoint filters.
94
+ :return: Eager frame containing validated change rows.
95
+ :raises TypeError: If request styles are mixed or the model type is wrong.
96
+ :raises CFBDError: If request, transport, response, or conversion fails.
97
+ """
98
+ return await self._fetch_many(
99
+ endpoint="/conferences/changes",
100
+ request_type=ConferenceChangesRequest,
101
+ request=request,
102
+ filters=filters,
103
+ response_adapter=_CHANGE_ROWS,
104
+ row_model=TeamConferenceChange,
105
+ )
106
+
107
+ @overload
108
+ async def affiliations(
109
+ self, request: ConferenceAffiliationsRequest, /
110
+ ) -> FrameT: ...
111
+
112
+ @overload
113
+ async def affiliations(
114
+ self,
115
+ request: None = None,
116
+ /,
117
+ *,
118
+ team: str | None = None,
119
+ conference: str | None = None,
120
+ year: int | None = None,
121
+ min_year: int | None = None,
122
+ max_year: int | None = None,
123
+ classification: _ClassificationArgument | None = None,
124
+ ) -> FrameT: ...
125
+
126
+ async def affiliations(
127
+ self,
128
+ request: ConferenceAffiliationsRequest | None = None,
129
+ /,
130
+ **filters: object,
131
+ ) -> FrameT:
132
+ """Return historical affiliations as the selected DataFrame type.
133
+
134
+ :param request: Validated request model, mutually exclusive with filters.
135
+ :param filters: Explicit snake-case endpoint filters.
136
+ :return: Eager frame containing validated affiliation rows.
137
+ :raises TypeError: If request styles are mixed or the model type is wrong.
138
+ :raises CFBDError: If request, transport, response, or conversion fails.
139
+ """
140
+ return await self._fetch_many(
141
+ endpoint="/conferences/affiliations",
142
+ request_type=ConferenceAffiliationsRequest,
143
+ request=request,
144
+ filters=filters,
145
+ response_adapter=_AFFILIATION_ROWS,
146
+ row_model=TeamConferenceAffiliation,
147
+ )
148
+
149
+ async def _fetch_many(
150
+ self,
151
+ *,
152
+ endpoint: str,
153
+ request_type: type[_RequestT],
154
+ request: _RequestT | None,
155
+ filters: Mapping[str, object],
156
+ response_adapter: TypeAdapter[builtins.list[_RowT]],
157
+ row_model: type[_RowT],
158
+ ) -> FrameT:
159
+ """Validate, fetch, and convert one Conferences list route."""
160
+ validated = _resolve_request(
161
+ endpoint=endpoint,
162
+ request_type=request_type,
163
+ request=request,
164
+ filters=filters,
165
+ )
166
+ rows = await self._executor.fetch_many(
167
+ endpoint=endpoint,
168
+ request=validated,
169
+ response_adapter=response_adapter,
170
+ )
171
+ return self._dataframe_adapter.from_models(
172
+ endpoint=endpoint,
173
+ row_model=row_model,
174
+ models=rows,
175
+ )
@@ -0,0 +1,19 @@
1
+ """Export the supported Draft namespace and public contracts."""
2
+
3
+ from .models.pydantic import (
4
+ DraftPick,
5
+ DraftPickHometown,
6
+ DraftPicksRequest,
7
+ DraftPosition,
8
+ DraftTeam,
9
+ )
10
+ from .resource import DraftResource
11
+
12
+ __all__ = [
13
+ "DraftPick",
14
+ "DraftPickHometown",
15
+ "DraftPicksRequest",
16
+ "DraftPosition",
17
+ "DraftResource",
18
+ "DraftTeam",
19
+ ]
@@ -0,0 +1 @@
1
+ """Export Draft model families."""
@@ -0,0 +1,12 @@
1
+ """Export validated Draft request and response models."""
2
+
3
+ from .requests import DraftPicksRequest
4
+ from .responses import DraftPick, DraftPickHometown, DraftPosition, DraftTeam
5
+
6
+ __all__ = [
7
+ "DraftPick",
8
+ "DraftPickHometown",
9
+ "DraftPicksRequest",
10
+ "DraftPosition",
11
+ "DraftTeam",
12
+ ]
@@ -0,0 +1,18 @@
1
+ """Validate request parameters for implemented Draft endpoints."""
2
+
3
+ from pydantic import BaseModel, ConfigDict, Field
4
+
5
+
6
+ class DraftPicksRequest(BaseModel):
7
+ """Validate filters accepted by ``GET /draft/picks``."""
8
+
9
+ model_config = ConfigDict(populate_by_name=True, extra="forbid")
10
+
11
+ year: int | None = Field(default=None, ge=1936)
12
+ team: str | None = Field(default=None, min_length=1)
13
+ school: str | None = Field(default=None, min_length=1)
14
+ conference: str | None = Field(default=None, min_length=1)
15
+ position: str | None = Field(default=None, min_length=1)
16
+
17
+
18
+ __all__ = ["DraftPicksRequest"]
@@ -0,0 +1,65 @@
1
+ """Validate responses from implemented CFBD Draft endpoints."""
2
+
3
+ from pydantic import BaseModel, ConfigDict, Field
4
+
5
+
6
+ class _ResponseModel(BaseModel):
7
+ """Apply the upstream closed-object contract to Draft responses."""
8
+
9
+ model_config = ConfigDict(populate_by_name=True, extra="forbid")
10
+
11
+
12
+ class DraftTeam(_ResponseModel):
13
+ """Represent an NFL team present in the historical draft data."""
14
+
15
+ location: str
16
+ nickname: str | None
17
+ display_name: str | None = Field(alias="displayName")
18
+ logo: str | None
19
+
20
+
21
+ class DraftPosition(_ResponseModel):
22
+ """Represent an NFL Draft position category."""
23
+
24
+ name: str
25
+ abbreviation: str
26
+
27
+
28
+ class DraftPickHometown(_ResponseModel):
29
+ """Represent the recorded hometown of an NFL Draft pick."""
30
+
31
+ city: str | None
32
+ state: str | None
33
+ country: str | None
34
+ latitude: str | None
35
+ longitude: str | None
36
+ county_fips: str | None = Field(alias="countyFips")
37
+
38
+
39
+ class DraftPick(_ResponseModel):
40
+ """Represent one historical NFL Draft selection."""
41
+
42
+ college_athlete_id: int | None = Field(alias="collegeAthleteId", gt=0)
43
+ nfl_athlete_id: int = Field(alias="nflAthleteId", gt=0)
44
+ college_id: int = Field(alias="collegeId", gt=0)
45
+ college_team: str = Field(alias="collegeTeam")
46
+ college_conference: str | None = Field(alias="collegeConference")
47
+ nfl_team_id: int = Field(alias="nflTeamId", gt=0)
48
+ nfl_team: str = Field(alias="nflTeam")
49
+ year: int = Field(ge=1936)
50
+ overall: int = Field(gt=0)
51
+ round: int = Field(gt=0)
52
+ pick: int = Field(gt=0)
53
+ name: str
54
+ position: str
55
+ height: float | None = Field(ge=0)
56
+ weight: int | None = Field(ge=0)
57
+ pre_draft_ranking: int | None = Field(alias="preDraftRanking", gt=0)
58
+ pre_draft_position_ranking: int | None = Field(
59
+ alias="preDraftPositionRanking", gt=0
60
+ )
61
+ pre_draft_grade: int | None = Field(alias="preDraftGrade", ge=0)
62
+ hometown_info: DraftPickHometown = Field(alias="hometownInfo")
63
+
64
+
65
+ __all__ = ["DraftPick", "DraftPickHometown", "DraftPosition", "DraftTeam"]
@@ -0,0 +1,132 @@
1
+ """Expose typed Draft endpoints through the primary client."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import TypeVar, overload
6
+
7
+ from pydantic import BaseModel, ConfigDict, 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.draft.models.pydantic.requests import DraftPicksRequest
13
+ from cfb_data.draft.models.pydantic.responses import (
14
+ DraftPick,
15
+ DraftPosition,
16
+ DraftTeam,
17
+ )
18
+
19
+ _RowT = TypeVar("_RowT", bound=BaseModel)
20
+
21
+ _DRAFT_TEAM_ROWS = TypeAdapter(list[DraftTeam])
22
+ _DRAFT_POSITION_ROWS = TypeAdapter(list[DraftPosition])
23
+ _DRAFT_PICK_ROWS = TypeAdapter(list[DraftPick])
24
+
25
+
26
+ class _EmptyRequest(BaseModel):
27
+ """Represent an endpoint that accepts no filters."""
28
+
29
+ model_config = ConfigDict(extra="forbid")
30
+
31
+
32
+ _EMPTY_REQUEST = _EmptyRequest()
33
+
34
+
35
+ class DraftResource[FrameT]:
36
+ """Provide validated Draft endpoints with selected frame results."""
37
+
38
+ def __init__(
39
+ self,
40
+ executor: _EndpointExecutor,
41
+ dataframe_adapter: _DataFrameAdapter[FrameT],
42
+ ) -> None:
43
+ """Bind the namespace to shared execution and presentation services."""
44
+ self._executor = executor
45
+ self._dataframe_adapter = dataframe_adapter
46
+
47
+ async def teams(self) -> FrameT:
48
+ """Return NFL teams represented in historical draft data.
49
+
50
+ :return: Eager frame containing validated NFL team rows.
51
+ :raises CFBDError: If transport, response, or conversion fails.
52
+ """
53
+ return await self._fetch_without_filters(
54
+ endpoint="/draft/teams",
55
+ response_adapter=_DRAFT_TEAM_ROWS,
56
+ row_model=DraftTeam,
57
+ )
58
+
59
+ async def positions(self) -> FrameT:
60
+ """Return position categories used in NFL Draft data.
61
+
62
+ :return: Eager frame containing validated position rows.
63
+ :raises CFBDError: If transport, response, or conversion fails.
64
+ """
65
+ return await self._fetch_without_filters(
66
+ endpoint="/draft/positions",
67
+ response_adapter=_DRAFT_POSITION_ROWS,
68
+ row_model=DraftPosition,
69
+ )
70
+
71
+ @overload
72
+ async def picks(self, request: DraftPicksRequest, /) -> FrameT: ...
73
+
74
+ @overload
75
+ async def picks(
76
+ self,
77
+ request: None = None,
78
+ /,
79
+ *,
80
+ year: int | None = None,
81
+ team: str | None = None,
82
+ school: str | None = None,
83
+ conference: str | None = None,
84
+ position: str | None = None,
85
+ ) -> FrameT: ...
86
+
87
+ async def picks(
88
+ self, request: DraftPicksRequest | None = None, /, **filters: object
89
+ ) -> FrameT:
90
+ """Return historical NFL Draft picks.
91
+
92
+ :param request: Validated request model, mutually exclusive with filters.
93
+ :param filters: Explicit snake-case endpoint filters.
94
+ :return: Eager frame containing validated draft-pick rows.
95
+ :raises TypeError: If request styles are mixed or the model type is wrong.
96
+ :raises CFBDError: If request, transport, response, or conversion fails.
97
+ """
98
+ endpoint = "/draft/picks"
99
+ validated = _resolve_request(
100
+ endpoint=endpoint,
101
+ request_type=DraftPicksRequest,
102
+ request=request,
103
+ filters=filters,
104
+ )
105
+ rows = await self._executor.fetch_many(
106
+ endpoint=endpoint,
107
+ request=validated,
108
+ response_adapter=_DRAFT_PICK_ROWS,
109
+ )
110
+ return self._dataframe_adapter.from_models(
111
+ endpoint=endpoint, row_model=DraftPick, models=rows
112
+ )
113
+
114
+ async def _fetch_without_filters(
115
+ self,
116
+ *,
117
+ endpoint: str,
118
+ response_adapter: TypeAdapter[list[_RowT]],
119
+ row_model: type[_RowT],
120
+ ) -> FrameT:
121
+ """Fetch and tabularize one filterless Draft endpoint."""
122
+ rows = await self._executor.fetch_many(
123
+ endpoint=endpoint,
124
+ request=_EMPTY_REQUEST,
125
+ response_adapter=response_adapter,
126
+ )
127
+ return self._dataframe_adapter.from_models(
128
+ endpoint=endpoint, row_model=row_model, models=rows
129
+ )
130
+
131
+
132
+ __all__ = ["DraftResource"]
@@ -0,0 +1,13 @@
1
+ """Export the supported Drives namespace and request contract."""
2
+
3
+ from cfb_data.enums import Classification, SeasonType
4
+
5
+ from .models.pydantic.requests import DrivesRequest
6
+ from .resource import DrivesResource
7
+
8
+ __all__ = [
9
+ "Classification",
10
+ "DrivesRequest",
11
+ "DrivesResource",
12
+ "SeasonType",
13
+ ]
@@ -0,0 +1 @@
1
+ """Provide authoritative Pydantic models for Drives endpoints."""
@@ -0,0 +1,17 @@
1
+ """Pydantic models for drives endpoints."""
2
+
3
+ from .requests import (
4
+ DrivesRequest,
5
+ )
6
+ from .responses import (
7
+ Drive,
8
+ DriveTime,
9
+ )
10
+
11
+ __all__ = [
12
+ # Request models
13
+ "DrivesRequest",
14
+ # Response models
15
+ "DriveTime",
16
+ "Drive",
17
+ ]
@@ -0,0 +1,42 @@
1
+ """Validate request parameters for the implemented Drives endpoint."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from pydantic import BaseModel, ConfigDict, Field
6
+
7
+ from cfb_data.enums import Classification, SeasonType
8
+
9
+
10
+ class DrivesRequest(BaseModel):
11
+ """Validate filters accepted by ``GET /drives``.
12
+
13
+ :param year: Required season year.
14
+ :param season_type: Optional season phase.
15
+ :param week: Optional non-negative season week.
16
+ :param team: Team appearing on offense or defense.
17
+ :param offense: Offensive-team selector.
18
+ :param defense: Defensive-team selector.
19
+ :param conference: Conference appearing on offense or defense.
20
+ :param offense_conference: Offensive-team conference selector.
21
+ :param defense_conference: Defensive-team conference selector.
22
+ :param classification: Division classification selector.
23
+ """
24
+
25
+ model_config = ConfigDict(populate_by_name=True, extra="forbid")
26
+
27
+ year: int = Field(ge=1869)
28
+ season_type: SeasonType | None = Field(default=None, alias="seasonType")
29
+ week: int | None = Field(default=None, ge=0)
30
+ team: str | None = None
31
+ offense: str | None = None
32
+ defense: str | None = None
33
+ conference: str | None = None
34
+ offense_conference: str | None = Field(
35
+ default=None,
36
+ alias="offenseConference",
37
+ )
38
+ defense_conference: str | None = Field(
39
+ default=None,
40
+ alias="defenseConference",
41
+ )
42
+ classification: Classification | None = None
@@ -0,0 +1,46 @@
1
+ """Pydantic models for drives endpoint responses."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from pydantic import BaseModel, ConfigDict, Field
6
+
7
+
8
+ class DriveTime(BaseModel):
9
+ """Time remaining in a period."""
10
+
11
+ seconds: int | None = Field(ge=0)
12
+ minutes: int | None = Field(ge=0)
13
+
14
+ model_config = ConfigDict(populate_by_name=True, extra="forbid")
15
+
16
+
17
+ class Drive(BaseModel):
18
+ """Drive data model for `/drives` endpoint."""
19
+
20
+ offense: str = Field(alias="offense")
21
+ offense_conference: str | None = Field(alias="offenseConference")
22
+ defense: str = Field(alias="defense")
23
+ defense_conference: str | None = Field(alias="defenseConference")
24
+ game_id: int = Field(alias="gameId", ge=0)
25
+ id: str = Field(alias="id")
26
+ drive_number: int | None = Field(alias="driveNumber", ge=0)
27
+ scoring: bool = Field(alias="scoring")
28
+ start_period: int = Field(alias="startPeriod", ge=0)
29
+ start_yardline: int = Field(alias="startYardline", ge=0)
30
+ start_yards_to_goal: int = Field(alias="startYardsToGoal", ge=0)
31
+ start_time: DriveTime = Field(alias="startTime")
32
+ end_period: int = Field(alias="endPeriod", ge=0)
33
+ end_yardline: int = Field(alias="endYardline", ge=0)
34
+ end_yards_to_goal: int = Field(alias="endYardsToGoal", ge=0)
35
+ end_time: DriveTime = Field(alias="endTime")
36
+ elapsed: DriveTime
37
+ plays: int = Field(alias="plays", ge=0)
38
+ yards: int = Field(alias="yards")
39
+ drive_result: str = Field(alias="driveResult")
40
+ is_home_offense: bool = Field(alias="isHomeOffense")
41
+ start_offense_score: int = Field(alias="startOffenseScore", ge=0)
42
+ start_defense_score: int = Field(alias="startDefenseScore", ge=0)
43
+ end_offense_score: int = Field(alias="endOffenseScore", ge=0)
44
+ end_defense_score: int = Field(alias="endDefenseScore", ge=0)
45
+
46
+ model_config = ConfigDict(populate_by_name=True, extra="forbid")
@@ -0,0 +1,94 @@
1
+ """Expose typed drive 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.drives.models.pydantic.requests import DrivesRequest
13
+ from cfb_data.drives.models.pydantic.responses import Drive
14
+ from cfb_data.enums import Classification, SeasonType
15
+
16
+ type _SeasonTypeArgument = (
17
+ SeasonType
18
+ | Literal[
19
+ "regular",
20
+ "postseason",
21
+ "both",
22
+ "allstar",
23
+ "spring_regular",
24
+ "spring_postseason",
25
+ ]
26
+ )
27
+ type _ClassificationArgument = Classification | Literal["fbs", "fcs", "ii", "iii"]
28
+ _DRIVE_ROWS = TypeAdapter(list[Drive])
29
+
30
+
31
+ class DrivesResource[FrameT]:
32
+ """Provide validated Drives endpoints with backend-specific frame results."""
33
+
34
+ def __init__(
35
+ self,
36
+ executor: _EndpointExecutor,
37
+ dataframe_adapter: _DataFrameAdapter[FrameT],
38
+ ) -> None:
39
+ """Bind the namespace to shared execution and presentation services."""
40
+ self._executor = executor
41
+ self._dataframe_adapter = dataframe_adapter
42
+
43
+ @overload
44
+ async def list(self, request: DrivesRequest, /) -> FrameT: ...
45
+
46
+ @overload
47
+ async def list(
48
+ self,
49
+ request: None = None,
50
+ /,
51
+ *,
52
+ year: int,
53
+ season_type: _SeasonTypeArgument | None = None,
54
+ week: int | None = None,
55
+ team: str | None = None,
56
+ offense: str | None = None,
57
+ defense: str | None = None,
58
+ conference: str | None = None,
59
+ offense_conference: str | None = None,
60
+ defense_conference: str | None = None,
61
+ classification: _ClassificationArgument | None = None,
62
+ ) -> FrameT: ...
63
+
64
+ async def list(
65
+ self,
66
+ request: DrivesRequest | None = None,
67
+ /,
68
+ **filters: object,
69
+ ) -> FrameT:
70
+ """Return drives in upstream order as the selected DataFrame type.
71
+
72
+ :param request: Validated request model, mutually exclusive with filters.
73
+ :param filters: Explicit snake-case endpoint filters.
74
+ :return: Eager frame containing validated ``Drive`` rows.
75
+ :raises TypeError: If request styles are mixed or the model type is wrong.
76
+ :raises CFBDError: If request, transport, response, or conversion fails.
77
+ """
78
+ endpoint = "/drives"
79
+ validated = _resolve_request(
80
+ endpoint=endpoint,
81
+ request_type=DrivesRequest,
82
+ request=request,
83
+ filters=filters,
84
+ )
85
+ rows = await self._executor.fetch_many(
86
+ endpoint=endpoint,
87
+ request=validated,
88
+ response_adapter=_DRIVE_ROWS,
89
+ )
90
+ return self._dataframe_adapter.from_models(
91
+ endpoint=endpoint,
92
+ row_model=Drive,
93
+ models=rows,
94
+ )