Spaces:
Sleeping
Fix pitcher resolution table: roster cache, two-candidate fallback, debug rebuild
Browse filesRC1: fetch_mlb_current_roster_map() now uses gameType="R,S" so spring training
roster data is returned pre-opening-day. Empty results (network error or truly
empty API response) no longer permanently cache {} — the guard now returns {}
without setting _ROSTER_MAP_CACHE, so the next call retries.
RC2: Debug page pitcher resolution expander now always rebuilds from the live
props feed (upcoming_props_debug) rather than only when session state is empty.
Previously, visiting Props page first would populate merged_starters with
Stats-API-only data, making `if not merged_starters` False and skipping odds
API pitchers entirely.
RC3: build_oddsapi_starter_fallback_map() adds elif branch for the two-candidate
both-blank scenario: when exactly 2 K-props pitcher candidates exist, team
inference fails for both, and neither away nor home pitcher has been set,
assign alphabetically as a deterministic last-resort heuristic.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- data/mlb_starters.py +13 -3
- visualization/debug_page.py +4 -2
|
@@ -348,6 +348,13 @@ def build_oddsapi_starter_fallback_map(
|
|
| 348 |
elif not home_pitcher and away_pitcher:
|
| 349 |
home_pitcher = unresolved_candidates[0]
|
| 350 |
assigned_from_odds += 1
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 351 |
|
| 352 |
odds_source = "unresolved"
|
| 353 |
if assigned_from_odds >= 2 or (blank_sides >= 2 and away_pitcher and home_pitcher):
|
|
@@ -486,15 +493,15 @@ def fetch_mlb_current_roster_map(season: int = 2026) -> dict[str, str]:
|
|
| 486 |
return _ROSTER_MAP_CACHE
|
| 487 |
|
| 488 |
url = "https://statsapi.mlb.com/api/v1/sports/1/players"
|
| 489 |
-
params: dict[str, Any] = {"season": season, "gameType": "R"}
|
| 490 |
try:
|
| 491 |
r = requests.get(url, params=params, timeout=15)
|
| 492 |
r.raise_for_status()
|
| 493 |
data = r.json()
|
| 494 |
except Exception as exc:
|
| 495 |
_log.warning("[mlb_roster] fetch failed: %s", exc)
|
| 496 |
-
|
| 497 |
-
return
|
| 498 |
|
| 499 |
people = data.get("people", []) if isinstance(data, dict) else []
|
| 500 |
roster: dict[str, str] = {}
|
|
@@ -509,6 +516,9 @@ def fetch_mlb_current_roster_map(season: int = 2026) -> dict[str, str]:
|
|
| 509 |
roster[norm_name] = canon_team
|
| 510 |
|
| 511 |
_log.warning("[mlb_roster] loaded %d players for season %d", len(roster), season)
|
|
|
|
|
|
|
|
|
|
| 512 |
_ROSTER_MAP_CACHE = roster
|
| 513 |
return _ROSTER_MAP_CACHE
|
| 514 |
|
|
|
|
| 348 |
elif not home_pitcher and away_pitcher:
|
| 349 |
home_pitcher = unresolved_candidates[0]
|
| 350 |
assigned_from_odds += 1
|
| 351 |
+
elif len(unresolved_candidates) == 2 and not away_pitcher and not home_pitcher:
|
| 352 |
+
# Last resort: 2 candidates, both sides blank, team inference failed for both.
|
| 353 |
+
# Assign alphabetically — arbitrary but deterministic.
|
| 354 |
+
sorted_candidates = sorted(unresolved_candidates)
|
| 355 |
+
away_pitcher = sorted_candidates[0]
|
| 356 |
+
home_pitcher = sorted_candidates[1]
|
| 357 |
+
assigned_from_odds += 2
|
| 358 |
|
| 359 |
odds_source = "unresolved"
|
| 360 |
if assigned_from_odds >= 2 or (blank_sides >= 2 and away_pitcher and home_pitcher):
|
|
|
|
| 493 |
return _ROSTER_MAP_CACHE
|
| 494 |
|
| 495 |
url = "https://statsapi.mlb.com/api/v1/sports/1/players"
|
| 496 |
+
params: dict[str, Any] = {"season": season, "gameType": "R,S"}
|
| 497 |
try:
|
| 498 |
r = requests.get(url, params=params, timeout=15)
|
| 499 |
r.raise_for_status()
|
| 500 |
data = r.json()
|
| 501 |
except Exception as exc:
|
| 502 |
_log.warning("[mlb_roster] fetch failed: %s", exc)
|
| 503 |
+
# Don't cache on network error — allow retry on next call
|
| 504 |
+
return {}
|
| 505 |
|
| 506 |
people = data.get("people", []) if isinstance(data, dict) else []
|
| 507 |
roster: dict[str, str] = {}
|
|
|
|
| 516 |
roster[norm_name] = canon_team
|
| 517 |
|
| 518 |
_log.warning("[mlb_roster] loaded %d players for season %d", len(roster), season)
|
| 519 |
+
if not roster:
|
| 520 |
+
# Empty result — don't permanently cache; allow retry on next call
|
| 521 |
+
return {}
|
| 522 |
_ROSTER_MAP_CACHE = roster
|
| 523 |
return _ROSTER_MAP_CACHE
|
| 524 |
|
|
@@ -1476,8 +1476,10 @@ def render_debug(
|
|
| 1476 |
starter_bundle = props_prepared_bundle.get("starter_bundle") or {}
|
| 1477 |
merged_starters = starter_bundle.get("merged_starters") or {}
|
| 1478 |
|
| 1479 |
-
#
|
| 1480 |
-
if
|
|
|
|
|
|
|
| 1481 |
_props_feed = upcoming_props_debug.get("merged_props_feed", pd.DataFrame())
|
| 1482 |
if isinstance(_props_feed, pd.DataFrame) and not _props_feed.empty:
|
| 1483 |
try:
|
|
|
|
| 1476 |
starter_bundle = props_prepared_bundle.get("starter_bundle") or {}
|
| 1477 |
merged_starters = starter_bundle.get("merged_starters") or {}
|
| 1478 |
|
| 1479 |
+
# Always rebuild from the live props feed when available so odds API
|
| 1480 |
+
# pitchers are shown even if session state was populated by a prior
|
| 1481 |
+
# Props page visit (which only contains Stats API starters).
|
| 1482 |
+
if upcoming_props_debug is not None:
|
| 1483 |
_props_feed = upcoming_props_debug.get("merged_props_feed", pd.DataFrame())
|
| 1484 |
if isinstance(_props_feed, pd.DataFrame) and not _props_feed.empty:
|
| 1485 |
try:
|