nfl-bigquery 0.1.0__tar.gz

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 (72) hide show
  1. nfl_bigquery-0.1.0/.github/workflows/release.yml +38 -0
  2. nfl_bigquery-0.1.0/.github/workflows/test.yml +28 -0
  3. nfl_bigquery-0.1.0/.gitignore +12 -0
  4. nfl_bigquery-0.1.0/CHANGELOG.md +11 -0
  5. nfl_bigquery-0.1.0/LICENSE +21 -0
  6. nfl_bigquery-0.1.0/PKG-INFO +73 -0
  7. nfl_bigquery-0.1.0/README.md +41 -0
  8. nfl_bigquery-0.1.0/nfl_bigquery/__init__.py +3 -0
  9. nfl_bigquery-0.1.0/nfl_bigquery/_transform_util.py +19 -0
  10. nfl_bigquery-0.1.0/nfl_bigquery/_version.py +1 -0
  11. nfl_bigquery-0.1.0/nfl_bigquery/cli.py +145 -0
  12. nfl_bigquery-0.1.0/nfl_bigquery/client.py +48 -0
  13. nfl_bigquery-0.1.0/nfl_bigquery/dictionaries/pbp.csv +373 -0
  14. nfl_bigquery-0.1.0/nfl_bigquery/dictionaries/player_stats.csv +49 -0
  15. nfl_bigquery-0.1.0/nfl_bigquery/dictionaries/players.csv +40 -0
  16. nfl_bigquery-0.1.0/nfl_bigquery/dictionaries/schedules.csv +46 -0
  17. nfl_bigquery-0.1.0/nfl_bigquery/dictionary.py +72 -0
  18. nfl_bigquery-0.1.0/nfl_bigquery/docs/__init__.py +0 -0
  19. nfl_bigquery-0.1.0/nfl_bigquery/docs/renderers.py +76 -0
  20. nfl_bigquery-0.1.0/nfl_bigquery/docs/taxonomy.py +41 -0
  21. nfl_bigquery-0.1.0/nfl_bigquery/games/__init__.py +0 -0
  22. nfl_bigquery-0.1.0/nfl_bigquery/games/schema.py +43 -0
  23. nfl_bigquery-0.1.0/nfl_bigquery/games/transform.py +13 -0
  24. nfl_bigquery-0.1.0/nfl_bigquery/partition.py +27 -0
  25. nfl_bigquery-0.1.0/nfl_bigquery/players/__init__.py +0 -0
  26. nfl_bigquery-0.1.0/nfl_bigquery/players/schema.py +19 -0
  27. nfl_bigquery-0.1.0/nfl_bigquery/players/sync.py +27 -0
  28. nfl_bigquery-0.1.0/nfl_bigquery/players/transform.py +10 -0
  29. nfl_bigquery-0.1.0/nfl_bigquery/players/writer.py +61 -0
  30. nfl_bigquery-0.1.0/nfl_bigquery/plays/__init__.py +0 -0
  31. nfl_bigquery-0.1.0/nfl_bigquery/plays/enrichment.py +154 -0
  32. nfl_bigquery-0.1.0/nfl_bigquery/plays/schema.py +18 -0
  33. nfl_bigquery-0.1.0/nfl_bigquery/plays/transform.py +10 -0
  34. nfl_bigquery-0.1.0/nfl_bigquery/runs.py +82 -0
  35. nfl_bigquery-0.1.0/nfl_bigquery/schema.py +68 -0
  36. nfl_bigquery-0.1.0/nfl_bigquery/stats/__init__.py +0 -0
  37. nfl_bigquery-0.1.0/nfl_bigquery/stats/schema.py +25 -0
  38. nfl_bigquery-0.1.0/nfl_bigquery/stats/transform.py +10 -0
  39. nfl_bigquery-0.1.0/nfl_bigquery/sync.py +82 -0
  40. nfl_bigquery-0.1.0/nfl_bigquery/verify/__init__.py +0 -0
  41. nfl_bigquery-0.1.0/nfl_bigquery/verify/base.py +10 -0
  42. nfl_bigquery-0.1.0/nfl_bigquery/verify/internal.py +116 -0
  43. nfl_bigquery-0.1.0/nfl_bigquery/writer.py +120 -0
  44. nfl_bigquery-0.1.0/pyproject.toml +69 -0
  45. nfl_bigquery-0.1.0/scripts/_norton_ssl.py +106 -0
  46. nfl_bigquery-0.1.0/scripts/make_fixtures.py +37 -0
  47. nfl_bigquery-0.1.0/scripts/refresh_dictionaries.py +146 -0
  48. nfl_bigquery-0.1.0/tests/fixtures/mini_pbp.csv +8 -0
  49. nfl_bigquery-0.1.0/tests/fixtures/pbp_2023_sample.parquet +0 -0
  50. nfl_bigquery-0.1.0/tests/fixtures/player_stats_2023_sample.parquet +0 -0
  51. nfl_bigquery-0.1.0/tests/fixtures/players_2023_sample.parquet +0 -0
  52. nfl_bigquery-0.1.0/tests/fixtures/schedules_2023_sample.parquet +0 -0
  53. nfl_bigquery-0.1.0/tests/test_cli_players.py +26 -0
  54. nfl_bigquery-0.1.0/tests/test_cli_sync.py +94 -0
  55. nfl_bigquery-0.1.0/tests/test_client.py +53 -0
  56. nfl_bigquery-0.1.0/tests/test_dictionary.py +51 -0
  57. nfl_bigquery-0.1.0/tests/test_docs.py +48 -0
  58. nfl_bigquery-0.1.0/tests/test_enrichment_keys.py +45 -0
  59. nfl_bigquery-0.1.0/tests/test_fixtures_present.py +35 -0
  60. nfl_bigquery-0.1.0/tests/test_live_smoke.py +13 -0
  61. nfl_bigquery-0.1.0/tests/test_other_schemas.py +29 -0
  62. nfl_bigquery-0.1.0/tests/test_partition.py +16 -0
  63. nfl_bigquery-0.1.0/tests/test_players_writer.py +92 -0
  64. nfl_bigquery-0.1.0/tests/test_plays_schema.py +30 -0
  65. nfl_bigquery-0.1.0/tests/test_runs.py +45 -0
  66. nfl_bigquery-0.1.0/tests/test_schema.py +42 -0
  67. nfl_bigquery-0.1.0/tests/test_schema_drift.py +26 -0
  68. nfl_bigquery-0.1.0/tests/test_smoke.py +17 -0
  69. nfl_bigquery-0.1.0/tests/test_transforms.py +40 -0
  70. nfl_bigquery-0.1.0/tests/test_verify.py +87 -0
  71. nfl_bigquery-0.1.0/tests/test_writer.py +83 -0
  72. nfl_bigquery-0.1.0/uv.lock +1369 -0
