Esta actualización reorganiza el proyecto de reverse engineering de la API de ADIF con los siguientes cambios: Estructura del proyecto: - Movida documentación principal a carpeta docs/ - Consolidados archivos markdown redundantes en CLAUDE.md (contexto completo del proyecto) - Organización de tests en carpeta tests/ con README explicativo - APK renombrado de base.apk a adif.apk para mayor claridad Archivos de código: - Movidos adif_auth.py y adif_client.py a la raíz (antes en api_testing_scripts/) - Eliminados scripts de testing obsoletos y scripts de Frida no utilizados - Nuevos tests detallados: test_endpoints_detailed.py y test_onepaths_with_real_trains.py Descubrimientos: - Documentados nuevos hallazgos en docs/NEW_DISCOVERIES.md - Actualización de onePaths funcionando con commercialNumber real (devuelve 200) - Extraídos 1587 códigos de estación en station_codes.txt Configuración: - Actualizado .gitignore con mejores patrones para Python e IDEs - Eliminados archivos temporales de depuración y logs
393 lines
14 KiB
Python
Executable File
393 lines
14 KiB
Python
Executable File
#!/usr/bin/env python3
|
||
"""
|
||
Cliente completo de la API de ADIF
|
||
|
||
Implementa todos los endpoints funcionales con métodos simples de usar.
|
||
Incluye manejo de errores y validación de datos.
|
||
"""
|
||
|
||
import requests
|
||
import uuid
|
||
from datetime import datetime
|
||
from typing import List, Dict, Optional, Any
|
||
from adif_auth import AdifAuthenticator
|
||
|
||
|
||
class AdifClient:
|
||
"""Cliente para interactuar con la API de ADIF"""
|
||
|
||
def __init__(self, access_key: str, secret_key: str):
|
||
"""
|
||
Inicializa el cliente
|
||
|
||
Args:
|
||
access_key: Clave de acceso
|
||
secret_key: Clave secreta
|
||
"""
|
||
self.auth = AdifAuthenticator(access_key=access_key, secret_key=secret_key)
|
||
self.session = requests.Session()
|
||
|
||
def _make_request(
|
||
self,
|
||
url: str,
|
||
payload: Dict[str, Any],
|
||
use_stations_key: bool = False
|
||
) -> Dict[str, Any]:
|
||
"""
|
||
Realiza una petición a la API
|
||
|
||
Args:
|
||
url: URL del endpoint
|
||
payload: Datos a enviar
|
||
use_stations_key: Si True, usa USER_KEY_STATIONS en lugar de USER_KEY_CIRCULATION
|
||
|
||
Returns:
|
||
Respuesta JSON
|
||
|
||
Raises:
|
||
Exception: Si hay un error en la petición
|
||
"""
|
||
user_id = str(uuid.uuid4())
|
||
headers = self.auth.get_auth_headers("POST", url, payload, user_id=user_id)
|
||
|
||
if use_stations_key:
|
||
headers["User-key"] = self.auth.USER_KEY_STATIONS
|
||
else:
|
||
headers["User-key"] = self.auth.USER_KEY_CIRCULATION
|
||
|
||
response = self.session.post(url, json=payload, headers=headers, timeout=30)
|
||
|
||
if response.status_code == 200:
|
||
return response.json()
|
||
elif response.status_code == 204:
|
||
return {"message": "No content available", "commercialPaths": []}
|
||
elif response.status_code == 401:
|
||
raise PermissionError(
|
||
f"Unauthorized - Las claves no tienen permisos para este endpoint"
|
||
)
|
||
elif response.status_code == 400:
|
||
raise ValueError(
|
||
f"Bad Request - Payload incorrecto: {response.text}"
|
||
)
|
||
else:
|
||
raise Exception(
|
||
f"Error {response.status_code}: {response.text}"
|
||
)
|
||
|
||
def get_departures(
|
||
self,
|
||
station_code: str,
|
||
traffic_type: str = "ALL",
|
||
page_number: int = 0,
|
||
commercial_service: str = "BOTH",
|
||
commercial_stop_type: str = "BOTH"
|
||
) -> List[Dict[str, Any]]:
|
||
"""
|
||
Obtiene las salidas de una estación
|
||
|
||
Args:
|
||
station_code: Código de la estación (ej: "10200")
|
||
traffic_type: Tipo de tráfico (ALL, CERCANIAS, AVLDMD, TRAVELERS, GOODS)
|
||
page_number: Número de página (por defecto 0)
|
||
commercial_service: BOTH, YES, NOT
|
||
commercial_stop_type: BOTH, YES, NOT
|
||
|
||
Returns:
|
||
Lista de trenes
|
||
|
||
Example:
|
||
>>> client = AdifClient(ACCESS_KEY, SECRET_KEY)
|
||
>>> trains = client.get_departures("10200", "AVLDMD")
|
||
>>> for train in trains:
|
||
... print(f"{train['commercialNumber']} - Destino: {train['destination']}")
|
||
"""
|
||
url = "https://circulacion.api.adif.es/portroyalmanager/secure/circulationpaths/departures/traffictype/"
|
||
payload = {
|
||
"commercialService": commercial_service,
|
||
"commercialStopType": commercial_stop_type,
|
||
"page": {"pageNumber": page_number},
|
||
"stationCode": station_code,
|
||
"trafficType": traffic_type
|
||
}
|
||
|
||
data = self._make_request(url, payload)
|
||
return data.get("commercialPaths", [])
|
||
|
||
def get_arrivals(
|
||
self,
|
||
station_code: str,
|
||
traffic_type: str = "ALL",
|
||
page_number: int = 0,
|
||
commercial_service: str = "BOTH",
|
||
commercial_stop_type: str = "BOTH"
|
||
) -> List[Dict[str, Any]]:
|
||
"""
|
||
Obtiene las llegadas a una estación
|
||
|
||
Args:
|
||
station_code: Código de la estación (ej: "10200")
|
||
traffic_type: Tipo de tráfico (ALL, CERCANIAS, AVLDMD, TRAVELERS, GOODS)
|
||
page_number: Número de página (por defecto 0)
|
||
commercial_service: BOTH, YES, NOT
|
||
commercial_stop_type: BOTH, YES, NOT
|
||
|
||
Returns:
|
||
Lista de trenes
|
||
|
||
Example:
|
||
>>> client = AdifClient(ACCESS_KEY, SECRET_KEY)
|
||
>>> trains = client.get_arrivals("71801", "ALL")
|
||
"""
|
||
url = "https://circulacion.api.adif.es/portroyalmanager/secure/circulationpaths/arrivals/traffictype/"
|
||
payload = {
|
||
"commercialService": commercial_service,
|
||
"commercialStopType": commercial_stop_type,
|
||
"page": {"pageNumber": page_number},
|
||
"stationCode": station_code,
|
||
"trafficType": traffic_type
|
||
}
|
||
|
||
data = self._make_request(url, payload)
|
||
return data.get("commercialPaths", [])
|
||
|
||
def get_train_route(
|
||
self,
|
||
commercial_number: str,
|
||
launching_date: int,
|
||
origin_station_code: str,
|
||
destination_station_code: str,
|
||
all_control_points: bool = True
|
||
) -> List[Dict[str, Any]]:
|
||
"""
|
||
Obtiene la ruta completa de un tren (todas las paradas)
|
||
|
||
Args:
|
||
commercial_number: Número comercial del tren (ej: "03194")
|
||
launching_date: Fecha de salida en milisegundos desde epoch
|
||
origin_station_code: Código de estación de origen
|
||
destination_station_code: Código de estación de destino
|
||
all_control_points: Si True, incluye todos los puntos de control
|
||
|
||
Returns:
|
||
Lista de paradas del tren
|
||
|
||
Example:
|
||
>>> # Primero obtener un tren real
|
||
>>> trains = client.get_departures("10200", "AVLDMD")
|
||
>>> train = trains[0]
|
||
>>> info = train['commercialPathInfo']
|
||
>>> key = info['commercialPathKey']
|
||
>>>
|
||
>>> # Obtener su ruta completa
|
||
>>> route = client.get_train_route(
|
||
... commercial_number=key['commercialCirculationKey']['commercialNumber'],
|
||
... launching_date=key['commercialCirculationKey']['launchingDate'],
|
||
... origin_station_code=key['originStationCode'],
|
||
... destination_station_code=key['destinationStationCode']
|
||
... )
|
||
>>> for stop in route:
|
||
... print(f"Parada: {stop['stationCode']}")
|
||
"""
|
||
url = "https://circulacion.api.adif.es/portroyalmanager/secure/circulationpathdetails/onepaths/"
|
||
payload = {
|
||
"allControlPoints": all_control_points,
|
||
"commercialNumber": commercial_number,
|
||
"destinationStationCode": destination_station_code,
|
||
"launchingDate": launching_date,
|
||
"originStationCode": origin_station_code
|
||
}
|
||
|
||
data = self._make_request(url, payload)
|
||
commercial_paths = data.get("commercialPaths", [])
|
||
|
||
if commercial_paths:
|
||
return commercial_paths[0].get("passthroughSteps", [])
|
||
return []
|
||
|
||
def get_station_observations(
|
||
self,
|
||
station_codes: List[str]
|
||
) -> List[Dict[str, Any]]:
|
||
"""
|
||
Obtiene observaciones de estaciones
|
||
|
||
Args:
|
||
station_codes: Lista de códigos de estación
|
||
|
||
Returns:
|
||
Lista de observaciones
|
||
|
||
Example:
|
||
>>> client = AdifClient(ACCESS_KEY, SECRET_KEY)
|
||
>>> obs = client.get_station_observations(["10200", "71801"])
|
||
"""
|
||
url = "https://estaciones.api.adif.es/portroyalmanager/secure/stationsobservations/"
|
||
payload = {"stationCodes": station_codes}
|
||
|
||
data = self._make_request(url, payload, use_stations_key=True)
|
||
return data.get("stationObservations", [])
|
||
|
||
def get_all_departures_with_routes(
|
||
self,
|
||
station_code: str,
|
||
traffic_type: str = "ALL",
|
||
max_trains: int = 5
|
||
) -> List[Dict[str, Any]]:
|
||
"""
|
||
Obtiene salidas de una estación Y sus rutas completas
|
||
|
||
Args:
|
||
station_code: Código de estación
|
||
traffic_type: Tipo de tráfico
|
||
max_trains: Número máximo de trenes a procesar
|
||
|
||
Returns:
|
||
Lista de trenes con sus rutas
|
||
|
||
Example:
|
||
>>> client = AdifClient(ACCESS_KEY, SECRET_KEY)
|
||
>>> trains_with_routes = client.get_all_departures_with_routes("10200", "AVLDMD", max_trains=3)
|
||
>>> for train in trains_with_routes:
|
||
... print(f"Tren {train['commercial_number']}")
|
||
... for stop in train['route']:
|
||
... print(f" - {stop['stationCode']}")
|
||
"""
|
||
departures = self.get_departures(station_code, traffic_type)
|
||
result = []
|
||
|
||
for i, train in enumerate(departures[:max_trains]):
|
||
info = train['commercialPathInfo']
|
||
key = info['commercialPathKey']
|
||
commercial_key = key['commercialCirculationKey']
|
||
|
||
try:
|
||
route = self.get_train_route(
|
||
commercial_number=commercial_key['commercialNumber'],
|
||
launching_date=commercial_key['launchingDate'],
|
||
origin_station_code=key['originStationCode'],
|
||
destination_station_code=key['destinationStationCode']
|
||
)
|
||
|
||
result.append({
|
||
"commercial_number": commercial_key['commercialNumber'],
|
||
"traffic_type": info['trafficType'],
|
||
"origin_station": key['originStationCode'],
|
||
"destination_station": key['destinationStationCode'],
|
||
"launching_date": commercial_key['launchingDate'],
|
||
"train_info": train,
|
||
"route": route
|
||
})
|
||
except Exception as e:
|
||
print(f"⚠️ Error obteniendo ruta del tren {commercial_key['commercialNumber']}: {e}")
|
||
continue
|
||
|
||
return result
|
||
|
||
|
||
def demo():
|
||
"""Demostración del cliente"""
|
||
print("="*70)
|
||
print("DEMO DEL CLIENTE DE ADIF")
|
||
print("="*70)
|
||
|
||
ACCESS_KEY = "and20210615"
|
||
SECRET_KEY = "Jthjtr946RTt"
|
||
|
||
client = AdifClient(ACCESS_KEY, SECRET_KEY)
|
||
|
||
# 1. Salidas de Madrid Atocha
|
||
print("\n1️⃣ SALIDAS DE MADRID ATOCHA (Alta Velocidad)")
|
||
print("-" * 70)
|
||
try:
|
||
departures = client.get_departures("10200", "AVLDMD")
|
||
print(f"✅ Encontrados {len(departures)} trenes")
|
||
|
||
for i, train in enumerate(departures[:3]):
|
||
info = train['commercialPathInfo']
|
||
key = info['commercialPathKey']
|
||
passthrough = train.get('passthroughStep', {})
|
||
dep_sides = passthrough.get('departurePassthroughStepSides', {})
|
||
|
||
planned_time = dep_sides.get('plannedTime', 0)
|
||
if planned_time:
|
||
time_str = datetime.fromtimestamp(planned_time/1000).strftime('%H:%M')
|
||
else:
|
||
time_str = "N/A"
|
||
|
||
print(f"\n {i+1}. Tren {key['commercialCirculationKey']['commercialNumber']}")
|
||
print(f" Destino: {key['destinationStationCode']}")
|
||
print(f" Hora salida: {time_str}")
|
||
print(f" Estado: {dep_sides.get('circulationState', 'N/A')}")
|
||
|
||
except Exception as e:
|
||
print(f"❌ Error: {e}")
|
||
|
||
# 2. Ruta completa de un tren
|
||
print("\n\n2️⃣ RUTA COMPLETA DE UN TREN")
|
||
print("-" * 70)
|
||
try:
|
||
departures = client.get_departures("10200", "ALL")
|
||
if departures:
|
||
train = departures[0]
|
||
info = train['commercialPathInfo']
|
||
key = info['commercialPathKey']
|
||
commercial_key = key['commercialCirculationKey']
|
||
|
||
print(f"Consultando ruta del tren {commercial_key['commercialNumber']}...")
|
||
|
||
route = client.get_train_route(
|
||
commercial_number=commercial_key['commercialNumber'],
|
||
launching_date=commercial_key['launchingDate'],
|
||
origin_station_code=key['originStationCode'],
|
||
destination_station_code=key['destinationStationCode']
|
||
)
|
||
|
||
print(f"✅ Ruta con {len(route)} paradas:\n")
|
||
for i, stop in enumerate(route[:10]): # Primeras 10 paradas
|
||
stop_type = stop.get('stopType', 'N/A')
|
||
station_code = stop.get('stationCode', 'N/A')
|
||
|
||
# Info de salida
|
||
dep_sides = stop.get('departurePassthroughStepSides', {})
|
||
arr_sides = stop.get('arrivalPassthroughStepSides', {})
|
||
|
||
if dep_sides:
|
||
time_ms = dep_sides.get('plannedTime', 0)
|
||
if time_ms:
|
||
time_str = datetime.fromtimestamp(time_ms/1000).strftime('%H:%M')
|
||
print(f" {i+1}. {station_code} - Salida: {time_str} ({stop_type})")
|
||
elif arr_sides:
|
||
time_ms = arr_sides.get('plannedTime', 0)
|
||
if time_ms:
|
||
time_str = datetime.fromtimestamp(time_ms/1000).strftime('%H:%M')
|
||
print(f" {i+1}. {station_code} - Llegada: {time_str} ({stop_type})")
|
||
else:
|
||
print(f" {i+1}. {station_code} ({stop_type})")
|
||
|
||
except Exception as e:
|
||
print(f"❌ Error: {e}")
|
||
|
||
# 3. Observaciones de estaciones
|
||
print("\n\n3️⃣ OBSERVACIONES DE ESTACIONES")
|
||
print("-" * 70)
|
||
try:
|
||
observations = client.get_station_observations(["10200", "71801"])
|
||
print(f"✅ Observaciones de {len(observations)} estaciones")
|
||
|
||
for obs in observations:
|
||
station_code = obs.get('stationCode', 'N/A')
|
||
observation_text = obs.get('observation', 'Sin observaciones')
|
||
print(f"\n Estación {station_code}:")
|
||
print(f" {observation_text}")
|
||
|
||
except Exception as e:
|
||
print(f"❌ Error: {e}")
|
||
|
||
print("\n" + "="*70)
|
||
print("DEMO COMPLETADA")
|
||
print("="*70)
|
||
|
||
|
||
if __name__ == "__main__":
|
||
demo()
|