Add full type annotations and black formatting across all modules
Annotated all functions with mypy --strict-compatible types (-> None, dict[str, Any], Generator types, etc.), added # type: ignore for untyped third-party libs (lxml), and reformatted with black. All 18 source files now pass mypy --strict with zero errors. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
453d6244ec
commit
13c4341f3a
14 changed files with 1802 additions and 974 deletions
|
|
@ -6,8 +6,10 @@ NewBookingSearch) returns departure time, arrival time, train number,
|
|||
Eurostar Standard fare price, and seats remaining at that price for every
|
||||
service on the requested date.
|
||||
"""
|
||||
|
||||
import random
|
||||
import string
|
||||
from typing import Any
|
||||
|
||||
import requests
|
||||
|
||||
|
|
@ -16,19 +18,19 @@ DEFAULT_UA = (
|
|||
"(KHTML, like Gecko) Chrome/122.0.0.0 Safari/537.36"
|
||||
)
|
||||
|
||||
ST_PANCRAS_STATION_ID = '7015400'
|
||||
ST_PANCRAS_STATION_ID = "7015400"
|
||||
ORIGIN_STATION_ID = ST_PANCRAS_STATION_ID
|
||||
|
||||
DESTINATION_STATION_IDS = {
|
||||
'Paris Gare du Nord': '8727100',
|
||||
'Brussels Midi': '8814001',
|
||||
'Lille Europe': '8722326',
|
||||
'Amsterdam Centraal': '8400058',
|
||||
'Rotterdam Centraal': '8400530',
|
||||
'Cologne Hbf': '8015458',
|
||||
"Paris Gare du Nord": "8727100",
|
||||
"Brussels Midi": "8814001",
|
||||
"Lille Europe": "8722326",
|
||||
"Amsterdam Centraal": "8400058",
|
||||
"Rotterdam Centraal": "8400530",
|
||||
"Cologne Hbf": "8015458",
|
||||
}
|
||||
|
||||
_GATEWAY_URL = 'https://site-api.eurostar.com/gateway'
|
||||
_GATEWAY_URL = "https://site-api.eurostar.com/gateway"
|
||||
|
||||
# Query requesting timing, train identity, and Standard fare price + seats.
|
||||
# Variable names and argument names match the site's own query so the
|
||||
|
|
@ -42,7 +44,7 @@ _GQL_QUERY = (
|
|||
"journeySearch("
|
||||
"outboundDate:$outbound inboundDate:$inbound origin:$origin destination:$destination"
|
||||
" adults:$adult currency:$currency"
|
||||
" productFamilies:[\"PUB\"] contractCode:\"EIL_ALL\""
|
||||
' productFamilies:["PUB"] contractCode:"EIL_ALL"'
|
||||
" adults16Plus:0 children:0 youths:0 children4Only:0 children5To11:0"
|
||||
" infants:0 adultsWheelchair:0 childrenWheelchair:0 guideDogs:0"
|
||||
" wheelchairCompanions:0 nonWheelchairCompanions:0"
|
||||
|
|
@ -85,11 +87,16 @@ _GQL_QUERY = (
|
|||
"}"
|
||||
)
|
||||
|
||||
_STANDARD = 'STANDARD'
|
||||
_STANDARD_PLUS = 'PLUS'
|
||||
_STANDARD = "STANDARD"
|
||||
_STANDARD_PLUS = "PLUS"
|
||||
|
||||
|
||||
def search_url(destination: str, travel_date: str, direction: str = "outbound", return_date: str | None = None) -> str:
|
||||
def search_url(
|
||||
destination: str,
|
||||
travel_date: str,
|
||||
direction: str = "outbound",
|
||||
return_date: str | None = None,
|
||||
) -> str:
|
||||
dest_id = DESTINATION_STATION_IDS[destination]
|
||||
origin = ST_PANCRAS_STATION_ID
|
||||
destination_id = dest_id
|
||||
|
|
@ -99,18 +106,20 @@ def search_url(destination: str, travel_date: str, direction: str = "outbound",
|
|||
origin, destination_id = dest_id, ST_PANCRAS_STATION_ID
|
||||
inbound = None
|
||||
return (
|
||||
f'https://www.eurostar.com/search/uk-en'
|
||||
f'?adult=1&origin={origin}&destination={destination_id}&outbound={outbound}'
|
||||
+ (f'&inbound={inbound}' if inbound else '')
|
||||
f"https://www.eurostar.com/search/uk-en"
|
||||
f"?adult=1&origin={origin}&destination={destination_id}&outbound={outbound}"
|
||||
+ (f"&inbound={inbound}" if inbound else "")
|
||||
)
|
||||
|
||||
|
||||
def _generate_cid() -> str:
|
||||
chars = string.ascii_letters + string.digits
|
||||
return 'SRCH-' + ''.join(random.choices(chars, k=22))
|
||||
return "SRCH-" + "".join(random.choices(chars, k=22))
|
||||
|
||||
|
||||
def _parse_journeys(journeys: list[dict], destination: str, direction: str) -> list[dict]:
|
||||
def _parse_journeys(
|
||||
journeys: list[dict[str, Any]], destination: str, direction: str
|
||||
) -> list[dict[str, Any]]:
|
||||
"""
|
||||
Parse a NewBookingSearch GraphQL response into a list of service dicts.
|
||||
|
||||
|
|
@ -121,101 +130,108 @@ def _parse_journeys(journeys: list[dict], destination: str, direction: str) -> l
|
|||
connecting trains); we keep the entry with the earliest arrival.
|
||||
Multi-leg train numbers are joined with ' + ' (e.g. 'ES 9116 + ER 9329').
|
||||
"""
|
||||
best: dict[str, dict] = {}
|
||||
best: dict[str, dict[str, Any]] = {}
|
||||
for journey in journeys:
|
||||
dep = journey['timing']['departureTime']
|
||||
arr = journey['timing']['arrivalTime']
|
||||
dep = journey["timing"]["departureTime"]
|
||||
arr = journey["timing"]["arrivalTime"]
|
||||
std_price = std_seats = plus_price = plus_seats = None
|
||||
train_number = ''
|
||||
for fare in (journey.get('fares') or []):
|
||||
cos = fare['classOfService']['code']
|
||||
p = fare.get('prices')
|
||||
price = float(p['displayPrice']) if p and p.get('displayPrice') else None
|
||||
seats = fare.get('seats')
|
||||
train_number = ""
|
||||
for fare in journey.get("fares") or []:
|
||||
cos = fare["classOfService"]["code"]
|
||||
p = fare.get("prices")
|
||||
price = float(p["displayPrice"]) if p and p.get("displayPrice") else None
|
||||
seats = fare.get("seats")
|
||||
if not train_number:
|
||||
legs = fare.get('legs') or []
|
||||
train_number = ' + '.join(
|
||||
legs = fare.get("legs") or []
|
||||
train_number = " + ".join(
|
||||
f"{(leg.get('serviceType') or {}).get('code', 'ES')} {leg['serviceName']}"
|
||||
for leg in legs if leg.get('serviceName')
|
||||
for leg in legs
|
||||
if leg.get("serviceName")
|
||||
)
|
||||
if cos == _STANDARD:
|
||||
std_price, std_seats = price, seats
|
||||
elif cos == _STANDARD_PLUS:
|
||||
plus_price, plus_seats = price, seats
|
||||
if direction == 'inbound':
|
||||
if direction == "inbound":
|
||||
service = {
|
||||
'depart_destination': dep,
|
||||
'arrive_st_pancras': arr,
|
||||
'destination': destination,
|
||||
'train_number': train_number,
|
||||
'price': std_price,
|
||||
'seats': std_seats,
|
||||
'plus_price': plus_price,
|
||||
'plus_seats': plus_seats,
|
||||
"depart_destination": dep,
|
||||
"arrive_st_pancras": arr,
|
||||
"destination": destination,
|
||||
"train_number": train_number,
|
||||
"price": std_price,
|
||||
"seats": std_seats,
|
||||
"plus_price": plus_price,
|
||||
"plus_seats": plus_seats,
|
||||
}
|
||||
key = dep
|
||||
arrive_key = 'arrive_st_pancras'
|
||||
arrive_key = "arrive_st_pancras"
|
||||
else:
|
||||
service = {
|
||||
'depart_st_pancras': dep,
|
||||
'arrive_destination': arr,
|
||||
'destination': destination,
|
||||
'train_number': train_number,
|
||||
'price': std_price,
|
||||
'seats': std_seats,
|
||||
'plus_price': plus_price,
|
||||
'plus_seats': plus_seats,
|
||||
"depart_st_pancras": dep,
|
||||
"arrive_destination": arr,
|
||||
"destination": destination,
|
||||
"train_number": train_number,
|
||||
"price": std_price,
|
||||
"seats": std_seats,
|
||||
"plus_price": plus_price,
|
||||
"plus_seats": plus_seats,
|
||||
}
|
||||
key = dep
|
||||
arrive_key = 'arrive_destination'
|
||||
arrive_key = "arrive_destination"
|
||||
if key not in best or arr < best[key][arrive_key]:
|
||||
best[key] = service
|
||||
sort_key = 'depart_destination' if direction == 'inbound' else 'depart_st_pancras'
|
||||
sort_key = "depart_destination" if direction == "inbound" else "depart_st_pancras"
|
||||
return sorted(best.values(), key=lambda s: s[sort_key])
|
||||
|
||||
|
||||
def _parse_graphql(data: dict, destination: str) -> list[dict]:
|
||||
journeys = data['data']['journeySearch']['outbound']['journeys']
|
||||
return _parse_journeys(journeys, destination, 'outbound')
|
||||
def _parse_graphql(data: dict[str, Any], destination: str) -> list[dict[str, Any]]:
|
||||
journeys = data["data"]["journeySearch"]["outbound"]["journeys"]
|
||||
return _parse_journeys(journeys, destination, "outbound")
|
||||
|
||||
|
||||
def _parse_graphql_leg(data: dict, destination: str, leg: str, direction: str) -> list[dict]:
|
||||
journeys = data['data']['journeySearch'][leg]['journeys']
|
||||
def _parse_graphql_leg(
|
||||
data: dict[str, Any], destination: str, leg: str, direction: str
|
||||
) -> list[dict[str, Any]]:
|
||||
journeys = data["data"]["journeySearch"][leg]["journeys"]
|
||||
return _parse_journeys(journeys, destination, direction)
|
||||
|
||||
|
||||
def _payload(origin: str, destination_id: str, outbound: str, inbound: str | None = None) -> dict:
|
||||
variables = {
|
||||
'origin': origin,
|
||||
'destination': destination_id,
|
||||
'outbound': outbound,
|
||||
'inbound': inbound,
|
||||
'currency': 'GBP',
|
||||
'adult': 1,
|
||||
'filteredClassesOfService': [_STANDARD, _STANDARD_PLUS],
|
||||
def _payload(
|
||||
origin: str, destination_id: str, outbound: str, inbound: str | None = None
|
||||
) -> dict[str, Any]:
|
||||
variables: dict[str, Any] = {
|
||||
"origin": origin,
|
||||
"destination": destination_id,
|
||||
"outbound": outbound,
|
||||
"inbound": inbound,
|
||||
"currency": "GBP",
|
||||
"adult": 1,
|
||||
"filteredClassesOfService": [_STANDARD, _STANDARD_PLUS],
|
||||
}
|
||||
return {
|
||||
'operationName': 'NewBookingSearch',
|
||||
'variables': variables,
|
||||
'query': _GQL_QUERY,
|
||||
"operationName": "NewBookingSearch",
|
||||
"variables": variables,
|
||||
"query": _GQL_QUERY,
|
||||
}
|
||||
|
||||
|
||||
def _headers() -> dict:
|
||||
def _headers() -> dict[str, str]:
|
||||
return {
|
||||
'User-Agent': DEFAULT_UA,
|
||||
'Content-Type': 'application/json',
|
||||
'Accept': '*/*',
|
||||
'Accept-Language':'en-GB',
|
||||
'Referer': 'https://www.eurostar.com/',
|
||||
'x-platform': 'web',
|
||||
'x-market-code': 'uk',
|
||||
'x-source-url': 'search-app/',
|
||||
'cid': _generate_cid(),
|
||||
"User-Agent": DEFAULT_UA,
|
||||
"Content-Type": "application/json",
|
||||
"Accept": "*/*",
|
||||
"Accept-Language": "en-GB",
|
||||
"Referer": "https://www.eurostar.com/",
|
||||
"x-platform": "web",
|
||||
"x-market-code": "uk",
|
||||
"x-source-url": "search-app/",
|
||||
"cid": _generate_cid(),
|
||||
}
|
||||
|
||||
|
||||
def fetch(destination: str, travel_date: str, direction: str = 'outbound') -> list[dict]:
|
||||
def fetch(
|
||||
destination: str, travel_date: str, direction: str = "outbound"
|
||||
) -> list[dict[str, Any]]:
|
||||
"""
|
||||
Return all Eurostar services for destination on travel_date.
|
||||
|
||||
|
|
@ -223,7 +239,7 @@ def fetch(destination: str, travel_date: str, direction: str = 'outbound') -> li
|
|||
train_number) plus pricing (price, seats) from a single GraphQL call.
|
||||
"""
|
||||
dest_id = DESTINATION_STATION_IDS[destination]
|
||||
if direction == 'inbound':
|
||||
if direction == "inbound":
|
||||
origin, destination_id = dest_id, ST_PANCRAS_STATION_ID
|
||||
else:
|
||||
origin, destination_id = ST_PANCRAS_STATION_ID, dest_id
|
||||
|
|
@ -234,11 +250,13 @@ def fetch(destination: str, travel_date: str, direction: str = 'outbound') -> li
|
|||
timeout=20,
|
||||
)
|
||||
resp.raise_for_status()
|
||||
leg_direction = 'inbound' if direction == 'inbound' else 'outbound'
|
||||
return _parse_graphql_leg(resp.json(), destination, 'outbound', leg_direction)
|
||||
leg_direction = "inbound" if direction == "inbound" else "outbound"
|
||||
return _parse_graphql_leg(resp.json(), destination, "outbound", leg_direction)
|
||||
|
||||
|
||||
def fetch_return(destination: str, outbound_date: str, return_date: str) -> dict[str, list[dict]]:
|
||||
def fetch_return(
|
||||
destination: str, outbound_date: str, return_date: str
|
||||
) -> dict[str, list[dict[str, Any]]]:
|
||||
dest_id = DESTINATION_STATION_IDS[destination]
|
||||
resp = requests.post(
|
||||
_GATEWAY_URL,
|
||||
|
|
@ -249,6 +267,6 @@ def fetch_return(destination: str, outbound_date: str, return_date: str) -> dict
|
|||
resp.raise_for_status()
|
||||
data = resp.json()
|
||||
return {
|
||||
'outbound': _parse_graphql_leg(data, destination, 'outbound', 'outbound'),
|
||||
'inbound': _parse_graphql_leg(data, destination, 'inbound', 'inbound'),
|
||||
"outbound": _parse_graphql_leg(data, destination, "outbound", "outbound"),
|
||||
"inbound": _parse_graphql_leg(data, destination, "inbound", "inbound"),
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue