Syntrex Claude Sonnet 4.6 commited on
Commit
a54e483
·
1 Parent(s): c16be6b

Fix pitcher resolution table: roster cache, two-candidate fallback, debug rebuild

Browse files

RC1: 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 CHANGED
@@ -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
- _ROSTER_MAP_CACHE = {}
497
- return _ROSTER_MAP_CACHE
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
 
visualization/debug_page.py CHANGED
@@ -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
- # If session state is empty, build directly from the live props feed
1480
- if not merged_starters and upcoming_props_debug is not None:
 
 
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: