PyTransportNSWv2 0.5.0__tar.gz → 0.5.2__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {PyTransportNSWv2-0.5.0 → PyTransportNSWv2-0.5.2}/PKG-INFO +1 -1
- {PyTransportNSWv2-0.5.0 → PyTransportNSWv2-0.5.2}/PyTransportNSWv2.egg-info/PKG-INFO +1 -1
- PyTransportNSWv2-0.5.2/TransportNSWv2/TransportNSWv2.py +430 -0
- {PyTransportNSWv2-0.5.0 → PyTransportNSWv2-0.5.2}/setup.py +1 -1
- PyTransportNSWv2-0.5.0/TransportNSWv2/TransportNSWv2.py +0 -346
- {PyTransportNSWv2-0.5.0 → PyTransportNSWv2-0.5.2}/LICENSE +0 -0
- {PyTransportNSWv2-0.5.0 → PyTransportNSWv2-0.5.2}/PyTransportNSWv2.egg-info/SOURCES.txt +0 -0
- {PyTransportNSWv2-0.5.0 → PyTransportNSWv2-0.5.2}/PyTransportNSWv2.egg-info/dependency_links.txt +0 -0
- {PyTransportNSWv2-0.5.0 → PyTransportNSWv2-0.5.2}/PyTransportNSWv2.egg-info/requires.txt +0 -0
- {PyTransportNSWv2-0.5.0 → PyTransportNSWv2-0.5.2}/PyTransportNSWv2.egg-info/top_level.txt +0 -0
- {PyTransportNSWv2-0.5.0 → PyTransportNSWv2-0.5.2}/README.md +0 -0
- {PyTransportNSWv2-0.5.0 → PyTransportNSWv2-0.5.2}/TransportNSWv2/__init__.py +0 -0
- {PyTransportNSWv2-0.5.0 → PyTransportNSWv2-0.5.2}/setup.cfg +0 -0
@@ -0,0 +1,430 @@
|
|
1
|
+
""" A module to query Transport NSW (Australia) departure times. """
|
2
|
+
""" First created by Dav0815 ( https://pypi.org/user/Dav0815/) """
|
3
|
+
""" Extended by AndyStewart999 ( https://pypi.org/user/andystewart999/ ) """
|
4
|
+
|
5
|
+
from datetime import datetime, timedelta
|
6
|
+
from google.transit import gtfs_realtime_pb2
|
7
|
+
import requests.exceptions
|
8
|
+
import requests
|
9
|
+
import logging
|
10
|
+
import re
|
11
|
+
import json #For the output
|
12
|
+
|
13
|
+
ATTR_DUE_IN = 'due'
|
14
|
+
|
15
|
+
ATTR_ORIGIN_STOP_ID = 'origin_stop_id'
|
16
|
+
ATTR_ORIGIN_NAME = 'origin_name'
|
17
|
+
ATTR_DEPARTURE_TIME = 'departure_time'
|
18
|
+
|
19
|
+
ATTR_DESTINATION_STOP_ID = 'destination_stop_id'
|
20
|
+
ATTR_DESTINATION_NAME = 'destination_name'
|
21
|
+
ATTR_ARRIVAL_TIME = 'arrival_time'
|
22
|
+
|
23
|
+
ATTR_ORIGIN_TRANSPORT_TYPE = 'origin_transport_type'
|
24
|
+
ATTR_ORIGIN_TRANSPORT_NAME = 'origin_transport_name'
|
25
|
+
ATTR_ORIGIN_LINE_NAME = 'origin_line_name'
|
26
|
+
ATTR_ORIGIN_LINE_NAME_SHORT = 'origin_line_name_short'
|
27
|
+
ATTR_CHANGES = 'changes'
|
28
|
+
|
29
|
+
ATTR_OCCUPANCY = 'occupancy'
|
30
|
+
|
31
|
+
ATTR_REAL_TIME_TRIP_ID = 'real_time_trip_id'
|
32
|
+
ATTR_LATITUDE = 'latitude'
|
33
|
+
ATTR_LONGITUDE = 'longitude'
|
34
|
+
|
35
|
+
logger = logging.getLogger(__name__)
|
36
|
+
|
37
|
+
class TransportNSWv2(object):
|
38
|
+
"""The Class for handling the data retrieval."""
|
39
|
+
|
40
|
+
# The application requires an API key. You can register for
|
41
|
+
# free on the service NSW website for it.
|
42
|
+
# You need to register for both the Trip Planner and Realtime Vehicle Position APIs
|
43
|
+
|
44
|
+
def __init__(self):
|
45
|
+
"""Initialize the data object with default values."""
|
46
|
+
self.origin_id = None
|
47
|
+
self.destination_id = None
|
48
|
+
self.api_key = None
|
49
|
+
self.journey_wait_time = None
|
50
|
+
self.transport_type = None
|
51
|
+
self.strict_transport_type = None
|
52
|
+
self.raw_output = None
|
53
|
+
self.journeys_to_return = None
|
54
|
+
self.info = {
|
55
|
+
ATTR_DUE_IN : 'n/a',
|
56
|
+
ATTR_ORIGIN_STOP_ID : 'n/a',
|
57
|
+
ATTR_ORIGIN_NAME : 'n/a',
|
58
|
+
ATTR_DEPARTURE_TIME : 'n/a',
|
59
|
+
ATTR_DESTINATION_STOP_ID : 'n/a',
|
60
|
+
ATTR_DESTINATION_NAME : 'n/a',
|
61
|
+
ATTR_ARRIVAL_TIME : 'n/a',
|
62
|
+
ATTR_ORIGIN_TRANSPORT_TYPE : 'n/a',
|
63
|
+
ATTR_ORIGIN_TRANSPORT_NAME : 'n/a',
|
64
|
+
ATTR_ORIGIN_LINE_NAME : 'n/a',
|
65
|
+
ATTR_ORIGIN_LINE_NAME_SHORT : 'n/a',
|
66
|
+
ATTR_CHANGES : 'n/a',
|
67
|
+
ATTR_OCCUPANCY : 'n/a',
|
68
|
+
ATTR_REAL_TIME_TRIP_ID : 'n/a',
|
69
|
+
ATTR_LATITUDE : 'n/a',
|
70
|
+
ATTR_LONGITUDE : 'n/a'
|
71
|
+
}
|
72
|
+
|
73
|
+
def get_trip(self, name_origin, name_destination , api_key, journey_wait_time = 0, transport_type = 0, \
|
74
|
+
strict_transport_type = False, raw_output = False, journeys_to_return = 1):
|
75
|
+
"""Get the latest data from Transport NSW."""
|
76
|
+
fmt = '%Y-%m-%dT%H:%M:%SZ'
|
77
|
+
|
78
|
+
self.name_origin = name_origin
|
79
|
+
self.destination = name_destination
|
80
|
+
self.api_key = api_key
|
81
|
+
self.journey_wait_time = journey_wait_time
|
82
|
+
self.transport_type = transport_type
|
83
|
+
self.strict_transport_type = strict_transport_type
|
84
|
+
self.raw_output = raw_output
|
85
|
+
self.journeys_to_return = journeys_to_return
|
86
|
+
|
87
|
+
# This query always uses the current date and time - but add in any 'journey_wait_time' minutes
|
88
|
+
now_plus_wait = datetime.now() + timedelta(minutes = journey_wait_time)
|
89
|
+
itdDate = now_plus_wait.strftime('%Y%m%d')
|
90
|
+
itdTime = now_plus_wait.strftime('%H%M')
|
91
|
+
|
92
|
+
# We don't control how many journeys are returned any more, so need to be careful of running out of valid journeys if there is a filter in place, particularly a strict filter
|
93
|
+
# It would be more efficient to return one journey, check if the filter is met and then retrieve the next one via a new query if not, but for now we'll only be making use of the journeys we've been given
|
94
|
+
|
95
|
+
# Build the entire URL
|
96
|
+
url = \
|
97
|
+
'https://api.transport.nsw.gov.au/v1/tp/trip?' \
|
98
|
+
'outputFormat=rapidJSON&coordOutputFormat=EPSG%3A4326' \
|
99
|
+
'&depArrMacro=dep&itdDate=' + itdDate + '&itdTime=' + itdTime + \
|
100
|
+
'&type_origin=any&name_origin=' + self.name_origin + \
|
101
|
+
'&type_destination=any&name_destination=' + self.destination + \
|
102
|
+
'&TfNSWTR=true'
|
103
|
+
# '&calcNumberOfTrips=' + str(journeys_to_retrieve) + \
|
104
|
+
|
105
|
+
|
106
|
+
auth = 'apikey ' + self.api_key
|
107
|
+
header = {'Accept': 'application/json', 'Authorization': auth}
|
108
|
+
|
109
|
+
# Send the query and return an error if something goes wrong
|
110
|
+
# Otherwise store the response
|
111
|
+
try:
|
112
|
+
response = requests.get(url, headers=header, timeout=10)
|
113
|
+
except:
|
114
|
+
logger.warning("Network or Timeout error")
|
115
|
+
return error_return(self.info, raw_output)
|
116
|
+
|
117
|
+
# If we get bad status code, log error and return with n/a or an empty string
|
118
|
+
if response.status_code != 200:
|
119
|
+
logger.warning("Error with the request sent; check api key")
|
120
|
+
return error_return(self.info, raw_output)
|
121
|
+
|
122
|
+
# Parse the result as a JSON object
|
123
|
+
result = response.json()
|
124
|
+
|
125
|
+
# The API will always return a valid trip, so it's just a case of grabbing what we need...
|
126
|
+
# We're only reporting on the origin and destination, it's out of scope to discuss the specifics of the ENTIRE journey
|
127
|
+
# This isn't a route planner, just a 'how long until the next journey I've specified' tool
|
128
|
+
# The assumption is that the travelee will know HOW to make the defined journey, they're just asking WHEN it's happening next
|
129
|
+
# All we potentially have to do is find the first trip that matches the transport_type filter
|
130
|
+
|
131
|
+
if raw_output == True:
|
132
|
+
# Just return the raw output
|
133
|
+
return json.dumps(result)
|
134
|
+
exit
|
135
|
+
|
136
|
+
retrieved_journeys = len(result['journeys'])
|
137
|
+
|
138
|
+
# Loop through the results applying filters where required, and generate the appropriate JSON output including an array of in-scope trips
|
139
|
+
json_output=''
|
140
|
+
found_journeys = 0
|
141
|
+
no_valid_journeys = False
|
142
|
+
|
143
|
+
for current_journey in range (0, retrieved_journeys, 1):
|
144
|
+
if transport_type == 0:
|
145
|
+
# Just grab the next trip
|
146
|
+
journey = result['journeys'][current_journey]
|
147
|
+
next_journey = current_journey + 1
|
148
|
+
else:
|
149
|
+
# Look for a trip with a matching class filter in at least one of its legs. Either ANY, or the first leg, depending on how strict we're being
|
150
|
+
journey, next_journey = self.find_next_journey(result['journeys'], current_journey, transport_type, strict_transport_type)
|
151
|
+
|
152
|
+
if (journey is None) or (journey['legs']) is None:
|
153
|
+
#We've reached the end
|
154
|
+
no_valid_journeys = True
|
155
|
+
self.info = {
|
156
|
+
ATTR_DUE_IN : 'n/a',
|
157
|
+
ATTR_ORIGIN_STOP_ID : 'n/a',
|
158
|
+
ATTR_ORIGIN_NAME : 'n/a',
|
159
|
+
ATTR_DEPARTURE_TIME : 'n/a',
|
160
|
+
ATTR_DESTINATION_STOP_ID : 'n/a',
|
161
|
+
ATTR_DESTINATION_NAME : 'n/a',
|
162
|
+
ATTR_ARRIVAL_TIME : 'n/a',
|
163
|
+
ATTR_ORIGIN_TRANSPORT_TYPE : 'n/a',
|
164
|
+
ATTR_ORIGIN_TRANSPORT_NAME : 'n/a',
|
165
|
+
ATTR_ORIGIN_LINE_NAME : 'n/a',
|
166
|
+
ATTR_ORIGIN_LINE_NAME_SHORT : 'n/a',
|
167
|
+
ATTR_CHANGES : 'n/a',
|
168
|
+
ATTR_OCCUPANCY : 'n/a',
|
169
|
+
ATTR_REAL_TIME_TRIP_ID : 'n/a',
|
170
|
+
ATTR_LATITUDE : 'n/a',
|
171
|
+
ATTR_LONGITUDE : 'n/a'
|
172
|
+
}
|
173
|
+
else:
|
174
|
+
legs = journey['legs']
|
175
|
+
first_leg = self.find_first_leg(legs, transport_type, strict_transport_type)
|
176
|
+
last_leg = self.find_last_leg(legs, transport_type, strict_transport_type)
|
177
|
+
changes = self.find_changes(legs, transport_type)
|
178
|
+
|
179
|
+
origin = first_leg['origin']
|
180
|
+
first_stop = first_leg['destination']
|
181
|
+
destination = last_leg['destination']
|
182
|
+
transportation = first_leg['transportation']
|
183
|
+
|
184
|
+
|
185
|
+
# Origin info
|
186
|
+
origin_stop_id = origin['id']
|
187
|
+
origin_name = origin['name']
|
188
|
+
origin_departure_time = origin['departureTimeEstimated']
|
189
|
+
|
190
|
+
# How long until it leaves?
|
191
|
+
due = self.get_due(datetime.strptime(origin_departure_time, fmt))
|
192
|
+
|
193
|
+
# Destination info
|
194
|
+
destination_stop_id = destination['id']
|
195
|
+
destination_name = destination['name']
|
196
|
+
destination_arrival_time = destination['arrivalTimeEstimated']
|
197
|
+
|
198
|
+
# Origin type info - train, bus, etc
|
199
|
+
origin_mode_temp = transportation['product']['class']
|
200
|
+
origin_mode = self.get_mode(origin_mode_temp)
|
201
|
+
origin_mode_name = transportation['product']['name']
|
202
|
+
|
203
|
+
# RealTimeTripID info so we can try and get the current location later
|
204
|
+
realtimetripid = 'n/a'
|
205
|
+
if 'properties' in transportation:
|
206
|
+
if 'RealtimeTripId' in transportation['properties']:
|
207
|
+
realtimetripid = transportation['properties']['RealtimeTripId']
|
208
|
+
|
209
|
+
# Line info
|
210
|
+
origin_line_name_short = "unknown"
|
211
|
+
if 'disassembledName' in transportation:
|
212
|
+
origin_line_name_short = transportation['disassembledName']
|
213
|
+
|
214
|
+
origin_line_name = "unknown"
|
215
|
+
if 'number' in transportation:
|
216
|
+
origin_line_name = transportation['number']
|
217
|
+
|
218
|
+
# Occupancy info, if it's there
|
219
|
+
occupancy = 'UNKNOWN'
|
220
|
+
if 'properties' in first_stop:
|
221
|
+
if 'occupancy' in first_stop['properties']:
|
222
|
+
occupancy = first_stop['properties']['occupancy']
|
223
|
+
|
224
|
+
# Now might be a good time to see if we can also find the latitude and longitude
|
225
|
+
# Using the Realtime Vehicle Positions API
|
226
|
+
latitude = 'n/a'
|
227
|
+
longitude = 'n/a'
|
228
|
+
|
229
|
+
if realtimetripid != 'n/a':
|
230
|
+
# Build the URL
|
231
|
+
url = \
|
232
|
+
'https://api.transport.nsw.gov.au/v1/gtfs/vehiclepos' \
|
233
|
+
+ self.get_url(origin_mode)
|
234
|
+
auth = 'apikey ' + self.api_key
|
235
|
+
header = {'Authorization': auth}
|
236
|
+
|
237
|
+
response = requests.get(url, headers=header, timeout=10)
|
238
|
+
|
239
|
+
# Only try and process the results if we got a good return code
|
240
|
+
if response.status_code == 200:
|
241
|
+
# Search the feed and see if we can find the trip_id
|
242
|
+
# If we do, capture the latitude and longitude
|
243
|
+
|
244
|
+
feed = gtfs_realtime_pb2.FeedMessage()
|
245
|
+
feed.ParseFromString(response.content)
|
246
|
+
|
247
|
+
# Unfortunately we need to do some mucking about for train-based trip_ids
|
248
|
+
# Define the appropriate regular expression to search for - usually just the full text
|
249
|
+
bFindLocation = True
|
250
|
+
|
251
|
+
if origin_mode == 'Train':
|
252
|
+
triparray = realtimetripid.split('.')
|
253
|
+
if len(triparray) == 7:
|
254
|
+
trip_id_wild = triparray[0] + '.' + triparray[1] + '.' + triparray[2] + '.+.' + triparray[4] + '.' + triparray[5] + '.' + triparray[6]
|
255
|
+
else:
|
256
|
+
# Hmm, it's not the right length (this happens rarely) - give up
|
257
|
+
bFindLocation = False
|
258
|
+
else:
|
259
|
+
trip_id_wild = realtimetripid
|
260
|
+
|
261
|
+
if bFindLocation:
|
262
|
+
reg = re.compile(trip_id_wild)
|
263
|
+
|
264
|
+
for entity in feed.entity:
|
265
|
+
if bool(re.match(reg, entity.vehicle.trip.trip_id)):
|
266
|
+
latitude = entity.vehicle.position.latitude
|
267
|
+
longitude = entity.vehicle.position.longitude
|
268
|
+
# We found it, so break out
|
269
|
+
break
|
270
|
+
|
271
|
+
self.info = {
|
272
|
+
ATTR_DUE_IN: due,
|
273
|
+
ATTR_ORIGIN_STOP_ID : origin_stop_id,
|
274
|
+
ATTR_ORIGIN_NAME : origin_name,
|
275
|
+
ATTR_DEPARTURE_TIME : origin_departure_time,
|
276
|
+
ATTR_DESTINATION_STOP_ID : destination_stop_id,
|
277
|
+
ATTR_DESTINATION_NAME : destination_name,
|
278
|
+
ATTR_ARRIVAL_TIME : destination_arrival_time,
|
279
|
+
ATTR_ORIGIN_TRANSPORT_TYPE : origin_mode,
|
280
|
+
ATTR_ORIGIN_TRANSPORT_NAME: origin_mode_name,
|
281
|
+
ATTR_ORIGIN_LINE_NAME : origin_line_name,
|
282
|
+
ATTR_ORIGIN_LINE_NAME_SHORT : origin_line_name_short,
|
283
|
+
ATTR_CHANGES: changes,
|
284
|
+
ATTR_OCCUPANCY : occupancy,
|
285
|
+
ATTR_REAL_TIME_TRIP_ID : realtimetripid,
|
286
|
+
ATTR_LATITUDE : latitude,
|
287
|
+
ATTR_LONGITUDE : longitude
|
288
|
+
}
|
289
|
+
|
290
|
+
found_journeys = found_journeys + 1
|
291
|
+
|
292
|
+
# Add to the return array
|
293
|
+
if (found_journeys == journeys_to_return) or (no_valid_journeys == True):
|
294
|
+
break
|
295
|
+
else:
|
296
|
+
if (found_journeys >= 2):
|
297
|
+
json_output = json_output + ',' + json.dumps(self.info)
|
298
|
+
else:
|
299
|
+
json_output = json_output + json.dumps(self.info)
|
300
|
+
|
301
|
+
current_journey = next_journey
|
302
|
+
|
303
|
+
json_output='{"journeys_to_return": ' + str(self.journeys_to_return) + ', "journeys_with_data": ' + str(found_journeys) + ', "journeys": [' + json_output + ']}'
|
304
|
+
return json_output
|
305
|
+
|
306
|
+
|
307
|
+
# def find_first_journey(self, journeys, journeytype, strictness):
|
308
|
+
# # Find the first journey that has a leg is of the requested type
|
309
|
+
# journey_count = len(journeys)
|
310
|
+
# for journey in range (0, journey_count, 1):
|
311
|
+
# leg = self.find_first_leg(journeys[journey]['legs'], journeytype, strictness)
|
312
|
+
# if leg is not None:
|
313
|
+
# return journeys[journey]
|
314
|
+
#
|
315
|
+
# # Hmm, we didn't find one
|
316
|
+
# return None
|
317
|
+
|
318
|
+
|
319
|
+
def find_next_journey(self, journeys, start_journey, journeytype, strict):
|
320
|
+
# Find the next journey that has a leg of the requested type
|
321
|
+
journey_count = len(journeys)
|
322
|
+
|
323
|
+
# Some basic error checking
|
324
|
+
if start_journey > journey_count:
|
325
|
+
return None, None
|
326
|
+
|
327
|
+
for journey in range (start_journey, journey_count, 1):
|
328
|
+
leg = self.find_first_leg(journeys[journey]['legs'], journeytype, strict)
|
329
|
+
if leg is not None:
|
330
|
+
return journeys[journey], journey + 1
|
331
|
+
else:
|
332
|
+
return None, None
|
333
|
+
|
334
|
+
# Hmm, we didn't find one
|
335
|
+
return None
|
336
|
+
|
337
|
+
|
338
|
+
def find_first_leg(self, legs, legtype, strict):
|
339
|
+
# Find the first leg of the requested type
|
340
|
+
leg_count = len(legs)
|
341
|
+
for leg in range (0, leg_count, 1):
|
342
|
+
leg_class = legs[leg]['transportation']['product']['class']
|
343
|
+
|
344
|
+
# We've got a filter, and the leg type matches it, so return that leg
|
345
|
+
if legtype != 0 and leg_class == legtype:
|
346
|
+
return legs[leg]
|
347
|
+
|
348
|
+
# We don't have a filter, and this is the first non-walk/cycle leg so return that leg
|
349
|
+
if legtype == 0 and leg_class < 99:
|
350
|
+
return legs[leg]
|
351
|
+
|
352
|
+
# Exit if we're doing strict filtering and we haven't found that type in the first leg
|
353
|
+
if legtype != 0 and strict == True:
|
354
|
+
return None
|
355
|
+
|
356
|
+
# Hmm, we didn't find one
|
357
|
+
return None
|
358
|
+
|
359
|
+
|
360
|
+
def find_last_leg(self, legs, legtype, strict):
|
361
|
+
# Find the last leg of the requested type
|
362
|
+
leg_count = len(legs)
|
363
|
+
for leg in range (leg_count - 1, -1, -1):
|
364
|
+
leg_class = legs[leg]['transportation']['product']['class']
|
365
|
+
|
366
|
+
# We've got a filter, and the leg type matches it, so return that leg
|
367
|
+
if legtype != 0 and leg_class == legtype:
|
368
|
+
return legs[leg]
|
369
|
+
|
370
|
+
# We don't have a filter, and this is the first non-walk/cycle leg so return that leg
|
371
|
+
if legtype == 0 and leg_class < 99:
|
372
|
+
return legs[leg]
|
373
|
+
|
374
|
+
# Exit if we're doing strict filtering and we haven't found that type in the first leg
|
375
|
+
if legtype != 0 and strict == True:
|
376
|
+
return None
|
377
|
+
|
378
|
+
# Hmm, we didn't find one
|
379
|
+
return None
|
380
|
+
|
381
|
+
|
382
|
+
def find_changes(self, legs, legtype):
|
383
|
+
# Find out how often we have to change
|
384
|
+
changes = 0
|
385
|
+
leg_count = len(legs)
|
386
|
+
|
387
|
+
for leg in range (0, leg_count, 1):
|
388
|
+
leg_class = legs[leg]['transportation']['product']['class']
|
389
|
+
if leg_class == legtype or legtype == 0:
|
390
|
+
changes = changes + 1
|
391
|
+
|
392
|
+
return changes - 1
|
393
|
+
|
394
|
+
|
395
|
+
def get_mode(self, iconId):
|
396
|
+
"""Map the iconId to a full text string"""
|
397
|
+
modes = {
|
398
|
+
1: "Train",
|
399
|
+
4: "Light rail",
|
400
|
+
5: "Bus",
|
401
|
+
7: "Coach",
|
402
|
+
9: "Ferry",
|
403
|
+
11: "School bus",
|
404
|
+
99: "Walk",
|
405
|
+
100: "Walk",
|
406
|
+
107: "Cycle"
|
407
|
+
}
|
408
|
+
return modes.get(iconId, None)
|
409
|
+
|
410
|
+
|
411
|
+
def get_url(self, mode):
|
412
|
+
"""Map the journey mode to the proper real time location URL """
|
413
|
+
|
414
|
+
url_options = {
|
415
|
+
"Train" : "/sydneytrains",
|
416
|
+
"Light rail" : "/lightrail/innerwest",
|
417
|
+
"Bus" : "/buses",
|
418
|
+
"Coach" : "/buses",
|
419
|
+
"Ferry" : "/ferries/sydneyferries",
|
420
|
+
"School bus" : "/buses"
|
421
|
+
}
|
422
|
+
return url_options.get(mode, None)
|
423
|
+
|
424
|
+
|
425
|
+
def get_due(self, estimated):
|
426
|
+
"""Min until departure"""
|
427
|
+
due = 0
|
428
|
+
if estimated > datetime.utcnow():
|
429
|
+
due = round((estimated - datetime.utcnow()).seconds / 60)
|
430
|
+
return due
|
@@ -5,7 +5,7 @@ with open("README.md", "r") as fh:
|
|
5
5
|
|
6
6
|
setuptools.setup(
|
7
7
|
name="PyTransportNSWv2",
|
8
|
-
version="0.5.
|
8
|
+
version="0.5.2",
|
9
9
|
author="andystewart999",
|
10
10
|
description="Get detailed per-trip transport information from TransportNSW",
|
11
11
|
long_description=long_description,
|
@@ -1,346 +0,0 @@
|
|
1
|
-
""" A module to query Transport NSW (Australia) departure times. """
|
2
|
-
""" First created by Dav0815 ( https://pypi.org/user/Dav0815/) """
|
3
|
-
""" Extended by AndyStewart999 ( https://pypi.org/user/andystewart999/ ) """
|
4
|
-
|
5
|
-
from datetime import datetime, timedelta
|
6
|
-
from google.transit import gtfs_realtime_pb2
|
7
|
-
import requests.exceptions
|
8
|
-
import requests
|
9
|
-
import logging
|
10
|
-
import re
|
11
|
-
import json #For the output
|
12
|
-
|
13
|
-
ATTR_DUE_IN = 'due'
|
14
|
-
|
15
|
-
ATTR_ORIGIN_STOP_ID = 'origin_stop_id'
|
16
|
-
ATTR_ORIGIN_NAME = 'origin_name'
|
17
|
-
ATTR_DEPARTURE_TIME = 'departure_time'
|
18
|
-
|
19
|
-
ATTR_DESTINATION_STOP_ID = 'destination_stop_id'
|
20
|
-
ATTR_DESTINATION_NAME = 'destination_name'
|
21
|
-
ATTR_ARRIVAL_TIME = 'arrival_time'
|
22
|
-
|
23
|
-
ATTR_ORIGIN_TRANSPORT_TYPE = 'origin_transport_type'
|
24
|
-
ATTR_ORIGIN_TRANSPORT_NAME = 'origin_transport_name'
|
25
|
-
ATTR_ORIGIN_LINE_NAME = 'origin_line_name'
|
26
|
-
ATTR_ORIGIN_LINE_NAME_SHORT = 'origin_line_name_short'
|
27
|
-
ATTR_CHANGES = 'changes'
|
28
|
-
|
29
|
-
ATTR_OCCUPANCY = 'occupancy'
|
30
|
-
|
31
|
-
ATTR_REAL_TIME_TRIP_ID = 'real_time_trip_id'
|
32
|
-
ATTR_LATITUDE = 'latitude'
|
33
|
-
ATTR_LONGITUDE = 'longitude'
|
34
|
-
|
35
|
-
logger = logging.getLogger(__name__)
|
36
|
-
|
37
|
-
class TransportNSWv2(object):
|
38
|
-
"""The Class for handling the data retrieval."""
|
39
|
-
|
40
|
-
# The application requires an API key. You can register for
|
41
|
-
# free on the service NSW website for it.
|
42
|
-
# You need to register for both the Trip Planner and Realtime Vehicle Position APIs
|
43
|
-
|
44
|
-
def __init__(self):
|
45
|
-
"""Initialize the data object with default values."""
|
46
|
-
self.origin_id = None
|
47
|
-
self.destination_id = None
|
48
|
-
self.api_key = None
|
49
|
-
self.trip_wait_time = None
|
50
|
-
self.transport_type = None
|
51
|
-
self.info = {
|
52
|
-
ATTR_DUE_IN : 'n/a',
|
53
|
-
ATTR_ORIGIN_STOP_ID : 'n/a',
|
54
|
-
ATTR_ORIGIN_NAME : 'n/a',
|
55
|
-
ATTR_DEPARTURE_TIME : 'n/a',
|
56
|
-
ATTR_DESTINATION_STOP_ID : 'n/a',
|
57
|
-
ATTR_DESTINATION_NAME : 'n/a',
|
58
|
-
ATTR_ARRIVAL_TIME : 'n/a',
|
59
|
-
ATTR_ORIGIN_TRANSPORT_TYPE : 'n/a',
|
60
|
-
ATTR_ORIGIN_TRANSPORT_NAME : 'n/a',
|
61
|
-
ATTR_ORIGIN_LINE_NAME : 'n/a',
|
62
|
-
ATTR_ORIGIN_LINE_NAME_SHORT : 'n/a',
|
63
|
-
ATTR_CHANGES : 'n/a',
|
64
|
-
ATTR_OCCUPANCY : 'n/a',
|
65
|
-
ATTR_REAL_TIME_TRIP_ID : 'n/a',
|
66
|
-
ATTR_LATITUDE : 'n/a',
|
67
|
-
ATTR_LONGITUDE : 'n/a'
|
68
|
-
}
|
69
|
-
|
70
|
-
def get_trip(self, name_origin, name_destination , api_key, trip_wait_time = 0, transport_type = 0):
|
71
|
-
"""Get the latest data from Transport NSW."""
|
72
|
-
fmt = '%Y-%m-%dT%H:%M:%SZ'
|
73
|
-
|
74
|
-
self.name_origin = name_origin
|
75
|
-
self.destination = name_destination
|
76
|
-
self.api_key = api_key
|
77
|
-
self.trip_wait_time = trip_wait_time
|
78
|
-
self.transport_type = transport_type
|
79
|
-
|
80
|
-
# This query always uses the current date and time - but add in any 'trip_wait_time' minutes
|
81
|
-
now_plus_wait = datetime.now() + timedelta(minutes = trip_wait_time)
|
82
|
-
itdDate = now_plus_wait.strftime('%Y%m%d')
|
83
|
-
itdTime = now_plus_wait.strftime('%H%M')
|
84
|
-
|
85
|
-
if transport_type == 0:
|
86
|
-
# We only need to retrieve one trip - a transport type filter hasn't been applied
|
87
|
-
numberOfTrips = 1
|
88
|
-
else:
|
89
|
-
# 5 trips seems like a safe number
|
90
|
-
numberOfTrips = 5
|
91
|
-
|
92
|
-
# Build the entire URL
|
93
|
-
url = \
|
94
|
-
'https://api.transport.nsw.gov.au/v1/tp/trip?' \
|
95
|
-
'outputFormat=rapidJSON&coordOutputFormat=EPSG%3A4326' \
|
96
|
-
'&depArrMacro=dep&itdDate=' + itdDate + '&itdTime=' + itdTime + \
|
97
|
-
'&type_origin=any&name_origin=' + name_origin + \
|
98
|
-
'&type_destination=any&name_destination=' + name_destination + \
|
99
|
-
'&calcNumberOfTrips=' + str(numberOfTrips) + \
|
100
|
-
'&TfNSWTR=true'
|
101
|
-
|
102
|
-
auth = 'apikey ' + self.api_key
|
103
|
-
header = {'Accept': 'application/json', 'Authorization': auth}
|
104
|
-
|
105
|
-
# Send the query and return an error if something goes wrong
|
106
|
-
# Otherwise store the response
|
107
|
-
try:
|
108
|
-
response = requests.get(url, headers=header, timeout=10)
|
109
|
-
except:
|
110
|
-
logger.warning("Network or Timeout error")
|
111
|
-
return self.info
|
112
|
-
|
113
|
-
# If there is no valid request (e.g. http code isn't 200)
|
114
|
-
# log error and return empty object
|
115
|
-
if response.status_code != 200:
|
116
|
-
logger.warning("Error with the request sent; check api key")
|
117
|
-
return self.info
|
118
|
-
|
119
|
-
# Parse the result as a JSON object
|
120
|
-
result = response.json()
|
121
|
-
|
122
|
-
# The API will always return a valid trip, so it's just a case of grabbing what we need...
|
123
|
-
# We're only reporting on the origin and destination, it's out of scope to discuss the specifics of the ENTIRE journey
|
124
|
-
# This isn't a route planner, just a 'how long until the next journey I've specified' tool
|
125
|
-
# The assumption is that the travelee will know HOW to make the defined journey, they're just asking WHEN it's happening next
|
126
|
-
# All we potentially have to do is find the first trip that matches the transport_type filter
|
127
|
-
|
128
|
-
if transport_type == 0:
|
129
|
-
# Just grab the first (and only) trip
|
130
|
-
journey = result['journeys'][0]
|
131
|
-
else:
|
132
|
-
# Look for a trip with a matching class filter in at least one of its legs. Could possibly be more stringent here, if ANY part of the journey fits the filter, it will be returned.
|
133
|
-
journey = self.find_first_journey(result['journeys'], transport_type)
|
134
|
-
|
135
|
-
if journey is None:
|
136
|
-
logger.warning("No journey information returned")
|
137
|
-
return self.info
|
138
|
-
|
139
|
-
if journey['legs'] is None:
|
140
|
-
logger.warning("No journey information returned")
|
141
|
-
return self.info
|
142
|
-
|
143
|
-
legs = journey['legs']
|
144
|
-
first_leg = self.find_first_leg(legs, transport_type)
|
145
|
-
last_leg = self.find_last_leg(legs, transport_type)
|
146
|
-
changes = self.find_changes(legs, transport_type)
|
147
|
-
|
148
|
-
origin = result['journeys'][0]['legs'][first_leg]['origin']
|
149
|
-
# probably tidy this up when we start to get occupancy data back
|
150
|
-
first_stop = result['journeys'][0]['legs'][first_leg]['destination']
|
151
|
-
destination = result['journeys'][0]['legs'][last_leg]['destination']
|
152
|
-
transportation = result['journeys'][0]['legs'][first_leg]['transportation']
|
153
|
-
|
154
|
-
|
155
|
-
# Origin info
|
156
|
-
origin_stop_id = origin['id']
|
157
|
-
origin_name = origin['name']
|
158
|
-
origin_departure_time = origin['departureTimeEstimated']
|
159
|
-
|
160
|
-
# How long until it leaves?
|
161
|
-
due = self.get_due(datetime.strptime(origin_departure_time, fmt))
|
162
|
-
|
163
|
-
# Destination info
|
164
|
-
destination_stop_id = destination['id']
|
165
|
-
destination_name = destination['name']
|
166
|
-
destination_arrival_time = destination['arrivalTimeEstimated']
|
167
|
-
|
168
|
-
# Origin type info - train, bus, etc
|
169
|
-
origin_mode_temp = transportation['product']['class']
|
170
|
-
origin_mode = self.get_mode(origin_mode_temp)
|
171
|
-
origin_mode_name = transportation['product']['name']
|
172
|
-
|
173
|
-
# RealTimeTripID info so we can try and get the current location later
|
174
|
-
realtimetripid = 'n/a'
|
175
|
-
if 'properties' in transportation:
|
176
|
-
if 'RealtimeTripId' in transportation['properties']:
|
177
|
-
realtimetripid = transportation['properties']['RealtimeTripId']
|
178
|
-
|
179
|
-
# Line info
|
180
|
-
origin_line_name_short = "unknown"
|
181
|
-
if 'disassembledName' in transportation:
|
182
|
-
origin_line_name_short = transportation['disassembledName']
|
183
|
-
|
184
|
-
origin_line_name = "unknown"
|
185
|
-
if 'number' in transportation:
|
186
|
-
origin_line_name = transportation['number']
|
187
|
-
|
188
|
-
# Occupancy info, if it's there
|
189
|
-
occupancy = 'UNKNOWN'
|
190
|
-
if 'properties' in first_stop:
|
191
|
-
if 'occupancy' in first_stop['properties']:
|
192
|
-
occupancy = first_stop['properties']['occupancy']
|
193
|
-
|
194
|
-
# Now might be a good time to see if we can also find the latitude and longitude
|
195
|
-
# Using the Realtime Vehicle Positions API
|
196
|
-
latitude = 'n/a'
|
197
|
-
longitude = 'n/a'
|
198
|
-
|
199
|
-
if realtimetripid != 'n/a':
|
200
|
-
# Build the URL
|
201
|
-
url = \
|
202
|
-
'https://api.transport.nsw.gov.au/v1/gtfs/vehiclepos' \
|
203
|
-
+ self.get_url(origin_mode)
|
204
|
-
auth = 'apikey ' + self.api_key
|
205
|
-
header = {'Authorization': auth}
|
206
|
-
|
207
|
-
response = requests.get(url, headers=header, timeout=10)
|
208
|
-
|
209
|
-
# Only try and process the results if we got a good return code
|
210
|
-
if response.status_code == 200:
|
211
|
-
# Search the feed and see if we can find the trip_id
|
212
|
-
# If we do, capture the latitude and longitude
|
213
|
-
|
214
|
-
feed = gtfs_realtime_pb2.FeedMessage()
|
215
|
-
feed.ParseFromString(response.content)
|
216
|
-
|
217
|
-
# Unfortunately we need to do some mucking about for train-based trip_ids
|
218
|
-
# Define the appropriate regular expression to search for - usually just the full text
|
219
|
-
bFindLocation = True
|
220
|
-
|
221
|
-
if origin_mode == 'Train':
|
222
|
-
triparray = realtimetripid.split('.')
|
223
|
-
if len(triparray) == 7:
|
224
|
-
trip_id_wild = triparray[0] + '.' + triparray[1] + '.' + triparray[2] + '.+.' + triparray[4] + '.' + triparray[5] + '.' + triparray[6]
|
225
|
-
else:
|
226
|
-
# Hmm, it's not the right length (this happens rarely) - give up
|
227
|
-
bFindLocation = False
|
228
|
-
else:
|
229
|
-
trip_id_wild = realtimetripid
|
230
|
-
|
231
|
-
if bFindLocation:
|
232
|
-
reg = re.compile(trip_id_wild)
|
233
|
-
|
234
|
-
for entity in feed.entity:
|
235
|
-
if bool(re.match(reg, entity.vehicle.trip.trip_id)):
|
236
|
-
latitude = entity.vehicle.position.latitude
|
237
|
-
longitude = entity.vehicle.position.longitude
|
238
|
-
# We found it, so break out
|
239
|
-
break
|
240
|
-
|
241
|
-
self.info = {
|
242
|
-
ATTR_DUE_IN: due,
|
243
|
-
ATTR_ORIGIN_STOP_ID : origin_stop_id,
|
244
|
-
ATTR_ORIGIN_NAME : origin_name,
|
245
|
-
ATTR_DEPARTURE_TIME : origin_departure_time,
|
246
|
-
ATTR_DESTINATION_STOP_ID : destination_stop_id,
|
247
|
-
ATTR_DESTINATION_NAME : destination_name,
|
248
|
-
ATTR_ARRIVAL_TIME : destination_arrival_time,
|
249
|
-
ATTR_ORIGIN_TRANSPORT_TYPE : origin_mode,
|
250
|
-
ATTR_ORIGIN_TRANSPORT_NAME: origin_mode_name,
|
251
|
-
ATTR_ORIGIN_LINE_NAME : origin_line_name,
|
252
|
-
ATTR_ORIGIN_LINE_NAME_SHORT : origin_line_name_short,
|
253
|
-
ATTR_CHANGES: changes,
|
254
|
-
ATTR_OCCUPANCY : occupancy,
|
255
|
-
ATTR_REAL_TIME_TRIP_ID : realtimetripid,
|
256
|
-
ATTR_LATITUDE : latitude,
|
257
|
-
ATTR_LONGITUDE : longitude
|
258
|
-
}
|
259
|
-
return json.dumps(self.info)
|
260
|
-
|
261
|
-
def find_first_journey(self, journeys, journeytype):
|
262
|
-
# Find the first journey whose first leg is of the requested type
|
263
|
-
journey_count = len(journeys)
|
264
|
-
for journey in range (0, journey_count, 1):
|
265
|
-
leg = self.find_first_leg(journeys[journey]['legs'], journeytype)
|
266
|
-
if leg is not None:
|
267
|
-
return journeys[journey]
|
268
|
-
|
269
|
-
# Hmm, we didn't find one
|
270
|
-
return None
|
271
|
-
|
272
|
-
|
273
|
-
def find_first_leg(self, legs, legtype):
|
274
|
-
# Find the first leg of the requested type
|
275
|
-
leg_count = len(legs)
|
276
|
-
for leg in range (0, leg_count, 1):
|
277
|
-
leg_class = legs[leg]['transportation']['product']['class']
|
278
|
-
|
279
|
-
if leg_class == legtype or legtype == 0:
|
280
|
-
return leg
|
281
|
-
|
282
|
-
# Hmm, we didn't find one
|
283
|
-
return None
|
284
|
-
|
285
|
-
|
286
|
-
def find_last_leg(self, legs, legtype):
|
287
|
-
# Find the last leg of the requested type
|
288
|
-
leg_count = len(legs)
|
289
|
-
for leg in range (leg_count - 1, -1, -1):
|
290
|
-
leg_class = legs[leg]['transportation']['product']['class']
|
291
|
-
|
292
|
-
if leg_class == legtype or legtype == 0:
|
293
|
-
return leg
|
294
|
-
|
295
|
-
# Hmm, we didn't find one
|
296
|
-
return None
|
297
|
-
|
298
|
-
def find_changes(self, legs, legtype):
|
299
|
-
# Find out how often we have to change
|
300
|
-
changes = 0
|
301
|
-
leg_count = len(legs)
|
302
|
-
|
303
|
-
for leg in range (0, leg_count, 1):
|
304
|
-
leg_class = legs[leg]['transportation']['product']['class']
|
305
|
-
if leg_class == legtype or legtype == 0:
|
306
|
-
changes = changes + 1
|
307
|
-
|
308
|
-
return changes - 1
|
309
|
-
|
310
|
-
|
311
|
-
def get_mode(self, iconId):
|
312
|
-
"""Map the iconId to a full text string"""
|
313
|
-
modes = {
|
314
|
-
1: "Train",
|
315
|
-
4: "Light rail",
|
316
|
-
5: "Bus",
|
317
|
-
7: "Coach",
|
318
|
-
9: "Ferry",
|
319
|
-
11: "School bus",
|
320
|
-
99: "Walk",
|
321
|
-
100: "Walk",
|
322
|
-
107: "Cycle"
|
323
|
-
}
|
324
|
-
return modes.get(iconId, None)
|
325
|
-
|
326
|
-
|
327
|
-
def get_url(self, mode):
|
328
|
-
"""Map the journey mode to the proper real time location URL """
|
329
|
-
|
330
|
-
url_options = {
|
331
|
-
"Train" : "/sydneytrains",
|
332
|
-
"Light rail" : "/lightrail/innerwest",
|
333
|
-
"Bus" : "/buses",
|
334
|
-
"Coach" : "/buses",
|
335
|
-
"Ferry" : "/ferries/sydneyferries",
|
336
|
-
"School bus" : "/buses"
|
337
|
-
}
|
338
|
-
return url_options.get(mode, None)
|
339
|
-
|
340
|
-
|
341
|
-
def get_due(self, estimated):
|
342
|
-
"""Min until departure"""
|
343
|
-
due = 0
|
344
|
-
if estimated > datetime.utcnow():
|
345
|
-
due = round((estimated - datetime.utcnow()).seconds / 60)
|
346
|
-
return due
|
File without changes
|
File without changes
|
{PyTransportNSWv2-0.5.0 → PyTransportNSWv2-0.5.2}/PyTransportNSWv2.egg-info/dependency_links.txt
RENAMED
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|