pablogrois Claude Opus 4.8 commited on
Commit
ebd7ac3
·
1 Parent(s): 7680fde

Post-partido: resolver partidos recientes ausentes del dataset del modelo

Browse files

El resolve usaba el dataset del modelo (cortado en marzo) y no encontraba partidos
posteriores aunque estén en el catálogo (p.ej. Racing vs Almería del 12/04, Racing
local). Ahora:
- web_meta.matches(): para ligas del modelo suma los partidos del catálogo (cubre
hasta la última fecha); esos van sin matchId.
- runner + MatchIndex.find_by_teams(): cuando no hay matchId, se resuelve del
preprocessed en vivo por local/visitante(+fecha).
- Se mantiene el ORDEN (primer equipo local, segundo visitante) que se había roto.

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

src/racing_reports/api/routes_reports.py CHANGED
@@ -63,10 +63,10 @@ def run(report_type: str, payload: RunRequest) -> RunResponse:
63
  # pre/post necesitan los dos equipos (local + visitante); los de equipo permiten 1.
64
  if report_type in ("pre_match", "post_match") and not payload.team_b:
65
  raise HTTPException(status_code=400, detail="Para previo/post-partido elegí local y visitante.")
66
- if report_type == "post_match" and not payload.match_id:
67
  raise HTTPException(
68
  status_code=400,
69
- detail="Para el post-partido elegí un partido jugado (match_id requerido).",
70
  )
71
 
72
  def _venue(v: str | None) -> str:
@@ -101,6 +101,7 @@ def run(report_type: str, payload: RunRequest) -> RunResponse:
101
  away_team=payload.team_b,
102
  report_types=[report_type],
103
  match_id=payload.match_id,
 
104
  settings=settings_blob,
105
  )
106
 
 
63
  # pre/post necesitan los dos equipos (local + visitante); los de equipo permiten 1.
64
  if report_type in ("pre_match", "post_match") and not payload.team_b:
65
  raise HTTPException(status_code=400, detail="Para previo/post-partido elegí local y visitante.")
66
+ if report_type == "post_match" and not payload.match_id and not payload.match_date:
67
  raise HTTPException(
68
  status_code=400,
69
+ detail="Para el post-partido elegí un partido jugado.",
70
  )
71
 
72
  def _venue(v: str | None) -> str:
 
101
  away_team=payload.team_b,
102
  report_types=[report_type],
103
  match_id=payload.match_id,
104
+ match_date=payload.match_date,
105
  settings=settings_blob,
106
  )
107
 
src/racing_reports/api/schemas.py CHANGED
@@ -79,6 +79,7 @@ class RunRequest(BaseModel):
79
  team_a: str = ""
80
  team_b: str = ""
81
  match_id: str = ""
 
82
  filters: FilterPayload = Field(default_factory=FilterPayload)
83
  only: list[str] = Field(default_factory=list) # nombres de figuras/tablas específicas
84
 
 
79
  team_a: str = ""
80
  team_b: str = ""
81
  match_id: str = ""
82
+ match_date: str = ""
83
  filters: FilterPayload = Field(default_factory=FilterPayload)
84
  only: list[str] = Field(default_factory=list) # nombres de figuras/tablas específicas
85
 
src/racing_reports/match_index.py CHANGED
@@ -90,5 +90,21 @@ class MatchIndex:
90
  row = hit.iloc[0]
91
  return MatchInfo(**row.to_dict())
92
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
93
  def preprocessed_path(self, league: str, season: str) -> Path:
94
  return self.datastore.require_preprocessed(league, season)
 
90
  row = hit.iloc[0]
91
  return MatchInfo(**row.to_dict())
92
 
93
+ def find_by_teams(self, league: str, season: str, home: str, away: str,
94
+ date: str | None = None) -> MatchInfo | None:
95
+ """Resuelve el partido por local/visitante (y fecha si se da) desde el
96
+ preprocessed en vivo. Para partidos recientes sin matchId en el dataset."""
97
+ idx = self.build(league, season)
98
+ h, a = str(home).casefold(), str(away).casefold()
99
+ hit = idx[(idx["home_team"].astype(str).str.casefold() == h)
100
+ & (idx["away_team"].astype(str).str.casefold() == a)]
101
+ if date and not hit.empty:
102
+ exact = hit[hit["date"].astype(str) == str(date)[:10]]
103
+ if not exact.empty:
104
+ hit = exact
105
+ if hit.empty:
106
+ return None
107
+ return MatchInfo(**hit.iloc[0].to_dict())
108
+
109
  def preprocessed_path(self, league: str, season: str) -> Path:
110
  return self.datastore.require_preprocessed(league, season)
