244 lines
7.5 KiB
Python
244 lines
7.5 KiB
Python
# -*- 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")
|