Files
IPSExtractor/services/extraction/domain/csv_manager.py
T

53 lines
1.7 KiB
Python

"""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-sig", 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)