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
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