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/_parquet.py
ADDED
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
"""Persist canonical tabular responses as versioned local Parquet files."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import os
|
|
6
|
+
import tempfile
|
|
7
|
+
from contextlib import suppress
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
from typing import Final, Literal
|
|
10
|
+
|
|
11
|
+
import pyarrow as pa
|
|
12
|
+
import pyarrow.parquet as pq
|
|
13
|
+
from pydantic import BaseModel, TypeAdapter, ValidationError
|
|
14
|
+
|
|
15
|
+
from cfb_data._tabular import (
|
|
16
|
+
_arrow_table_from_models,
|
|
17
|
+
_assert_canonical_arrow_table,
|
|
18
|
+
_CanonicalTableMetadataError,
|
|
19
|
+
_CanonicalTableSchemaError,
|
|
20
|
+
_logical_records_from_arrow_table,
|
|
21
|
+
_models_from_arrow_table,
|
|
22
|
+
_ScalarEncodingError,
|
|
23
|
+
_UnsupportedTableAnnotationError,
|
|
24
|
+
)
|
|
25
|
+
from cfb_data.errors import CFBDError, _sanitized_cause
|
|
26
|
+
|
|
27
|
+
_ParquetOperation = Literal["read", "write"]
|
|
28
|
+
_ParquetErrorCategory = Literal[
|
|
29
|
+
"io",
|
|
30
|
+
"format",
|
|
31
|
+
"metadata",
|
|
32
|
+
"schema",
|
|
33
|
+
"validation",
|
|
34
|
+
]
|
|
35
|
+
_ParquetValidation = Literal["full", "trusted_schema"]
|
|
36
|
+
|
|
37
|
+
_PARQUET_VERSION: Final = "1.0"
|
|
38
|
+
_PARQUET_COMPRESSION: Final = "snappy"
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
class _ParquetCodecError(CFBDError):
|
|
42
|
+
"""Report a safe categorized failure in the internal Parquet codec."""
|
|
43
|
+
|
|
44
|
+
operation: _ParquetOperation
|
|
45
|
+
category: _ParquetErrorCategory
|
|
46
|
+
|
|
47
|
+
def __init__(
|
|
48
|
+
self,
|
|
49
|
+
*,
|
|
50
|
+
operation: _ParquetOperation,
|
|
51
|
+
category: _ParquetErrorCategory,
|
|
52
|
+
) -> None:
|
|
53
|
+
"""Initialize a path- and payload-free codec failure.
|
|
54
|
+
|
|
55
|
+
:param operation: File operation that failed.
|
|
56
|
+
:param category: Safe failure classification for internal policy.
|
|
57
|
+
"""
|
|
58
|
+
self.operation = operation
|
|
59
|
+
self.category = category
|
|
60
|
+
super().__init__(f"Parquet {operation} failed ({category})")
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def _write_parquet(
|
|
64
|
+
path: str | os.PathLike[str],
|
|
65
|
+
*,
|
|
66
|
+
row_model: type[BaseModel],
|
|
67
|
+
table: pa.Table,
|
|
68
|
+
) -> None:
|
|
69
|
+
"""Atomically write a canonical Arrow table to one local Parquet file.
|
|
70
|
+
|
|
71
|
+
The destination parent must already exist. An existing destination is
|
|
72
|
+
replaced only after the complete temporary Parquet file closes successfully.
|
|
73
|
+
|
|
74
|
+
:param path: Local destination path.
|
|
75
|
+
:param row_model: Expected authoritative row model for the table.
|
|
76
|
+
:param table: Canonical Arrow table to persist.
|
|
77
|
+
:raises _ParquetCodecError: If validation, writing, or replacement fails.
|
|
78
|
+
"""
|
|
79
|
+
try:
|
|
80
|
+
_write_parquet_file(Path(path), row_model=row_model, table=table)
|
|
81
|
+
return
|
|
82
|
+
except Exception as exc:
|
|
83
|
+
category = _codec_error_category(exc)
|
|
84
|
+
safe_cause = _sanitized_cause(exc)
|
|
85
|
+
raise _ParquetCodecError(operation="write", category=category) from safe_cause
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def _read_parquet[ModelT: BaseModel](
|
|
89
|
+
path: str | os.PathLike[str],
|
|
90
|
+
*,
|
|
91
|
+
row_model: type[ModelT],
|
|
92
|
+
response_adapter: TypeAdapter[list[ModelT]],
|
|
93
|
+
validation: _ParquetValidation = "full",
|
|
94
|
+
) -> pa.Table:
|
|
95
|
+
"""Read and verify one versioned cfb-data Parquet file.
|
|
96
|
+
|
|
97
|
+
``full`` validation decodes and revalidates every row through Pydantic.
|
|
98
|
+
``trusted_schema`` is an internal fast path only for integrity-controlled
|
|
99
|
+
library caches; it still checks all metadata, Arrow types, and tagged scalar
|
|
100
|
+
invariants.
|
|
101
|
+
|
|
102
|
+
:param path: Local source path.
|
|
103
|
+
:param row_model: Expected authoritative row model.
|
|
104
|
+
:param response_adapter: Pydantic adapter for a list of expected rows.
|
|
105
|
+
:param validation: Full domain validation or trusted schema validation.
|
|
106
|
+
:return: Verified canonical Arrow table in stored row order.
|
|
107
|
+
:raises ValueError: If ``validation`` is not a supported literal.
|
|
108
|
+
:raises _ParquetCodecError: If reading or verification fails.
|
|
109
|
+
"""
|
|
110
|
+
if validation not in {"full", "trusted_schema"}:
|
|
111
|
+
raise ValueError("validation must be 'full' or 'trusted_schema'")
|
|
112
|
+
try:
|
|
113
|
+
table = _read_parquet_file(
|
|
114
|
+
Path(path),
|
|
115
|
+
row_model=row_model,
|
|
116
|
+
response_adapter=response_adapter,
|
|
117
|
+
validation=validation,
|
|
118
|
+
)
|
|
119
|
+
return table
|
|
120
|
+
except Exception as exc:
|
|
121
|
+
category = _codec_error_category(exc)
|
|
122
|
+
safe_cause = _sanitized_cause(exc)
|
|
123
|
+
raise _ParquetCodecError(operation="read", category=category) from safe_cause
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
def _write_parquet_file(
|
|
127
|
+
path: Path,
|
|
128
|
+
*,
|
|
129
|
+
row_model: type[BaseModel],
|
|
130
|
+
table: pa.Table,
|
|
131
|
+
) -> None:
|
|
132
|
+
"""Validate and atomically replace a local Parquet destination."""
|
|
133
|
+
_assert_canonical_arrow_table(row_model=row_model, table=table)
|
|
134
|
+
_logical_records_from_arrow_table(row_model=row_model, table=table)
|
|
135
|
+
if not path.parent.is_dir():
|
|
136
|
+
raise FileNotFoundError("Parquet destination parent does not exist")
|
|
137
|
+
|
|
138
|
+
descriptor, temporary_name = tempfile.mkstemp(
|
|
139
|
+
prefix=f".{path.name}.",
|
|
140
|
+
suffix=".tmp",
|
|
141
|
+
dir=path.parent,
|
|
142
|
+
)
|
|
143
|
+
temporary_path = Path(temporary_name)
|
|
144
|
+
try:
|
|
145
|
+
with os.fdopen(descriptor, "wb") as temporary_file:
|
|
146
|
+
pq.write_table(
|
|
147
|
+
table,
|
|
148
|
+
temporary_file,
|
|
149
|
+
version=_PARQUET_VERSION,
|
|
150
|
+
compression=_PARQUET_COMPRESSION,
|
|
151
|
+
write_statistics=True,
|
|
152
|
+
use_compliant_nested_type=True,
|
|
153
|
+
store_schema=True,
|
|
154
|
+
)
|
|
155
|
+
os.replace(temporary_path, path)
|
|
156
|
+
finally:
|
|
157
|
+
with suppress(FileNotFoundError):
|
|
158
|
+
temporary_path.unlink()
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
def _read_parquet_file[ModelT: BaseModel](
|
|
162
|
+
path: Path,
|
|
163
|
+
*,
|
|
164
|
+
row_model: type[ModelT],
|
|
165
|
+
response_adapter: TypeAdapter[list[ModelT]],
|
|
166
|
+
validation: _ParquetValidation,
|
|
167
|
+
) -> pa.Table:
|
|
168
|
+
"""Read a table and apply the selected internal validation policy."""
|
|
169
|
+
table = pq.read_table(path)
|
|
170
|
+
_assert_canonical_arrow_table(row_model=row_model, table=table)
|
|
171
|
+
if validation == "trusted_schema":
|
|
172
|
+
_logical_records_from_arrow_table(row_model=row_model, table=table)
|
|
173
|
+
return table
|
|
174
|
+
|
|
175
|
+
models = _models_from_arrow_table(
|
|
176
|
+
row_model=row_model,
|
|
177
|
+
response_adapter=response_adapter,
|
|
178
|
+
table=table,
|
|
179
|
+
)
|
|
180
|
+
return _arrow_table_from_models(row_model=row_model, models=models)
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
def _codec_error_category(source: Exception) -> _ParquetErrorCategory:
|
|
184
|
+
"""Classify a source exception without retaining its values or path."""
|
|
185
|
+
if isinstance(source, _CanonicalTableMetadataError):
|
|
186
|
+
return "metadata"
|
|
187
|
+
if isinstance(source, _CanonicalTableSchemaError):
|
|
188
|
+
return "schema"
|
|
189
|
+
if isinstance(source, _UnsupportedTableAnnotationError):
|
|
190
|
+
return "schema"
|
|
191
|
+
if isinstance(source, ValidationError):
|
|
192
|
+
return "validation"
|
|
193
|
+
if isinstance(source, OSError):
|
|
194
|
+
return "io"
|
|
195
|
+
if isinstance(source, _ScalarEncodingError):
|
|
196
|
+
return "format"
|
|
197
|
+
return "format"
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
"""Provide relational validation shared by endpoint request models."""
|
|
2
|
+
|
|
3
|
+
from collections.abc import Mapping, Sequence
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def _validate_year_or_game_id(
|
|
7
|
+
year: int | None,
|
|
8
|
+
game_id: int | None,
|
|
9
|
+
) -> None:
|
|
10
|
+
"""Require either a year or game identifier."""
|
|
11
|
+
if year is None and game_id is None:
|
|
12
|
+
raise ValueError("year is required when game_id is not specified")
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def _validate_at_least_one_of(
|
|
16
|
+
values: Mapping[str, object],
|
|
17
|
+
field_names: Sequence[str],
|
|
18
|
+
context_message: str = "At least one of the following fields is required",
|
|
19
|
+
) -> None:
|
|
20
|
+
"""Require a non-null value for at least one named field."""
|
|
21
|
+
if not any(values.get(field) is not None for field in field_names):
|
|
22
|
+
field_list = ", ".join(field_names)
|
|
23
|
+
raise ValueError(f"{context_message}: {field_list}")
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def _validate_game_stats_selectors(
|
|
27
|
+
year: int | None,
|
|
28
|
+
week: int | None,
|
|
29
|
+
team: str | None,
|
|
30
|
+
conference: str | None,
|
|
31
|
+
game_id: int | None,
|
|
32
|
+
) -> None:
|
|
33
|
+
"""Validate game-ID or grouped selectors for game-stat endpoints."""
|
|
34
|
+
if game_id is not None:
|
|
35
|
+
return
|
|
36
|
+
_validate_year_or_game_id(year, game_id)
|
|
37
|
+
if week is None and team is None and conference is None:
|
|
38
|
+
raise ValueError(
|
|
39
|
+
"At least one of week, team, or conference is required "
|
|
40
|
+
"when game_id is not specified"
|
|
41
|
+
)
|
cfb_data/_requests.py
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
"""Build endpoint request models from the two supported call styles."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from collections.abc import Mapping
|
|
6
|
+
|
|
7
|
+
from pydantic import BaseModel, ValidationError
|
|
8
|
+
|
|
9
|
+
from cfb_data.errors import CFBDRequestValidationError, _sanitized_cause
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def _resolve_request[RequestT: BaseModel](
|
|
13
|
+
*,
|
|
14
|
+
endpoint: str,
|
|
15
|
+
request_type: type[RequestT],
|
|
16
|
+
request: BaseModel | None,
|
|
17
|
+
filters: Mapping[str, object],
|
|
18
|
+
) -> RequestT:
|
|
19
|
+
"""Return a supplied request or validate explicit keyword filters."""
|
|
20
|
+
if request is not None:
|
|
21
|
+
if filters:
|
|
22
|
+
raise TypeError(
|
|
23
|
+
"Pass either one positional request model or keyword filters, not both"
|
|
24
|
+
)
|
|
25
|
+
if not isinstance(request, request_type):
|
|
26
|
+
raise TypeError(
|
|
27
|
+
f"{endpoint} requires {request_type.__name__}, "
|
|
28
|
+
f"not {type(request).__name__}"
|
|
29
|
+
)
|
|
30
|
+
return request
|
|
31
|
+
|
|
32
|
+
try:
|
|
33
|
+
return request_type.model_validate(filters)
|
|
34
|
+
except ValidationError as exc:
|
|
35
|
+
safe_cause = _sanitized_cause(exc)
|
|
36
|
+
raise CFBDRequestValidationError(endpoint=endpoint) from safe_cause
|