Conversión GTFS ↔ NeTEx-ES (bidireccional)
Convertidor Python bidireccional GTFS↔NeTEx-ES (EN 12896) con validador de 218 reglas, packaging multi-archivo, frequencies, pathways y soporte transporte a demanda. v3.0: 96 tests, 0 fallos, round-trip verificado.
Ubicación del proyecto
- Repositorio:
/root/workspace/netex/ - Convertidor:
/root/workspace/netex/converter/ - Validador:
/root/workspace/netex/validator/ - Especificación:
/root/workspace/netex/spec/NeTEx-ES.md(25 secciones, 1337 líneas) - Decisiones:
/root/workspace/netex/DECISIONES.md(12 decisiones arquitectónicas) - v3.0: 218 reglas (0 placeholders), 96 tests, conversión bidireccional, backend HTTP
Flujo básico de conversión
GTFS → NeTEx
from converter.config import Config
from converter.gtfs_reader import GTFSReader
from converter.netex_writer import NeTExWriter
reader = GTFSReader("/path/to/gtfs_dir_or_zip")
feed = reader.read()
config = Config(
publisher_name="Nombre Editor",
include_geography=True,
include_fares=True,
include_transfers=True,
include_shapes=True,
)
writer = NeTExWriter(feed, config)
xml = writer.to_string(pretty=True) # XML completo en string
NeTEx → GTFS (bidireccional, v3.0)
from converter.netex_reader import NeTExReader
from converter.gtfs_writer import GTFSWriter
reader = NeTExReader("entrada.xml")
feed = reader.read()
writer = GTFSWriter(feed)
writer.write("salida-gtfs/") # Genera 10 archivos .txt
Round-trip verificado
# GTFS → NeTEx → GTFS
reader = GTFSReader("mi-feed.zip")
feed_orig = reader.read()
writer = NeTExWriter(feed_orig, Config())
writer.to_file("tmp.xml")
reader2 = NeTExReader("tmp.xml")
feed_rt = reader2.read()
assert len(feed_rt.routes) == len(feed_orig.routes) # ✓
assert len(feed_rt.trips) == len(feed_orig.trips) # ✓
assert len(feed_rt.stop_times) == len(feed_orig.stop_times) # ✓
Backend HTTP (sin dependencias externas)
python app/server.py # http://localhost:5000
# POST /convert — GTFS (bytes JSON) → NeTEx XML
# POST /convert-back — NeTEx XML → GTFS (base64 zip)
# POST /validate — Validar NeTEx con 218 reglas
Arquitectura del convertidor
gtfs_reader.py — Lectura GTFS
- Soporta
.zipo directorio con.txt - Dataclasses:
Stop,Route,Trip,StopTime,Shape,Calendar,Transfer,FareAttribute,FareRule - Métodos helper:
get_stop(),get_route(),get_trip(),get_calendar()
netex_writer.py — Generación XML
Métodos principales:
_create_stop_places()— Jerarquía: StopPlace → Quay → StopPoint_create_lines()— Line + Route (separación GTFS)_create_journey_patterns()— JP + JPElements_create_vehicle_journeys()— VJ + Calls + SchedulePoints_create_service_frames()— ServiceFrame → DayType + DatePeriod_create_fares()— CRÍTICO: vea pitfalls abajo_create_shapes()— LineGeometry desde shapes GTFS_create_connections()— Connections desde transfers GTFS
multi_file_packager.py — Packaging multi-archivo
Divide en 7 XMLs según tipo de entidad:
| Archivo | Contenido |
|---|---|
netex_publication.xml |
PublicationDelivery + metadatos |
netex_stop_places.xml |
StopPlaces, Quays, StopPoints, Accessibility |
netex_lines.xml |
Lines, Routes, JourneyPatterns, LineGeometries |
netex_timetables.xml |
VehicleJourneys, ServiceFrames, DayTypes |
netex_fares.xml |
FareZones, FareStructures, FareProducts, SalesPoints |
netex_connections.xml |
Connections, Transfers |
netex_admin.xml |
AdministrativeAreas, GroupsOfLines |
Usa ENTITY_FILE_MAP para mapear contenedores y elementos individuales.
flexible_converter.py — Transporte a demanda
- Detección de rutas flexibles (
pickup_type > 0o keywords) FlexibleLine: lineType, bookingAccess, bookingMethodsFlexibleService: booking rules, flexibleServiceProperties- XML generation:
create_flexible_line_xml(),create_flexible_service_xml()
Sistema de tarifas
Generado automáticamente desde zone_ids del GTFS:
| Entidad | Ejemplo | Descripción |
|---|---|---|
FareZone |
ES:FareZone:ES:ZONA_A |
Zona tarifaria por zone_id |
FareProduct |
adult/child/senior/youth/family/day/week/month/annual | Tipos de billete |
FareStructure |
Single/DayPass/WeekPass/MonthPass | Estructura tarifaria |
FareComponent |
ES:FC:{pub}:{suffix}:{zone_id} |
Componente por zona |
FareElement |
Price + Currency + ZoneRef | Precio |
SalesPoint |
onBoard/ticketMachine/mobileApp/online | Puntos de venta |
TicketingMode |
SingleTicket/DayTicket/MultiRideTicket/SeasonTicket | Modo de pago |
Validador (218 reglas en 18 módulos — verificado 2026-07-07)
| Módulo | Reglas | Qué valida |
|---|---|---|
id_rules |
9 | Formato y unicidad IDs NeTEx-ES |
frame_rules |
24 | CompositeFrame, ValidBetween, versiones |
journey_rules |
26 | Coherencia temporales, llamadas |
stop_rules |
9 | Jerarquía parada/andén |
line_rules |
11 | Modos de transporte |
service_rules |
10 | ServiceFrames, DayTypes |
geometry_rules |
12 | LineGeometry, ShapePoint, coordenadas |
es_specific |
10 | Reglas contexto español |
flexible_rules |
15 | FlexibleLine/FlexibleService |
fare_structure_rules |
15 | FareComponents, Elements, Times |
vehicle_rules |
12 | VehicleType, SchedulePoints |
interchange_rules |
12 | Connections, TransferType |
multilingual_rules |
12 | MultilingualString, idiomas |
accessibility_rules |
10 | Wheelchair, Bikes |
packaging_rules |
15 | PublicationDelivery, Entity IDs |
validity_rules |
5 | ValidBetween, fechas |
mode_rules |
2 | Modos válidos |
# Ejecutar validador
python -m validator.reference_validator archivo.xml --report json
⚠️ Pitfalls Críticos
1. IDs duplicados en FareComponents
Cada FareStructure (Single, DayPass, WeekPass, MonthPass) genera sus propios FareComponents. El ID del FareComponent debe incluir un sufijo único por estructura:
# MALO — IDs duplicados
fc_id = f"{prefix}:FC:{pub}:{zone_id}"
# BUENO — sufijo por estructura
fc_id = f"{prefix}:FC:{pub}:{suffix}:{zone_id}"
Al llamar _add_fare_components(), pasar suffix="Single" o suffix="DayPass" etc.
2. Multi-file packager: mapear contenedores (plural) Y elementos (singular)
ENTITY_FILE_MAP debe incluir tanto el contenedor como los elementos individuales:
ENTITY_FILE_MAP = {
"VehicleJourneys": "netex_timetables.xml", # contenedor
"VehicleJourney": "netex_timetables.xml", # elemento
}
Si falta el contenedor (plural), todo cae al default netex_lines.xml.
3. TicketingMode como texto, no como children de _el()
_el("TicketingMode", None, "SingleTicket") falla porque _el espera lista de Elementos, no strings.
# MALO — TypeError
tm = _el("TicketingMode", None, "SingleTicket")
# BUENO
tm = _el("TicketingMode", None)
tm.text = "SingleTicket"
4. zone_id vs stop_id en zonas tarifarias
Al construir el dict de zonas desde stops, usar stop.zone_id como clave y stop.stop_id como valor:
# MALO
zones.setdefault(stop.zone_id, []).append(stop.zone_id)
# BUENO
zones.setdefault(stop.zone_id, []).append(stop.stop_id)
5. Validador XPath: lxml vs ElementTree incompatible
El xpath_validator.py usa lxml pero el resto usa stdlib xml.etree.ElementTree. Las reglas XPath fallan con 'NoneType' object has no attribute 'xpath'. Las 218 reglas semánticas Python funcionan correctamente.
6. ✅ RESUELTO — ElementTree: if elem: generaba DeprecationWarning
Fixeado v3.0. Cambiado a if elem is not None: en todas las ubicaciones de netex_writer.py y netex_reader.py.
7. ✅ RESUELTO — datetime.utcnow() deprecado
Fixeado v3.0. Cambiado a datetime.now(UTC) con from datetime import datetime, UTC.
8. Archivos grandes (>10MB)
GTFS grande (ej. Metro Bilbao: 5549 viajes, 6000 shapes → 24MB XML). Validador tarda 30-60s. Usar timeout alto en subprocess.
9. ✅ RESUELTO — Tests: camelCase NeTEx vs PascalCase
Fixeado v3.0. Los tests ahora buscan stopPlaces, lines, routes (camelCase correcto). 96 tests pasan, 0 fallan.
10. ✅ PARCIALMENTE RESUELTO — Spec ↔ implementación
Mejorado v3.0. El ejemplo XML fue regenerado desde el converter (IDs únicos, camelCase, MobilityImpairedAccess). La spec tiene 25 secciones. DECISIONES.md creado con 12 decisiones. Aún faltan: PlaceType específicos, transportSubMode correcto por nombre de ruta.
11. XSD validator: HTTP 404 al descargar schema
xsd_validator.py intenta descargar el schema NeTEx desde http://netex-cen.github.io/netex-schemas/... que devuelve 404. El schema local está en netex-xsd/xsd/NeTEx_publication.xsd. Usar cache local o actualizar URL.
12. route_type 1 siempre mapea a highSpeedTrain
config.py submode_map mapea route_type "1" a highSpeedTrain. Pero route_type 1 en GTFS es "Rail" genérico — debería distinguir AVE (highSpeedTrain) vs Cercanías (commuterTrain) vs Media Distancia (regionalTrain) por nombre de ruta, no solo route_type.
13. ✅ RESUELTO — Convertidor NeTEx→GTFS bidireccional
Creado v3.0. Ahora existe conversión bidireccional:
netex_reader.py(613 líneas) — Lee XML NeTEx → GTFSFeedgtfs_writer.py(280 líneas) — GTFSFeed → archivos GTFS .txt- Round-trip verificado: GTFS→NeTEx→GTFS preserva 100% routes, trips, stop_times
- 10 tests de round-trip en
tests/test_roundtrip.py
14. ✅ PARCIALMENTE RESUELTO — GTFS files adicionales soportados
Añadido v3.0. El reader ahora soporta:
frequencies.txt→ HeadwayJourneyGroup con FrequencyHeadwayInterval ✅pathways.txt→ SitePath con 7 modos (walkway, stairs, escalator, elevator, ramp) ✅levels.txt→ LevelStructure ✅translations.txt→ MultilingualString ✅fare_products.txt(GTFS-Fares v2) → NO (sin datos de prueba)bookings.txt→ NO (spec lo documenta, código no lo implementa)
15. ✅ RESUELTO — Claims del README
Fixeado v3.0. README reescrito con datos reales: 96 tests, 218 reglas, sin claims inflados. CHANGELOG.md creado.
16. IDs duplicados en AccessibilityAssessment y Call — RESUELTO v3.0
Bug crítico fixeado. Dos bugs de IDs duplicados:
AccessibilityAssessmentusabaacc-0/acc-1(solo 2 valores posibles, se repetían 56 veces). Ahora usa contador global + entity_id.Callno incluía trip_id ni stop_sequence. Ahora:ES:Call:MTM:CHAMARTIN:trip_001:3.
17. SSPs sintéticos para stop_times huérfanos — RESUELTO v3.0
Stop_ids que aparecen en stop_times pero no en stops.txt ahora generan SSPs automáticamente en el ServiceFrame. Antes se perdían 9 stop_times en el round-trip.
18. _parse_id con prefijo ES duplicado — RESUELTO v3.0
IDs con formato ES:Operator:ES:MTM ahora extraen MTM como operador correctamente (antes devolvía ES).
19. Backend HTTP sin dependencias — AÑADIDO v3.0
app/server.py usa solo http.server de stdlib (sin Flask). 3 endpoints: /convert (GTFS→NeTEx), /convert-back (NeTEx→GTFS), /validate. Verificado vía HTTP con round-trip completo.
20. Principio de honestidad técnica — REGLA DE ORO
Mejor no incluir algo que incluirlo con errores. Si no hay datos de prueba para verificar una feature (GTFS-Fares v2, bookings), no se añade al código. La spec puede documentarlo, pero el código solo incluye lo que funciona y está probado. Tras cualquier cambio: python -m pytest tests/ -v y actualizar README con números reales.
Archivos de referencia
references/gtfs-netex-mapping.md— Mapeo completo GTFS→NeTExreferences/validator-rules-index.md— Índice de 218 reglasreferences/mega-auditoria-2026-07-07.md— Mega auditoría inicial: 15 gaps críticos, plan de 8 fasesreferences/auditoria-final-2026-07-07.md— Auditoría final v3.0: comparativa real con nórdico (71/100) y francés (64/100), NeTEx-ES 91/100. Lista honesta de lo que NO está hecho.
Estado v3.0 (2026-07-07)
| Métrica | Valor |
|---|---|
| Tests | 96 passed, 0 failed |
| Reglas validador | 218 (0 placeholders) |
| Conversión | Bidireccional GTFS↔NeTEx |
| Round-trip | Verificado (10 tests) |
| Spec | 25 secciones, 1337 líneas |
| Dependencias | Solo stdlib Python |
| Backend | HTTP stdlib (sin Flask) |
| Score real | 91/100 (vs 71 nórdico, 64 francés) |
Lo que NO está hecho (honestamente)
- No probado con feeds GTFS reales de operadores españoles
- XSD oficial no valida (decisión deliberada, frames tipados)
- GTFS-Fares v2 y Bookings no implementados (sin datos de prueba)
- Multilingüe en código: spec documenta 6 idiomas, writer solo genera lang="es"
- Festivos por CCAA en código: spec los documenta, writer no los genera
- CRS conversión: spec documenta ETRS89/UTM, writer solo genera WGS84
Licencia
MIT — Compatible con RD 571/2023 (datos abiertos transporte público)
Comparativa de alternativas
- TransmodelEcosystem/NeTEx — esquema XSD oficial NeTEx (CEN/TS 16614) para construir validadores/convertidores correctos; la fuente de verdad del esquema que este skill convierte.