mirror of
https://github.com/ChuckBuilds/LEDMatrix.git
synced 2026-10-13 02:26:37 +00:00
feat(common): sports_live_fetch -- the reconciled live fetch and odds (sports family 9)
New hardware-free module src/common/sports_live_fetch.py, copied from
ledmatrix-plugins claude/family9-reconcile once the nine scoreboards made
_fetch_todays_games (seven bodies when measured) and _fetch_odds (three) one
body each. Both live on the plugins' SportsCore, so one mixin,
SportsLiveFetchMixin, carries exactly those two plus the odds worker
_drain_odds_queue; no manager gains a method it lacks today.
Owner decisions (2026-10-12):
- Live scores are always fresh: every poll asks ESPN through
get_espn_scoreboard and leaves the answer under the shared
espn_scoreboard_cache_key for its dates (max_age=1: only a copy another
reader fetched within the last second is reused). The plugins' 30 s
<sport_key>_scoreboard_current cache is gone.
- College basketball is asked by date like every other league (ESPN
answered single dates for both college leagues on 2026-10-12).
- Odds never block update(): one daemon worker per manager answers a queue
and attaches each line in place under _games_lock; one batch in flight per
manager, games queued meanwhile join it, every dict queued for an id gets
the one answer. show_odds, the two odds intervals and a missing odds
manager (debug, not ERROR) are respected.
- An answer whose events is not a list is no games.
Fix: get_espn_scoreboard() no longer caches an answer whose events is not a
list ("events": null), which every reader of the shared key inherited; it is
still returned to the caller.
- test/test_sports_live_fetch.py: the plugins' pinned cases (requests,
dates and keys at 10:00 and 02:00 Eastern, freshness, malformed and failed
answers, odds that never block and land later, the intervals, one batch per
manager, a worker that cannot start, update and display on two threads),
plus the host contract. Mutation-checked (11 mutants, all caught).
- test/test_sports_live_fetch_parity.py: with LEDMATRIX_PLUGINS, compares
each body with every plugin copy. Passes against claude/family9-reconcile;
fails against the pre-reconcile tree.
- test/test_espn_scoreboard_cache.py: the null-events regression.
- mypy ratchet, src/common/README.md, CHANGELOG (Unreleased: New modules,
Fixes), docs/SPORTS_UNIFICATION.md family 9 status and decisions.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
+60
-10
@@ -415,6 +415,45 @@ its id table too); the two differ only for a poll whose blocks carry ids and
|
||||
no abbreviations. The data sources' `fetch_standings` (held with
|
||||
`data_sources.py`) is still four bodies; only its endpoint order changed.
|
||||
|
||||
### Family 9: live fetch and odds (reconciled; promoted, awaiting release)
|
||||
|
||||
ledmatrix-plugins `scripts/test_live_fetch_odds.py` (#649) pinned 44 rows
|
||||
across the nine plugins first, every ESPN request answered offline the way
|
||||
ESPN answered on 2026-10-12 (a single date 200, a range 400): the requests,
|
||||
cache keys and TTLs of `_fetch_todays_games` per league and Eastern time,
|
||||
the live cache over time, empty, null and failed answers, `_fetch_odds` per
|
||||
game kind and odds endpoint on a virtual clock (which thread asks, how long
|
||||
the call waits, when the line lands), the real `update()` of every screen
|
||||
with a 3 s odds endpoint, and the league overrides (MiLB, HockeyTech, ufc's
|
||||
`MMALive`, which stay per-sport). Measuring found seven bodies of
|
||||
`_fetch_todays_games`, not five: the fetch service's shared key had split
|
||||
the old groups. The reconcile (ledmatrix-plugins `claude/family9-reconcile`)
|
||||
made `_fetch_todays_games` and `_fetch_odds` one body each on `SportsCore`,
|
||||
with the odds worker `_drain_odds_queue` beside them, and added a test that
|
||||
runs `update()` and `display()` on two threads against a slow endpoint.
|
||||
Every changed cell is one of the decisions below; the harness renders were
|
||||
byte-identical once the four fixtures that seeded the old live key seed the
|
||||
shared one. `src/common/sports_live_fetch.py` holds the bodies in one mixin,
|
||||
`SportsLiveFetchMixin`; `test/test_sports_live_fetch_parity.py` compares them
|
||||
with the plugin copies.
|
||||
|
||||
Measured `update()` time with a 3 s odds endpoint and four games, before and
|
||||
after: Upcoming 6 s (afl, baseball, football, nrl, soccer, ufc), 12 s
|
||||
(hockey, lacrosse), 0 s (basketball) to 0 s everywhere; Recent 6 s / 12 s to
|
||||
0 s; Live's first pass 8 s / 12 s and next poll 4 s / 6 s to 0 s. The line
|
||||
now lands when ESPN answers (3 s here) instead of at the next `update()`
|
||||
(15 s for a live game, an hour for an upcoming or recent one) in the six.
|
||||
|
||||
Left for later: ufc's MMA Recent manager never asks for odds and its Upcoming
|
||||
manager asks for every fight in the window before trimming to the headliners
|
||||
(both in `mma.py`'s `update()` overrides, family 13); basketball's NCAA
|
||||
Recent/Upcoming fallback still caches the undated scoreboard under
|
||||
`<sport_key>_scoreboard_current` with `limit=1000`, which ESPN truncates (held
|
||||
with the data fetching); `_attach_odds_to_rotated_games` still starts its own
|
||||
thread per rotation and can route through `_fetch_odds` once every plugin
|
||||
inherits this mixin; and ESPN lists only some college basketball games
|
||||
without `groups=50` (7 of 21 on 2026-03-14), dated or not.
|
||||
|
||||
### Why the method changes
|
||||
|
||||
Byte-identical promotion has nearly run dry. Measured on ledmatrix-plugins
|
||||
@@ -503,7 +542,7 @@ release.
|
||||
| 6 | Favourite matching | `_is_favorite_game` (7 across three classes), `_select_games_for_display` (2: nrl), `_select_recent_games_for_display` (3) | Everything that asks "is this a favourite" goes through the 3.5.0 `_favorite_key` seam. Reconciled to one body each and promoted as `sports_favorites`; shipped in 3.8.2 and adopted. See [Family 6](#family-6-favourite-matching-done-core-382-adopted) |
|
||||
| 7 | Other-games rotation | `_by_importance`, `_other_games_window`, `_advance_other_games_if_due` (2 each: football), `_rotate_other_games_on_display` (2: ufc), with `_attach_odds_to_rotated_games` (3; ufc had none) | One outlier each; football carried two fixes the other eight lacked. Reconciled to one body each, on a `_rankings_loaded` seam, and promoted as `sports_rotation`; done: core 3.8.4, adopted (ledmatrix-plugins #643). See [Family 7](#family-7-the-other-games-rotation-done-core-384-adopted) |
|
||||
| 8 | Rankings | `_fetch_team_rankings` (3), `_choose_poll` (3), `_load_division_team_ids`, `_passes_other_filters`, `_best_rank`, `_is_ranked_game` (2 each: football) | Needs 7; the rank badge and the "ranked only" filter read it. Reconciled to one body each and promoted as `sports_rankings`; done: core 3.8.5, adopted (ledmatrix-plugins #648). See [Family 8](#family-8-rankings-done-core-385-adopted) |
|
||||
| 9 | Live fetch and odds | `_fetch_todays_games` (5), `_fetch_odds` (3), `_attach_odds_to_rotated_games` (3) | The prerequisite for one shared ESPN poller across plugins |
|
||||
| 9 | Live fetch and odds | `_fetch_todays_games` (7 when measured), `_fetch_odds` (3); `_attach_odds_to_rotated_games` went with 7 | The prerequisite for one shared ESPN poller across plugins. Reconciled to one body each and promoted as `sports_live_fetch` (next release). See [Family 9](#family-9-live-fetch-and-odds-reconciled-promoted-awaiting-release) |
|
||||
| 10 | View model | `_extract_game_details_common` (9 of 9) | Every renderer reads it; its keys are additive-only, so reconcile to the superset and leave sport extras in `_extract_game_details` |
|
||||
| 11 | Switch scorebug | `_load_fonts` (4), `_load_custom_font_from_element_config` (6), `_get_layout_offset` (5), `_fit_score_font` (2: football), `_load_and_resize_logo` (9), `_draw_dynamic_odds` (9), then `_draw_scorebug_layout` (20: Upcoming 9, Recent 8, Live 2, ufc's Core 1) | Needs 10 and the twin decisions below. Several releases: fonts and offsets, logos, odds, then one mode's layout per release |
|
||||
| 12 | Scroll/Vegas card | `game_renderer.py`: `render_game_card` (6), `_load_and_resize_logo` (8), `_load_custom_font`, `preload_logos`, `__init__` (7 each), `_draw_records_or_rankings` (6), `_load_fonts`, `_draw_dynamic_odds`, `_draw_live_game_status`, `_draw_recent_game_status`, `_get_team_display_text` (5 each), `_draw_text_with_outline` (2: football) | Same decisions as 11; sequence after the scroll-performance work on pre-rendered strips lands |
|
||||
@@ -598,15 +637,26 @@ suspected behaviour that needs a payload or a rig to confirm first.
|
||||
seasons. A block without a readable `endDate` counts as current. (f)
|
||||
baseball's MLB, MiLB and NCAA Baseball managers drop their class-level
|
||||
shared copy of the table (none of those leagues publishes a poll).
|
||||
- **9, live fetch and odds.** (a) afl, basketball, nrl and soccer cache the
|
||||
live scoreboard for 30 s under `<sport_key>_scoreboard_current`; the others
|
||||
do not (the harness fixtures seed that key, so the change shows up there).
|
||||
(b) basketball fetches college games with no `dates` parameter, citing a
|
||||
404 (*verify* now that `espn_dates` handles ranges). (c) odds are fetched
|
||||
three ways: blocking (hockey, lacrosse), a thread waited on for 1.5–2 s
|
||||
(six plugins), or fire-and-forget for upcoming games (basketball). This
|
||||
sets how long `update()` takes and when an odds line appears. (d) nrl
|
||||
guards on a missing odds manager; port it.
|
||||
- **9, live fetch and odds. Decided 2026-10-12, reconciled.** (a) Live
|
||||
scores are always fresh: every poll asks ESPN and leaves the answer under
|
||||
the shared `espn_scoreboard_cache_key` for its dates (`max_age=1`: only a
|
||||
copy another reader fetched within the last second, the managers of one
|
||||
display tick, is reused). afl, basketball, nrl and soccer had held it 30 s
|
||||
under `<sport_key>_scoreboard_current`, so with the 15 s default poll every
|
||||
other poll repeated the last one; lacrosse and ufc now share their copy.
|
||||
(b) College basketball is asked by date like every other league: ESPN
|
||||
answered single dates for both college leagues on 2026-10-12 (the 404 no
|
||||
longer happens), so the lookback now reaches last night's late games there
|
||||
too. `groups=50` is a separate question (left above). (c) Odds never block
|
||||
`update()`: one daemon worker per manager answers a queue and attaches each
|
||||
line in place under `_games_lock`; one batch in flight per manager, games
|
||||
queued meanwhile join it. A card can draw once without odds and gain them;
|
||||
a pre-rendered scroll strip gains them when it is next rebuilt. (d) The
|
||||
missing-odds-manager guard (nrl's, already in `_attach_odds_to_rotated_games`
|
||||
since family 7) is in `_fetch_odds` too, logged at debug, not ERROR. Fix
|
||||
ported: an answer with `"events": null` is no games instead of a
|
||||
`TypeError` out of the fetch (all nine), and `get_espn_scoreboard` no longer
|
||||
caches it under the shared key.
|
||||
- **10, view model.** Per key, whether every sport emits it. Additive only:
|
||||
no key is renamed or removed.
|
||||
- **11 and 12, the scorebug and the card.** The pinned divergences in
|
||||
|
||||
Reference in New Issue
Block a user