PyTransportNSWv2 0.4.0__tar.gz → 0.5.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.
@@ -0,0 +1,77 @@
1
+ Metadata-Version: 2.1
2
+ Name: PyTransportNSWv2
3
+ Version: 0.5.0
4
+ Summary: Get detailed per-trip transport information from TransportNSW
5
+ Home-page: https://github.com/andystewart999/TransportNSW
6
+ Author: andystewart999
7
+ License: UNKNOWN
8
+ Description: # TransportNSWv2
9
+ Python lib to access Transport NSW information.
10
+
11
+ ## How to Use
12
+
13
+ ### Get an API Key
14
+ An OpenData account and API key is required to request the data. More information on how to create the free account can be found here:
15
+ https://opendata.transport.nsw.gov.au/user-guide. You need to register an application that needs both the Trip Planner and Realtime Vehicle Positions APIs
16
+
17
+ ### Get the stop IDs
18
+ The function needs the stop IDs for the source and destination, and optionally how many minutes from now the departure should be, and if you want to filter trips by a specific transport type. The easiest way to get the stop ID is via https://transportnsw.info/stops#/. It provides the option to search for either a location or a specific platform, bus stop or ferry wharf. Regardless of if you specify a general location for the origin or destination, the return information shows the stop_id for the actual arrival and destination platform, bus stop or ferry wharf.
19
+
20
+ If it's available, the general occupancy level and the latitude and longitude of the selected journey's vehicle (train, bus, etc) will be returned.
21
+
22
+ ### API Documentation
23
+ The source API details can be found here: https://opendata.transport.nsw.gov.au/sites/default/files/2023-08/Trip%20Planner%20API%20manual-opendataproduction%20v3.2.pdf
24
+
25
+ ### Parameters
26
+ ```python
27
+ .get_trip(origin_stop_id, destination_stop_id, api_key, [trip_wait_time = 0], [transport_type = 0])
28
+ ```
29
+
30
+ TransportNSW's trip planner can work better if you use the general location IDs (eg Central Station) rather than a specific Stop ID (eg Central Station, Platform 19) for the destination, depending on the transport type. Forcing a specific end destination sometimes results in much more complicated trips. Also note that the API expects (and returns) the Stop IDs as strings, although so far they all appear to be numeric.
31
+
32
+ ### Sample Code
33
+
34
+ The following example will return the next trip that starts from a bus stop in St. Ives (207537) five minutes from now, to Central Station's general stop ID (10101100):
35
+
36
+ **Code:**
37
+ ```python
38
+ from TransportNSWv2 import TransportNSWv2
39
+ tnsw = TransportNSWv2()
40
+ journey = tnsw.get_trip('207537', '10101100', 'YOUR_API_KEY', 5)
41
+ print(journey)
42
+ ```
43
+ **Result:**
44
+ ```python
45
+ {"due": 3, "origin_stop_id": "207537", "origin_name": "Mona Vale Rd at Shinfield Ave, St Ives", "departure_time": "2024-05-20T21:59:48Z", "destination_stop_id": "2000338", "destination_name": "Central Station, Platform 18, Sydney", "arrival_time": "2024-05-20T22:47:36Z", "origin_transport_type": "Bus", "origin_transport_name": "Sydney Buses Network", "origin_line_name": "195", "origin_line_name_short": "195", "changes": 1, "occupancy": "MANY_SEATS", "real_time_trip_id": "2096551", "latitude": -33.72665786743164, "longitude": 151.16305541992188}
46
+ ```
47
+ Fun fact: TransportNSW's raw API output calls itself JSON, but it uses single quotes for strings in defiance of the JSON standards. When using this wrapper the output is formatted such that `jq`, for example, is happy with it.
48
+
49
+ * due: the time (in minutes) before the journey starts
50
+ * origin_stop_id: the specific departure stop id
51
+ * origin_name: the name of the departure location
52
+ * departure_time: the departure time, in UTC
53
+ * destination_stop_id: the specific destination stop id
54
+ * destination_name: the name of the destination location
55
+ * arrival_time: the planned arrival time at the origin, in UTC
56
+ * origin_transport_type: the type of transport, eg train, bus, ferry etc
57
+ * origin_transport_name: the full name of the transport provider
58
+ * origin_line_name & origin_line_name_short: the full and short names of the journey
59
+ * changes: how many transport changes are needed on the journey
60
+ * occupancy: how full the vehicle is, if available
61
+ * real_time_trip_id: the unique TransportNSW id for that specific journey, if available
62
+ * latitude & longitude: The location of the vehicle, if available
63
+
64
+ Please note that the origin and destination detail is just that - information about the first and last stops on the journey at the time the request was made. We don't return any intermediate steps, transport change types etc other than the total number of changes - the assumption is that you'll know the details of your specified trip, you just want to know when the next departure is. If you need much more detailed information then I recommend that you use the full Transport NSW trip planner website or application.
65
+ Also note that the 'transport_type' filter, if present, only makes sure that at least one leg of the journey includes that transport type.
66
+
67
+ ## Thank you
68
+ Thank you Dav0815 for your TransportNSW library that the vast majority of this fork is based on. I couldn't have done it without you!
69
+ https://github.com/Dav0815/TransportNSW
70
+
71
+ Platform: UNKNOWN
72
+ Classifier: Programming Language :: Python :: 3
73
+ Classifier: License :: OSI Approved :: MIT License
74
+ Classifier: Operating System :: OS Independent
75
+ Classifier: Intended Audience :: Developers
76
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
77
+ Description-Content-Type: text/markdown
@@ -0,0 +1,77 @@
1
+ Metadata-Version: 2.1
2
+ Name: PyTransportNSWv2
3
+ Version: 0.5.0
4
+ Summary: Get detailed per-trip transport information from TransportNSW
5
+ Home-page: https://github.com/andystewart999/TransportNSW
6
+ Author: andystewart999
7
+ License: UNKNOWN
8
+ Description: # TransportNSWv2
9
+ Python lib to access Transport NSW information.
10
+
11
+ ## How to Use
12
+
13
+ ### Get an API Key
14
+ An OpenData account and API key is required to request the data. More information on how to create the free account can be found here:
15
+ https://opendata.transport.nsw.gov.au/user-guide. You need to register an application that needs both the Trip Planner and Realtime Vehicle Positions APIs
16
+
17
+ ### Get the stop IDs
18
+ The function needs the stop IDs for the source and destination, and optionally how many minutes from now the departure should be, and if you want to filter trips by a specific transport type. The easiest way to get the stop ID is via https://transportnsw.info/stops#/. It provides the option to search for either a location or a specific platform, bus stop or ferry wharf. Regardless of if you specify a general location for the origin or destination, the return information shows the stop_id for the actual arrival and destination platform, bus stop or ferry wharf.
19
+
20
+ If it's available, the general occupancy level and the latitude and longitude of the selected journey's vehicle (train, bus, etc) will be returned.
21
+
22
+ ### API Documentation
23
+ The source API details can be found here: https://opendata.transport.nsw.gov.au/sites/default/files/2023-08/Trip%20Planner%20API%20manual-opendataproduction%20v3.2.pdf
24
+
25
+ ### Parameters
26
+ ```python
27
+ .get_trip(origin_stop_id, destination_stop_id, api_key, [trip_wait_time = 0], [transport_type = 0])
28
+ ```
29
+
30
+ TransportNSW's trip planner can work better if you use the general location IDs (eg Central Station) rather than a specific Stop ID (eg Central Station, Platform 19) for the destination, depending on the transport type. Forcing a specific end destination sometimes results in much more complicated trips. Also note that the API expects (and returns) the Stop IDs as strings, although so far they all appear to be numeric.
31
+
32
+ ### Sample Code
33
+
34
+ The following example will return the next trip that starts from a bus stop in St. Ives (207537) five minutes from now, to Central Station's general stop ID (10101100):
35
+
36
+ **Code:**
37
+ ```python
38
+ from TransportNSWv2 import TransportNSWv2
39
+ tnsw = TransportNSWv2()
40
+ journey = tnsw.get_trip('207537', '10101100', 'YOUR_API_KEY', 5)
41
+ print(journey)
42
+ ```
43
+ **Result:**
44
+ ```python
45
+ {"due": 3, "origin_stop_id": "207537", "origin_name": "Mona Vale Rd at Shinfield Ave, St Ives", "departure_time": "2024-05-20T21:59:48Z", "destination_stop_id": "2000338", "destination_name": "Central Station, Platform 18, Sydney", "arrival_time": "2024-05-20T22:47:36Z", "origin_transport_type": "Bus", "origin_transport_name": "Sydney Buses Network", "origin_line_name": "195", "origin_line_name_short": "195", "changes": 1, "occupancy": "MANY_SEATS", "real_time_trip_id": "2096551", "latitude": -33.72665786743164, "longitude": 151.16305541992188}
46
+ ```
47
+ Fun fact: TransportNSW's raw API output calls itself JSON, but it uses single quotes for strings in defiance of the JSON standards. When using this wrapper the output is formatted such that `jq`, for example, is happy with it.
48
+
49
+ * due: the time (in minutes) before the journey starts
50
+ * origin_stop_id: the specific departure stop id
51
+ * origin_name: the name of the departure location
52
+ * departure_time: the departure time, in UTC
53
+ * destination_stop_id: the specific destination stop id
54
+ * destination_name: the name of the destination location
55
+ * arrival_time: the planned arrival time at the origin, in UTC
56
+ * origin_transport_type: the type of transport, eg train, bus, ferry etc
57
+ * origin_transport_name: the full name of the transport provider
58
+ * origin_line_name & origin_line_name_short: the full and short names of the journey
59
+ * changes: how many transport changes are needed on the journey
60
+ * occupancy: how full the vehicle is, if available
61
+ * real_time_trip_id: the unique TransportNSW id for that specific journey, if available
62
+ * latitude & longitude: The location of the vehicle, if available
63
+
64
+ Please note that the origin and destination detail is just that - information about the first and last stops on the journey at the time the request was made. We don't return any intermediate steps, transport change types etc other than the total number of changes - the assumption is that you'll know the details of your specified trip, you just want to know when the next departure is. If you need much more detailed information then I recommend that you use the full Transport NSW trip planner website or application.
65
+ Also note that the 'transport_type' filter, if present, only makes sure that at least one leg of the journey includes that transport type.
66
+
67
+ ## Thank you
68
+ Thank you Dav0815 for your TransportNSW library that the vast majority of this fork is based on. I couldn't have done it without you!
69
+ https://github.com/Dav0815/TransportNSW
70
+
71
+ Platform: UNKNOWN
72
+ Classifier: Programming Language :: Python :: 3
73
+ Classifier: License :: OSI Approved :: MIT License
74
+ Classifier: Operating System :: OS Independent
75
+ Classifier: Intended Audience :: Developers
76
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
77
+ Description-Content-Type: text/markdown
@@ -6,5 +6,5 @@ PyTransportNSWv2.egg-info/SOURCES.txt
6
6
  PyTransportNSWv2.egg-info/dependency_links.txt
