Files
IPSExtractor/services/poblacion_apta/domain/verificador_st/pipeline.py
T

226 lines
6.8 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")