chore: Remove obsolete build and GUI files, and introduce a new Docker Compose setup for microservices architecture. This change streamlines the project structure by eliminating legacy scripts and integrating a PostgreSQL database service alongside extraction and cleaning services.
This commit is contained in:
@@ -0,0 +1,152 @@
|
||||
"""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 parse_comma_separated(value: str) -> List[str]:
|
||||
"""Convierte una cadena coma-separada en lista normalizada (sin vacíos)."""
|
||||
return [v.strip() for v in value.split(',') if v and v.strip()]
|
||||
|
||||
|
||||
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]
|
||||
Reference in New Issue
Block a user