src/racing_reports/models.py CHANGED
@@ -15,6 +15,7 @@ class ReportRequest:
15
  away_team: str
16
  report_types: list[str]
17
  match_id: str = ""
 
18
  settings: dict[str, dict[str, Any]] = field(default_factory=dict)
19
 
20
 
 
15
  away_team: str
16
  report_types: list[str]
17
  match_id: str = ""
18
+ match_date: str = "" # fecha del partido (para resolver el matchId si no vino)
19
  settings: dict[str, dict[str, Any]] = field(default_factory=dict)
20
 
21
 
src/racing_reports/runner.py CHANGED
@@ -74,10 +74,10 @@ class ReportRunner:
74
  away_name = request.away_team
75
  match_date = None
76
 
77
- if "post_match" in report_types or request.match_id:
78
- # Resolvemos home/away/fecha desde el dataset bundleado (web_meta),
79
- # NO del preprocessed: éste todavía no se descargó (prepare_vendor corre
80
- # después) y además así el local/visitante es el real del partido.
81
  from racing_reports import web_meta
82
 
83
  match = web_meta.find_match(
@@ -86,6 +86,13 @@ class ReportRunner:
86
  home_name = match["home_team"]
87
  away_name = match["away_team"]
88
  match_date = match["date"]
 
 
 
 
 
 
 
89
 
90
  paths = self.datastore.prepare_vendor(
91
  report_types, request.league, request.season
@@ -148,6 +155,19 @@ class ReportRunner:
148
  venue_b=flt.get("venue_b", flt.get("venue", "both")),
149
  )
150
  elif report_type == "post_match":
 
 
 
 
 
 
 
 
 
 
 
 
 
151
  title = (
152
  f"{home_name} vs {away_name}"
153
  + (f" — {match_date}" if match_date else "")
@@ -160,7 +180,7 @@ class ReportRunner:
160
  matches_csv=paths["matches_csv"],
161
  league=request.league,
162
  season=request.season,
163
- match_id=request.match_id,
164
  home_name=home_name,
165
  away_name=away_name,
166
  team_ids=team_ids,
 
74
  away_name = request.away_team
75
  match_date = None
76
 
77
+ resolved_match_id = request.match_id
78
+ if request.match_id:
79
+ # Con matchId: resolvemos home/away/fecha desde el dataset bundleado (web_meta),
80
+ # NO del preprocessed (éste todavía no se descargó; prepare_vendor corre después).
81
  from racing_reports import web_meta
82
 
83
  match = web_meta.find_match(
 
86
  home_name = match["home_team"]
87
  away_name = match["away_team"]
88
  match_date = match["date"]
89
+ elif "post_match" in report_types:
90
+ # Partido reciente sin matchId en el dataset: usamos lo que mandó el front
91
+ # (local/visitante/fecha). El matchId real se resuelve más abajo desde el
92
+ # preprocessed, una vez descargado.
93
+ home_name = request.home_team
94
+ away_name = request.away_team
95
+ match_date = request.match_date or None
96
 
97
  paths = self.datastore.prepare_vendor(
98
  report_types, request.league, request.season
 
155
  venue_b=flt.get("venue_b", flt.get("venue", "both")),
156
  )
157
  elif report_type == "post_match":
158
+ # Si el partido no traía matchId (reciente, no está en el dataset),
159
+ # lo resolvemos del preprocessed ya descargado por local/visitante/fecha.
160
+ if not resolved_match_id:
161
+ info = self.match_index.find_by_teams(
162
+ request.league, request.season, home_name, away_name, match_date
163
+ )
164
+ if info is None:
165
+ raise ValueError(
166
+ f"No se encontró {home_name} vs {away_name}"
167
+ + (f" ({match_date})" if match_date else "")
168
+ + f" en el preprocessed de {request.league} {request.season}."
169
+ )
170
+ resolved_match_id = info.match_id
171
  title = (
172
  f"{home_name} vs {away_name}"
173
  + (f" — {match_date}" if match_date else "")
 
180
  matches_csv=paths["matches_csv"],
181
  league=request.league,
182
  season=request.season,
183
+ match_id=resolved_match_id,
184
  home_name=home_name,
185
  away_name=away_name,
186
  team_ids=team_ids,
src/racing_reports/web/templates/home.html CHANGED
@@ -740,15 +740,10 @@
740
  cands=await this._getJSON(`/api/matches/resolve?${q}`); }
741
  catch(e){ this.postMatchChecking=false; this._pmErr('Error verificando: '+e); return; }
742
  this.postMatchChecking=false;
743
- // Acepta el partido en cualquier orden (no importa a quién elegiste como local):
744
- // usamos la orientación REAL del partido (quién fue local de verdad).
745
- const pair=new Set([this.teamA, this.teamB]);
746
- const m=cands.find(c=>c.status==='played' && c.home_team && c.away_team
747
- && new Set([c.home_team, c.away_team]).size===2
748
- && [c.home_team, c.away_team].every(t=>pair.has(t)));
749
- if (!m){ this._pmErr(`No existe ${this.teamA} vs ${this.teamB} jugado en ${this.league} ${this.season}. (Datos hasta ${this.dataMaxDate||'la última actualización'}.)`); return; }
750
- await this._post('post_match', { league:this.league, season:this.season, team_a:m.home_team, team_b:m.away_team, match_id:m.match_id, filters:{}, only:[] },
751
- null, `Post-partido · ${m.home_team} vs ${m.away_team} (${m.date})`);
752
  },
753
  _pmErr(m){ this.postMatchMsg=m; this.postMatchMsgError=true; },
754
 
 
740
  cands=await this._getJSON(`/api/matches/resolve?${q}`); }
741
  catch(e){ this.postMatchChecking=false; this._pmErr('Error verificando: '+e); return; }
742
  this.postMatchChecking=false;
743
+ const m=cands.find(c=>c.home_team===this.teamA && c.away_team===this.teamB && c.status==='played');
744
+ if (!m){ this._pmErr(`No existe ${this.teamA} (local) vs ${this.teamB} (visitante) jugado en ${this.league} ${this.season}. (Datos hasta ${this.dataMaxDate||'la última actualización'}.)`); return; }
745
+ await this._post('post_match', { league:this.league, season:this.season, team_a:this.teamA, team_b:this.teamB, match_id:m.match_id, match_date:m.date, filters:{}, only:[] },
746
+ null, `Post-partido · ${this.teamA} vs ${this.teamB} (${m.date})`);
 
 
 
 
 
747
  },
