Skip to content

Arquitectura

BCN Extractor está diseñado como un pipeline ELT (Extract, Load, Transform) modular, con capas claramente separadas que pueden usarse de forma independiente.

┌─────────────────────────────────────────────────────────┐
│ INTERFACES │
│ CLI (bcn_cli.py) │ TUI (bcn_tui.py) │ API │
└──────────────┬──────────────────────────────────────────┘
┌─────────────────────────────────────────┐
│ BCN CLIENT │
│ HTTP requests → servicios web BCN │
│ Rate limiting · Reintentos · Caché │
└──────────────┬──────────────────────────┘
│ XML crudo
┌─────────────────────────────────────────┐
│ XML PARSER │
│ lxml · Extracción de metadatos │
│ Conversión XML → Markdown │
│ Hash MD5 para detección de cambios │
└──────────────┬──────────────────────────┘
│ datos estructurados
┌─────────────────────────────────────────┐
│ MANAGERS │
│ NormsManager · InstitutionManager │
│ TiposNormasManager · DownloadManager │
└──────────────┬──────────────────────────┘
┌─────────────────────────────────────────┐
│ POSTGRESQL │
│ FTS en español · GIN indexes · JSONB │
│ Docker volume persistente │
└─────────────────────────────────────────┘

Cliente HTTP que encapsula toda la comunicación con los servicios web de la BCN.

Responsabilidades:

  • Construir las URLs de consulta al web service de la BCN
  • Gestionar rate limiting configurable (via BCN_RATE_LIMIT en .env)
  • Reintentos automáticos con backoff exponencial
  • Caché en disco para evitar re-descargas

No hace: parsing, validación de datos ni persistencia.


Procesador de documentos XML que transforma la respuesta cruda de la BCN en datos estructurados.

Responsabilidades:

  • Parsear el XML con lxml
  • Extraer metadatos: título, fecha, organismo, tipo de norma, materias
  • Convertir el contenido legal a formato Markdown limpio
  • Calcular hash MD5 del XML para detección de cambios

Capa de abstracción para todas las operaciones de base de datos. Cada manager encapsula las queries SQL de su dominio.

ManagerResponsabilidad
NormsManagerCRUD de normas, búsqueda FTS, filtros por estado/tipo/fecha
InstitutionManagerCRUD de instituciones, búsqueda, estadísticas
TiposNormasManagerCatálogo de tipos de normas
DownloadManagerLog persistente de operaciones de descarga

La API está construida con FastAPI y organizada en un paquete modular con responsabilidades separadas.

ArchivoResponsabilidad
api/main.pyEntry point, registro de routers
api/dependencies.pyInstancias compartidas via Depends() (client, parser, managers)
api/routers/normas.pyEndpoints de normas
api/routers/instituciones.pyEndpoints de instituciones
api/services/sync.pyLógica de sincronización batch

Para levantar el servidor:

Terminal window
fastapi dev api/main.py # desarrollo (con recarga automática)
uvicorn api.main:app --port 8000 # producción

La CLI está construida con Typer y Rich, organizada en un paquete con responsabilidades separadas:

ArchivoResponsabilidad
bcn_cli.pyEntry point, registro de grupos de comandos
cli/output.pyToda la presentación (tablas, paneles, colores)
cli/console.pyInstancia compartida de Rich Console
cli/_internal.pyConexión a DB y managers compartidos
cli/commands/normas.pyComandos: list, get, sync, search
cli/commands/instituciones.pyComandos: list, get, load
cli/commands/sistema.pyComandos: init, stats, cache

  • DirectoryBCNExtractor/
    • bcn_client.py — Cliente HTTP para la BCN
    • bcn_tui.py — TUI (interfaz de terminal)
    • bcn_cli.py — Entry point de la CLI (Typer)
    • docker-compose.yml
    • requirements.txt
    • .env.example
    • Directoryapi/ — REST API con FastAPI
      • main.py — Entry point, registro de routers
      • dependencies.py — Instancias compartidas (Depends)
      • Directoryrouters/
        • normas.py — Endpoints de normas
        • instituciones.py — Endpoints de instituciones
      • Directoryservices/
        • sync.py — Lógica de sincronización batch
    • Directorycli/ — Lógica de la CLI
      • output.py — Presentación con Rich
      • console.py — Instancia compartida de Console
      • _internal.py — Conexión y managers compartidos
      • Directorycommands/
        • normas.py — list, get, sync, search
        • instituciones.py — list, get, load
        • sistema.py — init, stats, cache
    • Directorymanagers/
      • norms.py
      • institutions.py
      • norms_types.py
      • downloads.py
    • Directoryutils/
      • norm_parser.py — Parser XML → Markdown
      • db_logger.py — Logger de operaciones
    • Directoryloaders/
      • institutions.py — Carga desde CSV
    • Directorydata/
      • instituciones.csv — 700+ instituciones BCN
      • Directoryxml/ — Backups XML descargados
      • Directorylogs/
      • Directorycache/
    • Directorydocker/
      • Directoryinit-scripts/ — Inicialización de PostgreSQL
    • Directorytests/
      • test_api.py — Tests de integración de la API
  1. El usuario ejecuta python bcn_cli.py normas sync <id> o PUT /instituciones/{id}/normas
  2. BCN Client hace GET al web service XML de la BCN
  3. XML Parser extrae metadatos y convierte a Markdown
  4. Se calcula el hash MD5 del XML
  5. Si el hash difiere del almacenado → se hace upsert en PostgreSQL
  6. Si el hash es igual → se omite (no dirty write)
  7. DownloadManager registra el intento (exitoso o fallido) en la tabla descargas

¿Por qué PostgreSQL y no SQLite? Full-text search en español requiere diccionarios de idioma (to_tsvector('spanish', ...)) que solo PostgreSQL soporta nativamente. Además, el esquema relacional (normas ↔ instituciones muchos-a-muchos) y los índices GIN sobre JSONB justifican un motor completo.

¿Por qué guardar el XML en disco? El XML original sirve como backup auditable. Si el parser cambia en el futuro, se puede re-procesar el XML sin volver a descargar desde la BCN.

¿Por qué lxml y no el módulo xml de stdlib? lxml es considerablemente más rápido para documentos grandes y tiene mejor soporte para namespaces XML complejos como los que usa el web service de la BCN.

¿Por qué Typer + Rich para la CLI? Typer permite organizar comandos en grupos con validación de tipos y --help automático. Rich centraliza toda la presentación en cli/output.py, lo que hace que el output sea consistente y testeable sin acoplar la lógica de negocio a print().

¿Por qué separar la API en módulos? Con un solo api.py monolítico cualquier cambio en normas o instituciones implica tocar el mismo archivo. La separación en routers/ y services/ permite trabajar en cada dominio de forma independiente. Las instancias de managers y cliente se crean una sola vez via dependencies.py con @lru_cache, lo que también facilita el testing por inyección de dependencias.