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.
- cfb_data/__init__.py +234 -0
- cfb_data/_dataframes.py +279 -0
- cfb_data/_executor.py +129 -0
- cfb_data/_parquet.py +197 -0
- cfb_data/_request_rules.py +41 -0
- cfb_data/_requests.py +36 -0
- cfb_data/_tabular.py +676 -0
- cfb_data/_transport.py +452 -0
- cfb_data/adjusted_metrics/__init__.py +29 -0
- cfb_data/adjusted_metrics/models/__init__.py +1 -0
- cfb_data/adjusted_metrics/models/pydantic/__init__.py +29 -0
- cfb_data/adjusted_metrics/models/pydantic/requests.py +45 -0
- cfb_data/adjusted_metrics/models/pydantic/responses.py +88 -0
- cfb_data/adjusted_metrics/resource.py +220 -0
- cfb_data/base/__init__.py +6 -0
- cfb_data/base/types.py +112 -0
- cfb_data/betting/__init__.py +15 -0
- cfb_data/betting/models/__init__.py +1 -0
- cfb_data/betting/models/pydantic/__init__.py +6 -0
- cfb_data/betting/models/pydantic/requests.py +35 -0
- cfb_data/betting/models/pydantic/responses.py +62 -0
- cfb_data/betting/resource.py +91 -0
- cfb_data/client.py +305 -0
- cfb_data/coaches/__init__.py +57 -0
- cfb_data/coaches/models/__init__.py +1 -0
- cfb_data/coaches/models/pydantic/__init__.py +57 -0
- cfb_data/coaches/models/pydantic/requests.py +75 -0
- cfb_data/coaches/models/pydantic/responses.py +261 -0
- cfb_data/coaches/resource.py +216 -0
- cfb_data/conferences/__init__.py +23 -0
- cfb_data/conferences/models/__init__.py +1 -0
- cfb_data/conferences/models/pydantic/__init__.py +23 -0
- cfb_data/conferences/models/pydantic/requests.py +69 -0
- cfb_data/conferences/models/pydantic/responses.py +64 -0
- cfb_data/conferences/resource.py +175 -0
- cfb_data/draft/__init__.py +19 -0
- cfb_data/draft/models/__init__.py +1 -0
- cfb_data/draft/models/pydantic/__init__.py +12 -0
- cfb_data/draft/models/pydantic/requests.py +18 -0
- cfb_data/draft/models/pydantic/responses.py +65 -0
- cfb_data/draft/resource.py +132 -0
- cfb_data/drives/__init__.py +13 -0
- cfb_data/drives/models/__init__.py +1 -0
- cfb_data/drives/models/pydantic/__init__.py +17 -0
- cfb_data/drives/models/pydantic/requests.py +42 -0
- cfb_data/drives/models/pydantic/responses.py +46 -0
- cfb_data/drives/resource.py +94 -0
- cfb_data/enums.py +93 -0
- cfb_data/errors.py +234 -0
- cfb_data/games/__init__.py +42 -0
- cfb_data/games/models/__init__.py +1 -0
- cfb_data/games/models/pydantic/__init__.py +106 -0
- cfb_data/games/models/pydantic/requests.py +266 -0
- cfb_data/games/models/pydantic/responses.py +495 -0
- cfb_data/games/resource.py +486 -0
- cfb_data/info/__init__.py +25 -0
- cfb_data/info/models/__init__.py +1 -0
- cfb_data/info/models/pydantic/__init__.py +23 -0
- cfb_data/info/models/pydantic/requests.py +18 -0
- cfb_data/info/models/pydantic/responses.py +103 -0
- cfb_data/info/resource.py +88 -0
- cfb_data/metrics/__init__.py +53 -0
- cfb_data/metrics/models/__init__.py +1 -0
- cfb_data/metrics/models/pydantic/__init__.py +49 -0
- cfb_data/metrics/models/pydantic/requests.py +121 -0
- cfb_data/metrics/models/pydantic/responses.py +182 -0
- cfb_data/metrics/resource.py +371 -0
- cfb_data/players/__init__.py +44 -0
- cfb_data/players/models/__init__.py +1 -0
- cfb_data/players/models/pydantic/__init__.py +41 -0
- cfb_data/players/models/pydantic/requests.py +70 -0
- cfb_data/players/models/pydantic/responses.py +171 -0
- cfb_data/players/resource.py +258 -0
- cfb_data/playoffs/__init__.py +41 -0
- cfb_data/playoffs/models/__init__.py +1 -0
- cfb_data/playoffs/models/pydantic/__init__.py +37 -0
- cfb_data/playoffs/models/pydantic/requests.py +30 -0
- cfb_data/playoffs/models/pydantic/responses.py +173 -0
- cfb_data/playoffs/resource.py +149 -0
- cfb_data/plays/__init__.py +43 -0
- cfb_data/plays/models/__init__.py +1 -0
- cfb_data/plays/models/pydantic/__init__.py +35 -0
- cfb_data/plays/models/pydantic/requests.py +85 -0
- cfb_data/plays/models/pydantic/responses.py +231 -0
- cfb_data/plays/resource.py +249 -0
- cfb_data/py.typed +0 -0
- cfb_data/rankings/__init__.py +16 -0
- cfb_data/rankings/models/__init__.py +1 -0
- cfb_data/rankings/models/pydantic/__init__.py +6 -0
- cfb_data/rankings/models/pydantic/requests.py +36 -0
- cfb_data/rankings/models/pydantic/responses.py +42 -0
- cfb_data/rankings/resource.py +89 -0
- cfb_data/ratings/__init__.py +59 -0
- cfb_data/ratings/models/__init__.py +1 -0
- cfb_data/ratings/models/pydantic/__init__.py +55 -0
- cfb_data/ratings/models/pydantic/requests.py +87 -0
- cfb_data/ratings/models/pydantic/responses.py +215 -0
- cfb_data/ratings/resource.py +342 -0
- cfb_data/recruiting/__init__.py +26 -0
- cfb_data/recruiting/models/__init__.py +1 -0
- cfb_data/recruiting/models/pydantic/__init__.py +23 -0
- cfb_data/recruiting/models/pydantic/requests.py +69 -0
- cfb_data/recruiting/models/pydantic/responses.py +70 -0
- cfb_data/recruiting/resource.py +180 -0
- cfb_data/retry.py +49 -0
- cfb_data/stats/__init__.py +69 -0
- cfb_data/stats/models/__init__.py +1 -0
- cfb_data/stats/models/pydantic/__init__.py +65 -0
- cfb_data/stats/models/pydantic/requests.py +167 -0
- cfb_data/stats/models/pydantic/responses.py +291 -0
- cfb_data/stats/resource.py +400 -0
- cfb_data/teams/__init__.py +40 -0
- cfb_data/teams/models/__init__.py +1 -0
- cfb_data/teams/models/pydantic/__init__.py +26 -0
- cfb_data/teams/models/pydantic/requests.py +96 -0
- cfb_data/teams/models/pydantic/responses.py +116 -0
- cfb_data/teams/resource.py +270 -0
- cfb_data/venues/__init__.py +6 -0
- cfb_data/venues/models/__init__.py +1 -0
- cfb_data/venues/models/pydantic/__init__.py +5 -0
- cfb_data/venues/models/pydantic/responses.py +24 -0
- cfb_data/venues/resource.py +51 -0
- cfb_data-0.4.1.dist-info/METADATA +414 -0
- cfb_data-0.4.1.dist-info/RECORD +127 -0
- cfb_data-0.4.1.dist-info/WHEEL +5 -0
- cfb_data-0.4.1.dist-info/licenses/LICENSE +21 -0
- cfb_data-0.4.1.dist-info/top_level.txt +1 -0
cfb_data/__init__.py
ADDED
|
@@ -0,0 +1,234 @@
|
|
|
1
|
+
"""Access validated CollegeFootballData endpoints as pandas or Polars frames."""
|
|
2
|
+
|
|
3
|
+
from .adjusted_metrics.models.pydantic.requests import (
|
|
4
|
+
AdjustedPlayerPassingRequest,
|
|
5
|
+
AdjustedPlayerRushingRequest,
|
|
6
|
+
AdjustedTeamMetricsRequest,
|
|
7
|
+
KickerPAARRequest,
|
|
8
|
+
)
|
|
9
|
+
from .betting.models.pydantic.requests import BettingLinesRequest
|
|
10
|
+
from .client import CFBDClient, DataFrameBackend
|
|
11
|
+
from .coaches.models.pydantic.requests import (
|
|
12
|
+
CoachesRequest,
|
|
13
|
+
CoachProfileRequest,
|
|
14
|
+
CoachSeasonsRequest,
|
|
15
|
+
CoachTenuresRequest,
|
|
16
|
+
)
|
|
17
|
+
from .conferences.models.pydantic.requests import (
|
|
18
|
+
ConferenceAffiliationsRequest,
|
|
19
|
+
ConferenceChangesRequest,
|
|
20
|
+
ConferencesRequest,
|
|
21
|
+
)
|
|
22
|
+
from .conferences.models.pydantic.responses import ConferenceClassification
|
|
23
|
+
from .draft.models.pydantic.requests import DraftPicksRequest
|
|
24
|
+
from .drives.models.pydantic.requests import DrivesRequest
|
|
25
|
+
from .enums import (
|
|
26
|
+
Classification,
|
|
27
|
+
MediaType,
|
|
28
|
+
PlayoffCompetition,
|
|
29
|
+
PlayoffRound,
|
|
30
|
+
RankingPoll,
|
|
31
|
+
RecruitClassification,
|
|
32
|
+
SeasonType,
|
|
33
|
+
TransferEligibility,
|
|
34
|
+
UserUsageApi,
|
|
35
|
+
)
|
|
36
|
+
from .errors import (
|
|
37
|
+
CFBDAuthenticationError,
|
|
38
|
+
CFBDAuthorizationError,
|
|
39
|
+
CFBDClientStateError,
|
|
40
|
+
CFBDConfigurationError,
|
|
41
|
+
CFBDDataFrameConversionError,
|
|
42
|
+
CFBDError,
|
|
43
|
+
CFBDHTTPError,
|
|
44
|
+
CFBDOptionalDependencyError,
|
|
45
|
+
CFBDRateLimitError,
|
|
46
|
+
CFBDRequestValidationError,
|
|
47
|
+
CFBDResponseDecodeError,
|
|
48
|
+
CFBDResponseValidationError,
|
|
49
|
+
CFBDServerError,
|
|
50
|
+
CFBDTimeoutError,
|
|
51
|
+
CFBDTLSError,
|
|
52
|
+
CFBDTransportError,
|
|
53
|
+
)
|
|
54
|
+
from .games.models.pydantic.requests import (
|
|
55
|
+
AdvancedBoxScoreRequest,
|
|
56
|
+
CalendarRequest,
|
|
57
|
+
GameMediaRequest,
|
|
58
|
+
GamesRequest,
|
|
59
|
+
GameWeatherRequest,
|
|
60
|
+
PlayerGameStatsRequest,
|
|
61
|
+
RecordsRequest,
|
|
62
|
+
ScoreboardRequest,
|
|
63
|
+
TeamGameStatsRequest,
|
|
64
|
+
)
|
|
65
|
+
from .games.models.pydantic.responses import AdvancedBoxScore
|
|
66
|
+
from .info.models.pydantic.requests import InfoUsageRequest
|
|
67
|
+
from .info.models.pydantic.responses import UserInfo, UserUsage
|
|
68
|
+
from .metrics.models.pydantic.requests import (
|
|
69
|
+
PlayerGamePPARequest,
|
|
70
|
+
PlayerSeasonPPARequest,
|
|
71
|
+
PredictedPointsRequest,
|
|
72
|
+
PregameWinProbabilityRequest,
|
|
73
|
+
TeamGamePPARequest,
|
|
74
|
+
TeamSeasonPPARequest,
|
|
75
|
+
WinProbabilityRequest,
|
|
76
|
+
)
|
|
77
|
+
from .players.models.pydantic.requests import (
|
|
78
|
+
PlayerSearchRequest,
|
|
79
|
+
PlayerSeasonOverviewRequest,
|
|
80
|
+
PlayerUsageRequest,
|
|
81
|
+
ReturningProductionRequest,
|
|
82
|
+
TransferPortalRequest,
|
|
83
|
+
)
|
|
84
|
+
from .playoffs.models.pydantic.requests import (
|
|
85
|
+
CfpGamesRequest,
|
|
86
|
+
CfpParticipantsRequest,
|
|
87
|
+
CfpPlayoffRequest,
|
|
88
|
+
)
|
|
89
|
+
from .playoffs.models.pydantic.responses import CfpPlayoff
|
|
90
|
+
from .plays.models.pydantic.requests import (
|
|
91
|
+
LivePlaysRequest,
|
|
92
|
+
PlaysRequest,
|
|
93
|
+
PlayStatsRequest,
|
|
94
|
+
)
|
|
95
|
+
from .plays.models.pydantic.responses import DownType, HomeAway, LiveGame, RushPass
|
|
96
|
+
from .rankings.models.pydantic.requests import RankingsRequest
|
|
97
|
+
from .ratings.models.pydantic.requests import (
|
|
98
|
+
ConferenceSPRatingsRequest,
|
|
99
|
+
CoreRatingsRequest,
|
|
100
|
+
EloRatingsRequest,
|
|
101
|
+
ExpandedSRSRatingsRequest,
|
|
102
|
+
FPIRatingsRequest,
|
|
103
|
+
SPRatingsRequest,
|
|
104
|
+
SRSRatingsRequest,
|
|
105
|
+
)
|
|
106
|
+
from .recruiting.models.pydantic.requests import (
|
|
107
|
+
RecruitingGroupsRequest,
|
|
108
|
+
RecruitingPlayersRequest,
|
|
109
|
+
RecruitingTeamsRequest,
|
|
110
|
+
)
|
|
111
|
+
from .retry import RetryPolicy
|
|
112
|
+
from .stats.models.pydantic.requests import (
|
|
113
|
+
AdvancedGameStatsRequest,
|
|
114
|
+
AdvancedSeasonStatsRequest,
|
|
115
|
+
GameHavocRequest,
|
|
116
|
+
PlayerGameSuccessRequest,
|
|
117
|
+
PlayerSeasonStatsRequest,
|
|
118
|
+
PlayerSeasonSuccessRequest,
|
|
119
|
+
TeamSeasonStatsRequest,
|
|
120
|
+
)
|
|
121
|
+
from .teams.models.pydantic.requests import (
|
|
122
|
+
FBSTeamsRequest,
|
|
123
|
+
RosterRequest,
|
|
124
|
+
TalentRequest,
|
|
125
|
+
TeamATSRequest,
|
|
126
|
+
TeamMatchupRequest,
|
|
127
|
+
TeamsRequest,
|
|
128
|
+
)
|
|
129
|
+
|
|
130
|
+
__all__ = [
|
|
131
|
+
"AdjustedPlayerPassingRequest",
|
|
132
|
+
"AdjustedPlayerRushingRequest",
|
|
133
|
+
"AdjustedTeamMetricsRequest",
|
|
134
|
+
"AdvancedBoxScoreRequest",
|
|
135
|
+
"AdvancedBoxScore",
|
|
136
|
+
"AdvancedGameStatsRequest",
|
|
137
|
+
"AdvancedSeasonStatsRequest",
|
|
138
|
+
"BettingLinesRequest",
|
|
139
|
+
"CalendarRequest",
|
|
140
|
+
"CfpGamesRequest",
|
|
141
|
+
"CfpParticipantsRequest",
|
|
142
|
+
"CfpPlayoff",
|
|
143
|
+
"CfpPlayoffRequest",
|
|
144
|
+
"CFBDAuthenticationError",
|
|
145
|
+
"CFBDAuthorizationError",
|
|
146
|
+
"CFBDClient",
|
|
147
|
+
"CFBDClientStateError",
|
|
148
|
+
"CFBDConfigurationError",
|
|
149
|
+
"CFBDDataFrameConversionError",
|
|
150
|
+
"CFBDError",
|
|
151
|
+
"CFBDHTTPError",
|
|
152
|
+
"CFBDOptionalDependencyError",
|
|
153
|
+
"CFBDRateLimitError",
|
|
154
|
+
"CFBDRequestValidationError",
|
|
155
|
+
"CFBDResponseDecodeError",
|
|
156
|
+
"CFBDResponseValidationError",
|
|
157
|
+
"CFBDServerError",
|
|
158
|
+
"CFBDTimeoutError",
|
|
159
|
+
"CFBDTLSError",
|
|
160
|
+
"CFBDTransportError",
|
|
161
|
+
"Classification",
|
|
162
|
+
"CoachesRequest",
|
|
163
|
+
"CoachProfileRequest",
|
|
164
|
+
"CoachSeasonsRequest",
|
|
165
|
+
"CoachTenuresRequest",
|
|
166
|
+
"ConferenceAffiliationsRequest",
|
|
167
|
+
"ConferenceChangesRequest",
|
|
168
|
+
"ConferenceClassification",
|
|
169
|
+
"ConferencesRequest",
|
|
170
|
+
"ConferenceSPRatingsRequest",
|
|
171
|
+
"CoreRatingsRequest",
|
|
172
|
+
"DataFrameBackend",
|
|
173
|
+
"DraftPicksRequest",
|
|
174
|
+
"DrivesRequest",
|
|
175
|
+
"EloRatingsRequest",
|
|
176
|
+
"ExpandedSRSRatingsRequest",
|
|
177
|
+
"FPIRatingsRequest",
|
|
178
|
+
"GameMediaRequest",
|
|
179
|
+
"GamesRequest",
|
|
180
|
+
"GameWeatherRequest",
|
|
181
|
+
"GameHavocRequest",
|
|
182
|
+
"FBSTeamsRequest",
|
|
183
|
+
"HomeAway",
|
|
184
|
+
"InfoUsageRequest",
|
|
185
|
+
"KickerPAARRequest",
|
|
186
|
+
"LiveGame",
|
|
187
|
+
"LivePlaysRequest",
|
|
188
|
+
"MediaType",
|
|
189
|
+
"PlayerGameStatsRequest",
|
|
190
|
+
"PlayerGamePPARequest",
|
|
191
|
+
"PlayerGameSuccessRequest",
|
|
192
|
+
"PlayoffCompetition",
|
|
193
|
+
"PlayoffRound",
|
|
194
|
+
"PlaysRequest",
|
|
195
|
+
"PlayStatsRequest",
|
|
196
|
+
"PlayerSeasonStatsRequest",
|
|
197
|
+
"PlayerSearchRequest",
|
|
198
|
+
"PlayerSeasonOverviewRequest",
|
|
199
|
+
"PlayerSeasonPPARequest",
|
|
200
|
+
"PlayerSeasonSuccessRequest",
|
|
201
|
+
"PlayerUsageRequest",
|
|
202
|
+
"PredictedPointsRequest",
|
|
203
|
+
"PregameWinProbabilityRequest",
|
|
204
|
+
"RecordsRequest",
|
|
205
|
+
"RankingPoll",
|
|
206
|
+
"RankingsRequest",
|
|
207
|
+
"RecruitClassification",
|
|
208
|
+
"RecruitingGroupsRequest",
|
|
209
|
+
"RecruitingPlayersRequest",
|
|
210
|
+
"RecruitingTeamsRequest",
|
|
211
|
+
"RetryPolicy",
|
|
212
|
+
"RosterRequest",
|
|
213
|
+
"ReturningProductionRequest",
|
|
214
|
+
"RushPass",
|
|
215
|
+
"ScoreboardRequest",
|
|
216
|
+
"SeasonType",
|
|
217
|
+
"SPRatingsRequest",
|
|
218
|
+
"SRSRatingsRequest",
|
|
219
|
+
"TeamGamePPARequest",
|
|
220
|
+
"TeamGameStatsRequest",
|
|
221
|
+
"TeamSeasonStatsRequest",
|
|
222
|
+
"TeamSeasonPPARequest",
|
|
223
|
+
"TalentRequest",
|
|
224
|
+
"TeamATSRequest",
|
|
225
|
+
"TeamMatchupRequest",
|
|
226
|
+
"TeamsRequest",
|
|
227
|
+
"TransferPortalRequest",
|
|
228
|
+
"TransferEligibility",
|
|
229
|
+
"UserInfo",
|
|
230
|
+
"UserUsage",
|
|
231
|
+
"UserUsageApi",
|
|
232
|
+
"WinProbabilityRequest",
|
|
233
|
+
"DownType",
|
|
234
|
+
]
|
cfb_data/_dataframes.py
ADDED
|
@@ -0,0 +1,279 @@
|
|
|
1
|
+
"""Materialize canonical Arrow tables as pandas or Polars DataFrames."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from collections.abc import Sequence
|
|
6
|
+
from typing import TYPE_CHECKING, Protocol, TypeVar
|
|
7
|
+
|
|
8
|
+
import pandas as pd
|
|
9
|
+
import pyarrow as pa
|
|
10
|
+
from pydantic import BaseModel
|
|
11
|
+
|
|
12
|
+
from cfb_data._tabular import (
|
|
13
|
+
_arrow_table_from_models,
|
|
14
|
+
_assert_canonical_arrow_table,
|
|
15
|
+
_logical_records_from_arrow_table,
|
|
16
|
+
_logical_schema,
|
|
17
|
+
_LogicalType,
|
|
18
|
+
)
|
|
19
|
+
from cfb_data.errors import (
|
|
20
|
+
CFBDDataFrameConversionError,
|
|
21
|
+
CFBDOptionalDependencyError,
|
|
22
|
+
_sanitized_cause,
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
if TYPE_CHECKING:
|
|
26
|
+
import polars as pl
|
|
27
|
+
|
|
28
|
+
_FrameT_co = TypeVar("_FrameT_co", covariant=True)
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class _DataFrameAdapter(Protocol[_FrameT_co]):
|
|
32
|
+
"""Convert canonical tabular values to one eager DataFrame type."""
|
|
33
|
+
|
|
34
|
+
def from_models(
|
|
35
|
+
self,
|
|
36
|
+
*,
|
|
37
|
+
endpoint: str,
|
|
38
|
+
row_model: type[BaseModel],
|
|
39
|
+
models: Sequence[BaseModel],
|
|
40
|
+
) -> _FrameT_co:
|
|
41
|
+
"""Return a frame preserving all validated rows and columns."""
|
|
42
|
+
|
|
43
|
+
def from_table(
|
|
44
|
+
self,
|
|
45
|
+
*,
|
|
46
|
+
endpoint: str,
|
|
47
|
+
row_model: type[BaseModel],
|
|
48
|
+
table: pa.Table,
|
|
49
|
+
) -> _FrameT_co:
|
|
50
|
+
"""Return a frame materialized from a canonical Arrow table."""
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
class _PandasAdapter:
|
|
54
|
+
"""Create pandas DataFrames with explicit native and nullable dtypes."""
|
|
55
|
+
|
|
56
|
+
def from_models(
|
|
57
|
+
self,
|
|
58
|
+
*,
|
|
59
|
+
endpoint: str,
|
|
60
|
+
row_model: type[BaseModel],
|
|
61
|
+
models: Sequence[BaseModel],
|
|
62
|
+
) -> pd.DataFrame:
|
|
63
|
+
"""Return a pandas frame for validated model rows.
|
|
64
|
+
|
|
65
|
+
:param endpoint: Endpoint producing the rows.
|
|
66
|
+
:param row_model: Authoritative Pydantic row model.
|
|
67
|
+
:param models: Validated rows in API order.
|
|
68
|
+
:return: DataFrame with a normal :class:`pandas.RangeIndex`.
|
|
69
|
+
:raises CFBDDataFrameConversionError: If conversion loses the contract.
|
|
70
|
+
"""
|
|
71
|
+
try:
|
|
72
|
+
table = _arrow_table_from_models(row_model=row_model, models=models)
|
|
73
|
+
return self.from_table(
|
|
74
|
+
endpoint=endpoint,
|
|
75
|
+
row_model=row_model,
|
|
76
|
+
table=table,
|
|
77
|
+
)
|
|
78
|
+
except CFBDDataFrameConversionError:
|
|
79
|
+
raise
|
|
80
|
+
except Exception as exc:
|
|
81
|
+
safe_cause = _sanitized_cause(exc)
|
|
82
|
+
raise CFBDDataFrameConversionError(
|
|
83
|
+
endpoint=endpoint,
|
|
84
|
+
backend="pandas",
|
|
85
|
+
) from safe_cause
|
|
86
|
+
|
|
87
|
+
def from_table(
|
|
88
|
+
self,
|
|
89
|
+
*,
|
|
90
|
+
endpoint: str,
|
|
91
|
+
row_model: type[BaseModel],
|
|
92
|
+
table: pa.Table,
|
|
93
|
+
) -> pd.DataFrame:
|
|
94
|
+
"""Return a pandas frame from a canonical Arrow table.
|
|
95
|
+
|
|
96
|
+
:param endpoint: Endpoint associated with the table.
|
|
97
|
+
:param row_model: Authoritative Pydantic row model.
|
|
98
|
+
:param table: Canonical Arrow table in source row order.
|
|
99
|
+
:return: DataFrame with explicit pandas dtypes and nested objects.
|
|
100
|
+
:raises CFBDDataFrameConversionError: If conversion loses the contract.
|
|
101
|
+
"""
|
|
102
|
+
try:
|
|
103
|
+
schema = _logical_schema(row_model)
|
|
104
|
+
records = _logical_records_from_arrow_table(
|
|
105
|
+
row_model=row_model,
|
|
106
|
+
table=table,
|
|
107
|
+
)
|
|
108
|
+
columns = {
|
|
109
|
+
field.name: pd.Series(
|
|
110
|
+
[record[field.name] for record in records],
|
|
111
|
+
dtype=_pandas_dtype(field.type),
|
|
112
|
+
)
|
|
113
|
+
for field in schema.fields
|
|
114
|
+
}
|
|
115
|
+
frame = pd.DataFrame(columns)
|
|
116
|
+
_assert_frame_shape(
|
|
117
|
+
columns=list(frame.columns),
|
|
118
|
+
row_count=len(frame),
|
|
119
|
+
expected_columns=[field.name for field in schema.fields],
|
|
120
|
+
expected_rows=table.num_rows,
|
|
121
|
+
)
|
|
122
|
+
if not frame.index.equals(pd.RangeIndex(table.num_rows)):
|
|
123
|
+
raise ValueError("pandas conversion did not preserve a RangeIndex")
|
|
124
|
+
return frame
|
|
125
|
+
except CFBDDataFrameConversionError:
|
|
126
|
+
raise
|
|
127
|
+
except Exception as exc:
|
|
128
|
+
safe_cause = _sanitized_cause(exc)
|
|
129
|
+
raise CFBDDataFrameConversionError(
|
|
130
|
+
endpoint=endpoint,
|
|
131
|
+
backend="pandas",
|
|
132
|
+
) from safe_cause
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
class _PolarsAdapter:
|
|
136
|
+
"""Create strict Polars DataFrames with native nested columns."""
|
|
137
|
+
|
|
138
|
+
def from_models(
|
|
139
|
+
self,
|
|
140
|
+
*,
|
|
141
|
+
endpoint: str,
|
|
142
|
+
row_model: type[BaseModel],
|
|
143
|
+
models: Sequence[BaseModel],
|
|
144
|
+
) -> pl.DataFrame:
|
|
145
|
+
"""Return a Polars frame for validated model rows.
|
|
146
|
+
|
|
147
|
+
:param endpoint: Endpoint producing the rows.
|
|
148
|
+
:param row_model: Authoritative Pydantic row model.
|
|
149
|
+
:param models: Validated rows in API order.
|
|
150
|
+
:return: Strict eager Polars DataFrame.
|
|
151
|
+
:raises CFBDOptionalDependencyError: If Polars is not installed.
|
|
152
|
+
:raises CFBDDataFrameConversionError: If conversion loses the contract.
|
|
153
|
+
"""
|
|
154
|
+
self._require_polars()
|
|
155
|
+
try:
|
|
156
|
+
table = _arrow_table_from_models(row_model=row_model, models=models)
|
|
157
|
+
return self.from_table(
|
|
158
|
+
endpoint=endpoint,
|
|
159
|
+
row_model=row_model,
|
|
160
|
+
table=table,
|
|
161
|
+
)
|
|
162
|
+
except CFBDDataFrameConversionError:
|
|
163
|
+
raise
|
|
164
|
+
except Exception as exc:
|
|
165
|
+
safe_cause = _sanitized_cause(exc)
|
|
166
|
+
raise CFBDDataFrameConversionError(
|
|
167
|
+
endpoint=endpoint,
|
|
168
|
+
backend="polars",
|
|
169
|
+
) from safe_cause
|
|
170
|
+
|
|
171
|
+
def from_table(
|
|
172
|
+
self,
|
|
173
|
+
*,
|
|
174
|
+
endpoint: str,
|
|
175
|
+
row_model: type[BaseModel],
|
|
176
|
+
table: pa.Table,
|
|
177
|
+
) -> pl.DataFrame:
|
|
178
|
+
"""Return a Polars frame from a canonical Arrow table.
|
|
179
|
+
|
|
180
|
+
:param endpoint: Endpoint associated with the table.
|
|
181
|
+
:param row_model: Authoritative Pydantic row model.
|
|
182
|
+
:param table: Canonical Arrow table in source row order.
|
|
183
|
+
:return: DataFrame with Arrow-native nesting and decoded mixed scalars.
|
|
184
|
+
:raises CFBDOptionalDependencyError: If Polars is not installed.
|
|
185
|
+
:raises CFBDDataFrameConversionError: If conversion loses the contract.
|
|
186
|
+
"""
|
|
187
|
+
try:
|
|
188
|
+
import polars as pl
|
|
189
|
+
except ModuleNotFoundError as exc:
|
|
190
|
+
if exc.name == "polars":
|
|
191
|
+
raise CFBDOptionalDependencyError(
|
|
192
|
+
'Polars support requires pip install "cfb-data[polars]"'
|
|
193
|
+
) from exc
|
|
194
|
+
raise
|
|
195
|
+
|
|
196
|
+
try:
|
|
197
|
+
schema = _logical_schema(row_model)
|
|
198
|
+
_assert_canonical_arrow_table(row_model=row_model, table=table)
|
|
199
|
+
frame = pl.from_arrow(table, rechunk=True)
|
|
200
|
+
if not isinstance(frame, pl.DataFrame):
|
|
201
|
+
raise TypeError("Arrow table did not produce a Polars DataFrame")
|
|
202
|
+
scalar_fields = [
|
|
203
|
+
(index, field)
|
|
204
|
+
for index, field in enumerate(schema.fields)
|
|
205
|
+
if field.type.kind == "scalar"
|
|
206
|
+
]
|
|
207
|
+
if scalar_fields:
|
|
208
|
+
records = _logical_records_from_arrow_table(
|
|
209
|
+
row_model=row_model,
|
|
210
|
+
table=table,
|
|
211
|
+
)
|
|
212
|
+
for index, field in scalar_fields:
|
|
213
|
+
frame.replace_column(
|
|
214
|
+
index,
|
|
215
|
+
pl.Series(
|
|
216
|
+
field.name,
|
|
217
|
+
[record[field.name] for record in records],
|
|
218
|
+
dtype=pl.Object,
|
|
219
|
+
strict=True,
|
|
220
|
+
),
|
|
221
|
+
)
|
|
222
|
+
_assert_frame_shape(
|
|
223
|
+
columns=frame.columns,
|
|
224
|
+
row_count=frame.height,
|
|
225
|
+
expected_columns=[field.name for field in schema.fields],
|
|
226
|
+
expected_rows=table.num_rows,
|
|
227
|
+
)
|
|
228
|
+
return frame
|
|
229
|
+
except CFBDOptionalDependencyError:
|
|
230
|
+
raise
|
|
231
|
+
except Exception as exc:
|
|
232
|
+
safe_cause = _sanitized_cause(exc)
|
|
233
|
+
raise CFBDDataFrameConversionError(
|
|
234
|
+
endpoint=endpoint,
|
|
235
|
+
backend="polars",
|
|
236
|
+
) from safe_cause
|
|
237
|
+
|
|
238
|
+
@staticmethod
|
|
239
|
+
def _require_polars() -> None:
|
|
240
|
+
"""Fail with install guidance before doing backend-independent work."""
|
|
241
|
+
try:
|
|
242
|
+
import polars # noqa: F401
|
|
243
|
+
except ModuleNotFoundError as exc:
|
|
244
|
+
if exc.name == "polars":
|
|
245
|
+
raise CFBDOptionalDependencyError(
|
|
246
|
+
'Polars support requires pip install "cfb-data[polars]"'
|
|
247
|
+
) from exc
|
|
248
|
+
raise
|
|
249
|
+
|
|
250
|
+
|
|
251
|
+
def _pandas_dtype(logical_type: _LogicalType) -> object:
|
|
252
|
+
"""Return the exact pandas dtype for a logical type."""
|
|
253
|
+
if logical_type.kind == "integer":
|
|
254
|
+
return "Int64" if logical_type.nullable else "int64"
|
|
255
|
+
if logical_type.kind == "float":
|
|
256
|
+
return "Float64" if logical_type.nullable else "float64"
|
|
257
|
+
if logical_type.kind == "boolean":
|
|
258
|
+
return "boolean" if logical_type.nullable else "bool"
|
|
259
|
+
if logical_type.kind == "string":
|
|
260
|
+
return "string"
|
|
261
|
+
if logical_type.kind == "datetime":
|
|
262
|
+
return "datetime64[ns, UTC]"
|
|
263
|
+
if logical_type.kind in {"scalar", "struct", "list"}:
|
|
264
|
+
return object
|
|
265
|
+
raise AssertionError(f"Unreachable logical kind: {logical_type.kind}")
|
|
266
|
+
|
|
267
|
+
|
|
268
|
+
def _assert_frame_shape(
|
|
269
|
+
*,
|
|
270
|
+
columns: Sequence[str],
|
|
271
|
+
row_count: int,
|
|
272
|
+
expected_columns: Sequence[str],
|
|
273
|
+
expected_rows: int,
|
|
274
|
+
) -> None:
|
|
275
|
+
"""Reject conversions that changed column order or lost rows."""
|
|
276
|
+
if list(columns) != list(expected_columns):
|
|
277
|
+
raise ValueError("DataFrame columns do not match the logical schema")
|
|
278
|
+
if row_count != expected_rows:
|
|
279
|
+
raise ValueError("DataFrame row count does not match validated rows")
|
cfb_data/_executor.py
ADDED
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
"""Execute endpoints through request serialization and Pydantic validation."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import TypeVar
|
|
6
|
+
|
|
7
|
+
from pydantic import BaseModel, TypeAdapter, ValidationError
|
|
8
|
+
|
|
9
|
+
from cfb_data._transport import _HTTPTransport
|
|
10
|
+
from cfb_data.base.types import (
|
|
11
|
+
json_list,
|
|
12
|
+
json_object,
|
|
13
|
+
json_object_list,
|
|
14
|
+
query_parameters,
|
|
15
|
+
)
|
|
16
|
+
from cfb_data.errors import (
|
|
17
|
+
CFBDRequestValidationError,
|
|
18
|
+
CFBDResponseValidationError,
|
|
19
|
+
_sanitized_cause,
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
_ModelT = TypeVar("_ModelT", bound=BaseModel)
|
|
23
|
+
_ValueT = TypeVar("_ValueT")
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class _EndpointExecutor:
|
|
27
|
+
"""Return validated models without depending on DataFrame presentation."""
|
|
28
|
+
|
|
29
|
+
def __init__(self, transport: _HTTPTransport) -> None:
|
|
30
|
+
"""Bind endpoint execution to one owned transport.
|
|
31
|
+
|
|
32
|
+
:param transport: Context-managed transport used by all endpoint calls.
|
|
33
|
+
"""
|
|
34
|
+
self._transport = transport
|
|
35
|
+
|
|
36
|
+
async def fetch_many(
|
|
37
|
+
self,
|
|
38
|
+
*,
|
|
39
|
+
endpoint: str,
|
|
40
|
+
request: BaseModel,
|
|
41
|
+
response_adapter: TypeAdapter[list[_ModelT]],
|
|
42
|
+
) -> list[_ModelT]:
|
|
43
|
+
"""Fetch and validate a list response in API order.
|
|
44
|
+
|
|
45
|
+
:param endpoint: Fixed endpoint path.
|
|
46
|
+
:param request: Validated endpoint request model.
|
|
47
|
+
:param response_adapter: Typed list-response validator.
|
|
48
|
+
:return: Validated models in upstream row order.
|
|
49
|
+
:raises CFBDResponseValidationError: If response shape or values fail.
|
|
50
|
+
"""
|
|
51
|
+
raw = await self._transport.get_json(
|
|
52
|
+
endpoint,
|
|
53
|
+
_serialize_request(endpoint, request),
|
|
54
|
+
)
|
|
55
|
+
try:
|
|
56
|
+
payload = json_object_list(raw)
|
|
57
|
+
return response_adapter.validate_python(payload)
|
|
58
|
+
except (TypeError, ValidationError) as exc:
|
|
59
|
+
safe_cause = _sanitized_cause(exc)
|
|
60
|
+
raise CFBDResponseValidationError(endpoint=endpoint) from safe_cause
|
|
61
|
+
|
|
62
|
+
async def fetch_one(
|
|
63
|
+
self,
|
|
64
|
+
*,
|
|
65
|
+
endpoint: str,
|
|
66
|
+
request: BaseModel,
|
|
67
|
+
response_adapter: TypeAdapter[_ModelT],
|
|
68
|
+
) -> _ModelT:
|
|
69
|
+
"""Fetch and validate one model response.
|
|
70
|
+
|
|
71
|
+
:param endpoint: Fixed endpoint path.
|
|
72
|
+
:param request: Validated endpoint request model.
|
|
73
|
+
:param response_adapter: Typed object-response validator.
|
|
74
|
+
:return: Validated response model.
|
|
75
|
+
:raises CFBDResponseValidationError: If response shape or values fail.
|
|
76
|
+
"""
|
|
77
|
+
raw = await self._transport.get_json(
|
|
78
|
+
endpoint,
|
|
79
|
+
_serialize_request(endpoint, request),
|
|
80
|
+
)
|
|
81
|
+
try:
|
|
82
|
+
payload = json_object(raw)
|
|
83
|
+
return response_adapter.validate_python(payload)
|
|
84
|
+
except (TypeError, ValidationError) as exc:
|
|
85
|
+
safe_cause = _sanitized_cause(exc)
|
|
86
|
+
raise CFBDResponseValidationError(endpoint=endpoint) from safe_cause
|
|
87
|
+
|
|
88
|
+
async def fetch_values(
|
|
89
|
+
self,
|
|
90
|
+
*,
|
|
91
|
+
endpoint: str,
|
|
92
|
+
request: BaseModel,
|
|
93
|
+
response_adapter: TypeAdapter[list[_ValueT]],
|
|
94
|
+
) -> list[_ValueT]:
|
|
95
|
+
"""Fetch and validate a JSON-array response in API order.
|
|
96
|
+
|
|
97
|
+
:param endpoint: Fixed endpoint path.
|
|
98
|
+
:param request: Validated endpoint request model.
|
|
99
|
+
:param response_adapter: Typed array-response validator.
|
|
100
|
+
:return: Validated values in upstream order.
|
|
101
|
+
:raises CFBDResponseValidationError: If response shape or values fail.
|
|
102
|
+
"""
|
|
103
|
+
raw = await self._transport.get_json(
|
|
104
|
+
endpoint,
|
|
105
|
+
_serialize_request(endpoint, request),
|
|
106
|
+
)
|
|
107
|
+
try:
|
|
108
|
+
payload = json_list(raw)
|
|
109
|
+
return response_adapter.validate_python(payload)
|
|
110
|
+
except (TypeError, ValidationError) as exc:
|
|
111
|
+
safe_cause = _sanitized_cause(exc)
|
|
112
|
+
raise CFBDResponseValidationError(endpoint=endpoint) from safe_cause
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def _serialize_request(
|
|
116
|
+
endpoint: str, request: BaseModel
|
|
117
|
+
) -> dict[str, str | int | float | bool]:
|
|
118
|
+
"""Serialize one validated request using its upstream aliases."""
|
|
119
|
+
try:
|
|
120
|
+
parameters = query_parameters(
|
|
121
|
+
request.model_dump(mode="json", by_alias=True, exclude_none=True)
|
|
122
|
+
)
|
|
123
|
+
return {
|
|
124
|
+
key: str(value).lower() if isinstance(value, bool) else value
|
|
125
|
+
for key, value in parameters.items()
|
|
126
|
+
}
|
|
127
|
+
except TypeError as exc:
|
|
128
|
+
safe_cause = _sanitized_cause(exc)
|
|
129
|
+
raise CFBDRequestValidationError(endpoint=endpoint) from safe_cause
|