PyTransportNSWv2 0.8.9__tar.gz → 0.9.0__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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: PyTransportNSWv2
3
- Version: 0.8.9
3
+ Version: 0.9.0
4
4
  Summary: Get detailed per-trip transport information from TransportNSW
5
5
  Home-page: https://github.com/andystewart999/TransportNSW
6
6
  Author: andystewart999
@@ -51,7 +51,7 @@ Description: # TransportNSWv2
51
51
  high
52
52
  veryHigh
53
53
  ```
54
- Specifying an alert priority in ```include_alerts``` means that any alerts of that priority or higher will be included in the output as a raw JSON array, basically a collation of the alerts that the Trip API sent back. If you've specified that alerts of a given priority should be included then by default ALL alert types will be included - you can limit the output to specific alert types by setting ```alert_type``` to something like ```'lineInfo|stopInfo|bannerInfo'```.
54
+ Specifying an alert priority in ```include_alerts``` means that any alerts of that priority or higher will be included in the output as a raw JSON array, basically a collation of the alerts that the Trip API sent back. If you've specified that alerts of a given priority should be included then by default ALL alert types will be included - you can limit the output to specific alert types by setting ```alert_type``` to something like ```lineInfo|stopInfo|bannerInfo```.
55
55
 
56
56
  Alert types:
57
57
  ```
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: PyTransportNSWv2
3
- Version: 0.8.9
3
+ Version: 0.9.0
4
4
  Summary: Get detailed per-trip transport information from TransportNSW
5
5
  Home-page: https://github.com/andystewart999/TransportNSW
6
6
  Author: andystewart999
@@ -51,7 +51,7 @@ Description: # TransportNSWv2
51
51
  high
52
52
  veryHigh
53
53
  ```
54
- Specifying an alert priority in ```include_alerts``` means that any alerts of that priority or higher will be included in the output as a raw JSON array, basically a collation of the alerts that the Trip API sent back. If you've specified that alerts of a given priority should be included then by default ALL alert types will be included - you can limit the output to specific alert types by setting ```alert_type``` to something like ```'lineInfo|stopInfo|bannerInfo'```.
54
+ Specifying an alert priority in ```include_alerts``` means that any alerts of that priority or higher will be included in the output as a raw JSON array, basically a collation of the alerts that the Trip API sent back. If you've specified that alerts of a given priority should be included then by default ALL alert types will be included - you can limit the output to specific alert types by setting ```alert_type``` to something like ```lineInfo|stopInfo|bannerInfo```.
55
55
 
56
56
  Alert types:
57
57
  ```
@@ -3,6 +3,7 @@ README.md
3
3
  setup.py
4
4
  PyTransportNSWv2.egg-info/PKG-INFO
5
5
  PyTransportNSWv2.egg-info/SOURCES.txt
6
+ PyTransportNSWv2.egg-info/TransportNSWv2.py
6
7
  PyTransportNSWv2.egg-info/dependency_links.txt
7
8
  PyTransportNSWv2.egg-info/requires.txt
8
9
  PyTransportNSWv2.egg-info/top_level.txt
