Compare commits

..
2 Commits
Author SHA1 Message Date
ChuckandClaude Opus 5.5 e5bbfa2ae3 chore: prepare the 3.6.1 release (#669)
Bumps src.__version__ to 3.6.1 and records #667 (the favourite check's false
"season has finished") under ## 3.6.1; #667 had no CHANGELOG entry. Plugins
that drop their bundled favourite-check copy floor on 3.6.1.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-29 10:59:20 -04:00
ChuckandClaude Opus 5.5 db49275075 fix(common): favourite check no longer calls a started postseason a finished season (#667)
* fix(common): favourite check no longer calls a started postseason a finished season

The day after a regular season ends, ESPN's default scoreboard still
returns that last regular-season day, while leagues[0].season has moved
to Postseason. All events were in the past, so the check logged "the
season has finished" for MLB on 2026-09-29 while the upcoming manager in
the same process was showing TB's wild-card games.

When every event is past and the league is in a later in-season phase
(regular season or postseason) than all of the returned events, draw no
conclusion. The offseason is excluded, so a genuinely finished season is
still reported as finished.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(common): favourite check reports the next matchday between soccer rounds

Between matchdays ESPN's soccer scoreboard keeps showing the last one, so
every event is in the past and in the league's current phase, which the
postseason rule does not cover; the check said the Premier League season
had finished on 2026-09-29 (last games 20 September, next 10 October).
When the league calendar is a "day" whitelist, its entries are days with
games, so a future one is used as the next fixture. MLB's day calendar is
a blacklist and is not read that way; PLL's whitelist has no future days
and is still reported as finished.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-29 10:52:38 -04:00
4 changed files with 149 additions and 1 deletions
+18
View File
@@ -19,6 +19,24 @@ accepts both, but the store flags the old spelling as deprecated
## Unreleased
## 3.6.1
A fix to `src.common.favorite_team_check` (#667). Plugins that drop their
bundled copy of it should floor on 3.6.1, not 3.6.0.
### Fixes
- The favourite-team check no longer logs "the season has finished" for a
league that is still playing. ESPN's default scoreboard keeps showing the
last slate after it: MLB's regular-season games two days into the
postseason, a soccer league's previous matchday between rounds. When every
event is in the past, the check now looks first at the league's phase (a
regular season or postseason that has moved past the events shown draws no
conclusion) and at a match-day calendar (`calendarType` "day" with
`calendarIsWhitelist`, as soccer, the NHL and the NBA use), whose next date
becomes "nothing on until <date>". An offseason, or a payload without these
fields, is reported as before.
## 3.6.0
New modules a plugin may import via `src.*` (floor on 3.6.0). Both are
+1 -1
View File
@@ -4,5 +4,5 @@ LEDMatrix Display System
Core source package for the LED Matrix Display project.
"""
__version__ = "3.6.0"
__version__ = "3.6.1"
+44
View File
@@ -199,13 +199,25 @@ class FavoriteTeamCheck:
# has no events at all to roll forward to: events that exist but are
# all in the past mean the season is over, and an offseason calendar
# phase must not be dressed up as its next game.
#
# The exception is a calendar of match days. With calendarType "day"
# and calendarIsWhitelist true, every entry is a day that has games,
# so a future entry is a real next fixture. Soccer needs it: between
# matchdays the scoreboard keeps showing the last one, so on
# 2026-09-29 every Premier League event was from 20 September and the
# next games (10 October) were only in the calendar. A day calendar
# that is not a whitelist (MLB's) lists days *without* games.
if any(event_dates):
upcoming = future(event_dates)
if not upcoming and cls._calendar_is_match_days(payload):
upcoming = future(calendar_dates)
else:
upcoming = future(calendar_dates)
if not upcoming:
if not any(event_dates) and not any(calendar_dates):
return None # Nothing published either way; draw no conclusion.
if cls._moved_to_later_phase(payload):
return None # e.g. postseason under way; see the method.
return ("the season has finished and the next one's fixtures are "
"not published yet")
@@ -215,6 +227,38 @@ class FavoriteTeamCheck:
return "the league has nothing on until {}".format(
upcoming[0].strftime('%d %B %Y'))
@staticmethod
def _calendar_is_match_days(payload) -> bool:
"""Whether the league calendar lists the days that have games."""
league = (payload.get('leagues') or [{}])[0] or {}
return (league.get('calendarType') == 'day'
and league.get('calendarIsWhitelist') is True)
@staticmethod
def _moved_to_later_phase(payload) -> bool:
"""
Whether the league is in a later in-season phase than its events.
ESPN does not roll the scoreboard forward into a postseason. The day
after MLB's regular season ended, the default scoreboard still returned
that last regular-season day, while ``leagues[0].season`` already said
Postseason and the wild-card games were two days out. Past events alone
then read as a finished season while the same process's upcoming
manager was showing the favourite's playoff games.
Only regular season (2) and postseason (3) count as "later". The
offseason (4) follows the postseason too, and there past events really
do mean the season is over.
"""
season = ((payload.get('leagues') or [{}])[0] or {}).get('season') or {}
league_type = (season.get('type') or {}).get('type')
if league_type not in (2, 3):
return False
event_types = [(e.get('season') or {}).get('type')
for e in payload.get('events') or []]
known = [t for t in event_types if isinstance(t, int)]
return bool(known) and all(t < league_type for t in known)
@staticmethod
def _parse_date(raw) -> Optional[datetime]:
if not raw or not isinstance(raw, str):
+86
View File
@@ -353,3 +353,89 @@ class ScheduleNoteUsesGameDatesTests(unittest.TestCase):
"events": [{"date": self.iso(base, 1)}],
"leagues": [{"calendar": [{"startDate": self.iso(base, 6)}]}],
}))
class ScheduleNotePostseasonTests(unittest.TestCase):
"""Past regular-season events are not a finished season once the postseason starts.
Captured from ESPN's MLB scoreboard on 2026-09-29, two days after the
regular season ended: the default scoreboard still returned the last
regular-season day (event ``season.type`` 2), while ``leagues[0].season``
had already moved to Postseason (type 3). The check told a Rays fan the
season was over while their upcoming manager listed TB's wild-card games.
"""
note = ScheduleNoteTests.note
@staticmethod
def payload(league_type, event_type, days=-2):
date = (datetime.now(timezone.utc) + timedelta(days=days)).isoformat()
return {
"events": [{"date": date, "season": {"year": 2026, "type": event_type}}],
"leagues": [{"season": {"year": 2026, "type": {"type": league_type}},
"calendar": [date]}],
}
def test_postseason_after_regular_season_events_is_not_finished(self):
self.assertIsNone(self.note(self.payload(league_type=3, event_type=2)))
def test_a_finished_postseason_is_still_reported_as_finished(self):
# AFL the week after its grand final: league and events both postseason.
note = self.note(self.payload(league_type=3, event_type=3, days=-4))
self.assertIn("season has finished", note)
def test_offseason_after_the_postseason_is_still_finished(self):
note = self.note(self.payload(league_type=4, event_type=3, days=-40))
self.assertIn("season has finished", note)
class ScheduleNoteMatchdayTests(unittest.TestCase):
"""Between soccer matchdays the next fixture is only in the calendar.
Captured from ESPN's Premier League scoreboard on 2026-09-29: the
scoreboard still showed the 20 September matchday, the league was in the
same season phase as those events, and the next games (10 October) were
only in ``leagues[0].calendar`` -- a ``"day"`` calendar with
``calendarIsWhitelist`` true, i.e. the days that have games. The check
said the season had finished.
"""
note = ScheduleNoteTests.note
@staticmethod
def payload(event_days, calendar_days, whitelist=True):
from datetime import datetime, timedelta, timezone
now = datetime.now(timezone.utc)
def iso(days):
return (now + timedelta(days=days)).strftime("%Y-%m-%dT%H:%MZ")
return {
"events": [{"date": iso(d), "season": {"type": 14308}}
for d in event_days],
"leagues": [{"season": {"type": {"type": 14308}},
"calendarType": "day",
"calendarIsWhitelist": whitelist,
"calendar": [iso(d) for d in calendar_days]}],
}
def test_gap_between_matchdays_reports_the_next_matchday(self):
from datetime import datetime, timedelta, timezone
note = self.note(self.payload([-9], [-9, 11, 12]))
self.assertIn("nothing on until", note)
self.assertIn((datetime.now(timezone.utc) + timedelta(days=11))
.strftime("%d %B %Y"), note)
def test_next_matchday_within_a_couple_of_days_says_nothing(self):
self.assertIsNone(self.note(self.payload([-2], [-2, 1])))
def test_no_matchdays_left_is_still_finished(self):
# PLL on the same day: a match-day calendar whose last day is past.
note = self.note(self.payload([-9], [-30, -9]))
self.assertIn("season has finished", note)
def test_a_blacklist_day_calendar_is_not_read_as_fixtures(self):
# MLB's day calendar has calendarIsWhitelist false: it lists days
# without games, so a future entry there is not a next fixture.
note = self.note(self.payload([-9], [11], whitelist=False))
self.assertIn("season has finished", note)