7
7
  PyTransportNSWv2.egg-info/requires.txt
8
8
  PyTransportNSWv2.egg-info/top_level.txt
9
- TransportNSW/TransportNSW.py
10
- TransportNSW/__init__.py
9
+ TransportNSWv2/TransportNSWv2.py
10
+ TransportNSWv2/__init__.py
@@ -1,2 +1 @@
1
1
  gtfs-realtime-bindings
2
- requests
@@ -0,0 +1 @@
1
+ TransportNSWv2
@@ -1,4 +1,4 @@
1
- # TransportNSW
1
+ # TransportNSWv2
2
2
  Python lib to access Transport NSW information.
3
3
 
4
4
  ## How to Use
@@ -17,66 +17,25 @@ The source API details can be found here: https://opendata.transport.nsw.gov.au/
17
17
 
18
18
  ### Parameters
19
19
  ```python
20
- .get_trip(origin_stop_id, destination_stop_id, api_key, [journey_wait_time = 0], [transport_type = 0], [strict_transport_type = False], [raw_output = False], [journeys_to_return = 1])
20
+ .get_trip(origin_stop_id, destination_stop_id, api_key, [trip_wait_time = 0], [transport_type = 0])
21
21
  ```
22
- TransportNSW's trip planner can work better if you use the general location IDs (eg Central Station) rather than a specific Stop ID (eg Central Station, Platform 19) for the destination, depending on the transport type. Forcing a specific end destination sometimes results in much more complicated trips. Also note that the API expects (and returns) the Stop IDs as strings, although so far they all appear to be numeric.
23
-
24
- ### transport_type filters
25
- ```
26
- 1: Train
27
- 4: Light rail
28
- 5: Bus
29
- 7: Coach
30
- 9: Ferry
31
- 11: School bus
32
- 99: Walk
33
- 100: Walk
34
- 107: Cycle
35
- ```
36
- If you call the function with a `transport_type` filter and set `strict_transport_type` to `True`, only journeys whose **first** leg matches the desired filter will be considered. Otherwise the filter includes a journey if **any** of the legs includes the desired travel type.
37
22
 
