Arquitectura
BCN Extractor está diseñado como un pipeline ELT (Extract, Load, Transform) modular, con capas claramente separadas que pueden usarse de forma independiente.
Diagrama de componentes
Section titled “Diagrama de componentes”┌─────────────────────────────────────────────────────────┐│ 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 │└─────────────────────────────────────────┘Componentes
Section titled “Componentes”BCN Client (bcn_client.py)
Section titled “BCN Client (bcn_client.py)”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_LIMITen.env) - Reintentos automáticos con backoff exponencial
- Caché en disco para evitar re-descargas
No hace: parsing, validación de datos ni persistencia.
XML Parser (utils/norm_parser.py)
Section titled “XML Parser (utils/norm_parser.py)”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
Managers (managers/)
Section titled “Managers (managers/)”Capa de abstracción para todas las operaciones de base de datos. Cada manager encapsula las queries SQL de su dominio.
| Manager | Responsabilidad |
|---|---|
NormsManager | CRUD de normas, búsqueda FTS, filtros por estado/tipo/fecha |
InstitutionManager | CRUD de instituciones, búsqueda, estadísticas |
TiposNormasManager | Catálogo de tipos de normas |
DownloadManager | Log persistente de operaciones de descarga |
REST API (api/)
Section titled “REST API (api/)”La API está construida con FastAPI y organizada en un paquete modular con responsabilidades separadas.
| Archivo | Responsabilidad |
|---|---|
api/main.py | Entry point, registro de routers |
api/dependencies.py | Instancias compartidas via Depends() (client, parser, managers) |
api/routers/normas.py | Endpoints de normas |
api/routers/instituciones.py | Endpoints de instituciones |
api/services/sync.py | Lógica de sincronización batch |
Para levantar el servidor:
fastapi dev api/main.py # desarrollo (con recarga automática)uvicorn api.main:app --port 8000 # producciónCLI (cli/)
Section titled “CLI (cli/)”La CLI está construida con Typer y Rich, organizada en un paquete con responsabilidades separadas:
| Archivo | Responsabilidad |
|---|---|
bcn_cli.py | Entry point, registro de grupos de comandos |
cli/output.py | Toda la presentación (tablas, paneles, colores) |
cli/console.py | Instancia compartida de Rich Console |
cli/_internal.py | Conexión a DB y managers compartidos |
cli/commands/normas.py | Comandos: list, get, sync, search |
cli/commands/instituciones.py | Comandos: list, get, load |
cli/commands/sistema.py | Comandos: init, stats, cache |
Estructura del proyecto
Section titled “Estructura del proyecto”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
Flujo de una sincronización
Section titled “Flujo de una sincronización”- El usuario ejecuta
python bcn_cli.py normas sync <id>oPUT /instituciones/{id}/normas - BCN Client hace
GETal web service XML de la BCN - XML Parser extrae metadatos y convierte a Markdown
- Se calcula el hash MD5 del XML
- Si el hash difiere del almacenado → se hace upsert en PostgreSQL
- Si el hash es igual → se omite (no dirty write)
- DownloadManager registra el intento (exitoso o fallido) en la tabla
descargas
Decisiones de diseño
Section titled “Decisiones de diseño”¿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.