148 lines
5.1 KiB
Python
148 lines
5.1 KiB
Python
"""Utilidades generales: parseo, descubrimiento de documentos y recolección.
|
|
|
|
Incluye heurísticas para elegir el documento a procesar por radicado,
|
|
priorizando TIFF cuando existe y recurriendo a PDF/TIFF por keywords o patrones.
|
|
"""
|
|
import os
|
|
import fnmatch
|
|
from typing import List, Dict
|
|
|
|
|
|
# Extensiones de documentos soportados para búsqueda por patrones
|
|
ALLOWED_DOC_EXTS = (".pdf", ".tif", ".tiff")
|
|
|
|
|
|
def _matches_pattern(filename: str, pattern: str) -> bool:
|
|
"""Determina si `filename` coincide con `pattern` usando semántica tipo Windows.
|
|
|
|
- Admite comodines `*` y `?` en el patrón.
|
|
- Si el patrón incluye una extensión válida (.pdf/.tif/.tiff), se compara contra
|
|
el nombre completo (incluida la extensión).
|
|
- Si el patrón no incluye una extensión válida, se compara solo contra el nombre
|
|
sin extensión, pero siempre restringiendo a archivos con extensiones permitidas.
|
|
- Si el patrón no tiene comodines, se interpreta como 'contiene' (case-insensitive)
|
|
sobre el nombre completo, respetando igualmente las extensiones permitidas.
|
|
"""
|
|
if not isinstance(filename, str) or not isinstance(pattern, str):
|
|
return False
|
|
fname = filename.lower()
|
|
pat = pattern.lower().strip()
|
|
if not pat:
|
|
return False
|
|
|
|
# Solo considerar archivos con extensión permitida
|
|
stem, ext = os.path.splitext(fname)
|
|
if ext not in ALLOWED_DOC_EXTS:
|
|
return False
|
|
|
|
has_wildcards = "*" in pat or "?" in pat
|
|
|
|
if has_wildcards:
|
|
p_stem, p_ext = os.path.splitext(pat)
|
|
# Patrón con extensión explícita soportada: aplicar sobre nombre completo
|
|
if p_ext in ALLOWED_DOC_EXTS:
|
|
return fnmatch.fnmatch(fname, pat)
|
|
# Patrón sin extensión explícita: aplicar sobre el nombre sin extensión
|
|
return fnmatch.fnmatch(stem, pat)
|
|
|
|
# Sin comodines: interpretar como 'contiene' en el nombre completo
|
|
return pat in fname
|
|
|
|
|
|
def find_pdf_in_dir_by_keywords(dir_path: str, keywords: List[str]) -> str | None:
|
|
"""Devuelve la primera ruta (PDF/TIFF) cuyo nombre coincide con alguno de los patrones.
|
|
|
|
- Acepta keywords simples (sin `*`/`?`) como 'contiene' insensible a mayúsculas.
|
|
- Acepta patrones con `*` y `?` tipo Windows (p. ej. `*_1`, `*_OK?.pdf`). Solo
|
|
se consideran archivos con extensiones en `ALLOWED_DOC_EXTS`.
|
|
"""
|
|
if not os.path.isdir(dir_path):
|
|
return None
|
|
files = [f for f in os.listdir(dir_path) if f.lower().endswith(ALLOWED_DOC_EXTS)]
|
|
if not files or not keywords:
|
|
return None
|
|
for kw in keywords:
|
|
matches = [f for f in files if _matches_pattern(f, kw)]
|
|
if matches:
|
|
matches.sort()
|
|
return os.path.join(dir_path, matches[0])
|
|
return None
|
|
|
|
|
|
def find_first_tiff(dir_path: str) -> str | None:
|
|
"""Devuelve la primera ruta a TIFF encontrada (orden lexicográfico)."""
|
|
if not os.path.isdir(dir_path):
|
|
return None
|
|
for f in sorted(os.listdir(dir_path)):
|
|
if f.lower().endswith(('.tif', '.tiff')):
|
|
return os.path.join(dir_path, f)
|
|
return None
|
|
|
|
|
|
def collect_radicado_docs(root_path: str, keywords: List[str]) -> List[Dict[str, str]]:
|
|
"""Para cada radicado, prioriza TIFF; si no hay, busca PDF/TIFF por keywords/patrones."""
|
|
entries: List[Dict[str, str]] = []
|
|
if not os.path.isdir(root_path):
|
|
raise ValueError(f"La ruta raíz no es un directorio: {root_path}")
|
|
for radicado in sorted(os.listdir(root_path)):
|
|
subdir = os.path.join(root_path, radicado)
|
|
if not os.path.isdir(subdir):
|
|
continue
|
|
selected = find_first_tiff(subdir)
|
|
if not selected:
|
|
selected = find_pdf_in_dir_by_keywords(subdir, keywords)
|
|
if not selected:
|
|
continue
|
|
entries.append({
|
|
"radicado": radicado,
|
|
"path": selected,
|
|
})
|
|
return entries
|
|
|
|
|
|
def collect_documents_flat(root_path: str, keywords: List[str] = None) -> List[Dict[str, str]]:
|
|
"""Recolecta documentos PDF/TIFF directamente en la raíz.
|
|
|
|
El 'radicado' es el nombre del archivo sin extensión.
|
|
Si se proveen keywords, solo incluye archivos que coincidan.
|
|
"""
|
|
entries: List[Dict[str, str]] = []
|
|
if not os.path.isdir(root_path):
|
|
raise ValueError(f"La ruta raíz no es un directorio: {root_path}")
|
|
|
|
files = sorted([f for f in os.listdir(root_path) if f.lower().endswith(ALLOWED_DOC_EXTS)])
|
|
|
|
for f in files:
|
|
# Si hay keywords, filtrar
|
|
if keywords:
|
|
match = False
|
|
for kw in keywords:
|
|
if _matches_pattern(f, kw):
|
|
match = True
|
|
break
|
|
if not match:
|
|
continue
|
|
|
|
radicado = os.path.splitext(f)[0]
|
|
entries.append({
|
|
"radicado": radicado,
|
|
"path": os.path.join(root_path, f)
|
|
})
|
|
return entries
|
|
|
|
|
|
def find_first_matching_file(files: list[str], keyword: str) -> str | None:
|
|
"""Devuelve el primer nombre de archivo que coincide con el keyword/patrón.
|
|
|
|
- Usa `_matches_pattern` para soportar comodines `*` y `?`.
|
|
- Solo considera archivos con extensiones en `ALLOWED_DOC_EXTS`.
|
|
- Retorna el nombre de archivo tal cual se recibió.
|
|
"""
|
|
if not files or not keyword:
|
|
return None
|
|
candidates = [f for f in files if _matches_pattern(f, keyword)]
|
|
if not candidates:
|
|
return None
|
|
candidates.sort()
|
|
return candidates[0]
|