@@ -0,0 +1,592 @@
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
+
8
+ import httpx
9
+
10
+ import logging
11
+ import re
12
+ import json #For the output
13
+ import time
14
+
15
+ ATTR_DUE_IN = 'due'
16
+
17
+ ATTR_ORIGIN_STOP_ID = 'origin_stop_id'
18
+ ATTR_ORIGIN_NAME = 'origin_name'
19
+ ATTR_DEPARTURE_TIME = 'departure_time'
20
+ ATTR_DELAY = 'delay'
21
+
22
+ ATTR_DESTINATION_STOP_ID = 'destination_stop_id'
23
+ ATTR_DESTINATION_NAME = 'destination_name'
24
+ ATTR_ARRIVAL_TIME = 'arrival_time'
25
+
26
+ ATTR_ORIGIN_TRANSPORT_TYPE = 'origin_transport_type'
27
+ ATTR_ORIGIN_TRANSPORT_NAME = 'origin_transport_name'
28
+ ATTR_ORIGIN_LINE_NAME = 'origin_line_name'
29
+ ATTR_ORIGIN_LINE_NAME_SHORT = 'origin_line_name_short'
30
+ ATTR_CHANGES = 'changes'
31
+
32
+ ATTR_OCCUPANCY = 'occupancy'
33
+
34
+ ATTR_REAL_TIME_TRIP_ID = 'real_time_trip_id'
35
+ ATTR_LATITUDE = 'latitude'
36
+ ATTR_LONGITUDE = 'longitude'
37
+
38
+ ATTR_ALERTS = 'alerts'
39
+
40
+ logger = logging.getLogger(__name__)
41
+
42
+ class TransportNSWv2(object):
43
+ """The Class for handling the data retrieval."""
44
+
45
+ # The application requires an API key. You can register for
46
+ # free on the service NSW website for it.
47
+ # You need to register for both the Trip Planner and Realtime Vehicle Position APIs
48
+
49
+ def __init__(self):
50
+ """Initialize the data object with default values."""
51
+ self.origin_id = None
52
+ self.destination_id = None
53
+ self.api_key = None
54
+ self.journey_wait_time = None
55
+ self.transport_type = None
56
+ self.strict_transport_type = None
57
+ self.raw_output = None
58
+ self.journeys_to_return = None
59
+ self.info = {
60
+ ATTR_DUE_IN : 'n/a',
61
+ ATTR_ORIGIN_STOP_ID : 'n/a',
62
+ ATTR_ORIGIN_NAME : 'n/a',
63
+ ATTR_DEPARTURE_TIME : 'n/a',
64
+ ATTR_DELAY : 'n/a',
65
+ ATTR_DESTINATION_STOP_ID : 'n/a',
66
+ ATTR_DESTINATION_NAME : 'n/a',
67
+ ATTR_ARRIVAL_TIME : 'n/a',
68
+ ATTR_ORIGIN_TRANSPORT_TYPE : 'n/a',
69
+ ATTR_ORIGIN_TRANSPORT_NAME : 'n/a',
70
+ ATTR_ORIGIN_LINE_NAME : 'n/a',
71
+ ATTR_ORIGIN_LINE_NAME_SHORT : 'n/a',
72
+ ATTR_CHANGES : 'n/a',
73
+ ATTR_OCCUPANCY : 'n/a',
74
+ ATTR_REAL_TIME_TRIP_ID : 'n/a',
75
+ ATTR_LATITUDE : 'n/a',
76
+ ATTR_LONGITUDE : 'n/a',
77
+ ATTR_ALERTS: '[]'
78
+ }
79
+
80
+ def get_trip(self, name_origin, name_destination , api_key, journey_wait_time = 0, transport_type = 0, \
81
+ strict_transport_type = False, raw_output = False, journeys_to_return = 1, route_filter = '', \
82
+ include_realtime_location = True, include_alerts = 'none', alert_type = 'all', check_stop_ids = True, forced_gtfs_uri = []):
83
+
84
+ """Get the latest data from Transport NSW."""
85
+ fmt = '%Y-%m-%dT%H:%M:%SZ'
86
+
87
+ self.name_origin = name_origin
88
+ self.destination = name_destination
89
+ self.api_key = api_key
90
+ self.journey_wait_time = journey_wait_time
91
+ self.transport_type = transport_type
92
+ self.strict_transport_type = strict_transport_type
93
+ self.raw_output = raw_output
94
+ self.journeys_to_return = journeys_to_return
95
+ self.route_filter = route_filter.lower()
96
+ self.include_realtime_location = include_realtime_location
97
+ self.include_alerts = include_alerts.lower()
98
+ self.alert_type = alert_type.lower()
99
+
100
+ # This query always uses the current date and time - but add in any 'journey_wait_time' minutes
101
+ now_plus_wait = datetime.now() + timedelta(minutes = journey_wait_time)
102
+ itdDate = now_plus_wait.strftime('%Y%m%d')
103
+ itdTime = now_plus_wait.strftime('%H%M')
104
+
105
+ auth = 'apikey ' + self.api_key
106
+ header = {'Accept': 'application/json', 'Authorization': auth}
107
+
108
+
109
+ # First, check if the source and dest stops are valid unless we've been told not to
110
+ if check_stop_ids:
111
+ stop_list = [name_origin, name_destination]
112
+
113
+ for stop in stop_list:
114
+ url = \
115
+ 'https://api.transport.nsw.gov.au/v1/tp/stop_finder?' \
116
+ 'outputFormat=rapidJSON&coordOutputFormat=EPSG%3A4326' \
117
+ '&type_sf=stop&name_sf=' + stop + \
118
+ '&TfNSWSF=true'
119
+
120
+ # Send the query and return an error if something goes wrong
121
+ try:
122
+ response = httpx.get(url, headers=header, timeout=5)
123
+
124
+ # If we get bad status code, log error and return with None
125
+ if response.status_code != 200:
126
+ if response.status_code == 429:
127
+ # We've exceeded the rate limit but that doesn't mean future calls won't work
128
+ # So let's assume that the stop ID is ok but we'll still raise a warning
129
+ logger.warn(f"Error {str(response.status_code)} calling /v1/tp/stop_finder API; rate limit exceeded - assuming stop is valid")
130
+ else:
131
+ # If it's an API key issue there's no point in continuing, hence returning None
132
+ logger.error(f"Error {str(response.status_code)} calling /v1/tp/stop_finder API; check API key")
133
+ return None
134
+ else:
135
+ # Parse the result as a JSON object
136
+ result = response.json()
137
+
138
+ # Just a quick check - the presence of systemMessages signifies an error, otherwise we assume it's ok
139
+ if 'systemMessages' in result:
140
+ logger.error(f"Error - Stop ID {stop} doesn't exist")
141
+ return None
142
+
143
+ # Put in a pause here to try and make sure we stay under the 5 API calls/second limit
144
+ # Not usually an issue but if multiple processes are running multiple calls we might hit it
145
+ time.sleep(1.0)
146
+
147
+ except Exception as ex:
148
+ logger.error(f"Error {str(ex)} calling /v1/tp/stop_finder API; assuming stop is valid")
149
+ return None
150
+
151
+ # 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
152
+ # 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
153
+
154
+ # Build the entire URL
155
+ url = \
156
+ 'https://api.transport.nsw.gov.au/v1/tp/trip?' \
157
+ 'outputFormat=rapidJSON&coordOutputFormat=EPSG%3A4326' \
158
+ '&depArrMacro=dep&itdDate=' + itdDate + '&itdTime=' + itdTime + \
159
+ '&type_origin=any&name_origin=' + self.name_origin + \
160
+ '&type_destination=any&name_destination=' + self.destination + \
161
+ '&TfNSWTR=true'
162
+ # '&calcNumberOfTrips=' + str(journeys_to_retrieve) + \
163
+
164
+ # Send the query and return an error if something goes wrong
165
+ # Otherwise store the response for the next steps
166
+ try:
167
+ response = httpx.get(url, headers=header, timeout=10)
168
+
169
+
170
+ except Exception as ex:
171
+ logger.error(f"Error {str(ex)} calling /v1/tp/trip API")
172
+ return None
173
+
174
+ # If we get bad status code, log error and return with n/a or an empty string
175
+ if response.status_code != 200:
176
+ if response.status_code == 429:
177
+ logger.error(f"Error {str(response.status_code)} calling /v1/tp/trip API; rate limit exceeded")
178
+ else:
179
+ logger.error(f"Error {str(response.status_code)} calling /v1/tp/trip API; check API key")
180
+
181
+ return None
182
+
183
+ # Parse the result as a JSON object
184
+ result = response.json()
185
+
186
+ # The API will always return a valid trip, so it's just a case of grabbing the metadata that we need...
187
+ # We're only reporting on the origin and destination, it's out of scope to discuss the specifics of the ENTIRE journey
188
+ # This isn't a route planner, just a 'how long until the next journey I've specified' tool
189
+ # The assumption is that the travelee will know HOW to make the defined journey, they're just asking WHEN it's happening next
190
+ # All we potentially have to do is find the first trip that matches the transport_type filter
191
+
192
+ if raw_output == True:
193
+ # Just return the raw output
194
+ return json.dumps(result)
195
+
196
+ # Make sure we've got at least one journey
197
+ try:
198
+ retrieved_journeys = len(result['journeys'])
199
+
200
+ except:
201
+ # Looks like an empty response
202
+ logger.error(f"Error {(str(err))} calling /v1/tp/trip API")
203
+
204
+ return None
205
+
206
+ # Loop through the results applying filters where required, and generate the appropriate JSON output including an array of in-scope trips
207
+ json_output=''
208
+ found_journeys = 0
209
+ no_valid_journeys = False
210
+
211
+ for current_journey_index in range (0, retrieved_journeys, 1):
212
+ # Look for a trip with a matching transport type filter in at least one of its legs. Either ANY, or the first leg, depending on how strict we're being
213
+ journey, next_journey_index = self.find_next_journey(result['journeys'], current_journey_index, transport_type, strict_transport_type, route_filter)
214
+
215
+ if ((journey is None) or (journey['legs']) is None):
216
+ pass
217
+ else:
218
+ legs = journey['legs']
219
+ first_leg = self.find_first_leg(legs, transport_type, strict_transport_type, route_filter)
220
+
221
+ #Executive decision - don't be strict on the last leg, there's often some walking (transport type 100) involved.
222
+ last_leg = self.find_last_leg(legs, transport_type, False)
223
+ changes = self.find_changes(legs, transport_type)
224
+
225
+ origin = first_leg['origin']
226
+ first_stop = first_leg['destination']
227
+ destination = last_leg['destination']
228
+ transportation = first_leg['transportation']
229
+
230
+ # Origin info
231
+ origin_stop_id = origin['id']
232
+ origin_name = origin['name']
233
+ origin_departure_time = origin['departureTimeEstimated']
234
+ origin_departure_time_planned = origin['departureTimePlanned']
235
+
236
+ t1 = datetime.strptime(origin_departure_time, fmt).timestamp()
237
+ t2 = datetime.strptime(origin_departure_time_planned, fmt).timestamp()
238
+ delay = int((t1-t2) / 60)
239
+
240
+ # How long until it leaves?
241
+ due = self.get_due(datetime.strptime(origin_departure_time, fmt))
242
+
243
+ # Destination info
244
+ destination_stop_id = destination['id']
245
+ destination_name = destination['name']
246
+ destination_arrival_time = destination['arrivalTimeEstimated']
247
+
248
+ # Origin type info - train, bus, etc
249
+ origin_mode = self.get_mode(transportation['product']['class'])
250
+ origin_mode_name = transportation['product']['name']
251
+
252
+ # RealTimeTripID info so we can try and get the current location later
253
+ realtimetripid = 'n/a'
254
+ if 'properties' in transportation and 'RealtimeTripId' in transportation['properties']:
255
+ realtimetripid = transportation['properties']['RealtimeTripId']
256
+
257
+ # We're also going to need the agency_id if it's a bus journey
258
+ agencyid = transportation['operator']['id']
259
+
260
+ # Line info
261
+ origin_line_name_short = "unknown"
262
+ if 'disassembledName' in transportation:
263
+ origin_line_name_short = transportation['disassembledName']
264
+
265
+ origin_line_name = "unknown"
266
+ if 'number' in transportation:
267
+ origin_line_name = transportation['number']
268
+
269
+ # Occupancy info, if it's there
270
+ occupancy = 'unknown'
271
+ if 'properties' in first_stop and 'occupancy' in first_stop['properties']:
272
+ occupancy = first_stop['properties']['occupancy']
273
+
274
+ alerts = "[]"
275
+ if self.include_alerts != 'none':
276
+ # We'll be adding these to the returned JSON string as an array
277
+ # Only include alerts of the specified priority or greater, and of the specified type
278
+ alerts = self.find_alerts(legs, self.include_alerts, self.alert_type)
279
+
280
+ latitude = 'n/a'
281
+ longitude = 'n/a'
282
+
283
+ if self.include_realtime_location and realtimetripid != 'n/a':
284
+ # See if we can get the latitute and longitude via the Realtime Vehicle Positions API
285
+ # Build the URL(s) - some modes have multiple GTFS sources, unforunately
286
+ # Some travel modes require brute-forcing the API call a few times, so if we're sure of the URI,
287
+ # ie it's been determined elsewhere then it can be forced
288
+
289
+ bFoundTripID = False
290
+ url_base_path = self.get_base_url(origin_mode)
291
+
292
+ # Check for a forced URI
293
+ if not forced_gtfs_uri:
294
+ url_mode_list = self.get_mode_list(origin_mode, agencyid)
295
+ else:
296
+ # We've been forced to use a specific URI!
297
+ url_mode_list = forced_gtfs_uri
298
+
299
+ if not url_mode_list is None:
300
+ for mode_url in url_mode_list:
301
+ url = url_base_path + mode_url
302
+ response = httpx.get(url, headers=header, timeout=10)
303
+
304
+ # Only try and process the results if we got a good return code
305
+ if response.status_code == 200:
306
+ # Search the feed and see if we can match realtimetripid to trip_id
307
+ # If we do, capture the latitude and longitude
308
+ feed = gtfs_realtime_pb2.FeedMessage()
309
+ feed.ParseFromString(response.content)
310
+ reg = re.compile(realtimetripid)
311
+
312
+ for entity in feed.entity:
313
+ if bool(re.match(reg, entity.vehicle.trip.trip_id)):
314
+ latitude = entity.vehicle.position.latitude
315
+ longitude = entity.vehicle.position.longitude
316
+
317
+ # We found it, so flag it and break out
318
+ bFoundTripID = True
319
+ break
320
+ else:
321
+ # Warn that we didn't get a good return
322
+ if response.status_code == 429:
323
+ logger.error(f"Error {str(response.status_code)} calling {url} API; rate limit exceeded")
324
+ else:
325
+ logger.error(f"Error {str(response.status_code)} calling {url} API; check API key")
326
+
327
+ if bFoundTripID == True:
328
+ # No need to look any further
329
+ break
330
+
331
+ # Put in a quick pause here to try and make sure we stay under the 5 API calls/second limit
332
+ # Not usually an issue but if multiple processes are running multiple calls we might hit it
333
+ time.sleep(0.75)
334
+
335
+ self.info = {
336
+ ATTR_DUE_IN: due,
337
+ ATTR_DELAY: delay,
338
+ ATTR_ORIGIN_STOP_ID : origin_stop_id,
339
+ ATTR_ORIGIN_NAME : origin_name,
340
+ ATTR_DEPARTURE_TIME : origin_departure_time,
341
+ ATTR_DESTINATION_STOP_ID : destination_stop_id,
342
+ ATTR_DESTINATION_NAME : destination_name,
343
+ ATTR_ARRIVAL_TIME : destination_arrival_time,
344
+ ATTR_ORIGIN_TRANSPORT_TYPE : origin_mode,
345
+ ATTR_ORIGIN_TRANSPORT_NAME: origin_mode_name,
346
+ ATTR_ORIGIN_LINE_NAME : origin_line_name,
347
+ ATTR_ORIGIN_LINE_NAME_SHORT : origin_line_name_short,
348
+ ATTR_CHANGES: changes,
349
+ ATTR_OCCUPANCY : occupancy,
350
+ ATTR_REAL_TIME_TRIP_ID : realtimetripid,
351
+ ATTR_LATITUDE : latitude,
352
+ ATTR_LONGITUDE : longitude,
353
+ ATTR_ALERTS: json.loads(alerts)
354
+ }
355
+
356
+ found_journeys = found_journeys + 1
357
+
358
+ # Add to the return array
359
+ if (no_valid_journeys == True):
360
+ break
361
+
362
+ if (found_journeys >= 2):
363
+ json_output = json_output + ',' + json.dumps(self.info)
364
+ else:
365
+ json_output = json_output + json.dumps(self.info)
366
+
367
+ if (found_journeys == journeys_to_return):
368
+ break
369
+
370
+ current_journey_index = next_journey_index
371
+
372
+ json_output='{"journeys_to_return": ' + str(self.journeys_to_return) + ', "journeys_with_data": ' + str(found_journeys) + ', "journeys": [' + json_output + ']}'
373
+ return json_output
374
+
375
+
376
+ def find_next_journey(self, journeys, start_journey_index, journeytype, strict, route_filter):
377
+ # Fnd the next journey that has a leg of the requested type, and/or that satisfies the route filter
378
+ journey_count = len(journeys)
379
+
380
+ # Some basic error checking
381
+ if start_journey_index > journey_count:
382
+ return None, None
383
+
384
+ for journey_index in range (start_journey_index, journey_count, 1):
385
+ leg = self.find_first_leg(journeys[journey_index]['legs'], journeytype, strict, route_filter)
386
+ if leg is not None:
387
+ return journeys[journey_index], journey_index + 1
388
+ else:
389
+ return None, None
390
+
391
+ # Hmm, we didn't find one
392
+ return None, None
393
+
394
+
395
+ def find_first_leg(self, legs, legtype, strict, route_filter):
396
+ # Find the first leg of the requested type
397
+ leg_count = len(legs)
398
+ for leg_index in range (0, leg_count, 1):
399
+ #First, check against the route filter
400
+ origin_line_name_short = 'n/a'
401
+ origin_line_name = 'n/a'
402
+
403
+ if 'transportation' in legs[leg_index] and 'disassembledName' in legs[leg_index]['transportation']:
404
+ origin_line_name_short = legs[leg_index]['transportation']['disassembledName'].lower()
405
+ origin_line_name = legs[leg_index]['transportation']['number'].lower()
406
+
407
+ if (route_filter in origin_line_name_short or route_filter in origin_line_name):
408
+ leg_class = legs[leg_index]['transportation']['product']['class']
409
+ # We've got a filter, and the leg type matches it, so return that leg
410
+ if legtype != 0 and leg_class == legtype:
411
+ return legs[leg_index]
412
+
413
+ # We don't have a filter, and this is the first non-walk/cycle leg so return that leg
414
+ if legtype == 0 and leg_class < 99:
415
+ return legs[leg_index]
416
+
417
+ # Exit if we're doing strict filtering and we haven't found that type in the first leg
418
+ if legtype != 0 and strict == True:
419
+ return None
420
+
421
+ # Hmm, we didn't find one
422
+ return None
423
+
424
+
425
+ def find_last_leg(self, legs, legtype, strict):
426
+ # Find the last leg of the requested type
427
+ leg_count = len(legs)
428
+ for leg_index in range (leg_count - 1, -1, -1):
429
+ leg_class = legs[leg_index]['transportation']['product']['class']
430
+
431
+ # We've got a filter, and the leg type matches it, so return that leg
432
+ if legtype != 0 and leg_class == legtype:
433
+ return legs[leg_index]
434
+
435
+ # We don't have a filter, and this is the first non-walk/cycle leg so return that leg
436
+ if legtype == 0 and leg_class < 99:
437
+ return legs[leg_index]
438
+
439
+ # Exit if we're doing strict filtering and we haven't found that type in the first leg
440
+ if legtype != 0 and strict == True:
441
+ return None
442
+
443
+ # Hmm, we didn't find one
444
+ return None
445
+
446
+
447
+ def find_changes(self, legs, legtype):
448
+ # Find out how often we have to change
449
+ changes = 0
450
+ leg_count = len(legs)
451
+
452
+ for leg_index in range (0, leg_count, 1):
453
+ leg_class = legs[leg_index]['transportation']['product']['class']
454
+ if leg_class == legtype or legtype == 0:
455
+ changes = changes + 1
456
+
457
+ return changes - 1
458
+
459
+
460
+ def find_alerts(self, legs, priority_filter, alert_type):
461
+ # Return an array of all the alerts on this trip that meet the priority level and alert type
462
+ leg_count = len(legs)
463
+ found_alerts = []
464
+ priority_minimum = self.get_alert_priority(priority_filter)
465
+ alert_list = alert_type.split("|")
466
+
467
+ for leg_index in range (0, leg_count, 1):
468
+ current_leg = legs[leg_index]
469
+ if 'infos' in current_leg:
470
+ alerts = current_leg['infos']
471
+ for alert in alerts:
472
+ if (self.get_alert_priority(alert['priority'])) >= priority_minimum:
473
+ if (alert_type == 'all') or (alert['type'].lower() in alert_list):
474
+ found_alerts.append (alert)
475
+
476
+ return json.dumps(found_alerts)
477
+
478
+
479
+ def find_hints(self, legs, legtype, priority):
480
+ # Return an array of all the hints on this trip that meet the priority type
481
+ leg_count = len(legs)
482
+
483
+ for leg_index in range (0, leg_count, 1):
484
+ current_leg = legs[leg_index]
485
+ leg_class = current_leg['transportation']['product']['class']
486
+ if 'hints' in current_leg:
487
+ hints = current_leg['hints']
488
+
489
+
490
+ def get_mode(self, iconId):
491
+ """Map the iconId to a full text string"""
492
+ modes = {
493
+ 1 : "Train",
494
+ 2 : "Metro",
495
+ 4 : "Light rail",
496
+ 5 : "Bus",
497
+ 7 : "Coach",
498
+ 9 : "Ferry",
499
+ 11 : "School bus",
500
+ 99 : "Walk",
501
+ 100 : "Walk",
502
+ 107 : "Cycle"
503
+ }
504
+
505
+ return modes.get(iconId, None)
506
+
507
+ def get_base_url(self, mode):
508
+ # Map the journey mode to the proper base real time location URL
509
+ v1_url = "https://api.transport.nsw.gov.au/v1/gtfs/vehiclepos"
510
+ v2_url = "https://api.transport.nsw.gov.au/v2/gtfs/vehiclepos"
511
+
512
+ url_options = {
513
+ "Train" : v2_url,
514
+ "Metro" : v2_url,
515
+ "Light rail" : v1_url,
516
+ "Bus" : v1_url,
517
+ "Coach" : v1_url,
518
+ "Ferry" : v1_url,
519
+ "School bus" : v1_url
520
+ }
521
+
522
+ return url_options.get(mode, None)
523
+
524
+
525
+ def get_alert_priority(self, alert_priority):
526
+ # Map the alert priority to a number so we can filter later
527
+
528
+ alert_priorities = {
529
+ "all" : 0,
530
+ "verylow" : 1,
531
+ "low" : 2,
532
+ "normal" : 3,
533
+ "high" : 4,
534
+ "veryhigh" : 5
535
+ }
536
+ return alert_priorities.get(alert_priority.lower(), 4)
537
+
538
+
539
+ def get_mode_list(self, mode, agencyid):
540
+ """
541
+ Map the journey mode to the proper modifier URL. If the mode is Bus, Coach or School bus then use the agency ID to invoke the GTFS datastore search API
542
+ which will give us the appropriate URL to call later - we still have to do light rail the old-fashioned, brute-force way though
543
+ """
544
+
545
+ if mode in ["Bus", "Coach", "School bus"]:
546
+ # Use this CSV to determine the appropriate real-time location URL
547
+ # I'm hoping that this CSV resource URL is static when updated by TransportNSW!
548
+ url = "https://opendata.transport.nsw.gov.au/data/api/action/datastore_search?resource_id=30b850b7-f439-4e30-8072-e07ef62a2a36&filters={%22For%20Realtime%20GTFS%20agency_id%22:%22" + agencyid + "%22}&limit=1"
549
+
550
+ # Send the query and return an error if something goes wrong
551
+ try:
552
+ response = httpx.get(url, timeout=5)
553
+ except Exception as ex:
554
+ logger.error("Error " + str(ex) + " querying GTFS URL datastore")
555
+ return None
556
+
557
+ # If we get bad status code, log error and return with None
558
+ if response.status_code != 200:
559
+ if response.status_code == 429:
560
+ logger.error("Error " + str(response.status_code) + " calling /v1/tp/stop_finder API; rate limit exceeded")
561
+ else:
562
+ logger.error("Error " + str(response.status_code) + " calling /v1/tp/stop_finder API; check API key")
563
+
564
+ return None
565
+
566
+ # Parse the result as JSON
567
+ result = response.json()
568
+ if 'records' in result['result'] and len(result['result']['records']) > 0:
569
+ mode_path = result['result']['records'][0]['For Realtime parameter']
570
+ else:
571
+ return None
572
+
573
+ # Even though there's only one URL we need to return as a list as Light Rail still has multiple URLs that need to be brute-forced, unfortunately
574
+ bus_list = ["/" + mode_path]
575
+ return bus_list
576
+ else:
577
+ # Handle the other modes
578
+ url_options = {
579
+ "Train" : ["/sydneytrains"],
580
+ "Metro" : ["/metro"],
581
+ "Light rail" : ["/lightrail/innerwest", "/lightrail/cbdandsoutheast", "/lightrail/newcastle"],
582
+ "Ferry" : ["/ferries/sydneyferries"]
583
+ }
584
+ return url_options.get(mode, None)
585
+
586
+
587
+ def get_due(self, estimated):
588
+ # Minutes until departure
589
+ due = 0
590
+ if estimated > datetime.utcnow():
591
+ due = round((estimated - datetime.utcnow()).seconds / 60)
592
+ return due
@@ -43,7 +43,7 @@ normal
43
43
  high