38
- `raw_output` simply returns the entire API response string as JSON, without making any changes to it.
23
+ TransportNSW's trip planner can work better if you use the general location IDs (eg Central Station) rather than a specific Stop ID (eg Central Station, Platform 19) for the destination, depending on the transport type. Forcing a specific end destination sometimes results in much more complicated trips. Also note that the API expects (and returns) the Stop IDs as strings, although so far they all appear to be numeric.
39
24
 
40
25
  ### Sample Code
41
26
 
42
- The following example will return the next trip that starts from Pymble Station (207310) five minutes from now, to Gordon Station (207210). Note that specific platforms, such as Gordon Station, Platform 3 (207263) haven't been specified so any platform combination will be accepted:
27
+ The following example will return the next trip that starts from a bus stop in St. Ives (207537) five minutes from now, to Central Station's general stop ID (10101100):
43
28
 
44
29
  **Code:**
45
30
  ```python
46
- from TransportNSW import TransportNSW
47
- tnsw = TransportNSW()
31
+ from TransportNSWv2 import TransportNSWv2
32
+ tnsw = TransportNSWv2()
48
33
  journey = tnsw.get_trip('207537', '10101100', 'YOUR_API_KEY', 5)
49
34
  print(journey)
50
35
  ```
51
36
  **Result:**
