nhl-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.
- nhl_bigquery-0.1.0/.github/workflows/release.yml +41 -0
- nhl_bigquery-0.1.0/.github/workflows/test.yml +30 -0
- nhl_bigquery-0.1.0/.gitignore +16 -0
- nhl_bigquery-0.1.0/.python-version +1 -0
- nhl_bigquery-0.1.0/CHANGELOG.md +12 -0
- nhl_bigquery-0.1.0/CONTRIBUTING.md +12 -0
- nhl_bigquery-0.1.0/LICENSE +21 -0
- nhl_bigquery-0.1.0/PKG-INFO +92 -0
- nhl_bigquery-0.1.0/README.md +58 -0
- nhl_bigquery-0.1.0/nhl_bigquery/__init__.py +5 -0
- nhl_bigquery-0.1.0/nhl_bigquery/_version.py +1 -0
- nhl_bigquery-0.1.0/nhl_bigquery/boxscore/__init__.py +1 -0
- nhl_bigquery-0.1.0/nhl_bigquery/boxscore/schema.py +146 -0
- nhl_bigquery-0.1.0/nhl_bigquery/boxscore/transform.py +93 -0
- nhl_bigquery-0.1.0/nhl_bigquery/cli.py +443 -0
- nhl_bigquery-0.1.0/nhl_bigquery/client.py +85 -0
- nhl_bigquery-0.1.0/nhl_bigquery/docs/__init__.py +1 -0
- nhl_bigquery-0.1.0/nhl_bigquery/docs/renderers.py +169 -0
- nhl_bigquery-0.1.0/nhl_bigquery/docs/taxonomy.py +46 -0
- nhl_bigquery-0.1.0/nhl_bigquery/games/__init__.py +1 -0
- nhl_bigquery-0.1.0/nhl_bigquery/games/schema.py +126 -0
- nhl_bigquery-0.1.0/nhl_bigquery/games/transform.py +70 -0
- nhl_bigquery-0.1.0/nhl_bigquery/officials/__init__.py +1 -0
- nhl_bigquery-0.1.0/nhl_bigquery/officials/schema.py +48 -0
- nhl_bigquery-0.1.0/nhl_bigquery/officials/transform.py +37 -0
- nhl_bigquery-0.1.0/nhl_bigquery/plays/__init__.py +1 -0
- nhl_bigquery-0.1.0/nhl_bigquery/plays/merge.py +101 -0
- nhl_bigquery-0.1.0/nhl_bigquery/plays/schema.py +1014 -0
- nhl_bigquery-0.1.0/nhl_bigquery/plays/strength.py +49 -0
- nhl_bigquery-0.1.0/nhl_bigquery/plays/transform.py +231 -0
- nhl_bigquery-0.1.0/nhl_bigquery/runs.py +174 -0
- nhl_bigquery-0.1.0/nhl_bigquery/schema.py +65 -0
- nhl_bigquery-0.1.0/nhl_bigquery/shifts/__init__.py +1 -0
- nhl_bigquery-0.1.0/nhl_bigquery/shifts/schema.py +66 -0
- nhl_bigquery-0.1.0/nhl_bigquery/shifts/transform.py +44 -0
- nhl_bigquery-0.1.0/nhl_bigquery/standings/__init__.py +1 -0
- nhl_bigquery-0.1.0/nhl_bigquery/standings/schema.py +89 -0
- nhl_bigquery-0.1.0/nhl_bigquery/standings/transform.py +39 -0
- nhl_bigquery-0.1.0/nhl_bigquery/time_utils.py +45 -0
- nhl_bigquery-0.1.0/nhl_bigquery/verify/__init__.py +1 -0
- nhl_bigquery-0.1.0/nhl_bigquery/verify/base.py +31 -0
- nhl_bigquery-0.1.0/nhl_bigquery/verify/internal.py +150 -0
- nhl_bigquery-0.1.0/nhl_bigquery/verify/nhl_api.py +244 -0
- nhl_bigquery-0.1.0/nhl_bigquery/writer.py +162 -0
- nhl_bigquery-0.1.0/pyproject.toml +68 -0
- nhl_bigquery-0.1.0/scripts/capture_fixture.py +68 -0
- nhl_bigquery-0.1.0/tests/conftest.py +18 -0
- nhl_bigquery-0.1.0/tests/fixtures/.gitkeep +0 -0
- nhl_bigquery-0.1.0/tests/fixtures/games/2024020001/boxscore.json +1019 -0
- nhl_bigquery-0.1.0/tests/fixtures/games/2024020001/landing.json +678 -0
- nhl_bigquery-0.1.0/tests/fixtures/games/2024020001/play-by-play.json +8681 -0
- nhl_bigquery-0.1.0/tests/fixtures/games/2024020001/right-rail.json +563 -0
- nhl_bigquery-0.1.0/tests/fixtures/games/2024020001/shift-charts.json +17246 -0
- nhl_bigquery-0.1.0/tests/fixtures/games/2024020009/boxscore.json +998 -0
- nhl_bigquery-0.1.0/tests/fixtures/games/2024020009/landing.json +1239 -0
- nhl_bigquery-0.1.0/tests/fixtures/games/2024020009/play-by-play.json +8102 -0
- nhl_bigquery-0.1.0/tests/fixtures/games/2024020009/right-rail.json +363 -0
- nhl_bigquery-0.1.0/tests/fixtures/games/2024020009/shift-charts.json +18212 -0
- nhl_bigquery-0.1.0/tests/fixtures/games/2024020022/boxscore.json +1001 -0
- nhl_bigquery-0.1.0/tests/fixtures/games/2024020022/landing.json +940 -0
- nhl_bigquery-0.1.0/tests/fixtures/games/2024020022/play-by-play.json +8900 -0
- nhl_bigquery-0.1.0/tests/fixtures/games/2024020022/right-rail.json +336 -0
- nhl_bigquery-0.1.0/tests/fixtures/games/2024020022/shift-charts.json +17729 -0
- nhl_bigquery-0.1.0/tests/fixtures/score/2024-10-08.json +1346 -0
- nhl_bigquery-0.1.0/tests/fixtures/standings/2024-10-08.json +2730 -0
- nhl_bigquery-0.1.0/tests/integration/.gitkeep +0 -0
- nhl_bigquery-0.1.0/tests/integration/__init__.py +1 -0
- nhl_bigquery-0.1.0/tests/integration/test_sync_one_game.py +41 -0
- nhl_bigquery-0.1.0/tests/unit/.gitkeep +0 -0
- nhl_bigquery-0.1.0/tests/unit/test_boxscore_schema.py +31 -0
- nhl_bigquery-0.1.0/tests/unit/test_cli_docs.py +22 -0
- nhl_bigquery-0.1.0/tests/unit/test_cli_sync.py +58 -0
- nhl_bigquery-0.1.0/tests/unit/test_cli_verify.py +14 -0
- nhl_bigquery-0.1.0/tests/unit/test_games_schema.py +30 -0
- nhl_bigquery-0.1.0/tests/unit/test_nhl_client.py +74 -0
- nhl_bigquery-0.1.0/tests/unit/test_officials_schema.py +24 -0
- nhl_bigquery-0.1.0/tests/unit/test_other_transforms.py +64 -0
- nhl_bigquery-0.1.0/tests/unit/test_plays_merge.py +82 -0
- nhl_bigquery-0.1.0/tests/unit/test_plays_schema.py +147 -0
- nhl_bigquery-0.1.0/tests/unit/test_plays_transform.py +80 -0
- nhl_bigquery-0.1.0/tests/unit/test_renderers.py +38 -0
- nhl_bigquery-0.1.0/tests/unit/test_runs.py +37 -0
- nhl_bigquery-0.1.0/tests/unit/test_schema_invariants.py +53 -0
- nhl_bigquery-0.1.0/tests/unit/test_shifts_schema.py +17 -0
- nhl_bigquery-0.1.0/tests/unit/test_standings_schema.py +17 -0
- nhl_bigquery-0.1.0/tests/unit/test_strength_state.py +56 -0
- nhl_bigquery-0.1.0/tests/unit/test_time_utils.py +47 -0
- nhl_bigquery-0.1.0/tests/unit/test_verify_internal.py +49 -0
- nhl_bigquery-0.1.0/tests/unit/test_verify_nhl_api.py +55 -0
- nhl_bigquery-0.1.0/tests/unit/test_writer.py +52 -0
- nhl_bigquery-0.1.0/uv.lock +1152 -0
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
name: release
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
tags:
|
|
6
|
+
- "v*"
|
|
7
|
+
|
|
8
|
+
jobs:
|
|
9
|
+
build:
|
|
10
|
+
runs-on: ubuntu-latest
|
|
11
|
+
outputs:
|
|
12
|
+
dist-path: ${{ steps.build.outputs.dist-path }}
|
|
13
|
+
steps:
|
|
14
|
+
- uses: actions/checkout@v4
|
|
15
|
+
- uses: astral-sh/setup-uv@v3
|
|
16
|
+
- run: uv python install 3.13
|
|
17
|
+
- name: Create venv
|
|
18
|
+
run: uv venv
|
|
19
|
+
- name: Install build tools
|
|
20
|
+
run: uv pip install build
|
|
21
|
+
- id: build
|
|
22
|
+
run: |
|
|
23
|
+
uv run python -m build
|
|
24
|
+
echo "dist-path=dist" >> $GITHUB_OUTPUT
|
|
25
|
+
- uses: actions/upload-artifact@v4
|
|
26
|
+
with:
|
|
27
|
+
name: dist
|
|
28
|
+
path: dist/
|
|
29
|
+
|
|
30
|
+
publish:
|
|
31
|
+
needs: build
|
|
32
|
+
runs-on: ubuntu-latest
|
|
33
|
+
environment: pypi
|
|
34
|
+
permissions:
|
|
35
|
+
id-token: write
|
|
36
|
+
steps:
|
|
37
|
+
- uses: actions/download-artifact@v4
|
|
38
|
+
with:
|
|
39
|
+
name: dist
|
|
40
|
+
path: dist/
|
|
41
|
+
- uses: pypa/gh-action-pypi-publish@release/v1
|
|
@@ -0,0 +1,30 @@
|
|
|
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: Create venv
|
|
20
|
+
run: uv venv
|
|
21
|
+
- name: Install
|
|
22
|
+
run: uv pip install -e ".[dev]"
|
|
23
|
+
- name: Lint
|
|
24
|
+
run: uv run ruff check .
|
|
25
|
+
- name: Type check
|
|
26
|
+
run: uv run pyright
|
|
27
|
+
- name: Test
|
|
28
|
+
run: uv run pytest -v --cov=nhl_bigquery --cov-report=xml
|
|
29
|
+
- name: Build
|
|
30
|
+
run: uv run python -m build
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
3.13
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
## 0.1.0
|
|
4
|
+
|
|
5
|
+
Initial release.
|
|
6
|
+
|
|
7
|
+
- `sync` command writes six tables in lockstep chunks: nhl_plays (with on-ice arrays merged from shift-charts), games, game_officials, boxscore_stats, shifts, standings
|
|
8
|
+
- `--resume` support via `_nhl_ingest_runs` log
|
|
9
|
+
- 5 doc renderers (bq-apply / llm / dictionary / markdown / dbt) backed by ColumnSpec SSoT
|
|
10
|
+
- `verify --source internal` runs 8 zero-tolerance consistency checks
|
|
11
|
+
- `verify --source nhl-api` reconstructs and compares team-season / player-season / game-boxscore against authoritative endpoints
|
|
12
|
+
- Backfill from 2010-11 onward (coordinate era)
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
# Contributing
|
|
2
|
+
|
|
3
|
+
Bug reports and PRs welcome at https://github.com/blahovec-labs/nhl-bigquery.
|
|
4
|
+
|
|
5
|
+
## Dev setup
|
|
6
|
+
|
|
7
|
+
pip install -e ".[dev]"
|
|
8
|
+
pytest -q
|
|
9
|
+
|
|
10
|
+
## Capturing test fixtures
|
|
11
|
+
|
|
12
|
+
python scripts/capture_fixture.py --game-id 2024020001 --out tests/fixtures/games/
|
|
@@ -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,92 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: nhl-bigquery
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: NHL play-by-play → BigQuery: idempotent ingestion + LLM-friendly docs + verification
|
|
5
|
+
Project-URL: Homepage, https://github.com/blahovec-labs/nhl-bigquery
|
|
6
|
+
Project-URL: Issues, https://github.com/blahovec-labs/nhl-bigquery/issues
|
|
7
|
+
Project-URL: Changelog, https://github.com/blahovec-labs/nhl-bigquery/blob/main/CHANGELOG.md
|
|
8
|
+
Author: Jason Blahovec
|
|
9
|
+
License: MIT
|
|
10
|
+
License-File: LICENSE
|
|
11
|
+
Keywords: bigquery,data-engineering,hockey,nhl,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
|
+
Classifier: Topic :: Scientific/Engineering
|
|
20
|
+
Requires-Python: >=3.11
|
|
21
|
+
Requires-Dist: db-dtypes<2.0,>=1.0
|
|
22
|
+
Requires-Dist: google-cloud-bigquery<4.0,>=3.20
|
|
23
|
+
Requires-Dist: pandas<3.0,>=2.0
|
|
24
|
+
Requires-Dist: pyarrow<19.0,>=15.0
|
|
25
|
+
Requires-Dist: requests<3.0,>=2.31
|
|
26
|
+
Provides-Extra: dev
|
|
27
|
+
Requires-Dist: build>=1.2.0; extra == 'dev'
|
|
28
|
+
Requires-Dist: pyright>=1.1.380; extra == 'dev'
|
|
29
|
+
Requires-Dist: pytest-cov>=5.0; extra == 'dev'
|
|
30
|
+
Requires-Dist: pytest>=8.0; extra == 'dev'
|
|
31
|
+
Requires-Dist: responses>=0.25; extra == 'dev'
|
|
32
|
+
Requires-Dist: ruff>=0.6.0; extra == 'dev'
|
|
33
|
+
Description-Content-Type: text/markdown
|
|
34
|
+
|
|
35
|
+
# nhl-bigquery
|
|
36
|
+
|
|
37
|
+
Idempotent NHL play-by-play (with on-ice arrays merged from shift-charts) → BigQuery ingestion, with first-class documentation for SQL/LLM agents and verification against the NHL public API.
|
|
38
|
+
|
|
39
|
+
## Install
|
|
40
|
+
|
|
41
|
+
pip install nhl-bigquery
|
|
42
|
+
|
|
43
|
+
## Quickstart
|
|
44
|
+
|
|
45
|
+
gcloud auth application-default login
|
|
46
|
+
nhl-bigquery sync \
|
|
47
|
+
--start 2024-10-01 --end 2025-06-30 \
|
|
48
|
+
--plays-table myproject.mydataset.nhl_plays
|
|
49
|
+
|
|
50
|
+
This writes six tables to `myproject.mydataset.*`:
|
|
51
|
+
|
|
52
|
+
- `nhl_plays` — one row per event, with `home_on_ice_ids` / `away_on_ice_ids` arrays
|
|
53
|
+
- `games` — schedule dimension
|
|
54
|
+
- `game_officials` — referees + linesmen per game
|
|
55
|
+
- `boxscore_stats` — per-player per-game stats
|
|
56
|
+
- `shifts` — per-shift per-player intervals
|
|
57
|
+
- `standings` — daily team-standings snapshots
|
|
58
|
+
|
|
59
|
+
## Backfill
|
|
60
|
+
|
|
61
|
+
Backfill 15 seasons in resumable monthly chunks:
|
|
62
|
+
|
|
63
|
+
nhl-bigquery sync \
|
|
64
|
+
--start 2010-10-01 --end 2026-05-11 \
|
|
65
|
+
--chunk-by month --resume \
|
|
66
|
+
--plays-table myproject.mydataset.nhl_plays
|
|
67
|
+
|
|
68
|
+
`--resume` skips chunks already recorded as `success` or `empty` in
|
|
69
|
+
`<dataset>._nhl_ingest_runs`. Re-running with the same `--chunk-by` is
|
|
70
|
+
safe; switching between runs will re-process (chunks must match exactly).
|
|
71
|
+
|
|
72
|
+
## Documentation
|
|
73
|
+
|
|
74
|
+
nhl-bigquery docs --format llm > NHL_FOR_LLMS.md
|
|
75
|
+
nhl-bigquery docs --format bq-apply --table myproject.mydataset.nhl_plays
|
|
76
|
+
|
|
77
|
+
Five formats: `bq-apply` (push descriptions to BigQuery), `llm` (one
|
|
78
|
+
Markdown file packing every column for LLM context), `dictionary`
|
|
79
|
+
(JSON rows for a data dictionary table), `markdown` (human reference),
|
|
80
|
+
and `dbt` (dbt YAML schema stub).
|
|
81
|
+
|
|
82
|
+
## Verification
|
|
83
|
+
|
|
84
|
+
nhl-bigquery verify --source internal \
|
|
85
|
+
--aggregation internal-consistency \
|
|
86
|
+
--table myproject.mydataset.nhl_plays
|
|
87
|
+
|
|
88
|
+
nhl-bigquery verify --source nhl-api \
|
|
89
|
+
--aggregation team-season --metric all --season 2024 \
|
|
90
|
+
--table myproject.mydataset.nhl_plays
|
|
91
|
+
|
|
92
|
+
MIT licensed. This software does not include or distribute NHL data.
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
# nhl-bigquery
|
|
2
|
+
|
|
3
|
+
Idempotent NHL play-by-play (with on-ice arrays merged from shift-charts) → BigQuery ingestion, with first-class documentation for SQL/LLM agents and verification against the NHL public API.
|
|
4
|
+
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
pip install nhl-bigquery
|
|
8
|
+
|
|
9
|
+
## Quickstart
|
|
10
|
+
|
|
11
|
+
gcloud auth application-default login
|
|
12
|
+
nhl-bigquery sync \
|
|
13
|
+
--start 2024-10-01 --end 2025-06-30 \
|
|
14
|
+
--plays-table myproject.mydataset.nhl_plays
|
|
15
|
+
|
|
16
|
+
This writes six tables to `myproject.mydataset.*`:
|
|
17
|
+
|
|
18
|
+
- `nhl_plays` — one row per event, with `home_on_ice_ids` / `away_on_ice_ids` arrays
|
|
19
|
+
- `games` — schedule dimension
|
|
20
|
+
- `game_officials` — referees + linesmen per game
|
|
21
|
+
- `boxscore_stats` — per-player per-game stats
|
|
22
|
+
- `shifts` — per-shift per-player intervals
|
|
23
|
+
- `standings` — daily team-standings snapshots
|
|
24
|
+
|
|
25
|
+
## Backfill
|
|
26
|
+
|
|
27
|
+
Backfill 15 seasons in resumable monthly chunks:
|
|
28
|
+
|
|
29
|
+
nhl-bigquery sync \
|
|
30
|
+
--start 2010-10-01 --end 2026-05-11 \
|
|
31
|
+
--chunk-by month --resume \
|
|
32
|
+
--plays-table myproject.mydataset.nhl_plays
|
|
33
|
+
|
|
34
|
+
`--resume` skips chunks already recorded as `success` or `empty` in
|
|
35
|
+
`<dataset>._nhl_ingest_runs`. Re-running with the same `--chunk-by` is
|
|
36
|
+
safe; switching between runs will re-process (chunks must match exactly).
|
|
37
|
+
|
|
38
|
+
## Documentation
|
|
39
|
+
|
|
40
|
+
nhl-bigquery docs --format llm > NHL_FOR_LLMS.md
|
|
41
|
+
nhl-bigquery docs --format bq-apply --table myproject.mydataset.nhl_plays
|
|
42
|
+
|
|
43
|
+
Five formats: `bq-apply` (push descriptions to BigQuery), `llm` (one
|
|
44
|
+
Markdown file packing every column for LLM context), `dictionary`
|
|
45
|
+
(JSON rows for a data dictionary table), `markdown` (human reference),
|
|
46
|
+
and `dbt` (dbt YAML schema stub).
|
|
47
|
+
|
|
48
|
+
## Verification
|
|
49
|
+
|
|
50
|
+
nhl-bigquery verify --source internal \
|
|
51
|
+
--aggregation internal-consistency \
|
|
52
|
+
--table myproject.mydataset.nhl_plays
|
|
53
|
+
|
|
54
|
+
nhl-bigquery verify --source nhl-api \
|
|
55
|
+
--aggregation team-season --metric all --season 2024 \
|
|
56
|
+
--table myproject.mydataset.nhl_plays
|
|
57
|
+
|
|
58
|
+
MIT licensed. This software does not include or distribute NHL data.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "0.1.0"
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""boxscore_stats table: per-player per-game stats."""
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
"""BOXSCORE_SCHEMA: per-player per-game stats (skaters + goalies)."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from nhl_bigquery.games.schema import _col
|
|
6
|
+
from nhl_bigquery.schema import ColumnSpec, PartitioningSpec
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def get_partitioning() -> PartitioningSpec:
|
|
10
|
+
return PartitioningSpec(
|
|
11
|
+
field="game_date", type="DAY", clustering=["player_id", "team_id"],
|
|
12
|
+
)
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
BOXSCORE_SCHEMA: list[ColumnSpec] = [
|
|
16
|
+
# Common
|
|
17
|
+
_col("game_id", "INT64", "REQUIRED", "NHL game ID.",
|
|
18
|
+
"FK to games.game_id.", tags=["identifier", "join_key"], example=2024020001,
|
|
19
|
+
api_eq="id", api_src="id"),
|
|
20
|
+
_col("game_date", "DATE", "REQUIRED", "Game date.",
|
|
21
|
+
"Partition key for this table.", tags=["temporal"], example="2024-10-08",
|
|
22
|
+
api_eq="gameDate", api_src="gameDate"),
|
|
23
|
+
_col("player_id", "INT64", "REQUIRED", "NHL Player ID.",
|
|
24
|
+
"Canonical player identifier; FK to player landing API.",
|
|
25
|
+
tags=["identifier", "join_key"], example=8478402,
|
|
26
|
+
api_eq="playerId", api_src="playerId"),
|
|
27
|
+
_col("team_id", "INT64", "REQUIRED", "Team ID this game.",
|
|
28
|
+
"NHL team_id of the team the player suited up for. "
|
|
29
|
+
"Mid-season trades produce different team_ids across games.",
|
|
30
|
+
tags=["identifier", "join_key", "team"], example=10,
|
|
31
|
+
api_eq="(side teamId)", api_src="(side teamId)"),
|
|
32
|
+
_col("player_position_category", "STRING", "REQUIRED",
|
|
33
|
+
"Discriminator: 'skater' or 'goalie'.",
|
|
34
|
+
"Splits skater rows (with G/A/+-/PIM/...) from goalie rows (SA/SV/SV%/GAA/...).",
|
|
35
|
+
tags=["identifier"], values=["skater", "goalie"], example="skater",
|
|
36
|
+
api_eq="(derived: 'forwards'/'defense'/'goalies' bucket)",
|
|
37
|
+
api_src="(derived)"),
|
|
38
|
+
_col("position_code", "STRING", "NULLABLE", "Position code (C/L/R/D/G).",
|
|
39
|
+
"Single-letter position: C=center, L=left wing, R=right wing, D=defense, G=goalie.",
|
|
40
|
+
tags=["identifier"], values=["C", "L", "R", "D", "G"], example="C",
|
|
41
|
+
api_eq="position", api_src="position"),
|
|
42
|
+
_col("sweater_number", "INT64", "NULLABLE", "Jersey number worn this game.",
|
|
43
|
+
"Sweater number; not stable across trades.",
|
|
44
|
+
tags=["identifier"], range_=(1.0, 99.0), example=97,
|
|
45
|
+
api_eq="sweaterNumber", api_src="sweaterNumber"),
|
|
46
|
+
_col("toi", "STRING", "NULLABLE", "Time on ice (MM:SS).",
|
|
47
|
+
"Total time on ice. String MM:SS to preserve fidelity; use toi_seconds for arithmetic.",
|
|
48
|
+
tags=["measure"], example="20:34",
|
|
49
|
+
api_eq="toi", api_src="toi"),
|
|
50
|
+
# Skater-only
|
|
51
|
+
_col("goals", "INT64", "NULLABLE", "Goals scored.",
|
|
52
|
+
"Goals scored by this skater in this game.", tags=["measure"], example=1,
|
|
53
|
+
api_eq="goals", api_src="goals"),
|
|
54
|
+
_col("assists", "INT64", "NULLABLE", "Assists.",
|
|
55
|
+
"Assists in this game.", tags=["measure"], example=2,
|
|
56
|
+
api_eq="assists", api_src="assists"),
|
|
57
|
+
_col("points", "INT64", "NULLABLE", "Points (G + A).",
|
|
58
|
+
"Goals + assists. Stored directly to avoid query-time arithmetic.",
|
|
59
|
+
tags=["measure"], example=3, api_eq="points", api_src="points"),
|
|
60
|
+
_col("plus_minus", "INT64", "NULLABLE", "Plus/minus.",
|
|
61
|
+
"Plus/minus for this skater in this game.", tags=["measure"], example=2,
|
|
62
|
+
api_eq="plusMinus", api_src="plusMinus"),
|
|
63
|
+
_col("pim", "INT64", "NULLABLE", "Penalty minutes.",
|
|
64
|
+
"Penalty minutes assessed to this player.", tags=["measure"], example=0,
|
|
65
|
+
api_eq="pim", api_src="pim"),
|
|
66
|
+
_col("hits", "INT64", "NULLABLE", "Hits delivered.", "Body checks delivered.",
|
|
67
|
+
tags=["measure"], example=3, api_eq="hits", api_src="hits"),
|
|
68
|
+
_col("blocked_shots", "INT64", "NULLABLE", "Shots blocked.",
|
|
69
|
+
"Shots the player blocked.", tags=["measure"], example=1,
|
|
70
|
+
api_eq="blockedShots", api_src="blockedShots"),
|
|
71
|
+
_col("shots", "INT64", "NULLABLE", "Shots on goal.",
|
|
72
|
+
"Shots on goal (SOG); does not include missed or blocked shots.",
|
|
73
|
+
tags=["measure"], example=4, api_eq="shots", api_src="shots"),
|
|
74
|
+
_col("faceoffs", "INT64", "NULLABLE", "Faceoffs taken.",
|
|
75
|
+
"Total faceoffs taken (won + lost).", tags=["measure"], example=10,
|
|
76
|
+
api_eq="faceoffs", api_src="faceoffs"),
|
|
77
|
+
_col("faceoff_winning_pctg", "FLOAT64", "NULLABLE", "Faceoff win %.",
|
|
78
|
+
"Faceoff winning percentage as a decimal (0.0-1.0).",
|
|
79
|
+
tags=["measure"], range_=(0.0, 1.0), example=0.55,
|
|
80
|
+
api_eq="faceoffWinningPctg", api_src="faceoffWinningPctg"),
|
|
81
|
+
_col("power_play_goals", "INT64", "NULLABLE", "Power-play goals.",
|
|
82
|
+
"Goals scored while on the power play.", tags=["measure"], example=1,
|
|
83
|
+
api_eq="powerPlayGoals", api_src="powerPlayGoals"),
|
|
84
|
+
_col("power_play_points", "INT64", "NULLABLE", "Power-play points.",
|
|
85
|
+
"Goals + assists earned while on the power play.",
|
|
86
|
+
tags=["measure"], example=2, api_eq="powerPlayPoints", api_src="powerPlayPoints"),
|
|
87
|
+
_col("shorthanded_goals", "INT64", "NULLABLE", "Shorthanded goals.",
|
|
88
|
+
"Goals scored while shorthanded.", tags=["measure"], example=0,
|
|
89
|
+
api_eq="shorthandedGoals", api_src="shorthandedGoals"),
|
|
90
|
+
_col("sh_points", "INT64", "NULLABLE", "Shorthanded points.",
|
|
91
|
+
"Goals + assists earned while shorthanded.", tags=["measure"], example=0,
|
|
92
|
+
api_eq="shPoints", api_src="shPoints"),
|
|
93
|
+
_col("power_play_toi", "STRING", "NULLABLE", "Power-play TOI.",
|
|
94
|
+
"Time on ice while on the power play (MM:SS).",
|
|
95
|
+
tags=["measure"], example="2:14",
|
|
96
|
+
api_eq="powerPlayToi", api_src="powerPlayToi"),
|
|
97
|
+
_col("shorthanded_toi", "STRING", "NULLABLE", "Shorthanded TOI.",
|
|
98
|
+
"Time on ice while shorthanded (MM:SS).",
|
|
99
|
+
tags=["measure"], example="1:32",
|
|
100
|
+
api_eq="shorthandedToi", api_src="shorthandedToi"),
|
|
101
|
+
# Goalie-only
|
|
102
|
+
_col("shots_against", "INT64", "NULLABLE", "Total shots against.",
|
|
103
|
+
"Shots faced by this goalie in this game.", tags=["measure"], example=32,
|
|
104
|
+
api_eq="saveShotsAgainst (sum)", api_src="saveShotsAgainst"),
|
|
105
|
+
_col("saves", "INT64", "NULLABLE", "Total saves.",
|
|
106
|
+
"Saves made. Equals shots_against - goals_against.",
|
|
107
|
+
tags=["measure"], example=30,
|
|
108
|
+
api_eq="(saveShotsAgainst - goalsAgainst)", api_src="(derived)"),
|
|
109
|
+
_col("save_pctg", "FLOAT64", "NULLABLE", "Save percentage.",
|
|
110
|
+
"Save percentage as decimal (0.0-1.0).",
|
|
111
|
+
tags=["measure"], range_=(0.0, 1.0), example=0.938,
|
|
112
|
+
api_eq="savePctg", api_src="savePctg"),
|
|
113
|
+
_col("goals_against", "INT64", "NULLABLE", "Goals against.",
|
|
114
|
+
"Goals scored against this goalie in this game.",
|
|
115
|
+
tags=["measure"], example=2,
|
|
116
|
+
api_eq="goalsAgainst", api_src="goalsAgainst"),
|
|
117
|
+
_col("even_strength_shots_against", "INT64", "NULLABLE",
|
|
118
|
+
"Even-strength shots against.", "Shots faced at even strength.",
|
|
119
|
+
tags=["measure"], example=20,
|
|
120
|
+
api_eq="evenStrengthShotsAgainst", api_src="evenStrengthShotsAgainst"),
|
|
121
|
+
_col("power_play_shots_against", "INT64", "NULLABLE",
|
|
122
|
+
"PP shots against.", "Shots faced while shorthanded.",
|
|
123
|
+
tags=["measure"], example=8,
|
|
124
|
+
api_eq="powerPlayShotsAgainst", api_src="powerPlayShotsAgainst"),
|
|
125
|
+
_col("shorthanded_shots_against", "INT64", "NULLABLE",
|
|
126
|
+
"SH shots against.", "Shots faced while on the power play.",
|
|
127
|
+
tags=["measure"], example=4,
|
|
128
|
+
api_eq="shorthandedShotsAgainst", api_src="shorthandedShotsAgainst"),
|
|
129
|
+
_col("even_strength_goals_against", "INT64", "NULLABLE",
|
|
130
|
+
"EV goals against.", "Goals allowed at even strength.",
|
|
131
|
+
tags=["measure"], example=1,
|
|
132
|
+
api_eq="evenStrengthGoalsAgainst", api_src="evenStrengthGoalsAgainst"),
|
|
133
|
+
_col("power_play_goals_against", "INT64", "NULLABLE",
|
|
134
|
+
"PP goals against.", "Goals allowed while shorthanded.",
|
|
135
|
+
tags=["measure"], example=1,
|
|
136
|
+
api_eq="powerPlayGoalsAgainst", api_src="powerPlayGoalsAgainst"),
|
|
137
|
+
_col("shorthanded_goals_against", "INT64", "NULLABLE",
|
|
138
|
+
"SH goals against.", "Goals allowed while on the power play.",
|
|
139
|
+
tags=["measure"], example=0,
|
|
140
|
+
api_eq="shorthandedGoalsAgainst", api_src="shorthandedGoalsAgainst"),
|
|
141
|
+
_col("ingested_at", "TIMESTAMP", "REQUIRED",
|
|
142
|
+
"Ingestion timestamp.",
|
|
143
|
+
"UTC timestamp when this row was written.",
|
|
144
|
+
tags=["meta"], example="2026-05-22T17:00:00Z",
|
|
145
|
+
api_eq=None, api_src="(set by ingestion)"),
|
|
146
|
+
]
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
"""Boxscore transform: /gamecenter/{id}/boxscore → per-player rows (skater + goalie)."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from datetime import UTC, datetime
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
import pandas as pd
|
|
9
|
+
|
|
10
|
+
_SKATER_FIELDS = (
|
|
11
|
+
"goals", "assists", "points", "plusMinus", "pim", "hits", "blockedShots",
|
|
12
|
+
"shots", "faceoffs", "faceoffWinningPctg",
|
|
13
|
+
"powerPlayGoals", "powerPlayPoints", "shorthandedGoals", "shPoints",
|
|
14
|
+
"powerPlayToi", "shorthandedToi",
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
_GOALIE_FIELDS = (
|
|
18
|
+
"saveShotsAgainst", "goalsAgainst", "savePctg",
|
|
19
|
+
"evenStrengthShotsAgainst", "powerPlayShotsAgainst", "shorthandedShotsAgainst",
|
|
20
|
+
"evenStrengthGoalsAgainst", "powerPlayGoalsAgainst", "shorthandedGoalsAgainst",
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def _camel_to_snake(s: str) -> str:
|
|
25
|
+
out = []
|
|
26
|
+
for c in s:
|
|
27
|
+
if c.isupper():
|
|
28
|
+
out.append("_" + c.lower())
|
|
29
|
+
else:
|
|
30
|
+
out.append(c)
|
|
31
|
+
return "".join(out).lstrip("_")
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def transform_boxscore_to_df(bs: dict[str, Any]) -> pd.DataFrame:
|
|
35
|
+
"""Extract per-player per-game rows from a boxscore response."""
|
|
36
|
+
game_id = int(bs["id"])
|
|
37
|
+
game_date = bs.get("gameDate")
|
|
38
|
+
pgs = bs.get("playerByGameStats") or {}
|
|
39
|
+
rows: list[dict[str, Any]] = []
|
|
40
|
+
ingested_at = datetime.now(UTC)
|
|
41
|
+
|
|
42
|
+
for side in ("awayTeam", "homeTeam"):
|
|
43
|
+
team_id = int((bs.get(side) or {}).get("id"))
|
|
44
|
+
side_block = pgs.get(side) or {}
|
|
45
|
+
for cat, players in (
|
|
46
|
+
("skater", side_block.get("forwards") or []),
|
|
47
|
+
("skater", side_block.get("defense") or []),
|
|
48
|
+
("goalie", side_block.get("goalies") or []),
|
|
49
|
+
):
|
|
50
|
+
for p in players:
|
|
51
|
+
row: dict[str, Any] = {
|
|
52
|
+
"game_id": game_id,
|
|
53
|
+
"game_date": game_date,
|
|
54
|
+
"player_id": int(p["playerId"]),
|
|
55
|
+
"team_id": team_id,
|
|
56
|
+
"player_position_category": cat,
|
|
57
|
+
"position_code": p.get("position"),
|
|
58
|
+
"sweater_number": p.get("sweaterNumber"),
|
|
59
|
+
"toi": p.get("toi"),
|
|
60
|
+
"ingested_at": ingested_at,
|
|
61
|
+
}
|
|
62
|
+
# Skater fields
|
|
63
|
+
for f in _SKATER_FIELDS:
|
|
64
|
+
row[_camel_to_snake(f)] = p.get(f)
|
|
65
|
+
# Goalie fields (with renames)
|
|
66
|
+
# saveShotsAgainst may be "saves/shots" string (e.g. "19/22") or numeric
|
|
67
|
+
save_shots_raw = p.get("saveShotsAgainst")
|
|
68
|
+
goals_against = p.get("goalsAgainst")
|
|
69
|
+
shots_against_int = p.get("shotsAgainst")
|
|
70
|
+
saves_int = p.get("saves")
|
|
71
|
+
# Parse "saves/shots" string format if needed
|
|
72
|
+
if isinstance(save_shots_raw, str) and "/" in save_shots_raw:
|
|
73
|
+
parts = save_shots_raw.split("/")
|
|
74
|
+
try:
|
|
75
|
+
saves_int = saves_int if saves_int is not None else int(parts[0])
|
|
76
|
+
shots_against_int = (
|
|
77
|
+
shots_against_int if shots_against_int is not None else int(parts[1])
|
|
78
|
+
)
|
|
79
|
+
except (ValueError, IndexError):
|
|
80
|
+
pass
|
|
81
|
+
elif isinstance(save_shots_raw, (int, float)):
|
|
82
|
+
shots_against_int = (
|
|
83
|
+
shots_against_int if shots_against_int is not None else int(save_shots_raw)
|
|
84
|
+
)
|
|
85
|
+
row["shots_against"] = shots_against_int
|
|
86
|
+
row["saves"] = saves_int
|
|
87
|
+
row["save_pctg"] = p.get("savePctg")
|
|
88
|
+
row["goals_against"] = goals_against
|
|
89
|
+
for f in _GOALIE_FIELDS[3:]: # skip the three above
|
|
90
|
+
row[_camel_to_snake(f)] = p.get(f)
|
|
91
|
+
rows.append(row)
|
|
92
|
+
|
|
93
|
+
return pd.DataFrame(rows)
|