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,220 @@
1
+ """Expose typed Adjusted Metrics endpoints through the primary client."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from collections.abc import Mapping
6
+ from typing import TypeVar, overload
7
+
8
+ from pydantic import BaseModel, TypeAdapter
9
+
10
+ from cfb_data._dataframes import _DataFrameAdapter
11
+ from cfb_data._executor import _EndpointExecutor
12
+ from cfb_data._requests import _resolve_request
13
+ from cfb_data.adjusted_metrics.models.pydantic.requests import (
14
+ AdjustedPlayerPassingRequest,
15
+ AdjustedPlayerRushingRequest,
16
+ AdjustedTeamMetricsRequest,
17
+ KickerPAARRequest,
18
+ )
19
+ from cfb_data.adjusted_metrics.models.pydantic.responses import (
20
+ AdjustedTeamMetrics,
21
+ KickerPAAR,
22
+ PlayerWeightedEPA,
23
+ )
24
+
25
+ _RequestT = TypeVar("_RequestT", bound=BaseModel)
26
+ _RowT = TypeVar("_RowT", bound=BaseModel)
27
+
28
+ _ADJUSTED_TEAM_ROWS = TypeAdapter(list[AdjustedTeamMetrics])
29
+ _PLAYER_WEIGHTED_EPA_ROWS = TypeAdapter(list[PlayerWeightedEPA])
30
+ _KICKER_PAAR_ROWS = TypeAdapter(list[KickerPAAR])
31
+
32
+
33
+ class AdjustedMetricsResource[FrameT]:
34
+ """Provide validated opponent-adjusted metrics as selected frames."""
35
+
36
+ def __init__(
37
+ self,
38
+ executor: _EndpointExecutor,
39
+ dataframe_adapter: _DataFrameAdapter[FrameT],
40
+ ) -> None:
41
+ """Bind the namespace to shared execution and presentation services."""
42
+ self._executor = executor
43
+ self._dataframe_adapter = dataframe_adapter
44
+
45
+ @overload
46
+ async def team_season(self, request: AdjustedTeamMetricsRequest, /) -> FrameT: ...
47
+
48
+ @overload
49
+ async def team_season(
50
+ self,
51
+ request: None = None,
52
+ /,
53
+ *,
54
+ year: int | None = None,
55
+ team: str | None = None,
56
+ conference: str | None = None,
57
+ ) -> FrameT: ...
58
+
59
+ async def team_season(
60
+ self, request: AdjustedTeamMetricsRequest | None = None, /, **filters: object
61
+ ) -> FrameT:
62
+ """Return opponent-adjusted team metrics by season.
63
+
64
+ :param request: Validated request model, mutually exclusive with filters.
65
+ :param filters: Explicit snake-case endpoint filters.
66
+ :return: Eager frame containing validated adjusted-team rows.
67
+ :raises TypeError: If request styles are mixed or the model type is wrong.
68
+ :raises CFBDError: If request, transport, response, or conversion fails.
69
+ """
70
+ return await self._fetch_many(
71
+ endpoint="/wepa/team/season",
72
+ request_type=AdjustedTeamMetricsRequest,
73
+ request=request,
74
+ filters=filters,
75
+ response_adapter=_ADJUSTED_TEAM_ROWS,
76
+ row_model=AdjustedTeamMetrics,
77
+ )
78
+
79
+ @overload
80
+ async def player_passing(
81
+ self, request: AdjustedPlayerPassingRequest, /
82
+ ) -> FrameT: ...
83
+
84
+ @overload
85
+ async def player_passing(
86
+ self,
87
+ request: None = None,
88
+ /,
89
+ *,
90
+ year: int | None = None,
91
+ team: str | None = None,
92
+ conference: str | None = None,
93
+ position: str | None = None,
94
+ ) -> FrameT: ...
95
+
96
+ async def player_passing(
97
+ self,
98
+ request: AdjustedPlayerPassingRequest | None = None,
99
+ /,
100
+ **filters: object,
101
+ ) -> FrameT:
102
+ """Return opponent-adjusted player passing EPA.
103
+
104
+ :param request: Validated request model, mutually exclusive with filters.
105
+ :param filters: Explicit snake-case endpoint filters.
106
+ :return: Eager frame containing validated player WEPA rows.
107
+ :raises TypeError: If request styles are mixed or the model type is wrong.
108
+ :raises CFBDError: If request, transport, response, or conversion fails.
109
+ """
110
+ return await self._fetch_many(
111
+ endpoint="/wepa/players/passing",
112
+ request_type=AdjustedPlayerPassingRequest,
113
+ request=request,
114
+ filters=filters,
115
+ response_adapter=_PLAYER_WEIGHTED_EPA_ROWS,
116
+ row_model=PlayerWeightedEPA,
117
+ )
118
+
119
+ @overload
120
+ async def player_rushing(
121
+ self, request: AdjustedPlayerRushingRequest, /
122
+ ) -> FrameT: ...
123
+
124
+ @overload
125
+ async def player_rushing(
126
+ self,
127
+ request: None = None,
128
+ /,
129
+ *,
130
+ year: int | None = None,
131
+ team: str | None = None,
132
+ conference: str | None = None,
133
+ position: str | None = None,
134
+ ) -> FrameT: ...
135
+
136
+ async def player_rushing(
137
+ self,
138
+ request: AdjustedPlayerRushingRequest | None = None,
139
+ /,
140
+ **filters: object,
141
+ ) -> FrameT:
142
+ """Return opponent-adjusted player rushing EPA.
143
+
144
+ :param request: Validated request model, mutually exclusive with filters.
145
+ :param filters: Explicit snake-case endpoint filters.
146
+ :return: Eager frame containing validated player WEPA rows.
147
+ :raises TypeError: If request styles are mixed or the model type is wrong.
148
+ :raises CFBDError: If request, transport, response, or conversion fails.
149
+ """
150
+ return await self._fetch_many(
151
+ endpoint="/wepa/players/rushing",
152
+ request_type=AdjustedPlayerRushingRequest,
153
+ request=request,
154
+ filters=filters,
155
+ response_adapter=_PLAYER_WEIGHTED_EPA_ROWS,
156
+ row_model=PlayerWeightedEPA,
157
+ )
158
+
159
+ @overload
160
+ async def kicker_paar(self, request: KickerPAARRequest, /) -> FrameT: ...
161
+
162
+ @overload
163
+ async def kicker_paar(
164
+ self,
165
+ request: None = None,
166
+ /,
167
+ *,
168
+ year: int | None = None,
169
+ team: str | None = None,
170
+ conference: str | None = None,
171
+ ) -> FrameT: ...
172
+
173
+ async def kicker_paar(
174
+ self, request: KickerPAARRequest | None = None, /, **filters: object
175
+ ) -> FrameT:
176
+ """Return Points Added Above Replacement ratings for kickers.
177
+
178
+ :param request: Validated request model, mutually exclusive with filters.
179
+ :param filters: Explicit snake-case endpoint filters.
180
+ :return: Eager frame containing validated kicker PAAR rows.
181
+ :raises TypeError: If request styles are mixed or the model type is wrong.
182
+ :raises CFBDError: If request, transport, response, or conversion fails.
183
+ """
184
+ return await self._fetch_many(
185
+ endpoint="/wepa/players/kicking",
186
+ request_type=KickerPAARRequest,
187
+ request=request,
188
+ filters=filters,
189
+ response_adapter=_KICKER_PAAR_ROWS,
190
+ row_model=KickerPAAR,
191
+ )
192
+
193
+ async def _fetch_many(
194
+ self,
195
+ *,
196
+ endpoint: str,
197
+ request_type: type[_RequestT],
198
+ request: _RequestT | None,
199
+ filters: Mapping[str, object],
200
+ response_adapter: TypeAdapter[list[_RowT]],
201
+ row_model: type[_RowT],
202
+ ) -> FrameT:
203
+ """Resolve, validate, fetch, and tabularize one list endpoint."""
204
+ validated = _resolve_request(
205
+ endpoint=endpoint,
206
+ request_type=request_type,
207
+ request=request,
208
+ filters=filters,
209
+ )
210
+ rows = await self._executor.fetch_many(
211
+ endpoint=endpoint,
212
+ request=validated,
213
+ response_adapter=response_adapter,
214
+ )
215
+ return self._dataframe_adapter.from_models(
216
+ endpoint=endpoint, row_model=row_model, models=rows
217
+ )
218
+
219
+
220
+ __all__ = ["AdjustedMetricsResource"]
@@ -0,0 +1,6 @@
1
+ """Base module for College Football Data API package.
2
+
3
+ This module contains shared components and base classes used throughout
4
+ the CFB Data package, including API foundations, validation utilities,
5
+ and common data transformation tools.
6
+ """
cfb_data/base/types.py ADDED
@@ -0,0 +1,112 @@
1
+ """Define and validate values crossing API serialization boundaries."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from collections.abc import Mapping
6
+
7
+ type JSONScalar = str | int | float | bool | None
8
+ type JSONValue = JSONScalar | list["JSONValue"] | dict[str, "JSONValue"]
9
+ type JSONObject = dict[str, JSONValue]
10
+ type JSONResponse = JSONObject | list[JSONObject]
11
+ type QueryValue = str | int | float | bool
12
+ type QueryParameters = dict[str, QueryValue]
13
+
14
+
15
+ def json_value(value: object) -> JSONValue:
16
+ """Validate and return a recursively JSON-compatible value.
17
+
18
+ :param value: Untrusted value from a JSON decoder or serializer.
19
+ :return: A value containing only JSON-compatible primitives and containers.
20
+ :raises TypeError: If the value is not JSON-compatible or has non-string keys.
21
+ """
22
+ if value is None or isinstance(value, str | int | float | bool):
23
+ return value
24
+ if isinstance(value, list):
25
+ return [json_value(item) for item in value]
26
+ if isinstance(value, dict):
27
+ result: JSONObject = {}
28
+ for key, item in value.items():
29
+ if not isinstance(key, str):
30
+ raise TypeError("JSON object keys must be strings")
31
+ result[key] = json_value(item)
32
+ return result
33
+ raise TypeError(f"Unsupported JSON value type: {type(value).__name__}")
34
+
35
+
36
+ def json_response(value: object) -> JSONResponse:
37
+ """Validate and return the supported top-level API response shape.
38
+
39
+ :param value: Untrusted value returned by the HTTP JSON decoder.
40
+ :return: A JSON object or a list of JSON objects.
41
+ :raises TypeError: If the value is not a supported API response.
42
+ """
43
+ parsed = json_value(value)
44
+ if isinstance(parsed, dict):
45
+ return parsed
46
+ if isinstance(parsed, list):
47
+ objects: list[JSONObject] = []
48
+ for item in parsed:
49
+ if not isinstance(item, dict):
50
+ raise TypeError("API response lists must contain only JSON objects")
51
+ objects.append(item)
52
+ return objects
53
+ raise TypeError("API response must be a JSON object or a list of JSON objects")
54
+
55
+
56
+ def json_object(value: object) -> JSONObject:
57
+ """Validate and return a JSON object.
58
+
59
+ :param value: Untrusted value expected to contain a JSON object.
60
+ :return: A recursively validated JSON object.
61
+ :raises TypeError: If the value is not a JSON object.
62
+ """
63
+ parsed = json_value(value)
64
+ if isinstance(parsed, dict):
65
+ return parsed
66
+ raise TypeError("Expected a JSON object")
67
+
68
+
69
+ def json_object_list(value: object) -> list[JSONObject]:
70
+ """Validate and return a list of JSON objects.
71
+
72
+ :param value: Untrusted value expected to contain JSON objects.
73
+ :return: A recursively validated list of JSON objects.
74
+ :raises TypeError: If the value is not a list of JSON objects.
75
+ """
76
+ parsed = json_response(value)
77
+ if isinstance(parsed, list):
78
+ return parsed
79
+ raise TypeError("Expected a list of JSON objects")
80
+
81
+
82
+ def json_list(value: object) -> list[JSONValue]:
83
+ """Validate and return a list of recursively JSON-compatible values.
84
+
85
+ :param value: Untrusted value expected to contain a JSON array.
86
+ :return: Recursively validated JSON values in their original order.
87
+ :raises TypeError: If the value is not a JSON array.
88
+ """
89
+ parsed = json_value(value)
90
+ if isinstance(parsed, list):
91
+ return parsed
92
+ raise TypeError("Expected a JSON array")
93
+
94
+
95
+ def query_parameters(value: object) -> QueryParameters:
96
+ """Validate and return URL query parameters.
97
+
98
+ :param value: Serialized request-model data.
99
+ :return: Query parameters containing scalar URL values.
100
+ :raises TypeError: If a key or value cannot be represented in a query string.
101
+ """
102
+ if not isinstance(value, Mapping):
103
+ raise TypeError("Query parameters must be a mapping")
104
+
105
+ result: QueryParameters = {}
106
+ for key, item in value.items():
107
+ if not isinstance(key, str):
108
+ raise TypeError("Query parameter keys must be strings")
109
+ if not isinstance(item, str | int | float | bool):
110
+ raise TypeError(f"Unsupported query parameter type for {key!r}")
111
+ result[key] = item
112
+ return result
@@ -0,0 +1,15 @@
1
+ """Export the supported Betting namespace and public contracts."""
2
+
3
+ from cfb_data.enums import Classification, SeasonType
4
+
5
+ from .models.pydantic import BettingGame, BettingLinesRequest, GameLine
6
+ from .resource import BettingResource
7
+
8
+ __all__ = [
9
+ "BettingGame",
10
+ "BettingLinesRequest",
11
+ "BettingResource",
12
+ "Classification",
13
+ "GameLine",
14
+ "SeasonType",
15
+ ]
@@ -0,0 +1 @@
1
+ """Export Betting model families."""
@@ -0,0 +1,6 @@
1
+ """Export validated Betting request and response models."""
2
+
3
+ from .requests import BettingLinesRequest
4
+ from .responses import BettingGame, GameLine
5
+
6
+ __all__ = ["BettingGame", "BettingLinesRequest", "GameLine"]
@@ -0,0 +1,35 @@
1
+ """Validate request parameters for implemented Betting 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._request_rules import _validate_year_or_game_id
10
+ from cfb_data.enums import SeasonType
11
+
12
+
13
+ class BettingLinesRequest(BaseModel):
14
+ """Validate filters accepted by ``GET /lines``."""
15
+
16
+ model_config = ConfigDict(populate_by_name=True, extra="forbid")
17
+
18
+ game_id: int | None = Field(default=None, alias="gameId", gt=0)
19
+ year: int | None = Field(default=None, ge=1869)
20
+ season_type: SeasonType | None = Field(default=None, alias="seasonType")
21
+ week: int | None = Field(default=None, ge=0)
22
+ team: str | None = Field(default=None, min_length=1)
23
+ home: str | None = Field(default=None, min_length=1)
24
+ away: str | None = Field(default=None, min_length=1)
25
+ conference: str | None = Field(default=None, min_length=1)
26
+ provider: str | None = Field(default=None, min_length=1)
27
+
28
+ @model_validator(mode="after")
29
+ def validate_selectors(self) -> Self:
30
+ """Require a season or game identifier."""
31
+ _validate_year_or_game_id(self.year, self.game_id)
32
+ return self
33
+
34
+
35
+ __all__ = ["BettingLinesRequest"]
@@ -0,0 +1,62 @@
1
+ """Validate responses from implemented CFBD Betting 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.enums import Classification, SeasonType
10
+
11
+
12
+ class _ResponseModel(BaseModel):
13
+ """Apply the upstream closed-object contract to Betting responses."""
14
+
15
+ model_config = ConfigDict(populate_by_name=True, extra="forbid")
16
+
17
+ @field_validator("*", mode="after", check_fields=False)
18
+ @classmethod
19
+ def require_utc_datetimes(cls, value: object) -> object:
20
+ """Require aware response timestamps and normalize them to UTC."""
21
+ if not isinstance(value, datetime):
22
+ return value
23
+ if value.tzinfo is None or value.utcoffset() is None:
24
+ raise ValueError("Response timestamps must be timezone-aware")
25
+ return value.astimezone(UTC)
26
+
27
+
28
+ class GameLine(_ResponseModel):
29
+ """Represent one provider's line for a game."""
30
+
31
+ provider: str
32
+ spread: float | None
33
+ formatted_spread: str = Field(alias="formattedSpread")
34
+ spread_open: float | None = Field(alias="spreadOpen")
35
+ over_under: float | None = Field(alias="overUnder")
36
+ over_under_open: float | None = Field(alias="overUnderOpen")
37
+ home_moneyline: int | None = Field(alias="homeMoneyline")
38
+ away_moneyline: int | None = Field(alias="awayMoneyline")
39
+
40
+
41
+ class BettingGame(_ResponseModel):
42
+ """Represent one game and all returned provider lines."""
43
+
44
+ id: int = Field(gt=0)
45
+ season: int = Field(ge=1869)
46
+ season_type: SeasonType = Field(alias="seasonType")
47
+ week: int = Field(ge=0)
48
+ start_date: datetime = Field(alias="startDate")
49
+ home_team_id: int = Field(alias="homeTeamId", gt=0)
50
+ home_team: str = Field(alias="homeTeam")
51
+ home_conference: str | None = Field(alias="homeConference")
52
+ home_classification: Classification | None = Field(alias="homeClassification")
53
+ home_score: int | None = Field(alias="homeScore", ge=0)
54
+ away_team_id: int = Field(alias="awayTeamId", gt=0)
55
+ away_team: str = Field(alias="awayTeam")
56
+ away_conference: str | None = Field(alias="awayConference")
57
+ away_classification: Classification | None = Field(alias="awayClassification")
58
+ away_score: int | None = Field(alias="awayScore", ge=0)
59
+ lines: list[GameLine]
60
+
61
+
62
+ __all__ = ["BettingGame", "GameLine"]
@@ -0,0 +1,91 @@
1
+ """Expose typed Betting 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.betting.models.pydantic.requests import BettingLinesRequest
13
+ from cfb_data.betting.models.pydantic.responses import BettingGame
14
+ from cfb_data.enums import 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
+
28
+ _BETTING_GAME_ROWS = TypeAdapter(list[BettingGame])
29
+
30
+
31
+ class BettingResource[FrameT]:
32
+ """Provide validated Betting endpoints with selected 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 lines(self, request: BettingLinesRequest, /) -> FrameT: ...
45
+
46
+ @overload
47
+ async def lines(
48
+ self,
49
+ request: None = None,
50
+ /,
51
+ *,
52
+ game_id: int | None = None,
53
+ year: int | None = None,
54
+ season_type: _SeasonTypeArgument | None = None,
55
+ week: int | None = None,
56
+ team: str | None = None,
57
+ home: str | None = None,
58
+ away: str | None = None,
59
+ conference: str | None = None,
60
+ provider: str | None = None,
61
+ ) -> FrameT: ...
62
+
63
+ async def lines(
64
+ self, request: BettingLinesRequest | None = None, /, **filters: object
65
+ ) -> FrameT:
66
+ """Return games with nested historical provider lines.
67
+
68
+ :param request: Validated request model, mutually exclusive with filters.
69
+ :param filters: Explicit snake-case endpoint filters.
70
+ :return: Eager frame containing validated betting-game rows.
71
+ :raises TypeError: If request styles are mixed or the model type is wrong.
72
+ :raises CFBDError: If request, transport, response, or conversion fails.
73
+ """
74
+ endpoint = "/lines"
75
+ validated = _resolve_request(
76
+ endpoint=endpoint,
77
+ request_type=BettingLinesRequest,
78
+ request=request,
79
+ filters=filters,
80
+ )
81
+ rows = await self._executor.fetch_many(
82
+ endpoint=endpoint,
83
+ request=validated,
84
+ response_adapter=_BETTING_GAME_ROWS,
85
+ )
86
+ return self._dataframe_adapter.from_models(
87
+ endpoint=endpoint, row_model=BettingGame, models=rows
88
+ )
89
+
90
+
91
+ __all__ = ["BettingResource"]