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.
Files changed (103) hide show
  1. ffl_bigquery-0.1.0/.github/workflows/ci.yml +32 -0
  2. ffl_bigquery-0.1.0/.github/workflows/release.yml +45 -0
  3. ffl_bigquery-0.1.0/.gitignore +6 -0
  4. ffl_bigquery-0.1.0/CHANGELOG.md +119 -0
  5. ffl_bigquery-0.1.0/LICENSE +21 -0
  6. ffl_bigquery-0.1.0/PKG-INFO +264 -0
  7. ffl_bigquery-0.1.0/README.md +230 -0
  8. ffl_bigquery-0.1.0/ffl_bigquery/__init__.py +3 -0
  9. ffl_bigquery-0.1.0/ffl_bigquery/_schema_samples/__init__.py +54 -0
  10. ffl_bigquery-0.1.0/ffl_bigquery/_schema_samples/ff_opportunity.parquet +0 -0
  11. ffl_bigquery-0.1.0/ffl_bigquery/_schema_samples/ff_rankings.parquet +0 -0
  12. ffl_bigquery-0.1.0/ffl_bigquery/_schema_samples/ftn_charting.parquet +0 -0
  13. ffl_bigquery-0.1.0/ffl_bigquery/_schema_samples/injuries.parquet +0 -0
  14. ffl_bigquery-0.1.0/ffl_bigquery/_schema_samples/participation.parquet +0 -0
  15. ffl_bigquery-0.1.0/ffl_bigquery/_schema_samples/snap_counts.parquet +0 -0
  16. ffl_bigquery-0.1.0/ffl_bigquery/_transform_util.py +18 -0
  17. ffl_bigquery-0.1.0/ffl_bigquery/_version.py +1 -0
  18. ffl_bigquery-0.1.0/ffl_bigquery/adp/__init__.py +0 -0
  19. ffl_bigquery-0.1.0/ffl_bigquery/adp/ffc.py +66 -0
  20. ffl_bigquery-0.1.0/ffl_bigquery/adp/mfl.py +81 -0
  21. ffl_bigquery-0.1.0/ffl_bigquery/adp/resolve.py +132 -0
  22. ffl_bigquery-0.1.0/ffl_bigquery/adp/schema.py +193 -0
  23. ffl_bigquery-0.1.0/ffl_bigquery/adp/sync.py +175 -0
  24. ffl_bigquery-0.1.0/ffl_bigquery/adp/transform.py +124 -0
  25. ffl_bigquery-0.1.0/ffl_bigquery/cli.py +141 -0
  26. ffl_bigquery-0.1.0/ffl_bigquery/coaches/__init__.py +0 -0
  27. ffl_bigquery-0.1.0/ffl_bigquery/coaches/schema.py +63 -0
  28. ffl_bigquery-0.1.0/ffl_bigquery/coaches/sync.py +23 -0
  29. ffl_bigquery-0.1.0/ffl_bigquery/coaches/transform.py +43 -0
  30. ffl_bigquery-0.1.0/ffl_bigquery/coordinators/__init__.py +0 -0
  31. ffl_bigquery-0.1.0/ffl_bigquery/coordinators/schema.py +104 -0
  32. ffl_bigquery-0.1.0/ffl_bigquery/coordinators/sync.py +121 -0
  33. ffl_bigquery-0.1.0/ffl_bigquery/coordinators/wikipedia.py +160 -0
  34. ffl_bigquery-0.1.0/ffl_bigquery/derive/__init__.py +1 -0
  35. ffl_bigquery-0.1.0/ffl_bigquery/derive/personnel.py +122 -0
  36. ffl_bigquery-0.1.0/ffl_bigquery/derive/points_weekly.py +139 -0
  37. ffl_bigquery-0.1.0/ffl_bigquery/derive/scheme_week.py +469 -0
  38. ffl_bigquery-0.1.0/ffl_bigquery/http.py +90 -0
  39. ffl_bigquery-0.1.0/ffl_bigquery/nflverse/__init__.py +0 -0
  40. ffl_bigquery-0.1.0/ffl_bigquery/nflverse/driver.py +161 -0
  41. ffl_bigquery-0.1.0/ffl_bigquery/nflverse/runs.py +110 -0
  42. ffl_bigquery-0.1.0/ffl_bigquery/nflverse/spec.py +25 -0
  43. ffl_bigquery-0.1.0/ffl_bigquery/nflverse/tables/__init__.py +62 -0
  44. ffl_bigquery-0.1.0/ffl_bigquery/nflverse/tables/depth_charts.py +179 -0
  45. ffl_bigquery-0.1.0/ffl_bigquery/nflverse/tables/ftn_charting.py +92 -0
  46. ffl_bigquery-0.1.0/ffl_bigquery/nflverse/tables/injuries.py +54 -0
  47. ffl_bigquery-0.1.0/ffl_bigquery/nflverse/tables/opportunity.py +50 -0
  48. ffl_bigquery-0.1.0/ffl_bigquery/nflverse/tables/participation.py +131 -0
  49. ffl_bigquery-0.1.0/ffl_bigquery/nflverse/tables/rankings.py +54 -0
  50. ffl_bigquery-0.1.0/ffl_bigquery/nflverse/tables/snap_counts.py +44 -0
  51. ffl_bigquery-0.1.0/ffl_bigquery/partition.py +40 -0
  52. ffl_bigquery-0.1.0/ffl_bigquery/runs.py +135 -0
  53. ffl_bigquery-0.1.0/ffl_bigquery/schema.py +68 -0
  54. ffl_bigquery-0.1.0/ffl_bigquery/schema_gen.py +86 -0
  55. ffl_bigquery-0.1.0/ffl_bigquery/verify/__init__.py +60 -0
  56. ffl_bigquery-0.1.0/ffl_bigquery/verify/adp.py +138 -0
  57. ffl_bigquery-0.1.0/ffl_bigquery/verify/tables.py +220 -0
  58. ffl_bigquery-0.1.0/ffl_bigquery/writer.py +271 -0
  59. ffl_bigquery-0.1.0/ffl_bigquery/xref/__init__.py +0 -0
  60. ffl_bigquery-0.1.0/ffl_bigquery/xref/schema.py +100 -0
  61. ffl_bigquery-0.1.0/ffl_bigquery/xref/sync.py +39 -0
  62. ffl_bigquery-0.1.0/ffl_bigquery/xref/transform.py +15 -0
  63. ffl_bigquery-0.1.0/pyproject.toml +78 -0
  64. ffl_bigquery-0.1.0/tests/conftest.py +40 -0
  65. ffl_bigquery-0.1.0/tests/fixtures/ff_playerids_sample.csv +7 -0
  66. ffl_bigquery-0.1.0/tests/fixtures/ffc_empty_error.json +4 -0
  67. ffl_bigquery-0.1.0/tests/fixtures/ffc_empty_success.json +12 -0
  68. ffl_bigquery-0.1.0/tests/fixtures/ffc_ppr_2015.json +39 -0
  69. ffl_bigquery-0.1.0/tests/fixtures/mfl_adp_2015.json +28 -0
  70. ffl_bigquery-0.1.0/tests/fixtures/mfl_adp_empty_2005.json +5 -0
  71. ffl_bigquery-0.1.0/tests/fixtures/mfl_adp_single_player.json +17 -0
  72. ffl_bigquery-0.1.0/tests/fixtures/nflverse/depth_charts_legacy.parquet +0 -0
  73. ffl_bigquery-0.1.0/tests/fixtures/nflverse/depth_charts_modern.parquet +0 -0
  74. ffl_bigquery-0.1.0/tests/fixtures/wikipedia_seasons.json +5 -0
  75. ffl_bigquery-0.1.0/tests/test_adp_ffc.py +78 -0
  76. ffl_bigquery-0.1.0/tests/test_adp_mfl.py +87 -0
  77. ffl_bigquery-0.1.0/tests/test_adp_resolve.py +141 -0
  78. ffl_bigquery-0.1.0/tests/test_adp_schema.py +61 -0
  79. ffl_bigquery-0.1.0/tests/test_adp_sync.py +256 -0
  80. ffl_bigquery-0.1.0/tests/test_adp_transform.py +147 -0
  81. ffl_bigquery-0.1.0/tests/test_cli.py +128 -0
  82. ffl_bigquery-0.1.0/tests/test_clustering_types_are_valid.py +92 -0
  83. ffl_bigquery-0.1.0/tests/test_coaches.py +103 -0
  84. ffl_bigquery-0.1.0/tests/test_coordinators.py +292 -0
  85. ffl_bigquery-0.1.0/tests/test_depth_charts.py +122 -0
  86. ffl_bigquery-0.1.0/tests/test_http.py +65 -0
  87. ffl_bigquery-0.1.0/tests/test_nflverse_driver.py +385 -0
  88. ffl_bigquery-0.1.0/tests/test_nflverse_runs.py +68 -0
  89. ffl_bigquery-0.1.0/tests/test_nflverse_tables_registry.py +23 -0
  90. ffl_bigquery-0.1.0/tests/test_nflverse_tables_simple.py +69 -0
  91. ffl_bigquery-0.1.0/tests/test_partition.py +18 -0
  92. ffl_bigquery-0.1.0/tests/test_personnel.py +60 -0
  93. ffl_bigquery-0.1.0/tests/test_play_level_tables.py +54 -0
  94. ffl_bigquery-0.1.0/tests/test_points_weekly.py +85 -0
  95. ffl_bigquery-0.1.0/tests/test_runs.py +161 -0
  96. ffl_bigquery-0.1.0/tests/test_schema.py +59 -0
  97. ffl_bigquery-0.1.0/tests/test_schema_gen.py +88 -0
  98. ffl_bigquery-0.1.0/tests/test_scheme_week.py +101 -0
  99. ffl_bigquery-0.1.0/tests/test_verify_adp.py +166 -0
  100. ffl_bigquery-0.1.0/tests/test_verify_tables.py +283 -0
  101. ffl_bigquery-0.1.0/tests/test_writer.py +150 -0
  102. ffl_bigquery-0.1.0/tests/test_writer_write_season.py +159 -0
  103. 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,6 @@
1
+ .venv/
2
+ __pycache__/
3
+ dist/
4
+ *.egg-info/
5
+ .pytest_cache/
6
+ uv.lock
@@ -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.