# -*- coding: utf-8 -*- """Pipeline: filas limpias → consolidado Población apta.""" from __future__ import annotations from typing import Any import pandas as pd from domain.verificador_st.core import ( COLUMNAS_FINAL_IT, CSV_FINAL_IT_HEADERS, CSV_FINAL_IT_RENAME, aplicar_origen_servicio, construir_base_especial, construir_base_normal, es_archivo_especial, normalizar_registros, preparar_consolidado, preparar_resumen, ) def _records_to_dataframe(records: list[dict[str, Any]]) -> pd.DataFrame: if not records: return pd.DataFrame() return pd.DataFrame(records).fillna("") def process_cleaning_sources( sources: list[dict[str, Any]], mapa_servicios: dict[str, str] | None = None, ) -> tuple[pd.DataFrame, dict[str, Any], list[str]]: """ Procesa varias fuentes de limpieza. Cada source: { "cleaning_job_id": str, "label": str, # archivo_origen "rows": list[dict], } mapa_servicios: opcional, radicado -> tipo_servicio (Origen Servicio). """ log: list[str] = [] ok_parts: list[pd.DataFrame] = [] err_parts: list[pd.DataFrame] = [] resumen_archivos: list[dict[str, Any]] = [] mapa = mapa_servicios or {} if mapa: # len(mapa) cuenta variantes de clave; estimar radicados únicos aproximados log.append(f"Origen Servicio cargado: {len(mapa)} claves de cruce") for source in sources: cleaning_job_id = str(source.get("cleaning_job_id") or "") label = str(source.get("label") or cleaning_job_id) rows = source.get("rows") or [] df = _records_to_dataframe(list(rows)) log.append(f"Fuente {label}: {len(df)} filas") if df.empty: resumen_archivos.append( { "archivo": label, "cleaning_job_id": cleaning_job_id, "filas": 0, "paso_bot": 0, "duplicados": 0, "con_error": 0, } ) continue if es_archivo_especial(df): base = construir_base_especial(df, label) log.append(f" tipo ESPECIAL") else: base = construir_base_normal(df, label) log.append(f" tipo NORMAL") if mapa: base, encontrados, sin_cruce = aplicar_origen_servicio(base, mapa) log.append( f" Origen Servicio: {encontrados} con servicio / {sin_cruce} sin cruce" ) # Propagar cleaning_job_id para persistencia base["cleaning_job_id"] = cleaning_job_id df_ok, df_err = normalizar_registros(base) if not df_ok.empty: df_ok = df_ok.copy() df_ok["cleaning_job_id"] = cleaning_job_id ok_parts.append(df_ok) if not df_err.empty: df_err = df_err.copy() df_err["cleaning_job_id"] = cleaning_job_id err_parts.append(df_err) resumen_archivos.append( { "archivo": label, "cleaning_job_id": cleaning_job_id, "filas": len(df), "paso_bot": 0, "duplicados": 0, "con_error": 0, } ) df_ok_total = pd.concat(ok_parts, ignore_index=True) if ok_parts else pd.DataFrame() df_err_total = pd.concat(err_parts, ignore_index=True) if err_parts else pd.DataFrame() consolidado = preparar_consolidado(df_ok_total, df_err_total) # Reinyectar cleaning_job_id si se perdió en preparar_consolidado if "cleaning_job_id" not in consolidado.columns: consolidado["cleaning_job_id"] = "" # mapear por archivo_origen label_to_id = { str(s.get("label") or s.get("cleaning_job_id")): str(s.get("cleaning_job_id") or "") for s in sources } if "archivo_origen" in consolidado.columns: consolidado["cleaning_job_id"] = consolidado["archivo_origen"].map( lambda x: label_to_id.get(str(x), "") ) # Actualizar resumen por fuente if not consolidado.empty and "archivo_origen" in consolidado.columns: paso = ( consolidado[consolidado["OBSERVACION"].eq("PASO_BOT")] .groupby("archivo_origen") .size() .to_dict() ) dup = ( consolidado[consolidado["OBSERVACION"].str.contains("RADICADO_DUPLICADO", na=False)] .groupby("archivo_origen") .size() .to_dict() ) err = ( consolidado[~consolidado["OBSERVACION"].eq("PASO_BOT")] .groupby("archivo_origen") .size() .to_dict() ) for r in resumen_archivos: a = r.get("archivo", "") r["paso_bot"] = int(paso.get(a, 0)) r["duplicados"] = int(dup.get(a, 0)) r["con_error"] = int(err.get(a, 0)) resumen_obs, _ = preparar_resumen(consolidado, resumen_archivos) summary: dict[str, Any] = { "total": int(len(consolidado)), "paso_bot": int((consolidado["OBSERVACION"] == "PASO_BOT").sum()) if not consolidado.empty else 0, "con_error": int((consolidado["OBSERVACION"] != "PASO_BOT").sum()) if not consolidado.empty else 0, "duplicados": int( consolidado["OBSERVACION"].astype(str).str.contains("RADICADO_DUPLICADO", na=False).sum() ) if not consolidado.empty else 0, "por_fuente": resumen_archivos, "por_observacion": resumen_obs.to_dict(orient="records") if not resumen_obs.empty else [], } log.append( f"Consolidado: {summary['total']} filas, " f"{summary['paso_bot']} PASO_BOT, {summary['con_error']} con observación" ) return consolidado, summary, log def consolidado_to_records(df: pd.DataFrame) -> list[dict[str, Any]]: if df is None or df.empty: return [] cols = [ *COLUMNAS_FINAL_IT, "OBSERVACION", "archivo_origen", "tipo_archivo", "cleaning_job_id", ] out = df.copy() for c in cols: if c not in out.columns: out[c] = "" records = out[cols].fillna("").astype(str).to_dict(orient="records") return records def export_paso_bot_csv(rows: list[dict[str, Any]]) -> str: """CSV pipe-separated solo PASO_BOT, headers IT.""" aptos = [r for r in rows if str(r.get("OBSERVACION", "")).strip() == "PASO_BOT"] if not aptos: df = pd.DataFrame(columns=CSV_FINAL_IT_HEADERS) else: df = pd.DataFrame(aptos) for c in COLUMNAS_FINAL_IT: if c not in df.columns: df[c] = "" df = df.reindex(columns=COLUMNAS_FINAL_IT).fillna("") df = df.rename(columns=CSV_FINAL_IT_RENAME) df = df.reindex(columns=CSV_FINAL_IT_HEADERS) return df.to_csv(sep="|", index=False, encoding="utf-8-sig") def export_no_aptos_csv(rows: list[dict[str, Any]]) -> str: """CSV pipe-separated de filas que NO son PASO_BOT (incluye OBSERVACION).""" headers = [*CSV_FINAL_IT_HEADERS, "OBSERVACION"] no_aptos = [r for r in rows if str(r.get("OBSERVACION", "")).strip() != "PASO_BOT"] if not no_aptos: df = pd.DataFrame(columns=headers) else: df = pd.DataFrame(no_aptos) for c in COLUMNAS_FINAL_IT: if c not in df.columns: df[c] = "" if "OBSERVACION" not in df.columns: df["OBSERVACION"] = "" df = df.reindex(columns=[*COLUMNAS_FINAL_IT, "OBSERVACION"]).fillna("") df = df.rename(columns=CSV_FINAL_IT_RENAME) df = df.reindex(columns=headers) return df.to_csv(sep="|", index=False, encoding="utf-8-sig") def export_todos_csv(rows: list[dict[str, Any]]) -> str: """CSV pipe-separated con todas las filas (aptos y no aptos) e OBSERVACION.""" headers = [*CSV_FINAL_IT_HEADERS, "OBSERVACION"] if not rows: df = pd.DataFrame(columns=headers) else: df = pd.DataFrame(rows) for c in COLUMNAS_FINAL_IT: if c not in df.columns: df[c] = "" if "OBSERVACION" not in df.columns: df["OBSERVACION"] = "" df = df.reindex(columns=[*COLUMNAS_FINAL_IT, "OBSERVACION"]).fillna("") df = df.rename(columns=CSV_FINAL_IT_RENAME) df = df.reindex(columns=headers) return df.to_csv(sep="|", index=False, encoding="utf-8-sig")