748
  _pmErr(m){ this.postMatchMsg=m; this.postMatchMsgError=true; },
749
 
src/racing_reports/web_meta.py CHANGED
@@ -120,16 +120,23 @@ def _dataset_matches(league: str, season: str) -> list[dict]:
120
  def matches(league: str, season: str, played_only: bool = False) -> list[dict]:
121
  """Lista de partidos. Para ligas del modelo, con matchId real (post-partido);
122
  para el resto, del catálogo (sin matchId)."""
 
 
 
 
 
 
 
123
  if league in _model_leagues():
 
 
 
124
  rows = _dataset_matches(league, season)
 
 
 
125
  else:
126
- c = _catalog()
127
- sub = c[(c["league"] == league) & (c["season"] == str(season))].drop_duplicates(["date", "home_team", "away_team"])
128
- today = date.today().isoformat()
129
- rows = [{"match_id": "", "date": str(r["date"]), "home_team": str(r["home_team"]),
130
- "away_team": str(r["away_team"]),
131
- "status": "played" if str(r["date"]) <= today else "future"}
132
- for _, r in sub.sort_values("date", ascending=False).iterrows()]
133
  if played_only:
134
  rows = [m for m in rows if m["status"] == "played"]
135
  return rows
 
120
  def matches(league: str, season: str, played_only: bool = False) -> list[dict]:
121
  """Lista de partidos. Para ligas del modelo, con matchId real (post-partido);
122
  para el resto, del catálogo (sin matchId)."""
123
+ today = date.today().isoformat()
124
+ c = _catalog()
125
+ sub = c[(c["league"] == league) & (c["season"] == str(season))].drop_duplicates(["date", "home_team", "away_team"])
126
+ cat_rows = [{"match_id": "", "date": str(r["date"])[:10], "home_team": str(r["home_team"]),
127
+ "away_team": str(r["away_team"]),
128
+ "status": "played" if str(r["date"])[:10] <= today else "future"}
129
+ for _, r in sub.iterrows()]
130
  if league in _model_leagues():
131
+ # Dataset del modelo: trae matchId, pero puede estar cortado. Se completa con
132
+ # el catálogo (cubre hasta la última fecha) — esos van sin matchId y el runner
133
+ # lo resuelve del preprocessed al generar.
134
  rows = _dataset_matches(league, season)
135
+ have = {(m["date"], m["home_team"], m["away_team"]) for m in rows}
136
+ rows += [m for m in cat_rows if (m["date"], m["home_team"], m["away_team"]) not in have]
137
+ rows.sort(key=lambda m: m["date"], reverse=True)
138
  else:
139
+ rows = sorted(cat_rows, key=lambda m: m["date"], reverse=True)
 
 
 
 
 
 
140
  if played_only:
141
  rows = [m for m in rows if m["status"] == "played"]
142
  return rows