Initial import of ADIF API reverse-engineering toolkit

This commit is contained in:
2025-12-16 08:37:56 +01:00
commit 60388529c1
11486 changed files with 1086536 additions and 0 deletions

401
adif_client.py Executable file
View File

@@ -0,0 +1,401 @@
#!/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()