44
44
  veryHigh
45
45
  ```
46
- Specifying an alert priority in ```include_alerts``` means that any alerts of that priority or higher will be included in the output as a raw JSON array, basically a collation of the alerts that the Trip API sent back. If you've specified that alerts of a given priority should be included then by default ALL alert types will be included - you can limit the output to specific alert types by setting ```alert_type``` to something like ```'lineInfo|stopInfo|bannerInfo'```.
46
+ Specifying an alert priority in ```include_alerts``` means that any alerts of that priority or higher will be included in the output as a raw JSON array, basically a collation of the alerts that the Trip API sent back. If you've specified that alerts of a given priority should be included then by default ALL alert types will be included - you can limit the output to specific alert types by setting ```alert_type``` to something like ```lineInfo|stopInfo|bannerInfo```.
47
47
 
48
48
  Alert types:
49
49
  ```
@@ -5,7 +5,7 @@ with open("README.md", "r") as fh:
5
5
 
6
6
  setuptools.setup(
7
7
  name="PyTransportNSWv2",
8
- version="0.8.9",
8
+ version="0.9.0",
9
9
  author="andystewart999",
10
10
  author_email="andy.stewart@live.com",
11
11
  description="Get detailed per-trip transport information from TransportNSW",
@@ -15,6 +15,7 @@ setuptools.setup(
15
15
  packages=setuptools.find_packages(),
16
16
  install_requires=[
17
17
  'gtfs-realtime-bindings',
18
+ 'httpx',
18
19
  ],
19
20
  classifiers=[
20
21
  "Programming Language :: Python :: 3",