"""Gestor de escritura CSV. Maneja la escritura incremental segura (con lock) en archivos CSV. """ import csv import os import re import threading from typing import Any, Dict, List _LINE_BREAK_RE = re.compile(r"[\r\n\v\f\x85\u2028\u2029]+") def sanitize_csv_cell_value(value: Any) -> Any: """Reemplaza saltos de línea en valores de celda por un espacio (una fila física por registro). Conserva ``None`` y escalares numéricos/booleanos sin tocar. Cadenas y demás tipos se normalizan solo si la representación en texto contiene saltos. """ if value is None: return value if isinstance(value, bool): return value if isinstance(value, (int, float)): return value if isinstance(value, str): if not _LINE_BREAK_RE.search(value): return value return _LINE_BREAK_RE.sub(" ", value).strip() s = str(value) if not _LINE_BREAK_RE.search(s): return value return _LINE_BREAK_RE.sub(" ", s).strip() class CSVManager: def __init__(self): self._lock = threading.Lock() def append_rows(self, csv_path: str, rows: List[Dict[str, Any]], header: List[str]) -> None: """Anexa filas a un CSV, creando encabezado si el archivo no existe.""" os.makedirs(os.path.dirname(csv_path) or ".", exist_ok=True) file_exists = os.path.exists(csv_path) and os.path.getsize(csv_path) > 0 with self._lock: with open(csv_path, "a", encoding="utf-8", newline="") as f: writer = csv.DictWriter(f, fieldnames=header, extrasaction="ignore", delimiter="|") if not file_exists: writer.writeheader() for r in rows: sanitized = {k: sanitize_csv_cell_value(v) for k, v in r.items()} writer.writerow(sanitized)