52
-
53
- Unless `raw_output` is `True`, the return output always returns an array of journeys, even if `journeys_to_return` is 1. The journey array is preceded by how many journeys were requested, and how many were actually returned that contained usable data:
54
-
55
- ```json
56
- {
57
- "journeys_to_return": 1,
58
- "journeys_with_data": 1,
59
- "journeys": [
60
- {
61
- "due": 6,
62
- "origin_stop_id": "2073161",
63
- "origin_name": "Pymble Station, Platform 1, Pymble",
64
- "departure_time": "2024-05-28T22:40:24Z",
65
- "destination_stop_id": "207261",
66
- "destination_name": "Gordon Station, Platform 1, Gordon",
67
- "arrival_time": "2024-05-28T22:42:30Z",
68
- "origin_transport_type": "Train",
69
- "origin_transport_name": "Sydney Trains Network",
70
- "origin_line_name": "T1 North Shore & Western Line",
71
- "origin_line_name_short": "T1",
72
- "changes": 0,
73
- "occupancy": "UNKNOWN",
74
- "real_time_trip_id": "161E.1378.133.60.A.8.80758268",
75
- "latitude": "n/a",
76
- "longitude": "n/a"
77
- }
78
- ]
79
- }
37
+ ```python
38
+ {"due": 3, "origin_stop_id": "207537", "origin_name": "Mona Vale Rd at Shinfield Ave, St Ives", "departure_time": "2024-05-20T21:59:48Z", "destination_stop_id": "2000338", "destination_name": "Central Station, Platform 18, Sydney", "arrival_time": "2024-05-20T22:47:36Z", "origin_transport_type": "Bus", "origin_transport_name": "Sydney Buses Network", "origin_line_name": "195", "origin_line_name_short": "195", "changes": 1, "occupancy": "MANY_SEATS", "real_time_trip_id": "2096551", "latitude": -33.72665786743164, "longitude": 151.16305541992188}
80
39
  ```
81
40
  Fun fact: TransportNSW's raw API output calls itself JSON, but it uses single quotes for strings in defiance of the JSON standards. When using this wrapper the output is formatted such that `jq`, for example, is happy with it.
82
41
 
@@ -96,6 +55,7 @@ Fun fact: TransportNSW's raw API output calls itself JSON, but it uses single q
96
55
  * latitude & longitude: The location of the vehicle, if available
97
56
 
98
57
  Please note that the origin and destination detail is just that - information about the first and last stops on the journey at the time the request was made. We don't return any intermediate steps, transport change types etc other than the total number of changes - the assumption is that you'll know the details of your specified trip, you just want to know when the next departure is. If you need much more detailed information then I recommend that you use the full Transport NSW trip planner website or application.
58
+ Also note that the 'transport_type' filter, if present, only makes sure that at least one leg of the journey includes that transport type.
99
59
 
100
60
  ## Thank you
101
61
  Thank you Dav0815 for your TransportNSW library that the vast majority of this fork is based on. I couldn't have done it without you!
@@ -0,0 +1,346 @@
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
@@ -0,0 +1,2 @@
1
+ """Define module-level imports."""
2
+ from TransportNSWv2.TransportNSWv2 import TransportNSWv2
@@ -5,7 +5,7 @@ with open("README.md", "r") as fh:
5
5
 
6
6
  setuptools.setup(
7
7
  name="PyTransportNSWv2",
8
- version="0.4.0",
8
+ version="0.5.0",
9
9
  author="andystewart999",
10
10
  description="Get detailed per-trip transport information from TransportNSW",
11
11
  long_description=long_description,
@@ -14,7 +14,6 @@ setuptools.setup(
14
14
  packages=setuptools.find_packages(),
15
15
  install_requires=[
16
16
  'gtfs-realtime-bindings',
17
- 'requests'
18
17
  ],
19
18
  classifiers=[
20
19
  "Programming Language :: Python :: 3",
@@ -1,118 +0,0 @@
1
- Metadata-Version: 2.1
2
- Name: PyTransportNSWv2
3
- Version: 0.4.0
4
- Summary: Get detailed per-trip transport information from TransportNSW
5
- Home-page: https://github.com/andystewart999/TransportNSW
6
- Author: andystewart999
7
- Classifier: Programming Language :: Python :: 3
8
- Classifier: License :: OSI Approved :: MIT License
9
- Classifier: Operating System :: OS Independent
10
- Classifier: Intended Audience :: Developers
11
- Classifier: Topic :: Software Development :: Libraries :: Python Modules
12
- Description-Content-Type: text/markdown
13
- License-File: LICENSE
14
- Requires-Dist: gtfs-realtime-bindings
15
- Requires-Dist: requests
16
-
17
- # TransportNSW
18
- Python lib to access Transport NSW information.
19
-
20
- ## How to Use
21
-
22
- ### Get an API Key
23
- An OpenData account and API key is required to request the data. More information on how to create the free account can be found here:
24
- https://opendata.transport.nsw.gov.au/user-guide. You need to register an application that needs both the Trip Planner and Realtime Vehicle Positions APIs
25
-
26
- ### Get the stop IDs
27
- The function needs the stop IDs for the source and destination, and optionally how many minutes from now the departure should be, and if you want to filter trips by a specific transport type. The easiest way to get the stop ID is via https://transportnsw.info/stops#/. It provides the option to search for either a location or a specific platform, bus stop or ferry wharf. Regardless of if you specify a general location for the origin or destination, the return information shows the stop_id for the actual arrival and destination platform, bus stop or ferry wharf.
28
-
29
- If it's available, the general occupancy level and the latitude and longitude of the selected journey's vehicle (train, bus, etc) will be returned.
30
-
31
- ### API Documentation
32
- The source API details can be found here: https://opendata.transport.nsw.gov.au/sites/default/files/2023-08/Trip%20Planner%20API%20manual-opendataproduction%20v3.2.pdf
33
-
34
- ### Parameters
35
- ```python
36
- .get_trip(origin_stop_id, destination_stop_id, api_key, [journey_wait_time = 0], [transport_type = 0], [strict_transport_type = False], [raw_output = False], [journeys_to_return = 1])
37
- ```
38
- TransportNSW's trip planner can work better if you use the general location IDs (eg Central Station) rather than a specific Stop ID (eg Central Station, Platform 19) for the destination, depending on the transport type. Forcing a specific end destination sometimes results in much more complicated trips. Also note that the API expects (and returns) the Stop IDs as strings, although so far they all appear to be numeric.
39
-
40
- ### transport_type filters
41
- ```
42
- 1: Train
43
- 4: Light rail
44
- 5: Bus
45
- 7: Coach
46
- 9: Ferry
47
- 11: School bus
48
- 99: Walk
49
- 100: Walk
50
- 107: Cycle
51
- ```
52
- If you call the function with a `transport_type` filter and set `strict_transport_type` to `True`, only journeys whose **first** leg matches the desired filter will be considered. Otherwise the filter includes a journey if **any** of the legs includes the desired travel type.
53
-
54
- `raw_output` simply returns the entire API response string as JSON, without making any changes to it.
55
-
56
- ### Sample Code
57
-
58
- The following example will return the next trip that starts from Pymble Station (207310) five minutes from now, to Gordon Station (207210). Note that specific platforms, such as Gordon Station, Platform 3 (207263) haven't been specified so any platform combination will be accepted:
59
-
60
- **Code:**
61
- ```python
62
- from TransportNSW import TransportNSW
63
- tnsw = TransportNSW()
64
- journey = tnsw.get_trip('207537', '10101100', 'YOUR_API_KEY', 5)
65
- print(journey)
66
- ```
67
- **Result:**
68
-
69
- Unless `raw_output` is `True`, the return output always returns an array of journeys, even if `journeys_to_return` is 1. The journey array is preceded by how many journeys were requested, and how many were actually returned that contained usable data:
70
-
71
- ```json
72
- {
73
- "journeys_to_return": 1,
74
- "journeys_with_data": 1,
75
- "journeys": [
76
- {
77
- "due": 6,
78
- "origin_stop_id": "2073161",
79
- "origin_name": "Pymble Station, Platform 1, Pymble",
80
- "departure_time": "2024-05-28T22:40:24Z",
81
- "destination_stop_id": "207261",
82
- "destination_name": "Gordon Station, Platform 1, Gordon",
83
- "arrival_time": "2024-05-28T22:42:30Z",
84
- "origin_transport_type": "Train",
85
- "origin_transport_name": "Sydney Trains Network",
86
- "origin_line_name": "T1 North Shore & Western Line",
87
- "origin_line_name_short": "T1",
88
- "changes": 0,
89
- "occupancy": "UNKNOWN",
90
- "real_time_trip_id": "161E.1378.133.60.A.8.80758268",
91
- "latitude": "n/a",
92
- "longitude": "n/a"
93
- }
94
- ]
95
- }
96
- ```
97
- Fun fact: TransportNSW's raw API output calls itself JSON, but it uses single quotes for strings in defiance of the JSON standards. When using this wrapper the output is formatted such that `jq`, for example, is happy with it.
98
-
99
- * due: the time (in minutes) before the journey starts
100
- * origin_stop_id: the specific departure stop id
101
- * origin_name: the name of the departure location
102
- * departure_time: the departure time, in UTC
103
- * destination_stop_id: the specific destination stop id
104
- * destination_name: the name of the destination location
105
- * arrival_time: the planned arrival time at the origin, in UTC
106
- * origin_transport_type: the type of transport, eg train, bus, ferry etc
107
- * origin_transport_name: the full name of the transport provider
108
- * origin_line_name & origin_line_name_short: the full and short names of the journey
109
- * changes: how many transport changes are needed on the journey
110
- * occupancy: how full the vehicle is, if available
111
- * real_time_trip_id: the unique TransportNSW id for that specific journey, if available
112
- * latitude & longitude: The location of the vehicle, if available
113
-
114
- Please note that the origin and destination detail is just that - information about the first and last stops on the journey at the time the request was made. We don't return any intermediate steps, transport change types etc other than the total number of changes - the assumption is that you'll know the details of your specified trip, you just want to know when the next departure is. If you need much more detailed information then I recommend that you use the full Transport NSW trip planner website or application.
115
-
116
- ## Thank you
117
- Thank you Dav0815 for your TransportNSW library that the vast majority of this fork is based on. I couldn't have done it without you!
118
- https://github.com/Dav0815/TransportNSW
@@ -1,118 +0,0 @@
1
- Metadata-Version: 2.1
2
- Name: PyTransportNSWv2
3
- Version: 0.4.0
4
- Summary: Get detailed per-trip transport information from TransportNSW
5
- Home-page: https://github.com/andystewart999/TransportNSW
6
- Author: andystewart999
7
- Classifier: Programming Language :: Python :: 3
8
- Classifier: License :: OSI Approved :: MIT License
9
- Classifier: Operating System :: OS Independent
10
- Classifier: Intended Audience :: Developers
11
- Classifier: Topic :: Software Development :: Libraries :: Python Modules
12
- Description-Content-Type: text/markdown
13
- License-File: LICENSE
14
- Requires-Dist: gtfs-realtime-bindings
15
- Requires-Dist: requests
16
-
17
- # TransportNSW
18
- Python lib to access Transport NSW information.
19
-
20
- ## How to Use
21
-
22
- ### Get an API Key
23
- An OpenData account and API key is required to request the data. More information on how to create the free account can be found here:
24
- https://opendata.transport.nsw.gov.au/user-guide. You need to register an application that needs both the Trip Planner and Realtime Vehicle Positions APIs
25
-
26
- ### Get the stop IDs
27
- The function needs the stop IDs for the source and destination, and optionally how many minutes from now the departure should be, and if you want to filter trips by a specific transport type. The easiest way to get the stop ID is via https://transportnsw.info/stops#/. It provides the option to search for either a location or a specific platform, bus stop or ferry wharf. Regardless of if you specify a general location for the origin or destination, the return information shows the stop_id for the actual arrival and destination platform, bus stop or ferry wharf.
28
-
29
- If it's available, the general occupancy level and the latitude and longitude of the selected journey's vehicle (train, bus, etc) will be returned.
30
-
31
- ### API Documentation
32
- The source API details can be found here: https://opendata.transport.nsw.gov.au/sites/default/files/2023-08/Trip%20Planner%20API%20manual-opendataproduction%20v3.2.pdf
33
-
34
- ### Parameters
35
- ```python
36
- .get_trip(origin_stop_id, destination_stop_id, api_key, [journey_wait_time = 0], [transport_type = 0], [strict_transport_type = False], [raw_output = False], [journeys_to_return = 1])
37
- ```
38
- TransportNSW's trip planner can work better if you use the general location IDs (eg Central Station) rather than a specific Stop ID (eg Central Station, Platform 19) for the destination, depending on the transport type. Forcing a specific end destination sometimes results in much more complicated trips. Also note that the API expects (and returns) the Stop IDs as strings, although so far they all appear to be numeric.
39
-
40
- ### transport_type filters
41
- ```
42
- 1: Train
43
- 4: Light rail
44
- 5: Bus
45
- 7: Coach
46
- 9: Ferry
47
- 11: School bus
48
- 99: Walk
49
- 100: Walk
50
- 107: Cycle
51
- ```
52
- If you call the function with a `transport_type` filter and set `strict_transport_type` to `True`, only journeys whose **first** leg matches the desired filter will be considered. Otherwise the filter includes a journey if **any** of the legs includes the desired travel type.
53
-
54
- `raw_output` simply returns the entire API response string as JSON, without making any changes to it.
55
-
56
- ### Sample Code
57
-
58
- The following example will return the next trip that starts from Pymble Station (207310) five minutes from now, to Gordon Station (207210). Note that specific platforms, such as Gordon Station, Platform 3 (207263) haven't been specified so any platform combination will be accepted:
59
-
60
- **Code:**
61
- ```python
62
- from TransportNSW import TransportNSW
63
- tnsw = TransportNSW()
64
- journey = tnsw.get_trip('207537', '10101100', 'YOUR_API_KEY', 5)
65
- print(journey)
66
- ```
67
- **Result:**
68
-
69
- Unless `raw_output` is `True`, the return output always returns an array of journeys, even if `journeys_to_return` is 1. The journey array is preceded by how many journeys were requested, and how many were actually returned that contained usable data:
70
-
71
- ```json
72
- {
73
- "journeys_to_return": 1,
74
- "journeys_with_data": 1,
75
- "journeys": [
76
- {
77
- "due": 6,
78
- "origin_stop_id": "2073161",
79
- "origin_name": "Pymble Station, Platform 1, Pymble",
80
- "departure_time": "2024-05-28T22:40:24Z",
81
- "destination_stop_id": "207261",
82
- "destination_name": "Gordon Station, Platform 1, Gordon",
83
- "arrival_time": "2024-05-28T22:42:30Z",
84
- "origin_transport_type": "Train",
85
- "origin_transport_name": "Sydney Trains Network",
86
- "origin_line_name": "T1 North Shore & Western Line",
87
- "origin_line_name_short": "T1",
88
- "changes": 0,
89
- "occupancy": "UNKNOWN",
90
- "real_time_trip_id": "161E.1378.133.60.A.8.80758268",
91
- "latitude": "n/a",
92
- "longitude": "n/a"
93
- }
94
- ]
95
- }
96
- ```
97
- Fun fact: TransportNSW's raw API output calls itself JSON, but it uses single quotes for strings in defiance of the JSON standards. When using this wrapper the output is formatted such that `jq`, for example, is happy with it.
98
-
99
- * due: the time (in minutes) before the journey starts
100
- * origin_stop_id: the specific departure stop id
101
- * origin_name: the name of the departure location
102
- * departure_time: the departure time, in UTC
103
- * destination_stop_id: the specific destination stop id
104
- * destination_name: the name of the destination location
105
- * arrival_time: the planned arrival time at the origin, in UTC
106
- * origin_transport_type: the type of transport, eg train, bus, ferry etc
107
- * origin_transport_name: the full name of the transport provider
108
- * origin_line_name & origin_line_name_short: the full and short names of the journey
109
- * changes: how many transport changes are needed on the journey
110
- * occupancy: how full the vehicle is, if available
111
- * real_time_trip_id: the unique TransportNSW id for that specific journey, if available
112
- * latitude & longitude: The location of the vehicle, if available
113
-
114
- Please note that the origin and destination detail is just that - information about the first and last stops on the journey at the time the request was made. We don't return any intermediate steps, transport change types etc other than the total number of changes - the assumption is that you'll know the details of your specified trip, you just want to know when the next departure is. If you need much more detailed information then I recommend that you use the full Transport NSW trip planner website or application.
115
-
116
- ## Thank you
117
- Thank you Dav0815 for your TransportNSW library that the vast majority of this fork is based on. I couldn't have done it without you!
118
- https://github.com/Dav0815/TransportNSW
@@ -1 +0,0 @@
1
- TransportNSW
@@ -1,427 +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 TransportNSW(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
- json_output = json_output + json.dumps(self.info) + ']}'
295
- json_output='{"journeys_to_return": ' + str(self.journeys_to_return) + ', "journeys_with_data": ' + str(found_journeys) + ', "journeys": [' + json_output
296
- break
297
- else:
298
- json_output = json_output + json.dumps(self.info) + ', '
299
- current_journey = next_journey
300
-
301
- return json_output
302
-
303
-
304
- # def find_first_journey(self, journeys, journeytype, strictness):
305
- # # Find the first journey that has a leg is of the requested type
306
- # journey_count = len(journeys)
307
- # for journey in range (0, journey_count, 1):
308
- # leg = self.find_first_leg(journeys[journey]['legs'], journeytype, strictness)
309
- # if leg is not None:
310
- # return journeys[journey]
311
- #
312
- # # Hmm, we didn't find one
313
- # return None
314
-
315
-
316
- def find_next_journey(self, journeys, start_journey, journeytype, strict):
317
- # Find the next journey that has a leg of the requested type
318
- journey_count = len(journeys)
319
-
320
- # Some basic error checking
321
- if start_journey > journey_count:
322
- return None, None
323
-
324
- for journey in range (start_journey, journey_count, 1):
325
- leg = self.find_first_leg(journeys[journey]['legs'], journeytype, strict)
326
- if leg is not None:
327
- return journeys[journey], journey + 1
328
- else:
329
- return None, None
330
-
331
- # Hmm, we didn't find one
332
- return None
333
-
334
-
335
- def find_first_leg(self, legs, legtype, strict):
336
- # Find the first leg of the requested type
337
- leg_count = len(legs)
338
- for leg in range (0, leg_count, 1):
339
- leg_class = legs[leg]['transportation']['product']['class']
340
-
341
- # We've got a filter, and the leg type matches it, so return that leg
342
- if legtype != 0 and leg_class == legtype:
343
- return legs[leg]
344
-
345
- # We don't have a filter, and this is the first non-walk/cycle leg so return that leg
346
- if legtype == 0 and leg_class < 99:
347
- return legs[leg]
348
-
349
- # Exit if we're doing strict filtering and we haven't found that type in the first leg
350
- if legtype != 0 and strict == True:
351
- return None
352
-
353
- # Hmm, we didn't find one
354
- return None
355
-
356
-
357
- def find_last_leg(self, legs, legtype, strict):
358
- # Find the last leg of the requested type
359
- leg_count = len(legs)
360
- for leg in range (leg_count - 1, -1, -1):
361
- leg_class = legs[leg]['transportation']['product']['class']
362
-
363
- # We've got a filter, and the leg type matches it, so return that leg
364
- if legtype != 0 and leg_class == legtype:
365
- return legs[leg]
366
-
367
- # We don't have a filter, and this is the first non-walk/cycle leg so return that leg
368
- if legtype == 0 and leg_class < 99:
369
- return legs[leg]
370
-
371
- # Exit if we're doing strict filtering and we haven't found that type in the first leg
372
- if legtype != 0 and strict == True:
373
- return None
374
-
375
- # Hmm, we didn't find one
376
- return None
377
-
378
-
379
- def find_changes(self, legs, legtype):
380
- # Find out how often we have to change
381
- changes = 0
382
- leg_count = len(legs)
383
-
384
- for leg in range (0, leg_count, 1):
385
- leg_class = legs[leg]['transportation']['product']['class']
386
- if leg_class == legtype or legtype == 0:
387
- changes = changes + 1
388
-
389
- return changes - 1
390
-
391
-
392
- def get_mode(self, iconId):
393
- """Map the iconId to a full text string"""
394
- modes = {
395
- 1: "Train",
396
- 4: "Light rail",
397
- 5: "Bus",
398
- 7: "Coach",
399
- 9: "Ferry",
400
- 11: "School bus",
401
- 99: "Walk",
402
- 100: "Walk",
403
- 107: "Cycle"
404
- }
405
- return modes.get(iconId, None)
406
-
407
-
408
- def get_url(self, mode):
409
- """Map the journey mode to the proper real time location URL """
410
-
411
- url_options = {
412
- "Train" : "/sydneytrains",
413
- "Light rail" : "/lightrail/innerwest",
414
- "Bus" : "/buses",
415
- "Coach" : "/buses",
416
- "Ferry" : "/ferries/sydneyferries",
417
- "School bus" : "/buses"
418
- }
419
- return url_options.get(mode, None)
420
-
421
-
422
- def get_due(self, estimated):
423
- """Min until departure"""
424
- due = 0
425
- if estimated > datetime.utcnow():
426
- due = round((estimated - datetime.utcnow()).seconds / 60)
427
- return due
@@ -1,2 +0,0 @@
1
- """Define module-level imports."""
2
- from TransportNSW.TransportNSW import TransportNSW