Add return and inbound journey support
This commit is contained in:
parent
6ba71447ef
commit
9691632f65
12 changed files with 1687 additions and 486 deletions
|
|
@ -16,7 +16,8 @@ DEFAULT_UA = (
|
|||
"(KHTML, like Gecko) Chrome/122.0.0.0 Safari/537.36"
|
||||
)
|
||||
|
||||
ORIGIN_STATION_ID = '7015400'
|
||||
ST_PANCRAS_STATION_ID = '7015400'
|
||||
ORIGIN_STATION_ID = ST_PANCRAS_STATION_ID
|
||||
|
||||
DESTINATION_STATION_IDS = {
|
||||
'Paris Gare du Nord': '8727100',
|
||||
|
|
@ -35,11 +36,11 @@ _GATEWAY_URL = 'https://site-api.eurostar.com/gateway'
|
|||
_GQL_QUERY = (
|
||||
"query NewBookingSearch("
|
||||
"$origin:String!,$destination:String!,$outbound:String!,"
|
||||
"$currency:Currency!,$adult:Int,"
|
||||
"$inbound:String,$currency:Currency!,$adult:Int,"
|
||||
"$filteredClassesOfService:[ClassOfServiceEnum]"
|
||||
"){"
|
||||
"journeySearch("
|
||||
"outboundDate:$outbound origin:$origin destination:$destination"
|
||||
"outboundDate:$outbound inboundDate:$inbound origin:$origin destination:$destination"
|
||||
" adults:$adult currency:$currency"
|
||||
" productFamilies:[\"PUB\"] contractCode:\"EIL_ALL\""
|
||||
" adults16Plus:0 children:0 youths:0 children4Only:0 children5To11:0"
|
||||
|
|
@ -64,6 +65,22 @@ _GQL_QUERY = (
|
|||
"}"
|
||||
"}"
|
||||
"}"
|
||||
"inbound{"
|
||||
"journeys("
|
||||
"hideIndirectTrainsWhenDisruptedAndCancelled:false"
|
||||
" hideDepartedTrains:true"
|
||||
" hideExternalCarrierTrains:true"
|
||||
" hideDirectExternalCarrierTrains:true"
|
||||
"){"
|
||||
"timing{departureTime:departs arrivalTime:arrives}"
|
||||
"fares(filteredClassesOfService:$filteredClassesOfService){"
|
||||
"classOfService{code}"
|
||||
"prices{displayPrice}"
|
||||
"seats "
|
||||
"legs{serviceName serviceType{code}}"
|
||||
"}"
|
||||
"}"
|
||||
"}"
|
||||
"}"
|
||||
"}"
|
||||
)
|
||||
|
|
@ -72,11 +89,19 @@ _STANDARD = 'STANDARD'
|
|||
_STANDARD_PLUS = 'PLUS'
|
||||
|
||||
|
||||
def search_url(destination: str, travel_date: str) -> 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
|
||||
outbound = travel_date
|
||||
inbound = return_date
|
||||
if direction == "inbound":
|
||||
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_STATION_ID}&destination={dest_id}&outbound={travel_date}'
|
||||
f'?adult=1&origin={origin}&destination={destination_id}&outbound={outbound}'
|
||||
+ (f'&inbound={inbound}' if inbound else '')
|
||||
)
|
||||
|
||||
|
||||
|
|
@ -85,7 +110,7 @@ def _generate_cid() -> str:
|
|||
return 'SRCH-' + ''.join(random.choices(chars, k=22))
|
||||
|
||||
|
||||
def _parse_graphql(data: dict, destination: str) -> list[dict]:
|
||||
def _parse_journeys(journeys: list[dict], destination: str, direction: str) -> list[dict]:
|
||||
"""
|
||||
Parse a NewBookingSearch GraphQL response into a list of service dicts.
|
||||
|
||||
|
|
@ -97,7 +122,6 @@ def _parse_graphql(data: dict, destination: str) -> list[dict]:
|
|||
Multi-leg train numbers are joined with ' + ' (e.g. 'ES 9116 + ER 9329').
|
||||
"""
|
||||
best: dict[str, dict] = {}
|
||||
journeys = data['data']['journeySearch']['outbound']['journeys']
|
||||
for journey in journeys:
|
||||
dep = journey['timing']['departureTime']
|
||||
arr = journey['timing']['arrivalTime']
|
||||
|
|
@ -118,8 +142,21 @@ def _parse_graphql(data: dict, destination: str) -> list[dict]:
|
|||
std_price, std_seats = price, seats
|
||||
elif cos == _STANDARD_PLUS:
|
||||
plus_price, plus_seats = price, seats
|
||||
if dep not in best or arr < best[dep]['arrive_destination']:
|
||||
best[dep] = {
|
||||
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,
|
||||
}
|
||||
key = dep
|
||||
arrive_key = 'arrive_st_pancras'
|
||||
else:
|
||||
service = {
|
||||
'depart_st_pancras': dep,
|
||||
'arrive_destination': arr,
|
||||
'destination': destination,
|
||||
|
|
@ -129,18 +166,43 @@ def _parse_graphql(data: dict, destination: str) -> list[dict]:
|
|||
'plus_price': plus_price,
|
||||
'plus_seats': plus_seats,
|
||||
}
|
||||
return sorted(best.values(), key=lambda s: s['depart_st_pancras'])
|
||||
key = dep
|
||||
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'
|
||||
return sorted(best.values(), key=lambda s: s[sort_key])
|
||||
|
||||
|
||||
def fetch(destination: str, travel_date: str) -> list[dict]:
|
||||
"""
|
||||
Return all Eurostar services for destination on travel_date.
|
||||
def _parse_graphql(data: dict, destination: str) -> list[dict]:
|
||||
journeys = data['data']['journeySearch']['outbound']['journeys']
|
||||
return _parse_journeys(journeys, destination, 'outbound')
|
||||
|
||||
Each dict contains timetable info (depart_st_pancras, arrive_destination,
|
||||
train_number) plus pricing (price, seats) from a single GraphQL call.
|
||||
"""
|
||||
dest_id = DESTINATION_STATION_IDS[destination]
|
||||
headers = {
|
||||
|
||||
def _parse_graphql_leg(data: dict, destination: str, leg: str, direction: str) -> list[dict]:
|
||||
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],
|
||||
}
|
||||
return {
|
||||
'operationName': 'NewBookingSearch',
|
||||
'variables': variables,
|
||||
'query': _GQL_QUERY,
|
||||
}
|
||||
|
||||
|
||||
def _headers() -> dict:
|
||||
return {
|
||||
'User-Agent': DEFAULT_UA,
|
||||
'Content-Type': 'application/json',
|
||||
'Accept': '*/*',
|
||||
|
|
@ -151,18 +213,42 @@ def fetch(destination: str, travel_date: str) -> list[dict]:
|
|||
'x-source-url': 'search-app/',
|
||||
'cid': _generate_cid(),
|
||||
}
|
||||
payload = {
|
||||
'operationName': 'NewBookingSearch',
|
||||
'variables': {
|
||||
'origin': ORIGIN_STATION_ID,
|
||||
'destination': dest_id,
|
||||
'outbound': travel_date,
|
||||
'currency': 'GBP',
|
||||
'adult': 1,
|
||||
'filteredClassesOfService': [_STANDARD, _STANDARD_PLUS],
|
||||
},
|
||||
'query': _GQL_QUERY,
|
||||
}
|
||||
resp = requests.post(_GATEWAY_URL, json=payload, headers=headers, timeout=20)
|
||||
|
||||
|
||||
def fetch(destination: str, travel_date: str, direction: str = 'outbound') -> list[dict]:
|
||||
"""
|
||||
Return all Eurostar services for destination on travel_date.
|
||||
|
||||
Each dict contains timetable info (depart_st_pancras, arrive_destination,
|
||||
train_number) plus pricing (price, seats) from a single GraphQL call.
|
||||
"""
|
||||
dest_id = DESTINATION_STATION_IDS[destination]
|
||||
if direction == 'inbound':
|
||||
origin, destination_id = dest_id, ST_PANCRAS_STATION_ID
|
||||
else:
|
||||
origin, destination_id = ST_PANCRAS_STATION_ID, dest_id
|
||||
resp = requests.post(
|
||||
_GATEWAY_URL,
|
||||
json=_payload(origin, destination_id, travel_date),
|
||||
headers=_headers(),
|
||||
timeout=20,
|
||||
)
|
||||
resp.raise_for_status()
|
||||
return _parse_graphql(resp.json(), destination)
|
||||
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]]:
|
||||
dest_id = DESTINATION_STATION_IDS[destination]
|
||||
resp = requests.post(
|
||||
_GATEWAY_URL,
|
||||
json=_payload(ST_PANCRAS_STATION_ID, dest_id, outbound_date, return_date),
|
||||
headers=_headers(),
|
||||
timeout=20,
|
||||
)
|
||||
resp.raise_for_status()
|
||||
data = resp.json()
|
||||
return {
|
||||
'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