-
Notifications
You must be signed in to change notification settings - Fork 0
/
rtjp.py
199 lines (165 loc) · 7.14 KB
/
rtjp.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
#!/usr/bin/env python3
import argparse
import datetime
import json
import dill
import sys
from requests import Session
from requests.auth import HTTPBasicAuth
from zeep import Client, Settings
from zeep.cache import SqliteCache
from zeep.transports import Transport
from zeep.helpers import serialize_object
import xml.etree.ElementTree as etree
# See National Rail Enquiries Licence Request email
user = '<my rtjp username>'
password = '<my rtjp password>'
# WSDL location
wsdl_url = 'https://ojp.nationalrail.co.uk/webservices/jpdlr.wsdl'
cache_path = '/path/to/rtjp_zeep_cache.sqlite.db'
cache_life_seconds = 60*60*24*31
# Desired travel times:
# outward on 08:05 only
# inward on 16:00 or 16:29 or 16:30
want_from = 'ABD'
want_to = 'ARB'
want_outward_hour = 8
want_outward_min = 5
want_inward_hour = 16
want_fare_class = 'STANDARD'
# Configuration
debug = False
def remove_keys_recursively(dict_obj, keys):
""" Traverses the given dict_obj and removes all any keys
whose name appears in 'keys' (which can be a list of
key names, or it can be a string for a single key). """
for key in list(dict_obj.keys()):
if not isinstance(dict_obj, dict):
continue
elif key in keys:
dict_obj.pop(key, None)
elif isinstance(dict_obj[key], dict):
remove_keys_recursively(dict_obj[key], keys)
elif isinstance(dict_obj[key], list):
for item in dict_obj[key]:
remove_keys_recursively(item, keys)
return
def create_client(user, password):
""" Open a connection to the service, download the WSDL
if not already cached, and return the client object. """
# Create a HTTP session with Basic authentication
session = Session()
session.auth = HTTPBasicAuth(user, password)
# Determine the path to the WSDL cache
cache = SqliteCache(path=cache_path, timeout=cache_life_seconds)
# Create the transport using the HTTP session with the given cache
transport = Transport(session=session, cache=cache)
# Client settings
# currently turn off strict validation of XML otherwise it
# complains about missing items.
settings = Settings(strict=False)
# Open the client connection
client = Client(wsdl_url, transport=transport, settings=settings)
return client
def save_response_to_file(tomorrow, resp):
""" Save the response for the given day into a file. """
with open(f'response_{tomorrow}.dill', 'wb') as fd:
dill.dump(resp, fd)
def load_response_from_file(tomorrow):
""" Load the response for the given day (str: YYYY-MM-DD)
from a pickle file. """
response_file = f'response_{tomorrow}.dill'
with open(response_file, 'rb') as fd:
resp = dill.load(fd)
if debug:
print('LOADED RESPONSE FROM FILE %s WITH CONTENT: %s' % (response_file, resp.keys()))
return resp
def parse_response(resp):
""" Print out the train and fare details from the given response
which should be a dict that has no XML. """
bulletin = ''
prev_time_str = ''
for journey in resp['outwardJourney']:
journey_time = journey['timetable']['scheduled']['departure']
if journey_time.hour != want_outward_hour or journey_time.minute != want_outward_min:
continue
journey_time_str = journey_time.strftime('%a %Y-%m-%d %H:%M')
for fare in sorted(journey['fare'], key = lambda x: x['totalPrice']):
if fare['fareClass'] != want_fare_class:
continue
if journey_time_str == prev_time_str:
journey_time_str = '""""""""""""""""""""'
else:
prev_time_str = journey_time_str
print('%s = £%.02f %s' % (journey_time_str, int(fare['totalPrice'])/100.0, fare['description']))
for bull in journey['serviceBulletins']:
if not bull['cleared']:
bull_desc = bull['description']
if bull_desc not in bulletin:
bulletin += bull_desc + '. '
for journey in resp['inwardJourney']:
journey_time = journey['timetable']['scheduled']['departure']
if journey_time.hour != want_inward_hour:
continue
journey_time_str = journey_time.strftime('%a %Y-%m-%d %H:%M')
for fare in sorted(journey['fare'], key = lambda x: x['totalPrice']):
if fare['fareClass'] != want_fare_class:
continue
if journey_time_str == prev_time_str:
journey_time_str = '""""""""""""""""""""'
else:
prev_time_str = journey_time_str
print('%s = £%.02f %s' % (journey_time_str, int(fare['totalPrice'])/100.0, fare['description']))
for bull in journey['serviceBulletins']:
if not bull['cleared']:
bull_desc = bull['description']
if bull_desc not in bulletin:
bulletin += bull_desc + '. '
print(bulletin)
def debug_request():
node = client.create_message(client.service, 'RealtimeJourneyPlan',
origin = { 'stationCRS': 'BYF' }, destination = { 'stationCRS': 'EDB' },
realtimeEnquiry = 'STANDARD',
outwardTime = { 'departBy': f'{tomorrow}T08:00:00' },
directTrains = False
)
print('Message to be sent:')
print (etree.tostring(node))#, pretty_print=True))
def send_request(client, tomorrow):
""" Given a string in the form YYYY-MM-DD return the response dict
which has been sanitised so it doesn't contain any XML. """
response = client.service.RealtimeJourneyPlan(
origin = { 'stationCRS': want_from }, destination = { 'stationCRS': want_to },
realtimeEnquiry = 'STANDARD',
outwardTime = { 'departBy': f'{tomorrow}T08:00:00' },
inwardTime = { 'departBy': f'{tomorrow}T16:00:00' },
fareRequestDetails = { 'passengers': { 'adult': 1, 'child': 0 }, 'fareClass': want_fare_class },
directTrains = False,
includeAdditionalInformation = False,
)
# Convert the response into a dict
response_without_xml = serialize_object(response, dict)
# Remove all dict keys called _raw_elements because they contain
# raw XML which cannot be serialised to json or stored in a dill pickle
remove_keys_recursively(response_without_xml, '_raw_elements')
return response_without_xml
if __name__ == '__main__':
parser = argparse.ArgumentParser(description = 'OJP client')
parser.add_argument('-d', '--debug', action='store_true', help='debug')
parser.add_argument('-i', '--input', action='store', help='load an existing response for YYYY-MM-DD')
parser.add_argument('-q', '--query', action='store', help='query for YYYY-MM-DD')
args = parser.parse_args()
if args.debug:
debug = True
tomorrow = datetime.date.today() + datetime.timedelta(days=1)
tomorrow_str = tomorrow.strftime('%Y-%m-%d')
if args.input:
tomorrow_str = args.input
resp = load_response_from_file(tomorrow_str)
parse_response(resp)
if args.query:
tomorrow_str = args.query
client = create_client(user, password)
resp = send_request(client, tomorrow_str)
parse_response(resp)
save_response_to_file(tomorrow_str, resp)