@@ -0,0 +1,38 @@
1
+ name: release
2
+
3
+ on:
4
+ push:
5
+ tags:
6
+ - "v*"
7
+
8
+ jobs:
9
+ build:
10
+ runs-on: ubuntu-latest
11
+ steps:
12
+ - uses: actions/checkout@v4
13
+ - uses: astral-sh/setup-uv@v3
14
+ - run: uv python install 3.13
15
+ - name: Create venv
16
+ run: uv venv
17
+ - name: Install build tools
18
+ run: uv pip install build
19
+ - run: uv run python -m build
20
+ - uses: actions/upload-artifact@v4
21
+ with:
22
+ name: dist
23
+ path: dist/
24
+
25
+ publish:
26
+ runs-on: ubuntu-latest
27
+ needs: build
28
+ environment:
29
+ name: pypi
30
+ url: https://pypi.org/p/nfl-bigquery
31
+ permissions:
32
+ id-token: write
33
+ steps:
34
+ - uses: actions/download-artifact@v4
35
+ with:
36
+ name: dist
37
+ path: dist
38
+ - uses: pypa/gh-action-pypi-publish@release/v1
@@ -0,0 +1,28 @@
1
+ name: test
2
+
3
+ on:
4
+ push:
5
+ branches: [main]
6
+ pull_request:
7
+
8
+ jobs:
9
+ test:
10
+ runs-on: ubuntu-latest
11
+ strategy:
12
+ matrix:
13
+ python-version: ["3.11", "3.12", "3.13"]
14
+ steps:
15
+ - uses: actions/checkout@v4
16
+ - uses: astral-sh/setup-uv@v3
17
+ - name: Set up Python ${{ matrix.python-version }}
18
+ run: uv python install ${{ matrix.python-version }}
19
+ - name: Install
20
+ run: uv sync --extra dev
21
+ - name: Test
22
+ run: uv run pytest -m "not network"
23
+ - name: Lint
24
+ run: uv run ruff check nfl_bigquery tests
25
+ - name: Type check
26
+ run: uv run pyright nfl_bigquery tests
27
+ - name: Build
28
+ run: uv run python -m build
@@ -0,0 +1,12 @@
1
+ # .gitignore
2
+ __pycache__/
3
+ *.py[cod]
4
+ .venv/
5
+ venv/
6
+ dist/
7
+ build/
8
+ *.egg-info/
9
+ .pytest_cache/
10
+ .ruff_cache/
11
+ .coverage
12
+ htmlcov/
@@ -0,0 +1,11 @@
1
+ # Changelog
2
+
3
+ ## 0.1.0
4
+ - `sync`: season-chunked idempotent load of `nfl_plays` (372-col nflfastR pbp), `games`
5
+ (schedules), `weekly_player_stats`; `--resume` via `_nfl_ingest_runs`.
6
+ - `players`: `dim_players` MERGE upsert on `gsis_id`.
7
+ - `docs`: llm / bq-apply / markdown / dictionary / dbt renderers from dictionary-seeded
8
+ ColumnSpecs + enrichment overlay.
9
+ - `verify`: internal consistency + plays→weekly reconciliation for passing/rushing/receiving
10
+ yards and TDs (6 metrics, 8 checks total).
11
+ - Schema-drift guard test against nflverse fixtures.
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Jason Blahovec
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,73 @@
1
+ Metadata-Version: 2.4
2
+ Name: nfl-bigquery
3
+ Version: 0.1.0
4
+ Summary: NFL play-by-play → BigQuery: idempotent nflverse ingestion + LLM-friendly docs + verification
5
+ Project-URL: Homepage, https://github.com/blahovec-labs/nfl-bigquery
6
+ Project-URL: Issues, https://github.com/blahovec-labs/nfl-bigquery/issues
7
+ Project-URL: Changelog, https://github.com/blahovec-labs/nfl-bigquery/blob/main/CHANGELOG.md
8
+ Author: Jason Blahovec
9
+ License: MIT
10
+ License-File: LICENSE
11
+ Keywords: bigquery,data-engineering,nfl,nflfastr,nflverse,play-by-play
12
+ Classifier: Development Status :: 4 - Beta
13
+ Classifier: Intended Audience :: Developers
14
+ Classifier: License :: OSI Approved :: MIT License
15
+ Classifier: Programming Language :: Python :: 3.11
16
+ Classifier: Programming Language :: Python :: 3.12
17
+ Classifier: Programming Language :: Python :: 3.13
18
+ Classifier: Topic :: Database
19
+ Requires-Python: >=3.11
20
+ Requires-Dist: db-dtypes<2.0,>=1.0
21
+ Requires-Dist: google-cloud-bigquery<4.0,>=3.20
22
+ Requires-Dist: nflreadpy
23
+ Requires-Dist: pandas<3.0,>=2.0
24
+ Requires-Dist: pyarrow<19.0,>=15.0
25
+ Provides-Extra: dev
26
+ Requires-Dist: build>=1.2.0; extra == 'dev'
27
+ Requires-Dist: pyright>=1.1.380; extra == 'dev'
28
+ Requires-Dist: pytest-cov>=5.0; extra == 'dev'
29
+ Requires-Dist: pytest>=8.0; extra == 'dev'
30
+ Requires-Dist: ruff>=0.6.0; extra == 'dev'
31
+ Description-Content-Type: text/markdown
32
+
33
+ # nfl-bigquery
34
+
35
+ NFL play-by-play → BigQuery. Idempotent nflverse ingestion + LLM-friendly docs + verification.
36
+ Fourth in the `*-bigquery` family (`statcast-bigquery`, `nhl-bigquery`, `yfinance-bigquery`).
37
+
38
+ ## Install
39
+ ```bash
40
+ pip install nfl-bigquery
41
+ gcloud auth application-default login
42
+ ```
43
+
44
+ ## Sync
45
+ ```bash
46
+ # Backfill 1999–present into a shared dataset (idempotent, resumable)
47
+ nfl-bigquery sync --seasons 1999-2025 --resume \
48
+ --pbp-table myproject.mydataset.nfl_plays
49
+ # Player dimension
50
+ nfl-bigquery players --players-table myproject.mydataset.dim_players
51
+ ```
52
+
53
+ Writes `nfl_plays`, `games`, `weekly_player_stats` (season-chunked DELETE→INSERT) and
54
+ `dim_players` (MERGE on `gsis_id`), plus a `_nfl_ingest_runs` log for `--resume`.
55
+
56
+ ## Docs & verify
57
+ ```bash
58
+ nfl-bigquery docs --format llm
59
+ nfl-bigquery docs --format bq-apply --table myproject.mydataset.nfl_plays
60
+ nfl-bigquery verify --source internal --season 2024 \
61
+ --pbp-table myproject.mydataset.nfl_plays
62
+ ```
63
+
64
+ The `verify` command runs 8 checks: duplicate-play detection, orphan-play detection, and
65
+ plays→weekly reconciliation for **passing/rushing/receiving yards and TDs** (6 metrics).
66
+ The `--tolerance` flag (default 0.0) sets the maximum allowed absolute aggregate diff;
67
+ lateral plays cause small legitimate yard diffs, so consider `--tolerance 5` for yards.
68
+
69
+ ## Data source
70
+ All data from [nflverse](https://nflreadpy.nflverse.com/) via `nflreadpy`. Play-by-play is
71
+ nflfastR's 372-column feed (EPA/WPA/air-yards/CPOE modeled), back to 1999.
72
+
73
+ MIT licensed.
@@ -0,0 +1,41 @@
1
+ # nfl-bigquery
2
+
3
+ NFL play-by-play → BigQuery. Idempotent nflverse ingestion + LLM-friendly docs + verification.
4
+ Fourth in the `*-bigquery` family (`statcast-bigquery`, `nhl-bigquery`, `yfinance-bigquery`).
5
+
6
+ ## Install
7
+ ```bash
8
+ pip install nfl-bigquery
9
+ gcloud auth application-default login
10
+ ```
11
+
12
+ ## Sync
13
+ ```bash
14
+ # Backfill 1999–present into a shared dataset (idempotent, resumable)
15
+ nfl-bigquery sync --seasons 1999-2025 --resume \
16
+ --pbp-table myproject.mydataset.nfl_plays
17
+ # Player dimension
18
+ nfl-bigquery players --players-table myproject.mydataset.dim_players
19
+ ```
20
+
21
+ Writes `nfl_plays`, `games`, `weekly_player_stats` (season-chunked DELETE→INSERT) and
22
+ `dim_players` (MERGE on `gsis_id`), plus a `_nfl_ingest_runs` log for `--resume`.
23
+
24
+ ## Docs & verify
25
+ ```bash
26
+ nfl-bigquery docs --format llm
27
+ nfl-bigquery docs --format bq-apply --table myproject.mydataset.nfl_plays
28
+ nfl-bigquery verify --source internal --season 2024 \
29
+ --pbp-table myproject.mydataset.nfl_plays
30
+ ```
31
+
32
+ The `verify` command runs 8 checks: duplicate-play detection, orphan-play detection, and
33
+ plays→weekly reconciliation for **passing/rushing/receiving yards and TDs** (6 metrics).
34
+ The `--tolerance` flag (default 0.0) sets the maximum allowed absolute aggregate diff;
35
+ lateral plays cause small legitimate yard diffs, so consider `--tolerance 5` for yards.
36
+
37
+ ## Data source
38
+ All data from [nflverse](https://nflreadpy.nflverse.com/) via `nflreadpy`. Play-by-play is
39
+ nflfastR's 372-column feed (EPA/WPA/air-yards/CPOE modeled), back to 1999.
40
+
41
+ MIT licensed.
@@ -0,0 +1,3 @@
1
+ from nfl_bigquery._version import __version__
2
+
3
+ __all__ = ["__version__"]
@@ -0,0 +1,19 @@
1
+ """Shared transform helper: reindex to schema columns + stamp ingested_at."""
2
+ from __future__ import annotations
3
+
4
+ from datetime import UTC, datetime
5
+
6
+ import pandas as pd
7
+
8
+ from nfl_bigquery.schema import ColumnSpec, spec_names
9
+
10
+
11
+ def align_to_schema(
12
+ df: pd.DataFrame, schema: list[ColumnSpec], *, stamp: bool = True
13
+ ) -> pd.DataFrame:
14
+ df = df.copy()
15
+ if stamp:
16
+ df["ingested_at"] = datetime.now(UTC)
17
+ cols = spec_names(schema)
18
+ # Reindex adds missing NULLABLE columns as NA and drops columns not in schema.
19
+ return df.reindex(columns=cols)
@@ -0,0 +1 @@
1
+ __version__ = "0.1.0"
@@ -0,0 +1,145 @@
1
+ """nfl-bigquery CLI entrypoint."""
2
+ from __future__ import annotations
3
+
4
+ import argparse
5
+ import logging
6
+
7
+ from nfl_bigquery._version import __version__
8
+
9
+
10
+ def build_parser() -> argparse.ArgumentParser:
11
+ parser = argparse.ArgumentParser(prog="nfl-bigquery")
12
+ parser.add_argument("--version", action="version", version=__version__)
13
+ sub = parser.add_subparsers(dest="command")
14
+
15
+ sy = sub.add_parser("sync", help="Fetch nflverse data and write to BigQuery")
16
+ sy.add_argument("--seasons", required=True,
17
+ help="e.g. 1999-2025 | 2023,2025 | 2024 | latest")
18
+ sy.add_argument("--pbp-table", required=True, help="project.dataset.nfl_plays")
19
+ sy.add_argument("--games-table")
20
+ sy.add_argument("--weekly-stats-table")
21
+ sy.add_argument("--runs-table")
22
+ sy.add_argument("--skip-games", action="store_true")
23
+ sy.add_argument("--skip-weekly-stats", action="store_true")
24
+ sy.add_argument("--resume", action="store_true")
25
+ sy.add_argument("--dry-run", action="store_true")
26
+
27
+ pl = sub.add_parser("players", help="Upsert the dim_players dimension")
28
+ pl.add_argument("--players-table", required=True, help="project.dataset.dim_players")
29
+ pl.add_argument("--dry-run", action="store_true")
30
+
31
+ dc = sub.add_parser("docs", help="Render table documentation")
32
+ dc.add_argument("--format", required=True,
33
+ choices=["bq-apply", "llm", "dictionary", "markdown", "dbt"])
34
+ dc.add_argument("--table", help="project.dataset.table (required for bq-apply)")
35
+ dc.add_argument("--output", default="-", help="output path or - for stdout")
36
+
37
+ vf = sub.add_parser("verify", help="Run data-quality checks")
38
+ vf.add_argument("--source", default="internal", choices=["internal"])
39
+ vf.add_argument("--season", type=int, required=True)
40
+ vf.add_argument("--pbp-table", required=True)
41
+ vf.add_argument("--games-table")
42
+ vf.add_argument("--weekly-stats-table")
43
+ vf.add_argument("--tolerance", type=float, default=1.0)
44
+
45
+ return parser
46
+
47
+
48
+ def main(argv: list[str] | None = None) -> int:
49
+ logging.basicConfig(level=logging.INFO, format="%(levelname)s %(name)s: %(message)s")
50
+ parser = build_parser()
51
+ ns = parser.parse_args(argv)
52
+ if not getattr(ns, "command", None):
53
+ parser.print_help()
54
+ return 0
55
+ if ns.command == "sync":
56
+ from google.cloud import bigquery
57
+
58
+ from nfl_bigquery.client import NflverseClient
59
+ from nfl_bigquery.runs import RunsTable
60
+ from nfl_bigquery.sync import run_sync
61
+ from nfl_bigquery.writer import BigQueryWriter
62
+
63
+ bq = bigquery.Client()
64
+ return run_sync(ns, client=NflverseClient(), writer=BigQueryWriter(client=bq),
65
+ runs=RunsTable(client=bq))
66
+ if ns.command == "players":
67
+ from google.cloud import bigquery
68
+
69
+ from nfl_bigquery.client import NflverseClient
70
+ from nfl_bigquery.players.sync import run_players
71
+
72
+ return run_players(ns, client=NflverseClient(), bq_client=bigquery.Client())
73
+ if ns.command == "docs":
74
+ from nfl_bigquery.docs.renderers import (
75
+ render_bq_apply,
76
+ render_dbt,
77
+ render_dictionary_rows,
78
+ render_llm,
79
+ render_markdown,
80
+ )
81
+ from nfl_bigquery.docs.taxonomy import TABLES
82
+
83
+ if ns.format == "llm":
84
+ text = render_llm(TABLES)
85
+ elif ns.format == "markdown":
86
+ text = render_markdown(TABLES)
87
+ elif ns.format == "dbt":
88
+ text = render_dbt(TABLES)
89
+ elif ns.format == "bq-apply":
90
+ if not ns.table:
91
+ parser.error("--table is required for --format bq-apply")
92
+ from google.cloud import bigquery
93
+
94
+ bq = bigquery.Client()
95
+ table_name = ns.table.split(".")[-1]
96
+ if table_name not in TABLES:
97
+ parser.error(f"unknown table '{table_name}'; choices: {sorted(TABLES)}")
98
+ desc = {n: d for n, d in render_bq_apply(table_name)}
99
+ fetched = bq.get_table(ns.table)
100
+ new_schema = [
101
+ bigquery.SchemaField(
102
+ f.name, f.field_type, mode=f.mode,
103
+ description=desc.get(f.name, f.description or ""),
104
+ )
105
+ for f in fetched.schema
106
+ ]
107
+ fetched.schema = new_schema
108
+ bq.update_table(fetched, ["schema"])
109
+ print(f"applied descriptions to {ns.table}")
110
+ return 0
111
+ else: # dictionary
112
+ import json
113
+
114
+ text = json.dumps(render_dictionary_rows(TABLES), indent=2)
115
+ if ns.output == "-":
116
+ print(text)
117
+ else:
118
+ from pathlib import Path
119
+
120
+ Path(ns.output).write_text(text, encoding="utf-8")
121
+ return 0
122
+ if ns.command == "verify":
123
+ from google.cloud import bigquery
124
+
125
+ from nfl_bigquery.verify.internal import run_internal
126
+ from nfl_bigquery.writer import TableRef
127
+
128
+ pbp = TableRef.parse(ns.pbp_table)
129
+ games = ns.games_table or f"{pbp.project}.{pbp.dataset}.games"
130
+ stats = ns.weekly_stats_table or f"{pbp.project}.{pbp.dataset}.weekly_player_stats"
131
+ results = run_internal(
132
+ bigquery.Client(), plays_ref=str(pbp), games_ref=games, stats_ref=stats,
133
+ season=ns.season, tolerance=ns.tolerance)
134
+ failed = 0
135
+ for r in results:
136
+ mark = "PASS" if r.passed else "FAIL"
137
+ print(f"[{mark}] {r.name}: {r.detail}")
138
+ if not r.passed:
139
+ failed += 1
140
+ return 1 if failed else 0
141
+ return 0
142
+
143
+
144
+ if __name__ == "__main__":
145
+ raise SystemExit(main())
@@ -0,0 +1,48 @@
1
+ # nfl_bigquery/client.py
2
+ """Thin nflreadpy wrapper: fetch a season's parquet, convert to pandas, retry."""
3
+ from __future__ import annotations
4
+
5
+ import logging
6
+ import time
7
+
8
+ import nflreadpy as nfl
9
+ import pandas as pd
10
+
11
+ log = logging.getLogger(__name__)
12
+
13
+
14
+ class NflverseClient:
15
+ def __init__(self, max_retries: int = 5, backoff_seconds: float = 2.0) -> None:
16
+ self.max_retries = max_retries
17
+ self.backoff_seconds = backoff_seconds
18
+
19
+ def _retry(self, fn, *args) -> pd.DataFrame:
20
+ last: Exception | None = None
21
+ for attempt in range(1, self.max_retries + 1):
22
+ try:
23
+ return fn(*args).to_pandas()
24
+ except Exception as e: # noqa: BLE001 - retry any transient download error
25
+ last = e
26
+ wait = self.backoff_seconds * (2 ** (attempt - 1))
27
+ log.warning(
28
+ "nflverse load failed (attempt %d/%d): %s; retrying in %.1fs",
29
+ attempt,
30
+ self.max_retries,
31
+ e,
32
+ wait,
33
+ )
34
+ if attempt < self.max_retries:
35
+ time.sleep(wait)
36
+ raise RuntimeError(f"nflverse load failed after {self.max_retries} attempts") from last
37
+
38
+ def load_pbp(self, season: int) -> pd.DataFrame:
39
+ return self._retry(nfl.load_pbp, [season])
40
+
41
+ def load_schedules(self, season: int) -> pd.DataFrame:
42
+ return self._retry(nfl.load_schedules, [season])
43
+
44
+ def load_player_stats(self, season: int) -> pd.DataFrame:
45
+ return self._retry(nfl.load_player_stats, [season])
46
+
47
+ def load_players(self) -> pd.DataFrame:
48
+ return self._retry(nfl.load_players)