"""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]