Refactor: reorganización completa del proyecto y documentación consolidada
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
This commit is contained in:
392
adif_client.py
Executable file
392
adif_client.py
Executable file
@@ -0,0 +1,392 @@
|
||||
#!/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()
|
||||
Reference in New Issue
Block a user