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:
2025-12-05 11:22:13 +01:00
parent aa02d7c896
commit 68fac80520
42 changed files with 66402 additions and 4876 deletions

392
adif_client.py Executable file
View 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()