"""render_automatic_eda — EDA completo one-shot: perfil → ctx → PDF + PPTX. Pipeline impuro del grupo de capacidad `eda`. Dada UNA tabla DuckDB (o PostgreSQL), produce el informe AutomaticEDA COMPLETO en sus dos formatos a la vez (PDF móvil A5 + PPTX 16:9) con los 11 capítulos POBLADOS, en una sola llamada. Compone, sin reimplementar su lógica, cuatro funciones del registry: - profile_table : perfila la tabla end-to-end (TableProfile agregado), opcionalmente con modelos baratos y análisis de serie. - build_eda_render_ctx : construye el `ctx` con los DATOS CRUDOS que el TableProfile agregado no incluye (raw_numeric para modelos/geo, timeseries_raw para series, geo_points para el mapa, db_path/table para la agregación push-down). Sin él, esos capítulos degradan. - render_automatic_eda_pdf : renderiza el documento por capítulos a PDF. - render_automatic_eda_pptx : renderiza el mismo documento a PPTX. El TableProfile agregado basta para portada/overview/distribuciones/calidad/ correlación, pero los capítulos `modelos`, `timeseries`, `geospatial` y `agregacion` necesitan datos crudos (los clusters proyectados sobre el PCA, la serie valor-vs-tiempo, los puntos lat/lon, las filas para el groupby/pivot). `build_eda_render_ctx` los muestrea (LIMIT + push-down, sin traer la tabla entera a RAM) y los entrega en `ctx`; este pipeline los pasa como `meta['ctx']` a ambos renderers para que el informe salga completo. Estilo dict-no-throw del grupo `eda`: nunca lanza; captura cualquier error y degrada a `{"status": "error", "error": str}`. """ import os from datetime import datetime, timezone from datascience import ( build_eda_render_ctx, render_automatic_eda_pdf, render_automatic_eda_pptx, ) from pipelines.profile_table import profile_table # Tokens de almacenamiento por backend (para la portada del informe). _STORAGE = {"duckdb": "DuckDB", "postgres": "PostgreSQL"} def render_automatic_eda( db_path: str, table: str, backend: str = "duckdb", sample: int = 5000, run_models: bool = True, run_series: bool = True, run_llm: bool = False, out_dir: str = "reports", basename: str = None, ctx_extra: dict = None, ) -> dict: """Perfila una tabla y emite el informe AutomaticEDA completo (PDF + PPTX). Args: db_path: ruta al archivo DuckDB, o DSN PostgreSQL si backend="postgres". table: nombre de la tabla a perfilar. backend: "duckdb" (default) o "postgres". sample: máximo de filas/valores muestreados por columna para el perfil y para los datos crudos del ctx (LIMIT). Default 5000. run_models: si True (default) corre los modelos baratos (PCA/KMeans/IsolationForest/normalidad). Necesario para que el capítulo `modelos` pinte los clusters sobre el plano PCA. run_series: si True (default) calcula el análisis de serie temporal por columna numérica. Necesario para el análisis del capítulo `timeseries` (la gráfica de evolución sale de los datos crudos del ctx aunque run_series sea False). run_llm: si True (default False) hace la interpretación LLM del perfil y ACTIVA además la narrativa LLM de los capítulos modelos/geospatial/ agregacion (títulos de segmento, descripción de la zona, selección de agregaciones). Con False esos capítulos usan su derivación cuantitativa (siguen completos, sin llamadas de red). out_dir: directorio de salida (se crea si no existe). Default "reports". basename: nombre base de los archivos sin extensión. Default "aeda__". ctx_extra: dict opcional con claves de presentación/contexto extra que se mezclan en el ctx (p.ej. dataset_name, description, source_origin). No pisan las claves de datos calculadas por build_eda_render_ctx. Returns: dict (nunca lanza). En éxito:: {"status": "ok", "pdf_path": str, "pptx_path": str, "manifest_path": str|None, "n_pages": int, "n_slides": int, "pdf_note": str, "pptx_note": str, "profile": } En error: {"status": "error", "error": str}. """ try: # 1) Perfil base + modelos/serie opcionales. No escribe report propio # (write_report=False): este pipeline emite su propio par PDF/PPTX. pres = profile_table( db_path, table, backend=backend, sample=sample, run_models=run_models, run_llm=run_llm, run_series=run_series, emit_pdf=False, write_report=False, ) if pres.get("status") != "ok": return {"status": "error", "error": f"profile_table falló: {pres.get('error')}"} prof = pres.get("profile") or {} # 2) Contexto de presentación + datos crudos para los 4 capítulos que los # necesitan. Las claves de presentación van en base_ctx; build_eda_render_ctx # añade raw_numeric / timeseries_raw / geo_points / db_path / table. base_ctx = { "dataset_name": table, "source_origin": db_path, "storage": _STORAGE.get(backend, backend), } if run_llm: # Activa la narrativa LLM de los capítulos que la soportan. base_ctx.update({ "run_cluster_llm": True, "run_geo_llm": True, "run_agg_llm": True, }) if ctx_extra: base_ctx.update(ctx_extra) ctx = build_eda_render_ctx( db_path, table, prof, backend=backend, sample=sample, base_ctx=base_ctx, ) # 3) Render a ambos formatos desde el MISMO documento por capítulos. os.makedirs(out_dir, exist_ok=True) ts = datetime.now(timezone.utc).strftime("%Y%m%d-%H%M%S") base = basename or f"aeda_{table}_{ts}" pdf_path = os.path.join(out_dir, base + ".pdf") pptx_path = os.path.join(out_dir, base + ".pptx") meta = {"title": f"EDA — {table}", "ctx": ctx} rpdf = render_automatic_eda_pdf(prof, pdf_path, meta) or {} rpptx = render_automatic_eda_pptx(prof, pptx_path, meta) or {} return { "status": "ok", "pdf_path": rpdf.get("path"), "pptx_path": rpptx.get("path"), "manifest_path": rpdf.get("manifest_path"), "n_pages": rpdf.get("n_pages"), "n_slides": rpptx.get("n_slides"), "pdf_note": rpdf.get("note"), "pptx_note": rpptx.get("note"), "profile": prof, } except Exception as e: # noqa: BLE001 — dict-no-throw: degradar, nunca lanzar. return {"status": "error", "error": str(e)}