Files
adif-api-reverse-engineering/adif_client.py

402 lines
14 KiB
Python
Executable File
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/usr/bin/env python3
"""
Complete ADIF API client.
Implements every functional endpoint with easy-to-use methods.
Includes error handling and basic data validation.
"""
from __future__ import annotations
import uuid
from datetime import datetime
from typing import Any, Dict, List
import requests
from requests import Response
from adif_auth import AdifAuthenticator
class AdifClient:
"""Client to interact with the ADIF API"""
def __init__(self, access_key: str, secret_key: str) -> None:
"""
Initialize the client with HMAC credentials.
"""
self.auth: AdifAuthenticator = AdifAuthenticator(
access_key=access_key, secret_key=secret_key
)
self.session: requests.Session = requests.Session()
def _make_request(
self, url: str, payload: Dict[str, Any], use_stations_key: bool = False
) -> Dict[str, Any]:
"""
Execute a single authenticated POST request against the API.
Args:
url: Endpoint URL
payload: Data to send
use_stations_key: If True, use USER_KEY_STATIONS instead of USER_KEY_CIRCULATION
Returns:
Decoded JSON response body.
Raises:
PermissionError: When the API returns 401.
ValueError: When the API returns 400 due to payload issues.
Exception: For any other unexpected status code.
"""
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: Response = self.session.post(
url, json=payload, headers=headers, timeout=30
)
if response.status_code == 200:
return response.json()
if response.status_code == 204:
return {"message": "No content available", "commercialPaths": []}
if response.status_code == 401:
raise PermissionError(
"Unauthorized - Keys do not have permissions for this endpoint"
)
if response.status_code == 400:
raise ValueError(f"Bad Request - Invalid payload: {response.text}")
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]]:
"""
Gets departures from a station.
Args:
station_code: Station code (e.g.: "10200")
traffic_type: Traffic type (ALL, CERCANIAS, AVLDMD, TRAVELERS, GOODS)
page_number: Page number (default 0)
commercial_service: BOTH, YES, NOT
commercial_stop_type: BOTH, YES, NOT
Returns:
List of trains
Example:
>>> client = AdifClient(ACCESS_KEY, SECRET_KEY)
>>> trains = client.get_departures("10200", "AVLDMD")
>>> for train in trains:
... print(f\"{train['commercialNumber']} - Destination: {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]]:
"""
Gets arrivals to a station.
Args:
station_code: Station code (e.g.: "10200")
traffic_type: Traffic type (ALL, CERCANIAS, AVLDMD, TRAVELERS, GOODS)
page_number: Page number (default 0)
commercial_service: BOTH, YES, NOT
commercial_stop_type: BOTH, YES, NOT
Returns:
List of trains
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]]:
"""
Gets a train's full route (all stops).
Args:
commercial_number: Train commercial number (e.g.: "03194")
launching_date: Departure date in milliseconds since epoch
origin_station_code: Origin station code
destination_station_code: Destination station code
all_control_points: If True, include all control points
Returns:
List of train stops
Example:
>>> # First fetch a real train
>>> trains = client.get_departures("10200", "AVLDMD")
>>> train = trains[0]
>>> info = train['commercialPathInfo']
>>> key = info['commercialPathKey']
>>>
>>> # Fetch its complete route
>>> 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\"Stop: {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]]:
"""
Gets station observations.
Args:
station_codes: List of station codes
Returns:
List of observations
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]]:
"""
Gets departures from a station AND their complete routes.
Args:
station_code: Station code
traffic_type: Traffic type
max_trains: Maximum number of trains to process
Returns:
List of trains with their routes
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\"Train {train['commercial_number']}\")
... for stop in train['route']:
... print(f\" - {stop['stationCode']}\")
"""
departures = self.get_departures(station_code, traffic_type)
result: List[Dict[str, Any]] = []
for train in 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 getting route for train {commercial_key['commercialNumber']}: {e}"
)
continue
return result
def demo() -> None:
"""Run a simple interactive demo against live endpoints."""
print("=" * 70)
print("ADIF CLIENT DEMO")
print("=" * 70)
ACCESS_KEY = "and20210615"
SECRET_KEY = "Jthjtr946RTt"
client = AdifClient(ACCESS_KEY, SECRET_KEY)
# 1. Departures from Madrid Atocha
print("\n1⃣ DEPARTURES FROM MADRID ATOCHA (High Speed)")
print("-" * 70)
try:
departures = client.get_departures("10200", "AVLDMD")
print(f"✅ Found {len(departures)} trains")
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}. Train {key['commercialCirculationKey']['commercialNumber']}"
)
print(f" Destination: {key['destinationStationCode']}")
print(f" Departure time: {time_str}")
print(f" Status: {dep_sides.get('circulationState', 'N/A')}")
except Exception as e:
print(f"❌ Error: {e}")
# 2. Full route of a train
print("\n\n2⃣ FULL ROUTE OF A TRAIN")
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"Fetching route for train {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"✅ Route with {len(route)} stops:\n")
for i, stop in enumerate(route[:10]): # First 10 stops
stop_type = stop.get("stopType", "N/A")
station_code = stop.get("stationCode", "N/A")
# Departure/arrival info
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} - Departure: {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} - Arrival: {time_str} ({stop_type})"
)
else:
print(f" {i+1}. {station_code} ({stop_type})")
except Exception as e:
print(f"❌ Error: {e}")
# 3. Station observations
print("\n\n3⃣ STATION OBSERVATIONS")
print("-" * 70)
try:
observations = client.get_station_observations(["10200", "71801"])
print(f"✅ Observations from {len(observations)} stations")
for obs in observations:
station_code = obs.get("stationCode", "N/A")
observation_text = obs.get("observation", "No observations")
print(f"\n Station {station_code}:")
print(f" {observation_text}")
except Exception as e:
print(f"❌ Error: {e}")
print("\n" + "=" * 70)
print("DEMO COMPLETE")
print("=" * 70)
if __name__ == "__main__":
demo()