Repository navigation
Render the seasons nflverse has published, rather than failing on one it has not - #11
Merged
Merged
Conversation
… one it has not nflverse publishes a season's assets only once its games are played, while the season label turns over on 1 September. Every tool asked for the new label from that date, so for the ten days before the opener five of the six requested data that could not exist. A mixed request made it total: `import_snap_counts([2025, 2026])` raises HTTP 404 rather than returning the published 2025 season, so one unpublished season poisoned a request a full prior season would otherwise have satisfied. Measured against live nflverse for 2026 with the opener two weeks out: schedules and seasonal rosters resolve, published months ahead; snap counts, weekly data and injuries return 404; play-by-play raises NameError. ─── Tolerance ─── loader — a season list resolves one season at a time and drops a season the source has not published, logging which and why. The result carries whichever seasons resolved, so a window spanning a published and an unpublished season renders the published one. Nothing resolving raises, naming the dataset and the seasons: "nflverse has not published snap counts for season 2026". Returning an empty frame instead would hand the engines a frame with no columns, which raises ColumnNotFoundError at the first filter — the defect class start_sit and trade_value carry declared output schemas to prevent. Unavailability arrives in two shapes and both are caught at one narrowed seam. An absent release asset surfaces as urllib.error.HTTPError, an OSError subclass. `import_pbp_data` instead raises NameError: its download handler reads `except Error as e:` and `Error` is bound nowhere in that module, so the handler raises in place of the failure that reached it. TimeoutError is re-raised ahead of both, since it also derives from OSError and a stalled transfer is not a missing season. The seam wraps a single call into nfl_data_py, so a NameError raised by this repository stays a NameError. cache — entries key on one season rather than a season list. The request is already split per season, so the season is the unit of work; making it the unit of storage leaves a partial result structurally unrepresentable rather than defended against. Seasons are shared across windows, and a season published between two loads is picked up by the second because its key is simply absent. `load_weekly_stats` keeps its play-by-play fallback and gains the same tolerance one level deeper: a season neither the weekly asset nor play-by-play covers is skipped rather than fatal. The fallback issues one play-by-play request per derived season, which is what makes an individual season skippable. ─── Season resolution ─── `current_season` promised "the most recent season to have played games" and returned the new label from 1 September with none of its games played. The name carried the same promise: a caller reading "current season" reads "the season we are in, which therefore has data", and that reading is what aimed six tools at unpublished assets. `season_for(day)` names only the operation it performs — which season a date falls in — and takes its date, so the module is pure calendar arithmetic that six screens call with no I/O. `recent_seasons` keeps its name and its call signature. Resolving availability rather than the label was the alternative and buys nothing: the screens derive their filter from the seasons present in loaded data, so a season contributing no rows leaves the filter on its own. It would also make every caller load schedules before knowing what to load, inverting the data layer's one-way layering. The September boundary is confirmed against every published schedule from 1999: each season opens in September of its label year and ends in January or February of the next, the extremes being a 3 September opener and a 13 February final. ─── Season filters ─── start_sit, trade_value — season options came from the schedule frame rather than from the frame being ranked. Schedules publish months early, so both tools offered a season with no statistics and defaulted to it, rendering empty against data one season older that they held. Both derive their options from the frame they render, matching the other four tools. ─── Tests ─── Loader coverage models nflverse's wholesale failure directly: a request naming an unpublished season yields none of the seasons beside it. Both unavailability shapes, the all-unavailable message, the weekly fallback skip, a non-availability failure still propagating, and both halves of the per-season cache contract are pinned. Season tests cover both sides of the boundary and the winter months from an injected date. Co-authored-by: stxkxsbot <275011021+stxkxsbot@users.noreply.github.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Why
The app breaks on 1 September 2026, about a week before the opener.
current_season()rolls to 2026 on 1 September, but nflverse publishes a season's assets only onceits games are played. Verified against the live API with the opener two weeks out:
schedulesseasonal_rosterssnap_counts/weekly_data/injuriespbpNameErrorAnd a mixed request fails wholesale —
import_snap_counts([2025, 2026])raises 404 rather thanreturning the published 2025 season. So one unpublished season poisoned a request that a full prior
season would have satisfied, and five of six tools showed only an error toast for ~10 days.
This is pre-existing, not introduced by the recent refactor — the old hardcoded
[2024, 2025]hadthe same cliff, deferred until someone edited it by hand. The hermetic test suite couldn't catch it
because it never touches the network.
What changed
Tolerance, not prediction. A season list resolves one season at a time and drops what the source
hasn't published, so
[2025, 2026]renders 2025 throughout the gap. Only when nothing resolvesdoes it raise, naming the dataset and season. Returning an empty frame instead would hand the
engines a schema-less frame — the
ColumnNotFoundErrorclass thatstart_sitandtrade_valuecarry declared output schemas to prevent.
Both failure shapes at one narrowed seam. A missing asset is
HTTPError(anOSError).import_pbp_datainstead raisesNameError, because its handler readsexcept Error as e:andErroris bound nowhere in that module — the handler raises in place of the failure that reachedit.
TimeoutErroris re-raised ahead of both, since it also derives fromOSErrorand a stalledtransfer is not a missing season.
Cache keys on one season, not a season list. The request is already split per season, so a
partial result becomes structurally unrepresentable rather than defended against. Seasons are shared
across windows, and a season published between loads is picked up because its key is simply absent.
current_season→season_for(day). The old name promised "the season we are in, whichtherefore has data" — the reading that aimed six tools at unpublished assets. The new one names only
what it does. The September boundary is confirmed against every schedule since 1999.
A bug found while simulating September:
start_sitandtrade_valuederived their seasonfilter from the schedule frame, which publishes months early — so both offered a season with no
statistics and defaulted to it, rendering empty against older data they were holding. Both now
derive options from the frame they render, matching the other four tools.
How tested
Live probe, real network, this repo's own loaders, today:
Plus all six tools driven headlessly with the date resolved to 2026-09-02: none shows an error
toast, every season filter offers only seasons with data, and every table renders.
ruff format --check/ruff checkmypy src(strict, no carve-out)pytest --covuv buildReviewer notes
is what lets a season appear the moment nflverse publishes it. Measured at 14 failed requests
across one pass through all six tools on 2026-09-02.
_team_bye_weekswas checked and cleared — schedules publish complete before any game, and itreturned 32 teams with one bye each against the real 2026 schedule.
history and say so in their empty state rather than rendering a projection they cannot support.
only two have data.
snap_counts_2024_2025.parquet) are never read again andsurvive orphan reaping because their meta records exist. No correctness impact;
cache.invalidate()clears them if you want the disk back.