Files
LEDMatrix/src/common/sports_card_wrappers.py
T
ChuckandClaude Opus 5.5 1e4c890d59 feat(common): sports_celebration, sports_fetch and sports_card_wrappers, promoted from the scoreboards (sports consolidation stage 3) (#672)
Three new hardware-free modules holding code the scoreboard plugins carry
as identical copies (executable AST, docstrings stripped, checked across
every carrying plugin at ledmatrix-plugins 30455671). The bodies are the
plugins'; the changes are type annotations for the mypy ratchet, the
colour helpers losing their leading underscore as public free functions,
and two comments that described the plugins' files.

- src/common/sports_celebration.py: SportsCelebrationMixin, the score/win
  takeover drawn by afl, football, hockey, nrl and soccer
  (_draw_celebration_layout and the palette, backdrop, scenery, confetti,
  crest and _fit_font steps, with their class constants), plus the colour
  helpers (logo_palette, lift_color, cap_luminance, mix_color, ...). Only
  the drawing: _start_celebration, _check_for_goal/_check_for_score,
  _check_for_win and display() differ between the plugins and stay there.
- src/common/sports_fetch.py: SportsFetchMixin, the four SportsCore methods
  identical in all nine scoreboards: _fetch_season_directly,
  _background_fetches_espn_ranges, _needs_previous_day and
  _wants_live_odds, with _LOOKBACK_CUTOFF_HOUR and _LIVE_ODDS_LOOKAHEAD.
  _get_timezone, _extract_game_details and _fetch_data are as identical
  and stay behind, for the reasons sports_shared gives (a per-plugin
  import; the abstract contract); so does SportsUpcoming.__init__, since
  no src/common mixin has a constructor.
- src/common/sports_card_wrappers.py: SportsCardWrappersMixin, the
  seventeen sports_card delegations the eight game renderers carry (15 in
  all eight, 2 in all but football, whose own versions override them).
  _schema_font_size/_resolve_font_size look identical but read each
  plugin's own _SCHEMA_PATH, so they stay.

Each mixin has no __init__ and creates no attributes (the host contract is
declared as annotations only), defines no name the mixins beside it
define, and documents the attributes it reads; a host-contract test
parses each and fails on an undocumented read. A method kept on a
plugin's class wins over the mixin's.

Tests: behaviour ported from the plugins' celebration, odds, lookback and
date-range tests against stub hosts carrying exactly the contract, with
crests drawn by the test (test_sports_celebration.py, test_sports_fetch.py,
test_sports_card_wrappers.py), and test_sports_stage3_parity.py, which with
LEDMATRIX_PLUGINS set compares every body with every plugin copy that is
left (58 pass against the plugins today; a copy that is gone counts as
adopted). All three modules are on the mypy ratchet, in
src/common/README.md, the CHANGELOG's Unreleased section and
SPORTS_UNIFICATION's module table. Nothing in core uses them yet.

Full suite: the same 67 failing test ids as main (Windows-only), 77 more
passing.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-29 12:38:40 -04:00

137 lines
5.8 KiB
Python

"""The ``sports_card`` delegations every scoreboard's game renderer carries.
After the card helpers moved to ``sports_card`` (3.3.0), each of the eight
scoreboards with a ``game_renderer.py`` -- afl, baseball, basketball,
football, hockey, lacrosse, nrl and soccer -- kept one-line methods that
forward to them with its own ``config`` and ``logger``. Seventeen are
identical in all eight (executable AST, docstrings stripped) or in all but
football, and were copied here from ledmatrix-plugins ``30455671``
(origin/main, 2026-09-29) under their existing names. Football's own
``_format_game_date`` and ``_upcoming_center_mode`` (they follow the
switch-mode settings when it draws the full-screen scorebug) stay in football
and override these.
``_schema_font_size`` and ``_resolve_font_size`` look the same in every copy
but are not moved: they read ``_SCHEMA_PATH``, a module global that is each
plugin's own ``config_schema.json``.
These are the methods ``SportsGameRendererMixin`` (``sports_game_renderer``)
lists among what its host must provide, so a renderer that inherits both no
longer has to write them. Like that mixin this has no ``__init__`` and no
state. It is a separate module rather than more methods there for the reason
``sports_helpers`` gives: a missing module fails at load, where the version
checks see it; a missing method fails mid-render.
WHAT A HOST MUST PROVIDE
------------------------
Derived by walking every ``self.<attr>`` the mixin reads; the host-contract
test in ``test/test_sports_card_wrappers.py`` fails if a read is added
without being listed here.
- ``config`` and ``logger``.
- ``fonts``, read with ``getattr`` -- ``_font_color``.
- ``_FONT_NAME_ALIASES`` and ``_FONT_PIXEL_GRID`` class attributes --
``_crisp_size``, which passes them to ``sports_card.crisp_size`` so a
renderer that declares extra faces keeps them.
Add it as a base of the plugin's renderer, e.g.
``class GameRenderer(SportsCardWrappersMixin, SportsGameRendererMixin)``.
The two define no name in common; a method on the plugin's own class still
wins over either.
"""
import logging
from typing import Any, ClassVar, Dict, Optional, Tuple
from src.common import sports_card as _card
class SportsCardWrappersMixin:
"""The game renderer's ``sports_card`` delegations. See module docstring."""
# The host contract, declared for type checking only: these create no
# attributes, so the host's own values are what the methods read.
config: Dict[str, Any]
logger: logging.Logger
_FONT_NAME_ALIASES: ClassVar[Dict[str, str]]
_FONT_PIXEL_GRID: ClassVar[Dict[str, Any]]
# ---- fonts ---------------------------------------------------------
@classmethod
def _crisp_size(cls, font_file, desired):
"""``sports_card.crisp_size`` with this renderer's font tables."""
return _card.crisp_size(font_file, desired,
cls._FONT_NAME_ALIASES, cls._FONT_PIXEL_GRID)
def _unshare_element_fonts(self, fonts):
"""``sports_card.unshare_element_fonts``."""
return _card.unshare_element_fonts(self.logger, fonts)
def _font_color(self, font, default: Tuple[int, int, int] = (255, 255, 255)):
"""``sports_card.font_color`` for one of ``self.fonts``."""
return _card.font_color(self.config, getattr(self, "fonts", None), font, default)
# ---- colours and favourites ---------------------------------------
@staticmethod
def _coerce_rgb(value, fallback):
"""``sports_card.coerce_rgb``."""
return _card.coerce_rgb(value, fallback)
@staticmethod
def _side_is_favorite(game: Dict[str, Any], side: str, favorites: set) -> bool:
"""``sports_card.side_is_favorite``."""
return _card.side_is_favorite(game, side, favorites)
@staticmethod
def _side_score(game: Dict[str, Any], side: str) -> Optional[int]:
"""``sports_card.side_score``."""
return _card.side_score(game, side)
def _favorite_result(self, game: Dict[str, Any]) -> Optional[str]:
"""``sports_card.favorite_result``."""
return _card.favorite_result(self.config, game)
def _score_color_for(self, game: Dict[str, Any], game_type: str, default=None):
"""``sports_card.score_color_for``."""
return _card.score_color_for(self.config, self.logger, game, game_type, default)
def _recent_score_color(self, game: Dict[str, Any], default):
"""``sports_card.recent_score_color``."""
return _card.recent_score_color(self.config, self.logger, game, default)
def _element_color(self, element: str, default: Tuple[int, int, int] = (255, 255, 255)):
"""``sports_card.element_color``."""
return _card.element_color(self.config, element, default)
# ---- card options, dates and times --------------------------------
def _scroll_card_option(self, key: str, default: Any = None) -> Any:
"""``sports_card.scroll_card_option``."""
return _card.scroll_card_option(self.config, key, default)
def _upcoming_center_mode(self) -> str:
"""``sports_card.upcoming_center_mode``."""
return _card.upcoming_center_mode(self.config)
def _vs_text(self) -> str:
"""``sports_card.vs_text``."""
return _card.vs_text(self.config)
def _format_game_date(self, date_text: str, game: Optional[Dict] = None) -> str:
"""``sports_card.format_game_date``."""
return _card.format_game_date(self.config, self.logger, date_text, game)
def _weekday_for(self, game: Optional[Dict]) -> str:
"""``sports_card.weekday_for``."""
return _card.weekday_for(self.config, self.logger, game)
def _card_tzinfo(self):
"""``sports_card.card_tzinfo``."""
return _card.card_tzinfo(self.config, self.logger)
def _format_game_time(self, time_text: str) -> str:
"""``sports_card.format_game_time``."""
return _card.format_game_time(self.config, time_text)