PyTransportNSWv2 0.8.10__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.
- {PyTransportNSWv2-0.8.10 → PyTransportNSWv2-0.9.0}/PKG-INFO +2 -2
- {PyTransportNSWv2-0.8.10 → PyTransportNSWv2-0.9.0}/PyTransportNSWv2.egg-info/PKG-INFO +2 -2
- {PyTransportNSWv2-0.8.10 → PyTransportNSWv2-0.9.0}/PyTransportNSWv2.egg-info/SOURCES.txt +1 -0
- PyTransportNSWv2-0.9.0/PyTransportNSWv2.egg-info/TransportNSWv2.py +592 -0
- {PyTransportNSWv2-0.8.10 → PyTransportNSWv2-0.9.0}/PyTransportNSWv2.egg-info/requires.txt +1 -0
- {PyTransportNSWv2-0.8.10 → PyTransportNSWv2-0.9.0}/README.md +1 -1
- {PyTransportNSWv2-0.8.10 → PyTransportNSWv2-0.9.0}/setup.py +2 -1
- {PyTransportNSWv2-0.8.10 → PyTransportNSWv2-0.9.0}/LICENSE +0 -0
- {PyTransportNSWv2-0.8.10 → PyTransportNSWv2-0.9.0}/PyTransportNSWv2.egg-info/dependency_links.txt +0 -0
- {PyTransportNSWv2-0.8.10 → PyTransportNSWv2-0.9.0}/PyTransportNSWv2.egg-info/top_level.txt +0 -0
- {PyTransportNSWv2-0.8.10 → PyTransportNSWv2-0.9.0}/TransportNSWv2/TransportNSWv2.py +0 -0
- {PyTransportNSWv2-0.8.10 → PyTransportNSWv2-0.9.0}/TransportNSWv2/__init__.py +0 -0
- {PyTransportNSWv2-0.8.10 → PyTransportNSWv2-0.9.0}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.1
|
2
2
|
Name: PyTransportNSWv2
|
3
|
-
Version: 0.
|
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 ```
|
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.
|
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 ```
|
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 ```
|
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
|
+
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",
|
File without changes
|
{PyTransportNSWv2-0.8.10 → PyTransportNSWv2-0.9.0}/PyTransportNSWv2.egg-info/dependency_links.txt
RENAMED
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|