Spaces:
Running
Running
Commit ·
ebd7ac3
1
Parent(s): 7680fde
Post-partido: resolver partidos recientes ausentes del dataset del modelo
Browse filesEl 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
|
| 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 |
-
|
| 78 |
-
|
| 79 |
-
#
|
| 80 |
-
#
|
| 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=
|
| 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 |
-
|
| 744 |
-
|
| 745 |
-
|
| 746 |
-
|
| 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 |
-
|
| 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
|