ffl-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.
- ffl_bigquery-0.1.0/.github/workflows/ci.yml +32 -0
- ffl_bigquery-0.1.0/.github/workflows/release.yml +45 -0
- ffl_bigquery-0.1.0/.gitignore +6 -0
- ffl_bigquery-0.1.0/CHANGELOG.md +119 -0
- ffl_bigquery-0.1.0/LICENSE +21 -0
- ffl_bigquery-0.1.0/PKG-INFO +264 -0
- ffl_bigquery-0.1.0/README.md +230 -0
- ffl_bigquery-0.1.0/ffl_bigquery/__init__.py +3 -0
- ffl_bigquery-0.1.0/ffl_bigquery/_schema_samples/__init__.py +54 -0
- ffl_bigquery-0.1.0/ffl_bigquery/_schema_samples/ff_opportunity.parquet +0 -0
- ffl_bigquery-0.1.0/ffl_bigquery/_schema_samples/ff_rankings.parquet +0 -0
- ffl_bigquery-0.1.0/ffl_bigquery/_schema_samples/ftn_charting.parquet +0 -0
- ffl_bigquery-0.1.0/ffl_bigquery/_schema_samples/injuries.parquet +0 -0
- ffl_bigquery-0.1.0/ffl_bigquery/_schema_samples/participation.parquet +0 -0
- ffl_bigquery-0.1.0/ffl_bigquery/_schema_samples/snap_counts.parquet +0 -0
- ffl_bigquery-0.1.0/ffl_bigquery/_transform_util.py +18 -0
- ffl_bigquery-0.1.0/ffl_bigquery/_version.py +1 -0
- ffl_bigquery-0.1.0/ffl_bigquery/adp/__init__.py +0 -0
- ffl_bigquery-0.1.0/ffl_bigquery/adp/ffc.py +66 -0
- ffl_bigquery-0.1.0/ffl_bigquery/adp/mfl.py +81 -0
- ffl_bigquery-0.1.0/ffl_bigquery/adp/resolve.py +132 -0
- ffl_bigquery-0.1.0/ffl_bigquery/adp/schema.py +193 -0
- ffl_bigquery-0.1.0/ffl_bigquery/adp/sync.py +175 -0
- ffl_bigquery-0.1.0/ffl_bigquery/adp/transform.py +124 -0
- ffl_bigquery-0.1.0/ffl_bigquery/cli.py +141 -0
- ffl_bigquery-0.1.0/ffl_bigquery/coaches/__init__.py +0 -0
- ffl_bigquery-0.1.0/ffl_bigquery/coaches/schema.py +63 -0
- ffl_bigquery-0.1.0/ffl_bigquery/coaches/sync.py +23 -0
- ffl_bigquery-0.1.0/ffl_bigquery/coaches/transform.py +43 -0
- ffl_bigquery-0.1.0/ffl_bigquery/coordinators/__init__.py +0 -0
- ffl_bigquery-0.1.0/ffl_bigquery/coordinators/schema.py +104 -0
- ffl_bigquery-0.1.0/ffl_bigquery/coordinators/sync.py +121 -0
- ffl_bigquery-0.1.0/ffl_bigquery/coordinators/wikipedia.py +160 -0
- ffl_bigquery-0.1.0/ffl_bigquery/derive/__init__.py +1 -0
- ffl_bigquery-0.1.0/ffl_bigquery/derive/personnel.py +122 -0
- ffl_bigquery-0.1.0/ffl_bigquery/derive/points_weekly.py +139 -0
- ffl_bigquery-0.1.0/ffl_bigquery/derive/scheme_week.py +469 -0
- ffl_bigquery-0.1.0/ffl_bigquery/http.py +90 -0
- ffl_bigquery-0.1.0/ffl_bigquery/nflverse/__init__.py +0 -0
- ffl_bigquery-0.1.0/ffl_bigquery/nflverse/driver.py +161 -0
- ffl_bigquery-0.1.0/ffl_bigquery/nflverse/runs.py +110 -0
- ffl_bigquery-0.1.0/ffl_bigquery/nflverse/spec.py +25 -0
- ffl_bigquery-0.1.0/ffl_bigquery/nflverse/tables/__init__.py +62 -0
- ffl_bigquery-0.1.0/ffl_bigquery/nflverse/tables/depth_charts.py +179 -0
- ffl_bigquery-0.1.0/ffl_bigquery/nflverse/tables/ftn_charting.py +92 -0
- ffl_bigquery-0.1.0/ffl_bigquery/nflverse/tables/injuries.py +54 -0
- ffl_bigquery-0.1.0/ffl_bigquery/nflverse/tables/opportunity.py +50 -0
- ffl_bigquery-0.1.0/ffl_bigquery/nflverse/tables/participation.py +131 -0
- ffl_bigquery-0.1.0/ffl_bigquery/nflverse/tables/rankings.py +54 -0
- ffl_bigquery-0.1.0/ffl_bigquery/nflverse/tables/snap_counts.py +44 -0
- ffl_bigquery-0.1.0/ffl_bigquery/partition.py +40 -0
- ffl_bigquery-0.1.0/ffl_bigquery/runs.py +135 -0
- ffl_bigquery-0.1.0/ffl_bigquery/schema.py +68 -0
- ffl_bigquery-0.1.0/ffl_bigquery/schema_gen.py +86 -0
- ffl_bigquery-0.1.0/ffl_bigquery/verify/__init__.py +60 -0
- ffl_bigquery-0.1.0/ffl_bigquery/verify/adp.py +138 -0
- ffl_bigquery-0.1.0/ffl_bigquery/verify/tables.py +220 -0
- ffl_bigquery-0.1.0/ffl_bigquery/writer.py +271 -0
- ffl_bigquery-0.1.0/ffl_bigquery/xref/__init__.py +0 -0
- ffl_bigquery-0.1.0/ffl_bigquery/xref/schema.py +100 -0
- ffl_bigquery-0.1.0/ffl_bigquery/xref/sync.py +39 -0
- ffl_bigquery-0.1.0/ffl_bigquery/xref/transform.py +15 -0
- ffl_bigquery-0.1.0/pyproject.toml +78 -0
- ffl_bigquery-0.1.0/tests/conftest.py +40 -0
- ffl_bigquery-0.1.0/tests/fixtures/ff_playerids_sample.csv +7 -0
- ffl_bigquery-0.1.0/tests/fixtures/ffc_empty_error.json +4 -0
- ffl_bigquery-0.1.0/tests/fixtures/ffc_empty_success.json +12 -0
- ffl_bigquery-0.1.0/tests/fixtures/ffc_ppr_2015.json +39 -0
- ffl_bigquery-0.1.0/tests/fixtures/mfl_adp_2015.json +28 -0
- ffl_bigquery-0.1.0/tests/fixtures/mfl_adp_empty_2005.json +5 -0
- ffl_bigquery-0.1.0/tests/fixtures/mfl_adp_single_player.json +17 -0
- ffl_bigquery-0.1.0/tests/fixtures/nflverse/depth_charts_legacy.parquet +0 -0
- ffl_bigquery-0.1.0/tests/fixtures/nflverse/depth_charts_modern.parquet +0 -0
- ffl_bigquery-0.1.0/tests/fixtures/wikipedia_seasons.json +5 -0
- ffl_bigquery-0.1.0/tests/test_adp_ffc.py +78 -0
- ffl_bigquery-0.1.0/tests/test_adp_mfl.py +87 -0
- ffl_bigquery-0.1.0/tests/test_adp_resolve.py +141 -0
- ffl_bigquery-0.1.0/tests/test_adp_schema.py +61 -0
- ffl_bigquery-0.1.0/tests/test_adp_sync.py +256 -0
- ffl_bigquery-0.1.0/tests/test_adp_transform.py +147 -0
- ffl_bigquery-0.1.0/tests/test_cli.py +128 -0
- ffl_bigquery-0.1.0/tests/test_clustering_types_are_valid.py +92 -0
- ffl_bigquery-0.1.0/tests/test_coaches.py +103 -0
- ffl_bigquery-0.1.0/tests/test_coordinators.py +292 -0
- ffl_bigquery-0.1.0/tests/test_depth_charts.py +122 -0
- ffl_bigquery-0.1.0/tests/test_http.py +65 -0
- ffl_bigquery-0.1.0/tests/test_nflverse_driver.py +385 -0
- ffl_bigquery-0.1.0/tests/test_nflverse_runs.py +68 -0
- ffl_bigquery-0.1.0/tests/test_nflverse_tables_registry.py +23 -0
- ffl_bigquery-0.1.0/tests/test_nflverse_tables_simple.py +69 -0
- ffl_bigquery-0.1.0/tests/test_partition.py +18 -0
- ffl_bigquery-0.1.0/tests/test_personnel.py +60 -0
- ffl_bigquery-0.1.0/tests/test_play_level_tables.py +54 -0
- ffl_bigquery-0.1.0/tests/test_points_weekly.py +85 -0
- ffl_bigquery-0.1.0/tests/test_runs.py +161 -0
- ffl_bigquery-0.1.0/tests/test_schema.py +59 -0
- ffl_bigquery-0.1.0/tests/test_schema_gen.py +88 -0
- ffl_bigquery-0.1.0/tests/test_scheme_week.py +101 -0
- ffl_bigquery-0.1.0/tests/test_verify_adp.py +166 -0
- ffl_bigquery-0.1.0/tests/test_verify_tables.py +283 -0
- ffl_bigquery-0.1.0/tests/test_writer.py +150 -0
- ffl_bigquery-0.1.0/tests/test_writer_write_season.py +159 -0
- ffl_bigquery-0.1.0/tests/test_xref.py +113 -0
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
name: ci
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches: [main]
|
|
6
|
+
pull_request:
|
|
7
|
+
|
|
8
|
+
permissions:
|
|
9
|
+
contents: read
|
|
10
|
+
|
|
11
|
+
jobs:
|
|
12
|
+
test:
|
|
13
|
+
runs-on: ubuntu-latest
|
|
14
|
+
strategy:
|
|
15
|
+
matrix:
|
|
16
|
+
python-version: ["3.11", "3.12", "3.13"]
|
|
17
|
+
steps:
|
|
18
|
+
- uses: actions/checkout@v4
|
|
19
|
+
- uses: astral-sh/setup-uv@v3
|
|
20
|
+
- name: Set up Python ${{ matrix.python-version }}
|
|
21
|
+
run: uv python install ${{ matrix.python-version }}
|
|
22
|
+
- name: Install
|
|
23
|
+
run: uv sync --extra dev
|
|
24
|
+
# -m "not network" is load-bearing: FFC/MFL/nflverse must never be hit in
|
|
25
|
+
# CI. Those are third-party live services (FFC's terms ask callers not to
|
|
26
|
+
# poll frequently) and CI has no business calling them on every push.
|
|
27
|
+
- name: Test
|
|
28
|
+
run: uv run pytest -m "not network" -q --cov=ffl_bigquery
|
|
29
|
+
- name: Lint
|
|
30
|
+
run: uv run ruff check .
|
|
31
|
+
- name: Type check
|
|
32
|
+
run: uv run pyright ffl_bigquery
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
name: release
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
tags: ["v*"]
|
|
6
|
+
|
|
7
|
+
jobs:
|
|
8
|
+
publish:
|
|
9
|
+
runs-on: ubuntu-latest
|
|
10
|
+
environment: pypi
|
|
11
|
+
permissions:
|
|
12
|
+
contents: read
|
|
13
|
+
id-token: write
|
|
14
|
+
steps:
|
|
15
|
+
- uses: actions/checkout@v4
|
|
16
|
+
- uses: astral-sh/setup-uv@v3
|
|
17
|
+
- run: uv python install 3.13
|
|
18
|
+
- name: Install
|
|
19
|
+
run: uv sync --extra dev
|
|
20
|
+
# Same network exclusion as ci.yml: a release build must never depend on
|
|
21
|
+
# FFC/MFL/nflverse being reachable at publish time.
|
|
22
|
+
- name: Test
|
|
23
|
+
run: uv run pytest -m "not network" -q
|
|
24
|
+
- name: Build
|
|
25
|
+
run: uv run python -m build
|
|
26
|
+
# Installs the actual built artifact into a throwaway venv (not the
|
|
27
|
+
# `uv sync --extra dev` env the test step used, which has ffl_bigquery
|
|
28
|
+
# importable from the repo checkout and would never catch this) and
|
|
29
|
+
# runs it from outside the repo. This is exactly the check that would
|
|
30
|
+
# have caught the pre-tag Critical 1 bug: six table modules read a
|
|
31
|
+
# parquet schema-sample via `Path(__file__).parents[3] / "tests/..."`,
|
|
32
|
+
# which resolves inside a repo checkout but not from a real `pip
|
|
33
|
+
# install` into site-packages -- FileNotFoundError on every
|
|
34
|
+
# sync-nflverse invocation, dry-run included. A passing `pytest` run
|
|
35
|
+
# from the repo root cannot detect that class of bug; only installing
|
|
36
|
+
# the wheel and running it somewhere `tests/` doesn't exist can.
|
|
37
|
+
- name: Smoke-test the built wheel
|
|
38
|
+
run: |
|
|
39
|
+
uv venv /tmp/wheel-smoke
|
|
40
|
+
uv pip install --python /tmp/wheel-smoke/bin/python dist/*.whl
|
|
41
|
+
cd /tmp
|
|
42
|
+
/tmp/wheel-smoke/bin/ffl-bigquery --version
|
|
43
|
+
/tmp/wheel-smoke/bin/ffl-bigquery sync-nflverse --dataset p.d --dry-run
|
|
44
|
+
- name: Publish to PyPI
|
|
45
|
+
uses: pypa/gh-action-pypi-publish@release/v1
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to this project will be documented in this file.
|
|
4
|
+
|
|
5
|
+
## 0.1.0 — 2026-07-30
|
|
6
|
+
|
|
7
|
+
Initial release: the fantasy layer on top of `nfl-bigquery`. 13 tables plus 2 run logs,
|
|
8
|
+
covering fantasy ADP/rankings, nine season-chunked nflverse/derived tables, and an
|
|
9
|
+
opt-in coordinators table. Still the first public release — Plan 1's ADP-only surface was
|
|
10
|
+
never published, so there is no 0.0.x history to account for.
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
|
|
14
|
+
**ADP + id bridge**
|
|
15
|
+
- `ff_adp` — historical ADP at snapshot grain from Fantasy Football Calculator (2010→,
|
|
16
|
+
no 2025) and MyFantasyLeague (2011→), MERGE-upserted on
|
|
17
|
+
`(source, season, scoring_format, teams, snapshot_date, source_player_id)` so a
|
|
18
|
+
repeated daily sync is a no-op.
|
|
19
|
+
- `ff_player_xref` — the 20-system nflverse id bridge, MERGE-upserted on `mfl_id`.
|
|
20
|
+
- `gsis_id` resolution on `ff_adp`: exact `mfl_id` join for MFL; normalized-name join
|
|
21
|
+
for FFC, which publishes an id present in no nflverse id system. Ambiguous name
|
|
22
|
+
matches are refused rather than guessed.
|
|
23
|
+
- `_ffl_ingest_runs` — chunk-keyed run log powering `sync-adp --resume`.
|
|
24
|
+
- `sync-xref` — standalone `ff_player_xref` upsert.
|
|
25
|
+
|
|
26
|
+
**nine season-chunked nflverse/derived tables, one shared driver (`sync-nflverse`)**
|
|
27
|
+
- `ff_opportunity` — weekly fantasy opportunity/usage metrics (2006–2025, 159 cols).
|
|
28
|
+
- `snap_counts` — weekly offense/defense/special-teams snap counts (2013–2025), joining
|
|
29
|
+
`ff_player_xref` via `pfr_id`.
|
|
30
|
+
- `injuries` — weekly injury reports (2009–2025).
|
|
31
|
+
- `depth_charts` — two disjoint upstream schemas (legacy 2001–2024 season-keyed, modern
|
|
32
|
+
2025+ timestamp-keyed) reconciled into one normalized table with a `source_era`
|
|
33
|
+
discriminator (2001–2026).
|
|
34
|
+
- `participation` — play-level offense/defense personnel and coverage (2016–2025), with
|
|
35
|
+
`season`/`week` derived from `nflverse_game_id` (upstream publishes neither directly).
|
|
36
|
+
- `ftn_charting` — FTN's manually-charted play-level features: play action, motion, RPO,
|
|
37
|
+
screen, blitz (2022–2025).
|
|
38
|
+
- `nfl_coaches` — one row per `(game_id, team)`, unpivoted from `load_schedules()`'s
|
|
39
|
+
side-by-side home/away coach columns so a mid-season firing is just two different
|
|
40
|
+
values across two weeks (1999–2026).
|
|
41
|
+
- `ff_points_weekly` — half-PPR + positional rank derived from `load_player_stats()`.
|
|
42
|
+
`fantasy_points_ppr` is carried through unchanged from upstream (never recomputed) so
|
|
43
|
+
it stays a correctness oracle a `verify` check can compare a recomputed total against.
|
|
44
|
+
- `team_scheme_week` — the marquee derived table: per-`(season, week, team)` scheme
|
|
45
|
+
fingerprint (shotgun/no-huddle/pass rate/PROE/EPA from `load_pbp()`, personnel
|
|
46
|
+
groupings, coverage/pressure, FTN's play-action/motion/RPO/blitz) joined to that
|
|
47
|
+
week's head coach. Every charted-metric rate ships beside its own denominator column
|
|
48
|
+
and is `pd.NA` — never `0.0` — when that denominator is zero.
|
|
49
|
+
- `_ffl_nflverse_runs` — a second, separately-keyed `(table_name, season)` run log
|
|
50
|
+
powering `sync-nflverse --resume`; kept independent of ADP's run log rather than
|
|
51
|
+
generalized, since that one is tested and already in production use.
|
|
52
|
+
|
|
53
|
+
**opt-in**
|
|
54
|
+
- `nfl_coordinators` — offensive/defensive coordinator by `(season, team)`, scraped from
|
|
55
|
+
Wikipedia team-season infobox pages (Pro Football Reference returns HTTP 403 and is
|
|
56
|
+
unusable). Every row carries its own provenance (`source`, `confidence`,
|
|
57
|
+
`retrieved_at`) rather than presenting silence as completeness. Deliberately never
|
|
58
|
+
part of `sync-nflverse` — only the explicit `sync-coordinators` command.
|
|
59
|
+
- `ff_rankings` — FantasyPros ECR, current snapshot only (not season-chunked).
|
|
60
|
+
|
|
61
|
+
**verify**
|
|
62
|
+
- `--checks adp` — `gsis_id` resolution rate (floor `--min-resolution-rate`, default a
|
|
63
|
+
conservative 0.60), plus grain uniqueness (which, since the grain includes
|
|
64
|
+
`snapshot_date`, also guarantees same-day MERGE idempotency — the same underlying
|
|
65
|
+
fact, reported as two checks for clearer failure messages).
|
|
66
|
+
- `--checks points-weekly` — recomputes full-PPR from `fantasy_points_standard +
|
|
67
|
+
receptions` and asserts it still agrees with upstream's carried-through
|
|
68
|
+
`fantasy_points_ppr`.
|
|
69
|
+
- `--checks scheme-denominators` — asserts every charted rate in `team_scheme_week` has
|
|
70
|
+
a populated denominator and never exceeds 1.0.
|
|
71
|
+
- `--checks participation-coverage` — a whole-table regression guard: measured
|
|
72
|
+
per-season coverage-charting fill still matches the documented shape (0.000 in
|
|
73
|
+
2016–2017, never above .496 afterward), catching an upstream backfill that silently
|
|
74
|
+
changes the data under a shipped chart.
|
|
75
|
+
|
|
76
|
+
### Known limitations
|
|
77
|
+
|
|
78
|
+
- **FFC ignores `start_date`/`end_date`**, so intra-preseason ADP drift is
|
|
79
|
+
forward-capture-only and cannot be backfilled.
|
|
80
|
+
- **FFC has no 2025 data.** Both `ppr` and `standard` return empty for 2025 while
|
|
81
|
+
2010–2024 and 2026 return normally. This is an upstream gap, not a client bug — the
|
|
82
|
+
sync records those chunks as `empty` and continues.
|
|
83
|
+
- FFC `half-ppr` history is shallow; pre-recent seasons return no data.
|
|
84
|
+
- **MFL starts at 2011** (2010 and earlier return empty). An earlier probe had this
|
|
85
|
+
wrong as 2012 — it tested 2010 and 2012 and never tried 2011, which turns out to have
|
|
86
|
+
812 rows; corrected once a real backfill was run.
|
|
87
|
+
- **`gsis_id` is NULL for 37.9% of `ff_playerids` rows** (66.9% filled among rostered
|
|
88
|
+
QB/RB/WR/TE/K), which caps resolution everywhere it's used. Measured on the real
|
|
89
|
+
2010–2026 backfill (18,309 `ff_adp` rows): FFC resolves 89.9%, MFL 91.8%; the worst
|
|
90
|
+
legitimate `(source, season)` is MFL 2026 at 63.1%, where unresolved rookies have no
|
|
91
|
+
`gsis_id` yet. `--min-resolution-rate`'s 0.60 default sits just below that floor.
|
|
92
|
+
- **Coordinators are partially available — 46.2% measured.** A 24-team-season sample
|
|
93
|
+
(6 teams x 2005/2012/2019/2024) suggested ~37%; the actual 2010-2025 backfill
|
|
94
|
+
produced **473 rows of a possible 1,024** (16 seasons x 32 teams x 2 roles) =
|
|
95
|
+
**46.2%**. Head coach, by contrast, was 24/24 in that sample and is already
|
|
96
|
+
first-class in `nfl_coaches` at per-game grain — only coordinators are sparse.
|
|
97
|
+
All six 2005 team-seasons sampled had neither coordinator field. Pro Football
|
|
98
|
+
Reference returns HTTP 403 to automated fetches and is unusable, leaving Wikipedia
|
|
99
|
+
team-season infoboxes as the only fetchable source. Hence `sync-coordinators` is
|
|
100
|
+
never part of `sync-nflverse` — it is a separate, explicit, opt-in command. Also:
|
|
101
|
+
the team-abbreviation to Wikipedia-page-title map uses each franchise's current
|
|
102
|
+
name, so pre-relocation/rename seasons resolve poorly.
|
|
103
|
+
- **`offense_personnel` is unreliable from 2023 on** — it reports 100% fill while
|
|
104
|
+
carrying defensive players in the offense column (a real 2023 row reads "2 CB, 2 ILB,
|
|
105
|
+
1 OLB, 1 RB, 1 SS, 2 TE, 2 WR"). `team_scheme_week` therefore parses personnel from
|
|
106
|
+
`offense_positions` for 2023+, and from `offense_personnel` for 2016–2022 (where
|
|
107
|
+
`offense_positions` is 0% filled). `personnel_source` records which parser ran.
|
|
108
|
+
- **Charted coverage/pressure/FTN metrics are a sample, never a census.**
|
|
109
|
+
`defense_man_zone_type`/`defense_coverage_type` fill is 0.000 in 2016–2017 and never
|
|
110
|
+
exceeds .496 thereafter; `was_pressure` fill is ~.38 pre-2023 and 1.000 from 2023 on.
|
|
111
|
+
Every derived rate in `team_scheme_week` ships beside its own denominator column, and
|
|
112
|
+
a zero denominator yields `pd.NA`, never `0.0`. Out-of-era columns are NULL, not 0.
|
|
113
|
+
- **Upstream dtypes are vintage-dependent** — `season`, `week`, `play_id`, and
|
|
114
|
+
`pos_slot` each change type between years across the nflverse feeds this library
|
|
115
|
+
reads (e.g. `injuries.season` is Float64 for older seasons, Int32 for others;
|
|
116
|
+
`participation.play_id` is Float64 in some seasons, Int32 in others). Every one is
|
|
117
|
+
normalized to a stable BigQuery type before writing; a float↔int join left uncast
|
|
118
|
+
silently under-matches rather than erroring, so casts are applied even where a single
|
|
119
|
+
sampled season looks fine on its own.
|
|
@@ -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,264 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: ffl-bigquery
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Historical fantasy football ADP, usage, and coaching/scheme data → BigQuery: the fantasy layer on top of nfl-bigquery
|
|
5
|
+
Project-URL: Homepage, https://github.com/blahovec-labs/ffl-bigquery
|
|
6
|
+
Project-URL: Issues, https://github.com/blahovec-labs/ffl-bigquery/issues
|
|
7
|
+
Project-URL: Changelog, https://github.com/blahovec-labs/ffl-bigquery/blob/main/CHANGELOG.md
|
|
8
|
+
Author: Jason Blahovec
|
|
9
|
+
License: MIT
|
|
10
|
+
License-File: LICENSE
|
|
11
|
+
Keywords: adp,bigquery,data-engineering,fantasy-football,nfl,nflverse
|
|
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<1.0,>=0.1
|
|
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: ruff>=0.6.0; extra == 'dev'
|
|
32
|
+
Requires-Dist: truststore>=0.9; extra == 'dev'
|
|
33
|
+
Description-Content-Type: text/markdown
|
|
34
|
+
|
|
35
|
+
# ffl-bigquery
|
|
36
|
+
|
|
37
|
+
Historical fantasy football ADP, usage, and coaching/scheme data → BigQuery. The fantasy
|
|
38
|
+
layer on top of [`nfl-bigquery`](https://github.com/blahovec-labs/nfl-bigquery). Sixth in
|
|
39
|
+
the `*-bigquery` family (`statcast-bigquery`, `yfinance-bigquery`, `nhl-bigquery`,
|
|
40
|
+
`nhl-hut-bigquery`, `nfl-bigquery`).
|
|
41
|
+
|
|
42
|
+
## Install
|
|
43
|
+
|
|
44
|
+
pip install ffl-bigquery
|
|
45
|
+
|
|
46
|
+
Requires Python 3.11+. Writing to BigQuery needs Application Default Credentials with
|
|
47
|
+
permission to create/query tables in the target dataset.
|
|
48
|
+
|
|
49
|
+
## Quickstart
|
|
50
|
+
|
|
51
|
+
ffl-bigquery --version
|
|
52
|
+
|
|
53
|
+
ffl-bigquery sync-adp \
|
|
54
|
+
--adp-table PROJECT.DATASET.ff_adp \
|
|
55
|
+
--xref-table PROJECT.DATASET.ff_player_xref \
|
|
56
|
+
--seasons 2010-2026 --sources ffc,mfl --formats ppr,standard --teams 12 \
|
|
57
|
+
--resume
|
|
58
|
+
|
|
59
|
+
ffl-bigquery sync-nflverse --dataset PROJECT.DATASET --seasons 1999-2025 --resume
|
|
60
|
+
|
|
61
|
+
ffl-bigquery verify --checks adp --season 2026 --adp-table PROJECT.DATASET.ff_adp
|
|
62
|
+
|
|
63
|
+
Every `sync-*` command fans its work into independent chunks (by source/season/format for
|
|
64
|
+
ADP, by table/season for the nflverse surface) and records each attempt in a run log, so
|
|
65
|
+
one bad upstream season or source degrades coverage instead of aborting the whole run.
|
|
66
|
+
Pass `--resume` to skip chunks already recorded `success` or `empty`.
|
|
67
|
+
|
|
68
|
+
## What it writes
|
|
69
|
+
|
|
70
|
+
13 tables plus 2 run logs. Row counts below are measured from a real backfill against
|
|
71
|
+
live BigQuery (not estimates); `nfl_coordinators` is opt-in and its count depends on what
|
|
72
|
+
you choose to fetch.
|
|
73
|
+
|
|
74
|
+
| Table | Rows (measured) | Seasons | Written by |
|
|
75
|
+
| --- | --- | --- | --- |
|
|
76
|
+
| `ff_adp` | 18,309 | 2010–2026 | `sync-adp` |
|
|
77
|
+
| `ff_player_xref` | 12,468 | snapshot | `sync-xref` |
|
|
78
|
+
| `ff_rankings` | 6,391 | snapshot, forward-only | `sync-rankings` |
|
|
79
|
+
| `ff_opportunity` | 112,297 | 2006–2025 | `sync-nflverse` |
|
|
80
|
+
| `snap_counts` | 324,611 | 2013–2025 | `sync-nflverse` |
|
|
81
|
+
| `injuries` | 90,752 | 2009–2025 | `sync-nflverse` |
|
|
82
|
+
| `depth_charts` | 1,771,856 | 2001–2026 | `sync-nflverse` |
|
|
83
|
+
| `participation` | 478,989 | 2016–2025 | `sync-nflverse` |
|
|
84
|
+
| `ftn_charting` | 185,215 | 2022–2025 | `sync-nflverse` |
|
|
85
|
+
| `nfl_coaches` | 15,096 | 1999–2026 | `sync-nflverse` |
|
|
86
|
+
| `ff_points_weekly` | 476,156 | 1999–2025 | `sync-nflverse` |
|
|
87
|
+
| `team_scheme_week` | 14,546 | 1999–2025 | `sync-nflverse` |
|
|
88
|
+
| `nfl_coordinators` | opt-in, 46.2% measured fill | 2010–2025 (as backfilled) | `sync-coordinators` |
|
|
89
|
+
| `_ffl_ingest_runs` | run log, keyed `(source, season, scoring_format, teams)` | — | `sync-adp` |
|
|
90
|
+
| `_ffl_nflverse_runs` | run log, keyed `(table_name, season)` | — | `sync-nflverse` |
|
|
91
|
+
|
|
92
|
+
That's ~3.5M rows across the twelve non-opt-in tables. Nine of the thirteen
|
|
93
|
+
(`ff_opportunity`, `snap_counts`, `injuries`, `depth_charts`, `participation`,
|
|
94
|
+
`ftn_charting`, `nfl_coaches`, `ff_points_weekly`, `team_scheme_week`) share one driver —
|
|
95
|
+
`sync-nflverse` — because they're all the same shape: load a frame for season *S*, align
|
|
96
|
+
it to a schema, replace that season. `ff_adp` and `nfl_coordinators` are chunked
|
|
97
|
+
differently (by source/format and by team-season respectively) because their upstreams
|
|
98
|
+
are; `ff_player_xref` and `ff_rankings` aren't season-chunked at all — they're
|
|
99
|
+
whole-table/current-snapshot syncs.
|
|
100
|
+
|
|
101
|
+
`team_scheme_week` is the marquee derived table: a per-`(season, week, team)` scheme
|
|
102
|
+
fingerprint (shotgun/no-huddle/pass rate/PROE/EPA, personnel groupings, coverage/pressure,
|
|
103
|
+
FTN's play-action/motion/RPO/blitz) joined to that week's head coach from `nfl_coaches` —
|
|
104
|
+
built for "what changed when the coach changed." Every `gsis_id` column resolves against
|
|
105
|
+
`ff_player_xref` (via `pfr_id` for `snap_counts`, directly elsewhere); see Known
|
|
106
|
+
limitations for the resolution ceiling that imposes.
|
|
107
|
+
|
|
108
|
+
## Known limitations
|
|
109
|
+
|
|
110
|
+
Every one of these was measured against the real feeds, not assumed:
|
|
111
|
+
|
|
112
|
+
- **FFC ignores `start_date`/`end_date`.** Requests for different date windows return the
|
|
113
|
+
identical current window, so intra-preseason ADP drift is forward-capture-only and
|
|
114
|
+
cannot be backfilled — it exists only if a sync actually ran that day.
|
|
115
|
+
- **FFC has no 2025 data.** Both `ppr` and `standard` return empty for 2025 while
|
|
116
|
+
2010–2024 and 2026 return normally. This is an upstream gap, not a client bug — the
|
|
117
|
+
sync records those chunks as `empty` and continues.
|
|
118
|
+
- **MFL starts at 2011.** 2010 and earlier return `{"adp": {"totalPicks": "0", ...}}`
|
|
119
|
+
with no player data.
|
|
120
|
+
- **`gsis_id` is NULL for 37.9% of `ff_playerids` rows** (66.9% filled among rostered
|
|
121
|
+
QB/RB/WR/TE/K; the rest are college prospects and players who never reached an NFL
|
|
122
|
+
roster), which caps every downstream resolution rate. Measured on the real 2010–2026
|
|
123
|
+
backfill (18,309 `ff_adp` rows): **FFC resolves 89.9%, MFL 91.8%**; the worst legitimate
|
|
124
|
+
`(source, season)` is **MFL 2026 at 63.1%** (current-year rookies without a `gsis_id`
|
|
125
|
+
yet). `verify`'s `--min-resolution-rate` defaults to a conservative **0.60**, just below
|
|
126
|
+
that floor.
|
|
127
|
+
- **Coordinators are partially available — 46.2% measured.** A 24-team-season sample
|
|
128
|
+
(6 teams x 2005/2012/2019/2024) suggested ~37%; the actual 2010-2025 backfill
|
|
129
|
+
produced **473 rows of a possible 1,024** (16 seasons x 32 teams x 2 roles) =
|
|
130
|
+
**46.2%**. Head coach, by contrast, was 24/24 in that sample and is already
|
|
131
|
+
first-class in `nfl_coaches` at per-game grain — only coordinators are sparse.
|
|
132
|
+
All six 2005 team-seasons sampled had neither coordinator field. Pro Football
|
|
133
|
+
Reference returns HTTP 403 to automated fetches and is unusable, leaving Wikipedia
|
|
134
|
+
team-season infoboxes as the only fetchable source. Hence `sync-coordinators` is
|
|
135
|
+
never part of `sync-nflverse` — it is a separate, explicit, opt-in command. Also:
|
|
136
|
+
the team-abbreviation to Wikipedia-page-title map uses each franchise's current
|
|
137
|
+
name, so pre-relocation/rename seasons resolve poorly.
|
|
138
|
+
- **`offense_personnel` is unreliable from 2023 on.** It still reports 100% fill for
|
|
139
|
+
2023–2025, but its content changed without notice — a real 2023 row reads
|
|
140
|
+
`"2 CB, 2 ILB, 1 OLB, 1 RB, 1 SS, 2 TE, 2 WR"`, defensive players inside the offense
|
|
141
|
+
column. `team_scheme_week` therefore parses personnel from `offense_positions` (a
|
|
142
|
+
per-player position list) for 2023+, and falls back to `offense_personnel` for
|
|
143
|
+
2016–2022, where `offense_positions` is 0% filled. Each row's `personnel_source` column
|
|
144
|
+
records which parser ran.
|
|
145
|
+
- **Charted coverage/pressure/FTN metrics are a sample, never a census.** Coverage
|
|
146
|
+
charting fill (`defense_man_zone_type`/`defense_coverage_type`) is **0.000 in
|
|
147
|
+
2016–2017** and never exceeds **.496** afterward; `was_pressure` fill is ~.38 pre-2023
|
|
148
|
+
and 1.000 from 2023 on. Every derived rate in `team_scheme_week` therefore ships beside
|
|
149
|
+
its own denominator column (e.g. `plays_charted_coverage` next to `man_rate`), and the
|
|
150
|
+
rate is `pd.NA` — never `0.0` — when that denominator is 0, so "nobody charted it" never
|
|
151
|
+
reads as "this team never blitzed." Out-of-era columns are NULL, not 0, for the same
|
|
152
|
+
reason.
|
|
153
|
+
- **Upstream dtypes are vintage-dependent.** `season`, `week`, `play_id`, and `pos_slot`
|
|
154
|
+
each change type between years in the raw nflverse feeds (e.g. `season` arrives as a
|
|
155
|
+
string in some tables, a float in others; `play_id` is Float64 in some seasons and
|
|
156
|
+
Int32 in others). This library normalizes every one of them to a stable BigQuery type
|
|
157
|
+
before writing — consumers reading nflverse directly should expect the raw dtype to
|
|
158
|
+
vary by season and cast defensively before joining.
|
|
159
|
+
|
|
160
|
+
See `CHANGELOG.md` for the full per-release list, including two claims an earlier probe
|
|
161
|
+
got wrong and this backfill corrected (MFL's true start season, and FFC's missing 2025).
|
|
162
|
+
|
|
163
|
+
## CLI
|
|
164
|
+
|
|
165
|
+
ffl-bigquery --version
|
|
166
|
+
|
|
167
|
+
# ADP + the id bridge it resolves against
|
|
168
|
+
ffl-bigquery sync-adp \
|
|
169
|
+
--adp-table PROJECT.DATASET.ff_adp --xref-table PROJECT.DATASET.ff_player_xref \
|
|
170
|
+
--seasons 2010-2026 --sources ffc,mfl --formats ppr,standard --teams 12 \
|
|
171
|
+
--min-interval 1.0 --resume
|
|
172
|
+
|
|
173
|
+
ffl-bigquery sync-xref --xref-table PROJECT.DATASET.ff_player_xref
|
|
174
|
+
|
|
175
|
+
# the nine season-chunked nflverse/derived tables, one dataset, one command
|
|
176
|
+
ffl-bigquery sync-nflverse --dataset PROJECT.DATASET --seasons 1999-2025 --resume
|
|
177
|
+
|
|
178
|
+
# a subset, if you only want a few
|
|
179
|
+
ffl-bigquery sync-nflverse --dataset PROJECT.DATASET --seasons latest \
|
|
180
|
+
--tables ff_opportunity,snap_counts,injuries --resume
|
|
181
|
+
|
|
182
|
+
# current ECR snapshot -- not season-chunked
|
|
183
|
+
ffl-bigquery sync-rankings --rankings-table PROJECT.DATASET.ff_rankings
|
|
184
|
+
|
|
185
|
+
# opt-in, 46.2% measured fill -- never part of sync-nflverse
|
|
186
|
+
ffl-bigquery sync-coordinators \
|
|
187
|
+
--coordinators-table PROJECT.DATASET.nfl_coordinators \
|
|
188
|
+
--seasons 2019-2024 --teams all --min-interval 1.0
|
|
189
|
+
|
|
190
|
+
ffl-bigquery verify --checks adp --season 2026 --adp-table PROJECT.DATASET.ff_adp
|
|
191
|
+
ffl-bigquery verify --checks points-weekly --season 2025 \
|
|
192
|
+
--points-weekly-table PROJECT.DATASET.ff_points_weekly
|
|
193
|
+
ffl-bigquery verify --checks scheme-denominators --season 2025 \
|
|
194
|
+
--scheme-week-table PROJECT.DATASET.team_scheme_week
|
|
195
|
+
ffl-bigquery verify --checks participation-coverage \
|
|
196
|
+
--participation-table PROJECT.DATASET.participation
|
|
197
|
+
|
|
198
|
+
Notes:
|
|
199
|
+
|
|
200
|
+
- `sync-adp` fans a season/source/format/teams matrix into independent chunks, so one dead
|
|
201
|
+
upstream source degrades coverage instead of aborting the run; `--resume` skips chunks
|
|
202
|
+
already recorded `success` or `empty` in `_ffl_ingest_runs`.
|
|
203
|
+
- `sync-nflverse` derives each table's ref as `project.dataset.<name>` from a single
|
|
204
|
+
`--dataset` — no per-table flags needed. `--tables` defaults to all nine and is
|
|
205
|
+
validated against the known registry before any fetch, so a typo fails fast. Its
|
|
206
|
+
`--resume` reads `_ffl_nflverse_runs`, a second run log keyed `(table_name, season)`
|
|
207
|
+
— deliberately separate from ADP's `(source, season, scoring_format, teams)` log rather
|
|
208
|
+
than a shared generalization of it.
|
|
209
|
+
- `sync-rankings` takes `--rankings-table` (not `--dataset`) because `ff_rankings` is a
|
|
210
|
+
single current-snapshot table, not a season matrix.
|
|
211
|
+
- `sync-coordinators` takes `--coordinators-table`, `--seasons`, `--teams` (comma-separated
|
|
212
|
+
abbreviations, or `all` for all 32), and `--min-interval`. A missing or unparseable
|
|
213
|
+
Wikipedia page is a normal, exception-free outcome here (not every team-season has an
|
|
214
|
+
infobox with the fields populated), so there's no separate failed/empty run log — just a
|
|
215
|
+
fetched/missing/unavailable tally in the final log line.
|
|
216
|
+
- `--min-interval` (seconds, default `1.0`, both `sync-adp` and `sync-coordinators`)
|
|
217
|
+
throttles the delay between requests to third-party sources — the minimum respectful
|
|
218
|
+
spacing backing FFC's "do not poll frequently" terms and general politeness toward
|
|
219
|
+
Wikipedia's API; see Data sources & attribution below.
|
|
220
|
+
- `verify --checks` accepts a comma-separated subset of `adp`, `points-weekly`,
|
|
221
|
+
`scheme-denominators`, `participation-coverage`. Each group validates its own required
|
|
222
|
+
flags at dispatch time (e.g. `scheme-denominators` needs `--scheme-week-table` and
|
|
223
|
+
`--season`) rather than making every flag globally required. `--min-resolution-rate`
|
|
224
|
+
(default `0.60`) and `--ppr-tolerance` (default `0.01`) are the two numeric knobs.
|
|
225
|
+
- `sync-adp`, `sync-xref`, `sync-nflverse`, and `sync-coordinators` all accept `--dry-run`
|
|
226
|
+
to print what would happen without writing or fetching.
|
|
227
|
+
|
|
228
|
+
## Data sources & attribution
|
|
229
|
+
|
|
230
|
+
- **nflverse** via [`nflreadpy`](https://nflreadpy.nflverse.com/) — player IDs, usage,
|
|
231
|
+
snap counts, injuries, depth charts, participation, FTN charting, schedules/coaches, and
|
|
232
|
+
weekly player stats.
|
|
233
|
+
- **[Fantasy Football Calculator](https://fantasyfootballcalculator.com/)** — historical
|
|
234
|
+
ADP (2010→present, no 2025). Their ADP REST API is free for personal and commercial use;
|
|
235
|
+
this project provides attribution as requested. Data updates once daily — do not poll
|
|
236
|
+
frequently (`--min-interval` backs this).
|
|
237
|
+
- **[MyFantasyLeague](https://www.myfantasyleague.com/)** — historical ADP
|
|
238
|
+
(2011→present) via the free `export?TYPE=adp` endpoint.
|
|
239
|
+
- **Wikipedia** — offensive/defensive coordinator names, via the `action=parse` API
|
|
240
|
+
against team-season infobox pages (`nfl_coordinators`, opt-in, 46.2% measured fill; see Known
|
|
241
|
+
limitations).
|
|
242
|
+
|
|
243
|
+
## Local development (Windows / TLS interception)
|
|
244
|
+
|
|
245
|
+
If HTTPS requests fail with `unable to get local issuer certificate` or
|
|
246
|
+
`CERTIFICATE_VERIFY_FAILED`, a local security product (e.g. Norton) is intercepting TLS
|
|
247
|
+
and re-signing it with a certificate that OpenSSL's bundled trust store doesn't trust,
|
|
248
|
+
even though the OS does. `--native-tls` alone does **not** fix this — it only affects
|
|
249
|
+
uv's own downloads of packages/pythons, not certificate validation inside the Python
|
|
250
|
+
process that makes the actual HTTP requests.
|
|
251
|
+
|
|
252
|
+
Running the test suite already handles this automatically: `tests/conftest.py` calls
|
|
253
|
+
`truststore.inject_into_ssl()` (which validates against the OS-native trust store
|
|
254
|
+
instead of OpenSSL's) for `network`-marked tests only. You just need `truststore`
|
|
255
|
+
installed, which the `dev` extra provides:
|
|
256
|
+
|
|
257
|
+
uv run --native-tls --extra dev pytest -m network -v
|
|
258
|
+
|
|
259
|
+
Outside the test suite (e.g. exploring interactively), you have to invoke that
|
|
260
|
+
injection yourself, since it isn't compiled into the shipped package:
|
|
261
|
+
|
|
262
|
+
uv run --native-tls --extra dev python -c "import truststore; truststore.inject_into_ssl(); ..."
|
|
263
|
+
|
|
264
|
+
MIT licensed.
|