Initial import of ADIF API reverse-engineering toolkit
This commit is contained in:
401
adif_client.py
Executable file
401
adif_client.py
Executable 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()
|
||||
Reference in New Issue
Block a user