feature/validaciones-clarion-agentes-aduanales

This commit is contained in:
hreyes
2026-03-05 15:54:58 -07:00
parent 8ac071aac9
commit 77bbcff223
10 changed files with 470 additions and 222 deletions

View File

@@ -1,11 +1,11 @@
"""
Lectura de CSV con detección de delimitador (compartida por layouts_csv).
Si se pasa fieldnames, no se usa la primera fila como cabecera y se toma como dato (CSV sin cabeceras).
Si fieldnames es None, cabeceras vacías se normalizan a _COL_0_, _COL_1_, ... para no colapsar columnas.
Si se pasa headerless_first_cell_values, se detecta si la primera fila es cabecera o dato por el valor de la primera celda.
"""
import csv
import io
from typing import Iterator, Tuple, Dict, Any, Optional, List
from typing import Iterator, Tuple, Dict, Any, Optional, List, Set
def _normalize_empty_headers(headers: List[str]) -> List[str]:
@@ -24,12 +24,15 @@ def _normalize_empty_headers(headers: List[str]) -> List[str]:
def iter_csv_rows(
file_path: str,
fieldnames: Optional[List[str]] = None,
headerless_first_cell_values: Optional[Set[str]] = None,
headerless_second_cell_key_pattern: Optional[str] = None,
) -> Iterator[Tuple[int, Dict[str, Any]]]:
"""
Abre el CSV, detecta dialecto y devuelve (line_num, row_dict) por cada fila.
line_num empieza en 1 (primera fila de datos).
Si fieldnames es None: la primera fila del archivo se usa como cabecera (comportamiento por defecto).
Si fieldnames es una lista: no se usa cabecera; la primera fila se considera dato y se usan fieldnames como columnas.
Si fieldnames y headerless_first_cell_values se pasan: si la primera celda de la primera fila
(quitando BOM, strip, upper) está en headerless_first_cell_values, se trata como dato y se usan fieldnames.
headerless_second_cell_key_pattern se ignora si no se usa (reservado para otros layouts).
"""
with open(file_path, "r", encoding="utf-8-sig") as f:
sample = f.read(2048)
@@ -38,7 +41,37 @@ def iter_csv_rows(
dialect = csv.Sniffer().sniff(sample, delimiters=",;\t")
except Exception:
dialect = "excel"
if fieldnames:
if fieldnames and headerless_first_cell_values is not None:
first_line = f.readline()
if not first_line:
return
row_reader = csv.reader(io.StringIO(first_line), dialect=dialect)
first_cells = next(row_reader, None)
if not first_cells:
return
first_cell_clean = (first_cells[0] or "").lstrip("\ufeff").strip().upper()
use_headerless = first_cell_clean in headerless_first_cell_values
if use_headerless:
pad = len(fieldnames) - len(first_cells)
cells = first_cells[: len(fieldnames)] + ([""] * pad if pad > 0 else [])
yield 1, dict(zip(fieldnames, cells))
reader = csv.DictReader(f, fieldnames=fieldnames, dialect=dialect, restval="")
for i, row in enumerate(reader, start=2):
yield i, dict(row)
return
f.seek(0)
first_line = f.readline()
if not first_line:
return
row_reader = csv.reader(io.StringIO(first_line), dialect=dialect)
raw_headers = next(row_reader, None)
if not raw_headers:
return
normalized = _normalize_empty_headers(raw_headers)
reader = csv.DictReader(f, fieldnames=normalized, dialect=dialect, restval="")
for i, row in enumerate(reader, start=1):
yield i, row
elif fieldnames:
reader = csv.DictReader(f, fieldnames=fieldnames, dialect=dialect)
for i, row in enumerate(reader, start=1):
yield i, dict(row)