Spaces:
Running
Running
Download scripts/gen_referencia.py from pablogrois/RRC: direct link, hf CLI and curl.
- Browser
- Download file 13.5 kB
-
https://huggingface.co/spaces/pablogrois/RRC/resolve/main/scripts/gen_referencia.py
- Command line
-
hf download hf://spaces/pablogrois/RRC/scripts/gen_referencia.py
-
curl -L -o gen_referencia.py https://huggingface.co/spaces/pablogrois/RRC/resolve/main/scripts/gen_referencia.py
13.5 kB
| #!/usr/bin/env python3 | |
| """Arma docs/diccionarios/referencia.html: navegador de la documentación, organizado | |
| como las apps (elegís app → sección → categoría → variable). | |
| NO tiene contenido propio: todo sale de las mismas fuentes que los .md/.csv | |
| (por eso no se puede desincronizar, que fue lo que le pasó al referencia.html viejo). | |
| USO: python scripts/gen_referencia.py | |
| """ | |
| from __future__ import annotations | |
| import csv, json, re | |
| from pathlib import Path | |
| ROOT = Path(__file__).resolve().parents[1] | |
| RACING = ROOT.parent | |
| DIC = ROOT / "docs" / "diccionarios" | |
| var_dict = json.loads((ROOT / "vendor/data/var_dict.json").read_text(encoding="utf-8")) | |
| groups = json.loads((ROOT / "scripts/feature_groups.json").read_text(encoding="utf-8")) | |
| cats_rep = json.loads((DIC / "_reportes_categorias.json").read_text(encoding="utf-8")) | |
| ejes_def = json.loads((RACING / "data/analysis/next_goal/ejes_definition.json").read_text(encoding="utf-8")) | |
| lb = json.loads((ROOT / "webapp-static/site/lowblock.json").read_text(encoding="utf-8"))["options"] | |
| # ── variables (de variables.csv, la salida del otro generador) ──────────────── | |
| VARS: dict[str, dict] = {} | |
| for r in csv.DictReader((DIC / "variables.csv").open(encoding="utf-8")): | |
| key = r["variable"] if r["variable"] not in VARS else f'{r["variable"]}@{r["dataset"]}' | |
| VARS[key] = {"n": r["variable"], "l": r["nombre_de_uso"], "ds": r["dataset"], | |
| "o": r["origen"], "g": r["generado_por"], "e": r["entradas"], | |
| "c": r["como_se_calcula"], "t": r["tipo"], "sc": r.get("subcategoria", ""), | |
| "gr": r["grupos"], "nt": r["nota"]} | |
| # ── resumen "De qué se trata" de cada .md ───────────────────────────────────── | |
| def resumen(rel: str) -> str: | |
| p = DIC / f"{rel}.md" | |
| if not p.exists(): | |
| return "" | |
| m = re.search(r"## De qué se trata\s*\n(.+?)(?=\n#{2,3} |\n\| )", p.read_text(encoding="utf-8"), re.S) | |
| if not m: | |
| return "" | |
| txt = m.group(1).strip() | |
| # re.S: en los .md las negritas/código suelen cruzar el salto de línea | |
| txt = re.sub(r"\*\*(.+?)\*\*", r"<b>\1</b>", txt, flags=re.S) | |
| txt = re.sub(r"`(.+?)`", r"<code>\1</code>", txt, flags=re.S) | |
| txt = re.sub(r"\[(.+?)\]\([^)]+\)", r"\1", txt, flags=re.S) | |
| txt = re.sub(r"(?<![*\w])\*([^*\n]+?)\*(?![*\w])", r"<em>\1</em>", txt) # itálicas | |
| out = [] | |
| for b in txt.split("\n\n"): | |
| b = b.strip() | |
| if not b: | |
| continue | |
| lines = [l.strip() for l in b.split("\n")] | |
| if all(re.match(r"^(\d+\.|[-•])\s+", l) for l in lines): # lista | |
| tag = "ol" if re.match(r"^\d+\.", lines[0]) else "ul" | |
| items = "".join(f"<li>{re.sub(r'^(\d+\.|[-•])\s+', '', l)}</li>" for l in lines) | |
| out.append(f"<{tag}>{items}</{tag}>") | |
| else: | |
| out.append(f"<p>{' '.join(lines)}</p>") | |
| return "".join(out) | |
| # ── markdown → HTML (lo justo para nuestras fichas) ────────────────────────── | |
| def _inline(t: str) -> str: | |
| t = (t.replace("&", "&").replace("<", "<").replace(">", ">")) | |
| t = re.sub(r"`(.+?)`", r"<code>\1</code>", t) | |
| t = re.sub(r"\*\*(.+?)\*\*", r"<b>\1</b>", t) | |
| t = re.sub(r"(?<![*\w])\*([^*\n]+?)\*(?![*\w])", r"<em>\1</em>", t) | |
| t = re.sub(r"\[(.+?)\]\([^)]+\)", r"\1", t) # links: se deja el texto | |
| return t | |
| def md_to_html(rel: str) -> str: | |
| """Convierte la ficha .md entera a HTML (headings, tablas, listas, citas).""" | |
| path = DIC / f"{rel}.md" | |
| if not path.exists(): | |
| return "" | |
| txt = path.read_text(encoding="utf-8") | |
| txt = re.sub(r"\n---\n\n\*\*Versión CSV\*\*.*\Z", "", txt, flags=re.S) # el pie del CSV sobra acá | |
| txt = re.sub(r"^# .+?\n", "", txt, count=1).lstrip("\n") # el h1 ya es el título | |
| # el "De qué se trata" ya se muestra arriba como resumen: no repetirlo en la ficha | |
| txt = re.sub(r"^## De qué se trata\s*\n.*?(?=\n## )", "", txt, count=1, flags=re.S).lstrip("\n") | |
| out, i, lines = [], 0, txt.split("\n") | |
| while i < len(lines): | |
| ln = lines[i] | |
| if not ln.strip() or ln.strip() == "---": | |
| i += 1; continue | |
| if ln.startswith("### "): | |
| out.append(f"<h4>{_inline(ln[4:].strip())}</h4>"); i += 1; continue | |
| if ln.startswith("## "): | |
| out.append(f"<h3>{_inline(ln[3:].strip())}</h3>"); i += 1; continue | |
| if ln.lstrip().startswith("|"): # tabla | |
| rows = [] | |
| while i < len(lines) and lines[i].lstrip().startswith("|"): | |
| rows.append([c.strip() for c in lines[i].strip().strip("|").split("|")]); i += 1 | |
| if len(rows) >= 2 and set("".join(rows[1]).replace(" ", "")) <= set("-:"): | |
| head, body = rows[0], rows[2:] | |
| else: | |
| head, body = None, rows | |
| t = ['<div class="tw"><table class="doc">'] | |
| if head and any(h for h in head): | |
| t.append("<thead><tr>" + "".join(f"<th>{_inline(c)}</th>" for c in head) + "</tr></thead>") | |
| t.append("<tbody>" + "".join("<tr>" + "".join(f"<td>{_inline(c)}</td>" for c in r) + "</tr>" | |
| for r in body) + "</tbody></table></div>") | |
| out.append("".join(t)); continue | |
| if ln.startswith("> "): # cita | |
| buf = [] | |
| while i < len(lines) and lines[i].startswith(">"): | |
| buf.append(lines[i].lstrip("> ").rstrip()); i += 1 | |
| out.append(f'<blockquote>{_inline(" ".join(buf))}</blockquote>'); continue | |
| m = re.match(r"^(\d+\.|[-*])\s+", ln) # lista | |
| if m: | |
| tag = "ol" if ln[0].isdigit() else "ul" | |
| items = [] | |
| while i < len(lines) and re.match(r"^(\d+\.|[-*])\s+", lines[i]): | |
| cur = re.sub(r"^(\d+\.|[-*])\s+", "", lines[i]); i += 1 | |
| while i < len(lines) and lines[i].startswith(" ") and lines[i].strip() \ | |
| and not re.match(r"^\s*(\d+\.|[-*])\s+", lines[i]): | |
| cur += " " + lines[i].strip(); i += 1 | |
| items.append(f"<li>{_inline(cur)}</li>") | |
| out.append(f"<{tag}>{''.join(items)}</{tag}>"); continue | |
| buf = [] # párrafo | |
| while i < len(lines) and lines[i].strip() and not re.match( | |
| r"^(#{2,3} |\||> |\d+\.\s|[-*]\s|---)", lines[i]): | |
| buf.append(lines[i].strip()); i += 1 | |
| if buf: | |
| out.append(f"<p>{_inline(' '.join(buf))}</p>") | |
| return "".join(out) | |
| def cat_grupos() -> list: | |
| porg: dict[str, list[str]] = {} | |
| for col, gs in groups.items(): | |
| for g in gs: | |
| porg.setdefault(g, []).append(col) | |
| return [{"n": g, "v": sorted(porg[g], key=lambda c: (var_dict.get(c, {}).get("label", c) or c).lower())} | |
| for g in sorted(porg)] | |
| def cat_tipos() -> list: | |
| port: dict[str, list[str]] = {} | |
| for c, i in var_dict.items(): | |
| port.setdefault(i.get("tipo", "Otras"), []).append(c) | |
| return [{"n": t, "v": sorted(port[t], key=lambda c: (var_dict.get(c, {}).get("label", c) or c).lower())} | |
| for t in sorted(port)] | |
| EJE_LBL = {"verticalidad": "Verticalidad", "elaboracion": "Elaboración", "individual": "Juego individual", | |
| "rotura_profundidad": "Rotura de líneas / profundidad", "amplitud_centro": "Amplitud / centros", | |
| "robo_alto": "Robo alto", "aereo_segunda": "Aéreo / 2ª jugada"} | |
| cat_ejes = [{"n": EJE_LBL.get(e, e), "v": list(cs)} for e, cs in ejes_def.items()] | |
| cat_recursos = [ | |
| {"n": "Lado", "v": [], "txt": ["Ofensivo — el equipo usa el recurso", "Defensivo — se lo hacen al equipo"]}, | |
| {"n": "Bloque", "v": [], "txt": list(lb.get("bloques", {}).values())}, | |
| {"n": "Recursos", "v": [], "txt": list(lb.get("recursos", {}).values())}, | |
| {"n": "Métricas", "v": ["pv_jugada", "pv_jugada_exitosa", "evento_clave", "tiros_jugada", "xg_acum", | |
| "xg_jugada", "xg_jugada_exitosa", "goles", "pct_goles", "pct_exito", | |
| "n_intentos", "intentos_90"]}, | |
| ] | |
| def cats_de(rel: str) -> list: | |
| """Categorías de un reporte/modelo: si el ítem es una variable conocida, se enlaza.""" | |
| out = [] | |
| for cat, items in cats_rep.get(rel, {}).items(): | |
| v = [i for i in items if i in VARS] | |
| txt = [i for i in items if i not in VARS] | |
| out.append({"n": cat, "v": v, "txt": txt}) | |
| return out | |
| EV = [ | |
| ("top-equipos", "Top equipos", "equipos-y-variables/top-equipos", cat_grupos(), "Los grupos del botón «Agregar grupo»"), | |
| ("perfil-y-similares", "Perfil y similares", "equipos-y-variables/perfil-y-similares", cat_grupos(), "Los grupos del botón «Agregar grupo»"), | |
| ("ejes", "Ejes ataque/defensa", "equipos-y-variables/ejes-ataque-defensa", cat_ejes, "Los 7 ejes y las métricas que promedian"), | |
| ("recursos", "Recursos", "equipos-y-variables/recursos", cat_recursos, "Las opciones de cada desplegable"), | |
| ("scatter-libre", "Scatter libre", "equipos-y-variables/scatter-libre", cat_tipos(), "Las 262 variables, por tipo"), | |
| ] | |
| REPORTES = [("pre_match", "Pre-partido (head-to-head)"), ("block_zscore", "Bloques (z-score)"), | |
| ("triple_multitag", "Multi-Tag"), ("triple_centros", "Centros"), | |
| ("recuperaciones", "Recuperaciones"), ("post_match", "Post-partido"), | |
| ("ejes_pre", "Ejes — previo"), ("ejes_post", "Ejes — post")] | |
| MODELOS = [("attack_matchup_gnn", "Zonas de ataque (GNN)"), ("pv_distribution_gnn", "Peligro por zona (GNN)"), | |
| ("cruce", "Cruce: bloque y pasillos"), ("ptiro", "P(tiro) por jugada")] | |
| def sec_ev(): | |
| return [{"id": i, "n": n, "res": resumen(rel), "doc": md_to_html(rel), "cats": c, "hint": h} | |
| for i, n, rel, c, h in EV] | |
| APPS = [ | |
| {"id": "vercel", "n": "Vercel", "sub": "Gráficos de equipos", | |
| "url": "graficos-equipos-racing.vercel.app", | |
| "d": "La app estática. Solo consulta de equipos y variables: no genera reportes de partido.", | |
| "grupos": [{"n": "Equipos y variables", "secs": sec_ev()}]}, | |
| {"id": "hf", "n": "Hugging Face", "sub": "Space RRC", | |
| "url": "pablogrois/RRC", | |
| "d": "La app completa. Tiene lo mismo que Vercel más los reportes por partido y los modelos.", | |
| "grupos": [ | |
| {"n": "Equipos y variables", "secs": sec_ev()}, | |
| {"n": "Reportes de partido", "secs": [ | |
| {"id": f"r_{i}", "n": n, "res": resumen(f"reportes-de-partido/{i}"), | |
| "doc": md_to_html(f"reportes-de-partido/{i}"), | |
| "cats": cats_de(f"reportes-de-partido/{i}"), "hint": "Qué usa cada parte del reporte"} | |
| for i, n in REPORTES]}, | |
| {"n": "Modelos", "secs": [ | |
| {"id": f"m_{i}", "n": n, "res": resumen(f"modelos/{i}"), | |
| "doc": md_to_html(f"modelos/{i}"), | |
| "cats": cats_de(f"modelos/{i}"), "hint": "Datos, target y features"} | |
| for i, n in MODELOS]}, | |
| ]}, | |
| ] | |
| # ── índice inverso: variable → dónde aparece (app · sección · categoría) ───── | |
| USOS: dict[str, list] = {} | |
| for a in APPS: | |
| for g in a["grupos"]: | |
| for s in g["secs"]: | |
| for ci, c in enumerate(s["cats"]): | |
| for n in c.get("v", []): | |
| USOS.setdefault(n, []) | |
| ref = [a["id"], s["id"], ci, f'{s["n"]} · {c["n"]}', a["n"]] | |
| if ref not in USOS[n]: | |
| USOS[n].append(ref) | |
| # nº de variables distintas por sección (para las tarjetas) | |
| for a in APPS: | |
| for g in a["grupos"]: | |
| for s in g["secs"]: | |
| s["nv"] = len({n for c in s["cats"] for n in c.get("v", [])}) | |
| from datetime import date | |
| DB = {"apps": APPS, "vars": VARS, "usos": USOS, | |
| "meta": {"n": len(VARS), "dir": sum(1 for v in VARS.values() if v["o"] == "directa"), | |
| "fecha": date.today().isoformat()}} | |
| (DIC / "_referencia_db.json").write_text(json.dumps(DB, ensure_ascii=False), encoding="utf-8") | |
| # ── ensamblar el HTML (plantilla + datos embebidos) ────────────────────────── | |
| HERE = Path(__file__).resolve().parent | |
| head = (HERE / "_referencia_head.html").read_text(encoding="utf-8") | |
| body = (HERE / "_referencia_body.html").read_text(encoding="utf-8") | |
| n, d = len(VARS), DB["meta"]["dir"] | |
| body = (body.replace("__DB__", json.dumps(DB, ensure_ascii=False)) | |
| .replace("__N__", str(n)).replace("__D__", str(d)).replace("__C__", str(n - d)) | |
| .replace("__F__", DB["meta"]["fecha"])) | |
| html = head + "\n" + body | |
| out = DIC / "referencia.html" | |
| out.write_text(html, encoding="utf-8") | |
| # copias servidas por las dos apps (mismo candado en cada una, sin proyectos nuevos): | |
| # · Vercel: el sitio estático la publica tal cual | |
| # · Space HF: va dentro de src/ porque es lo único que el Dockerfile copia | |
| copias = [ROOT / "webapp-static" / "site" / "referencia.html", | |
| ROOT / "src" / "racing_reports" / "web" / "templates" / "referencia.html"] | |
| for c in copias: | |
| c.write_text(html, encoding="utf-8") | |
| print(f"OK referencia.html — {len(html)/1024:.0f} KB · {len(VARS)} variables · {len(APPS)} apps") | |
| for c in copias: | |
| print(f" copiada a {c.relative_to(ROOT)}") | |