53 lines
1.7 KiB
Python
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)
|