feat: Add poblacion_apta service to docker-compose, enhance API gateway for new service integration, and update README with service details. Introduce new database tables for poblacion_apta_jobs and poblacion_apta_rows, and implement functionality for handling cop/mod calculations in the cleaning service.
This commit is contained in:
@@ -0,0 +1,14 @@
|
||||
"""Paquete de validación Población apta (Verificador ST)."""
|
||||
from domain.verificador_st.pipeline import (
|
||||
consolidado_to_records,
|
||||
export_no_aptos_csv,
|
||||
export_paso_bot_csv,
|
||||
process_cleaning_sources,
|
||||
)
|
||||
|
||||
__all__ = [
|
||||
"consolidado_to_records",
|
||||
"export_no_aptos_csv",
|
||||
"export_paso_bot_csv",
|
||||
"process_cleaning_sources",
|
||||
]
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,225 @@
|
||||
# -*- 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")
|
||||
Reference in New Issue
Block a user