#!/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()