PyTransportNSWv2 0.9.2__tar.gz → 1.0.1__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,32 +1,31 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: PyTransportNSWv2
3
- Version: 0.9.2
3
+ Version: 1.0.1
4
4
  Summary: Get detailed per-trip transport information from TransportNSW
5
5
  Home-page: https://github.com/andystewart999/TransportNSW
6
6
  Author: andystewart999
7
7
  Author-email: andy.stewart@live.com
8
8
  License: UNKNOWN
9
9
  Description: # TransportNSWv2
10
- Python lib to access Transport NSW information.
10
+ Python lib to access Transport NSW stop and journey information.
11
11
 
12
12
  ## How to Use
13
13
 
14
14
  ### Get an API Key
15
- 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:
16
- 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
15
+ 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](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.
17
16
 
18
17
  ### Get the stop IDs
19
- 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, or to only include trips that have specific text somewhere in the line/service details (see below). 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.
18
+ The only mandatory parameters are the API key and the from/to stop IDs - the easiest way to get the stop ID is via https://transportnsw.info/stops#/ - that page 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.
20
19
 
21
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, unless you specifically set ```include_realtime_location``` to ```False```.
22
21
 
23
22
  ### API Documentation
24
- 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
23
+ The source Transport NSW 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).
25
24
 
26
25
  ### Exposed functions
27
26
  Two functions are available:
28
27
  ```get_trip()``` returns trip information between two stop IDs
29
- ```check_stops()``` lets you confirm that the two stop IDs are valid, plus it returns all the stop ID metadata. Note that ```get_trip()``` calls this function internally and fails relatively gracefully if either of the stop IDs are invalid, so there's no specific need to call ```check_stops()``` unless you want the stop ID metadata.
28
+ ```check_stops()``` lets you confirm that the two stop IDs are valid, plus it returns all the stop ID metadata. Note that ```get_trip()``` calls this function internally (unless you tell it not to) and fails with a ```StopError``` Exception if either of the stop IDs are invalid, so there's no specific need to call ```check_stops()``` unless you want the stop ID metadata, or know you'll be calling the same journey multiple times and want to reduce your daily API calls by pre-checking once.
30
29
 
31
30
  ### check_stops() parameters
32
31
  All parameters are mandatory. Note that ```stop_list``` can be a single string or a list of strings:
@@ -63,8 +62,33 @@ Description: # TransportNSWv2
63
62
  ]
64
63
  }
65
64
  ```
66
- Most of the top-level properties are pretty self-explanatory. If all you want to do is get a general yes/no then ```all_stops_valid``` is the quickest check. If any of the stops are invalid or there was an error calling the stop-finder API then ```error_code``` will point you to the issue. If the API call was successful then ```stop_detail``` will contain everything that the API sent back for the closest match it found.
65
+ Most of the top-level properties are pretty self-explanatory. If all you want to do is get a general yes/no then ```all_stops_valid``` is the quickest check, although with the latest version raising a StopError exception if a stop ID check fails that's become a little bit academic.
66
+ If the API call was successful then ```stop_detail``` will contain everything that the API sent back for the closest match it found.
67
67
 
68
+ ### Sample Code - catching an invalid stop
69
+
70
+ The following example checks two stops to see if they're valid, and it turns out that one of them isn't.
71
+
72
+ **Code:**
73
+ ```python
74
+ from TransportNSWv2 import TransportNSWv2, StopError
75
+
76
+ tnsw = TransportNSWv2()
77
+ try:
78
+ _data = tnsw.check_stops(<your API key>, ['20006012345', '229310'])
79
+ print (_data['all_stops_valid'])
80
+
81
+ except StopError as ex:
82
+ print (f"Stop error - {ex}")
83
+
84
+ except Exception as ex:
85
+ print (f"Misc error - {ex}")
86
+ ```
87
+
88
+ **Result:**
89
+ ```python
90
+ Stop error - Error 'stop invalid' calling stop finder API for stop ID 20006012345
91
+ ```
68
92
 
69
93
  ### get_trip() parameters
70
94
  Only the first three parameters are mandatory, the rest are optional. All parameters and their defaults are as follows:
@@ -72,6 +96,11 @@ Description: # TransportNSWv2
72
96
  .get_trip(origin_stop_id, destination_stop_id, api_key, trip_wait_time = 0, transport_type = 0, strict_transport_type = False, raw_output = False, journeys_to_return = 1, route_filter = '', include_realtime_location = True, include_alerts = 'none', alert_type = 'all', check_stop_ids = True, forced_gtfs_uri = [])
73
97
  ```
74
98
 
99
+ ```trip_wait_time``` is how many minutes from now the departure should be
100
+ If you specify a ```transport_type``` then only journeys with at least **one** leg of the journey including that transport type are included, unless ```strict_transport_type``` is ```True```, in which case the **first** leg must be of the requested type to be returned.
101
+ If ```route_filter``` has a value then only journeys with that value in either the ```origin_line_name``` or ```origin_line_name_short``` fields are included - it's a caseless wildcard search so ```north``` would include ```T1 North Shore & Western Line``` journeys
102
+ ```raw_output``` means that function returns whatever came back from the API call as-is
103
+
75
104
  Transport types:
76
105
  ```
77
106
  1: Train
@@ -105,27 +134,7 @@ Description: # TransportNSWv2
105
134
  bannerInfo: Alerts potentially relating to network-wide impacts
106
135
  ```
107
136
 
108
- 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.
109
-
110
- ### Sample Code - bus journey, no alerts included
111
-
112
- The following example returns the next trip that starts from a bus stop in St. Ives (207537) at least five minutes from now, to Central Station's general stop ID (200060):
113
-
114
- **Code:**
115
- ```python
116
- from TransportNSWv2 import TransportNSWv2
117
- tnsw = TransportNSWv2()
118
- journey = tnsw.get_trip('207537', '200060', 'YOUR_API_KEY', journey_wait_time = 5, transport_type = 5)
119
- print(journey)
120
- ```
121
- **Result:**
122
- ```python
123
- {"journeys_to_return": 1, "journeys_with_data": 1, "journeys": [
124
- {"due": 22, "origin_stop_id": "207537", "origin_name": "Mona Vale Rd at Shinfield Ave, St Ives", "departure_time": "2024-09-10T06:34:24Z", "destination_stop_id": "207235", "destination_name": "Gordon Station, Stand C, Gordon", "arrival_time": "2024-09-
125
- 10T06:40:36Z", "origin_transport_type": "Bus", "origin_transport_name": "Sydney Buses Network", "origin_line_name": "195", "origin_line_name_short": "195", "changes": 0, "occupancy": "FEW_SEATS", "real_time_trip_id": "2197645", "latitude": -33.728271484375,
126
- "longitude": 151.1637420654297, "alerts": []
127
- }]}
128
- ```
137
+ 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.
129
138
 
130
139
  ### Sample Code - train journey, all stop-related alerts normal priority or higher included
131
140
 
@@ -141,23 +150,23 @@ Description: # TransportNSWv2
141
150
  **Result:**
142
151
  ```python
143
152
  {"journeys_to_return": 2, "journeys_with_data": 2, "journeys":[
144
- {"due": 8, "origin_stop_id": "207262", "origin_name": "Gordon Station, Platform 2, Gordon", "departure_time": "2024-09-10T05:18:00Z", "destination_stop_id": "2000338", "destination_name": "Central Station, Platform 18, Sydney", "arrival_time": "2024-09-
145
- 10T05:54:00Z", "origin_transport_type": "Train", "origin_transport_name": "Sydney Trains Network", "origin_line_name": "T1 North Shore & Western Line", "origin_line_name_short": "T1", "changes": 0, "occupancy": "unknown", "real_time_trip_id":
153
+ {"due": 8, "origin_stop_id": "207262", "origin_name": "Gordon Station, Platform 2, Gordon", "departure_time": "2024-09-10T05:18:00Z", "destination_stop_id": "2000338", "destination_name": "Central Station, Platform 18, Sydney", "arrival_time": "2024-09-
154
+ 10T05:54:00Z", "origin_transport_type": "Train", "origin_transport_name": "Sydney Trains Network", "origin_line_name": "T1 North Shore & Western Line", "origin_line_name_short": "T1", "changes": 0, "occupancy": "unknown", "real_time_trip_id":
146
155
  "171L.1915.100.8.A.8.83064399", "latitude": -33.755828857421875, "longitude": 151.1542205810547, "alerts": [
147
- {"priority": "normal", "id": "ems-39380", "version": 3, "type": "stopInfo", "infoLinks": [{"urlText": "Central Station Lift 20 between Central Walk and Platform 20/21 is not available", "url": "https://transportnsw.info/alerts/details#/ems-39380", "content":
148
- "At Central Station Lift 20 between Central Walk and Platform 20/21 is temporarily out of service.\n\nIf you need help, ask staff or phone 02 9379 1777.", "subtitle": "Central Station Lift 20 between Central Walk and Platform 20/21 is not available",
149
- "smsText": "Central Station Lift 20 between Central Walk and Platform 20/21 is not available", "speechText": "At Central Station Lift 20 between Central Walk and Platform 20/21 is temporarily out of service.\n\nIf you need help, ask staff or phone 02 9379
156
+ {"priority": "normal", "id": "ems-39380", "version": 3, "type": "stopInfo", "infoLinks": [{"urlText": "Central Station Lift 20 between Central Walk and Platform 20/21 is not available", "url": "https://transportnsw.info/alerts/details#/ems-39380", "content":
157
+ "At Central Station Lift 20 between Central Walk and Platform 20/21 is temporarily out of service.\n\nIf you need help, ask staff or phone 02 9379 1777.", "subtitle": "Central Station Lift 20 between Central Walk and Platform 20/21 is not available",
158
+ "smsText": "Central Station Lift 20 between Central Walk and Platform 20/21 is not available", "speechText": "At Central Station Lift 20 between Central Walk and Platform 20/21 is temporarily out of service.\n\nIf you need help, ask staff or phone 02 9379
150
159
  1777.", "properties": {"publisher": "ems.comm.addinfo", "infoType": "stopInfo", "appliesTo": "departingArriving", "stopIDglobalID": "200060:2000340,2000341"}}
151
160
  ]}
152
161
  ]},
153
- {"due": 11, "origin_stop_id": "207261", "origin_name": "Gordon Station, Platform 1, Gordon", "departure_time": "2024-09-10T05:21:00Z", "destination_stop_id": "2067141", "destination_name": "Chatswood Station, Platform 1, Chatswood", "arrival_time": "2024-09-
154
- 10T05:30:00Z", "origin_transport_type": "Train", "origin_transport_name": "Sydney Trains Network", "origin_line_name": "T1 North Shore & Western Line", "origin_line_name_short": "T1", "changes": 0, "occupancy": "unknown", "real_time_trip_id":
162
+ {"due": 11, "origin_stop_id": "207261", "origin_name": "Gordon Station, Platform 1, Gordon", "departure_time": "2024-09-10T05:21:00Z", "destination_stop_id": "2067141", "destination_name": "Chatswood Station, Platform 1, Chatswood", "arrival_time": "2024-09-
163
+ 10T05:30:00Z", "origin_transport_type": "Train", "origin_transport_name": "Sydney Trains Network", "origin_line_name": "T1 North Shore & Western Line", "origin_line_name_short": "T1", "changes": 0, "occupancy": "unknown", "real_time_trip_id":
155
164
  "281G.1915.100.12.H.8.83062682", "latitude": -33.709938049316406, "longitude": 151.10427856445312, "alerts": [
156
- {"priority": "normal", "id": "ems-38565", "version": 145217, "type": "lineInfo", "infoLinks": [{"urlText": "Metro services temporarily end by 10.30pmMonday to Thursday evenings between Chatswood and Sydenham, please check service times and plan your trip",
157
- "url": "https://transportnsw.info/alerts/details#/ems-38565", "content": "<div>\n<div>For the first four weeks after opening, there are reduced operating hours from Monday to Thursday evenings in the City section between Chatswood and Sydenham to support
158
- essential engineering and maintenance works during the early phases of operations.</div>\n<div>&nbsp;</div>\n<div>This is temporary and only affects services between Chatswood and Sydenham.&nbsp;Following the first four weeks, metro services will operate
159
- between Tallawong and Sydenham on the normal timetable.</div>\n</div>", "subtitle": "Metro services temporarily end by 10.30pm Monday to Thursday evenings between Chatswood and Sydenham, please check service times and plan your trip", "smsText": "Metro
160
- services temporarily end by 10.30pm Monday to Thursday evenings between Chatswood and Sydenham, please check service times and plan your trip", "speechText": "There are reduced operating hours from Monday to Thursday evenings in the City section between
165
+ {"priority": "normal", "id": "ems-38565", "version": 145217, "type": "lineInfo", "infoLinks": [{"urlText": "Metro services temporarily end by 10.30pmMonday to Thursday evenings between Chatswood and Sydenham, please check service times and plan your trip",
166
+ "url": "https://transportnsw.info/alerts/details#/ems-38565", "content": "<div>\n<div>For the first four weeks after opening, there are reduced operating hours from Monday to Thursday evenings in the City section between Chatswood and Sydenham to support
167
+ essential engineering and maintenance works during the early phases of operations.</div>\n<div>&nbsp;</div>\n<div>This is temporary and only affects services between Chatswood and Sydenham.&nbsp;Following the first four weeks, metro services will operate
168
+ between Tallawong and Sydenham on the normal timetable.</div>\n</div>", "subtitle": "Metro services temporarily end by 10.30pm Monday to Thursday evenings between Chatswood and Sydenham, please check service times and plan your trip", "smsText": "Metro
169
+ services temporarily end by 10.30pm Monday to Thursday evenings between Chatswood and Sydenham, please check service times and plan your trip", "speechText": "There are reduced operating hours from Monday to Thursday evenings in the City section between
161
170
  Chatswood and Sydenham to support essential engineering and maintenance works during the early phases of operations.", "properties": {"publisher": "ems.comm.addinfo", "infoType": "lineInfo"}}
162
171
  ]}
163
172
  ]}
@@ -185,13 +194,16 @@ Description: # TransportNSWv2
185
194
  ### Notes ###
186
195
  Requesting multiple journeys to be returned doesn't always return that exact number of journeys. The API only ever returns five or six, and if you have any filters applied then that might further reduce the number of 'valid' journeys.
187
196
 
188
- ```origin_line_name``` and ```origin_line_name_short``` are the fields that have the route filter applied, if present. For buses they are usually the same, but for trains and ferries they generally show the full and short line names. Both fields are checked and if either meet the filter then the journey is returned.
189
-
190
- Note also that the origin and destination details are 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.
197
+ Note that the origin and destination details are just that - information about the first and last stops on the journey at the time the request was made. The output doesn't include 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, or parse the raw output by adding ```raw_output = True``` to your call.
191
198
 
192
- Also note that the ```transport_type``` filter, if present, only makes sure that at least **one** leg of the journey includes that transport type unless ```strict_transport_type``` is True, in which case the **first** leg must be of the requested type to be returned.
199
+ ## Exceptions ##
200
+ The latest release of TransportNSWv2 now uses custom Exceptions when things go wrong, instead of returning None - I think that's probably more 'Pythonic'. The Exceptions that can be imported are as follows:
201
+ * InvalidAPIKey - API key-related issues
202
+ * APIRateLimitExceeded - API rate-limit issues
203
+ * StopError - stop ID issues, usually when checking that a stop ID is valid
204
+ * TripError - trip-related issues, including no journeys being returned when calling ```.get_trip()```
193
205
 
194
- ### Rate limits ###
206
+ ## Rate limits ##
195
207
  By default the TransportNSW API allows each API key to make 60,000 calls in a day and up to 5 calls per second. When requesting real-time location information some services required me to brute-force up to 12 (!) URIs until I found the right one which sometimes resulted in an API rate limt breach. From version 0.8.7 I found a TransportNSW-maintained CSV that contains mappings of bus agency IDs to URIs so I'm using that, plus adding in a 0.75 second delay between API calls. Alternatively, if you're confident that the origin and destination IDs are correct you can reduce your API calls by adding ```check_trip_ids = False``` in the parameters. Additionally there's a final option ```forced_gtfs_uri``` which, if you're super-confident you know what the GTFS URI is for your particular journey, will again reduce the API calls per trip query... although I'd use this one with caution! ```forced_gtfs_uri``` needs to be a single-item list, here's an example:
196
208
 
197
209
  ```forced_gtfs_uri = ["/lightrail/innerwest"]```
@@ -1,32 +1,31 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: PyTransportNSWv2
3
- Version: 0.9.2
3
+ Version: 1.0.1
4
4
  Summary: Get detailed per-trip transport information from TransportNSW
5
5
  Home-page: https://github.com/andystewart999/TransportNSW
6
6
  Author: andystewart999
7
7
  Author-email: andy.stewart@live.com
8
8
  License: UNKNOWN
9
9
  Description: # TransportNSWv2
10
- Python lib to access Transport NSW information.
10
+ Python lib to access Transport NSW stop and journey information.
11
11
 
12
12
  ## How to Use
13
13
 
14
14
  ### Get an API Key
15
- 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:
16
- 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
15
+ 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](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.
17
16
 
18
17
  ### Get the stop IDs
19
- 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, or to only include trips that have specific text somewhere in the line/service details (see below). 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.
18
+ The only mandatory parameters are the API key and the from/to stop IDs - the easiest way to get the stop ID is via https://transportnsw.info/stops#/ - that page 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.
20
19
 
21
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, unless you specifically set ```include_realtime_location``` to ```False```.
22
21
 
23
22
  ### API Documentation
24
- 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
23
+ The source Transport NSW 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).
25
24
 
26
25
  ### Exposed functions
27
26
  Two functions are available:
28
27
  ```get_trip()``` returns trip information between two stop IDs
29
- ```check_stops()``` lets you confirm that the two stop IDs are valid, plus it returns all the stop ID metadata. Note that ```get_trip()``` calls this function internally and fails relatively gracefully if either of the stop IDs are invalid, so there's no specific need to call ```check_stops()``` unless you want the stop ID metadata.
28
+ ```check_stops()``` lets you confirm that the two stop IDs are valid, plus it returns all the stop ID metadata. Note that ```get_trip()``` calls this function internally (unless you tell it not to) and fails with a ```StopError``` Exception if either of the stop IDs are invalid, so there's no specific need to call ```check_stops()``` unless you want the stop ID metadata, or know you'll be calling the same journey multiple times and want to reduce your daily API calls by pre-checking once.
30
29
 
31
30
  ### check_stops() parameters
32
31
  All parameters are mandatory. Note that ```stop_list``` can be a single string or a list of strings:
@@ -63,8 +62,33 @@ Description: # TransportNSWv2
63
62
  ]
64
63
  }
65
64
  ```
66
- Most of the top-level properties are pretty self-explanatory. If all you want to do is get a general yes/no then ```all_stops_valid``` is the quickest check. If any of the stops are invalid or there was an error calling the stop-finder API then ```error_code``` will point you to the issue. If the API call was successful then ```stop_detail``` will contain everything that the API sent back for the closest match it found.
65
+ Most of the top-level properties are pretty self-explanatory. If all you want to do is get a general yes/no then ```all_stops_valid``` is the quickest check, although with the latest version raising a StopError exception if a stop ID check fails that's become a little bit academic.
66
+ If the API call was successful then ```stop_detail``` will contain everything that the API sent back for the closest match it found.
67
67
 
68
+ ### Sample Code - catching an invalid stop
69
+
70
+ The following example checks two stops to see if they're valid, and it turns out that one of them isn't.
71
+
72
+ **Code:**
73
+ ```python
74
+ from TransportNSWv2 import TransportNSWv2, StopError
75
+
76
+ tnsw = TransportNSWv2()
77
+ try:
78
+ _data = tnsw.check_stops(<your API key>, ['20006012345', '229310'])
79
+ print (_data['all_stops_valid'])
80
+
81
+ except StopError as ex:
82
+ print (f"Stop error - {ex}")
83
+
84
+ except Exception as ex:
85
+ print (f"Misc error - {ex}")
86
+ ```
87
+
88
+ **Result:**
89
+ ```python
90
+ Stop error - Error 'stop invalid' calling stop finder API for stop ID 20006012345
91
+ ```
68
92
 
69
93
  ### get_trip() parameters
70
94
  Only the first three parameters are mandatory, the rest are optional. All parameters and their defaults are as follows:
@@ -72,6 +96,11 @@ Description: # TransportNSWv2
72
96
  .get_trip(origin_stop_id, destination_stop_id, api_key, trip_wait_time = 0, transport_type = 0, strict_transport_type = False, raw_output = False, journeys_to_return = 1, route_filter = '', include_realtime_location = True, include_alerts = 'none', alert_type = 'all', check_stop_ids = True, forced_gtfs_uri = [])
73
97
  ```
74
98
 
99
+ ```trip_wait_time``` is how many minutes from now the departure should be
100
+ If you specify a ```transport_type``` then only journeys with at least **one** leg of the journey including that transport type are included, unless ```strict_transport_type``` is ```True```, in which case the **first** leg must be of the requested type to be returned.
101
+ If ```route_filter``` has a value then only journeys with that value in either the ```origin_line_name``` or ```origin_line_name_short``` fields are included - it's a caseless wildcard search so ```north``` would include ```T1 North Shore & Western Line``` journeys
102
+ ```raw_output``` means that function returns whatever came back from the API call as-is
103
+
75
104
  Transport types:
76
105
  ```
77
106
  1: Train
@@ -105,27 +134,7 @@ Description: # TransportNSWv2
105
134
  bannerInfo: Alerts potentially relating to network-wide impacts
106
135
  ```
107
136
 
108
- 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.
109
-
110
- ### Sample Code - bus journey, no alerts included
111
-
112
- The following example returns the next trip that starts from a bus stop in St. Ives (207537) at least five minutes from now, to Central Station's general stop ID (200060):
113
-
114
- **Code:**
115
- ```python
116
- from TransportNSWv2 import TransportNSWv2
117
- tnsw = TransportNSWv2()
118
- journey = tnsw.get_trip('207537', '200060', 'YOUR_API_KEY', journey_wait_time = 5, transport_type = 5)
119
- print(journey)
120
- ```
121
- **Result:**
122
- ```python
123
- {"journeys_to_return": 1, "journeys_with_data": 1, "journeys": [
124
- {"due": 22, "origin_stop_id": "207537", "origin_name": "Mona Vale Rd at Shinfield Ave, St Ives", "departure_time": "2024-09-10T06:34:24Z", "destination_stop_id": "207235", "destination_name": "Gordon Station, Stand C, Gordon", "arrival_time": "2024-09-
125
- 10T06:40:36Z", "origin_transport_type": "Bus", "origin_transport_name": "Sydney Buses Network", "origin_line_name": "195", "origin_line_name_short": "195", "changes": 0, "occupancy": "FEW_SEATS", "real_time_trip_id": "2197645", "latitude": -33.728271484375,
126
- "longitude": 151.1637420654297, "alerts": []
127
- }]}
128
- ```
137
+ 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.
129
138
 
130
139
  ### Sample Code - train journey, all stop-related alerts normal priority or higher included
131
140
 
@@ -141,23 +150,23 @@ Description: # TransportNSWv2
141
150
  **Result:**
142
151
  ```python
143
152
  {"journeys_to_return": 2, "journeys_with_data": 2, "journeys":[
144
- {"due": 8, "origin_stop_id": "207262", "origin_name": "Gordon Station, Platform 2, Gordon", "departure_time": "2024-09-10T05:18:00Z", "destination_stop_id": "2000338", "destination_name": "Central Station, Platform 18, Sydney", "arrival_time": "2024-09-
145
- 10T05:54:00Z", "origin_transport_type": "Train", "origin_transport_name": "Sydney Trains Network", "origin_line_name": "T1 North Shore & Western Line", "origin_line_name_short": "T1", "changes": 0, "occupancy": "unknown", "real_time_trip_id":
153
+ {"due": 8, "origin_stop_id": "207262", "origin_name": "Gordon Station, Platform 2, Gordon", "departure_time": "2024-09-10T05:18:00Z", "destination_stop_id": "2000338", "destination_name": "Central Station, Platform 18, Sydney", "arrival_time": "2024-09-
154
+ 10T05:54:00Z", "origin_transport_type": "Train", "origin_transport_name": "Sydney Trains Network", "origin_line_name": "T1 North Shore & Western Line", "origin_line_name_short": "T1", "changes": 0, "occupancy": "unknown", "real_time_trip_id":
146
155
  "171L.1915.100.8.A.8.83064399", "latitude": -33.755828857421875, "longitude": 151.1542205810547, "alerts": [
147
- {"priority": "normal", "id": "ems-39380", "version": 3, "type": "stopInfo", "infoLinks": [{"urlText": "Central Station Lift 20 between Central Walk and Platform 20/21 is not available", "url": "https://transportnsw.info/alerts/details#/ems-39380", "content":
148
- "At Central Station Lift 20 between Central Walk and Platform 20/21 is temporarily out of service.\n\nIf you need help, ask staff or phone 02 9379 1777.", "subtitle": "Central Station Lift 20 between Central Walk and Platform 20/21 is not available",
149
- "smsText": "Central Station Lift 20 between Central Walk and Platform 20/21 is not available", "speechText": "At Central Station Lift 20 between Central Walk and Platform 20/21 is temporarily out of service.\n\nIf you need help, ask staff or phone 02 9379
156
+ {"priority": "normal", "id": "ems-39380", "version": 3, "type": "stopInfo", "infoLinks": [{"urlText": "Central Station Lift 20 between Central Walk and Platform 20/21 is not available", "url": "https://transportnsw.info/alerts/details#/ems-39380", "content":
157
+ "At Central Station Lift 20 between Central Walk and Platform 20/21 is temporarily out of service.\n\nIf you need help, ask staff or phone 02 9379 1777.", "subtitle": "Central Station Lift 20 between Central Walk and Platform 20/21 is not available",
158
+ "smsText": "Central Station Lift 20 between Central Walk and Platform 20/21 is not available", "speechText": "At Central Station Lift 20 between Central Walk and Platform 20/21 is temporarily out of service.\n\nIf you need help, ask staff or phone 02 9379
150
159
  1777.", "properties": {"publisher": "ems.comm.addinfo", "infoType": "stopInfo", "appliesTo": "departingArriving", "stopIDglobalID": "200060:2000340,2000341"}}
151
160
  ]}
152
161
  ]},
153
- {"due": 11, "origin_stop_id": "207261", "origin_name": "Gordon Station, Platform 1, Gordon", "departure_time": "2024-09-10T05:21:00Z", "destination_stop_id": "2067141", "destination_name": "Chatswood Station, Platform 1, Chatswood", "arrival_time": "2024-09-
154
- 10T05:30:00Z", "origin_transport_type": "Train", "origin_transport_name": "Sydney Trains Network", "origin_line_name": "T1 North Shore & Western Line", "origin_line_name_short": "T1", "changes": 0, "occupancy": "unknown", "real_time_trip_id":
162
+ {"due": 11, "origin_stop_id": "207261", "origin_name": "Gordon Station, Platform 1, Gordon", "departure_time": "2024-09-10T05:21:00Z", "destination_stop_id": "2067141", "destination_name": "Chatswood Station, Platform 1, Chatswood", "arrival_time": "2024-09-
163
+ 10T05:30:00Z", "origin_transport_type": "Train", "origin_transport_name": "Sydney Trains Network", "origin_line_name": "T1 North Shore & Western Line", "origin_line_name_short": "T1", "changes": 0, "occupancy": "unknown", "real_time_trip_id":
155
164
  "281G.1915.100.12.H.8.83062682", "latitude": -33.709938049316406, "longitude": 151.10427856445312, "alerts": [
156
- {"priority": "normal", "id": "ems-38565", "version": 145217, "type": "lineInfo", "infoLinks": [{"urlText": "Metro services temporarily end by 10.30pmMonday to Thursday evenings between Chatswood and Sydenham, please check service times and plan your trip",
157
- "url": "https://transportnsw.info/alerts/details#/ems-38565", "content": "<div>\n<div>For the first four weeks after opening, there are reduced operating hours from Monday to Thursday evenings in the City section between Chatswood and Sydenham to support
158
- essential engineering and maintenance works during the early phases of operations.</div>\n<div>&nbsp;</div>\n<div>This is temporary and only affects services between Chatswood and Sydenham.&nbsp;Following the first four weeks, metro services will operate
159
- between Tallawong and Sydenham on the normal timetable.</div>\n</div>", "subtitle": "Metro services temporarily end by 10.30pm Monday to Thursday evenings between Chatswood and Sydenham, please check service times and plan your trip", "smsText": "Metro
160
- services temporarily end by 10.30pm Monday to Thursday evenings between Chatswood and Sydenham, please check service times and plan your trip", "speechText": "There are reduced operating hours from Monday to Thursday evenings in the City section between
165
+ {"priority": "normal", "id": "ems-38565", "version": 145217, "type": "lineInfo", "infoLinks": [{"urlText": "Metro services temporarily end by 10.30pmMonday to Thursday evenings between Chatswood and Sydenham, please check service times and plan your trip",
166
+ "url": "https://transportnsw.info/alerts/details#/ems-38565", "content": "<div>\n<div>For the first four weeks after opening, there are reduced operating hours from Monday to Thursday evenings in the City section between Chatswood and Sydenham to support
167
+ essential engineering and maintenance works during the early phases of operations.</div>\n<div>&nbsp;</div>\n<div>This is temporary and only affects services between Chatswood and Sydenham.&nbsp;Following the first four weeks, metro services will operate
168
+ between Tallawong and Sydenham on the normal timetable.</div>\n</div>", "subtitle": "Metro services temporarily end by 10.30pm Monday to Thursday evenings between Chatswood and Sydenham, please check service times and plan your trip", "smsText": "Metro
169
+ services temporarily end by 10.30pm Monday to Thursday evenings between Chatswood and Sydenham, please check service times and plan your trip", "speechText": "There are reduced operating hours from Monday to Thursday evenings in the City section between
161
170
  Chatswood and Sydenham to support essential engineering and maintenance works during the early phases of operations.", "properties": {"publisher": "ems.comm.addinfo", "infoType": "lineInfo"}}
162
171
  ]}
163
172
  ]}
@@ -185,13 +194,16 @@ Description: # TransportNSWv2
185
194
  ### Notes ###
186
195
  Requesting multiple journeys to be returned doesn't always return that exact number of journeys. The API only ever returns five or six, and if you have any filters applied then that might further reduce the number of 'valid' journeys.
187
196
 
188
- ```origin_line_name``` and ```origin_line_name_short``` are the fields that have the route filter applied, if present. For buses they are usually the same, but for trains and ferries they generally show the full and short line names. Both fields are checked and if either meet the filter then the journey is returned.
189
-
190
- Note also that the origin and destination details are 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.
197
+ Note that the origin and destination details are just that - information about the first and last stops on the journey at the time the request was made. The output doesn't include 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, or parse the raw output by adding ```raw_output = True``` to your call.
191
198
 
192
- Also note that the ```transport_type``` filter, if present, only makes sure that at least **one** leg of the journey includes that transport type unless ```strict_transport_type``` is True, in which case the **first** leg must be of the requested type to be returned.
199
+ ## Exceptions ##
200
+ The latest release of TransportNSWv2 now uses custom Exceptions when things go wrong, instead of returning None - I think that's probably more 'Pythonic'. The Exceptions that can be imported are as follows:
201
+ * InvalidAPIKey - API key-related issues
202
+ * APIRateLimitExceeded - API rate-limit issues
203
+ * StopError - stop ID issues, usually when checking that a stop ID is valid
204
+ * TripError - trip-related issues, including no journeys being returned when calling ```.get_trip()```
193
205
 
194
- ### Rate limits ###
206
+ ## Rate limits ##
195
207
  By default the TransportNSW API allows each API key to make 60,000 calls in a day and up to 5 calls per second. When requesting real-time location information some services required me to brute-force up to 12 (!) URIs until I found the right one which sometimes resulted in an API rate limt breach. From version 0.8.7 I found a TransportNSW-maintained CSV that contains mappings of bus agency IDs to URIs so I'm using that, plus adding in a 0.75 second delay between API calls. Alternatively, if you're confident that the origin and destination IDs are correct you can reduce your API calls by adding ```check_trip_ids = False``` in the parameters. Additionally there's a final option ```forced_gtfs_uri``` which, if you're super-confident you know what the GTFS URI is for your particular journey, will again reduce the API calls per trip query... although I'd use this one with caution! ```forced_gtfs_uri``` needs to be a single-item list, here's an example:
196
208
 
197
209
  ```forced_gtfs_uri = ["/lightrail/innerwest"]```
@@ -1,24 +1,23 @@
1
1
  # TransportNSWv2
2
- Python lib to access Transport NSW information.
2
+ Python lib to access Transport NSW stop and journey information.
3
3
 
4
4
  ## How to Use
5
5
 
6
6
  ### Get an API Key
7
- 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:
8
- 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
7
+ 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](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.
9
8
 
10
9
  ### Get the stop IDs
11
- 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, or to only include trips that have specific text somewhere in the line/service details (see below). 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.
10
+ The only mandatory parameters are the API key and the from/to stop IDs - the easiest way to get the stop ID is via https://transportnsw.info/stops#/ - that page 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.
12
11
 
13
12
  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, unless you specifically set ```include_realtime_location``` to ```False```.
14
13
 
15
14
  ### API Documentation
16
- 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
15
+ The source Transport NSW 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).
17
16
 
18
17
  ### Exposed functions
19
18
  Two functions are available:
20
19
  ```get_trip()``` returns trip information between two stop IDs
21
- ```check_stops()``` lets you confirm that the two stop IDs are valid, plus it returns all the stop ID metadata. Note that ```get_trip()``` calls this function internally and fails relatively gracefully if either of the stop IDs are invalid, so there's no specific need to call ```check_stops()``` unless you want the stop ID metadata.
20
+ ```check_stops()``` lets you confirm that the two stop IDs are valid, plus it returns all the stop ID metadata. Note that ```get_trip()``` calls this function internally (unless you tell it not to) and fails with a ```StopError``` Exception if either of the stop IDs are invalid, so there's no specific need to call ```check_stops()``` unless you want the stop ID metadata, or know you'll be calling the same journey multiple times and want to reduce your daily API calls by pre-checking once.
22
21
 
23
22
  ### check_stops() parameters
24
23
  All parameters are mandatory. Note that ```stop_list``` can be a single string or a list of strings:
@@ -55,8 +54,33 @@ The return is a JSON-compatible Python object as per the example here:
55
54
  ]
56
55
  }
57
56
  ```
58
- Most of the top-level properties are pretty self-explanatory. If all you want to do is get a general yes/no then ```all_stops_valid``` is the quickest check. If any of the stops are invalid or there was an error calling the stop-finder API then ```error_code``` will point you to the issue. If the API call was successful then ```stop_detail``` will contain everything that the API sent back for the closest match it found.
57
+ Most of the top-level properties are pretty self-explanatory. If all you want to do is get a general yes/no then ```all_stops_valid``` is the quickest check, although with the latest version raising a StopError exception if a stop ID check fails that's become a little bit academic.
58
+ If the API call was successful then ```stop_detail``` will contain everything that the API sent back for the closest match it found.
59
59
 
60
+ ### Sample Code - catching an invalid stop
61
+
62
+ The following example checks two stops to see if they're valid, and it turns out that one of them isn't.
63
+
64
+ **Code:**
65
+ ```python
66
+ from TransportNSWv2 import TransportNSWv2, StopError
67
+
68
+ tnsw = TransportNSWv2()
69
+ try:
70
+ _data = tnsw.check_stops(<your API key>, ['20006012345', '229310'])
71
+ print (_data['all_stops_valid'])
72
+
73
+ except StopError as ex:
74
+ print (f"Stop error - {ex}")
75
+
76
+ except Exception as ex:
77
+ print (f"Misc error - {ex}")
78
+ ```
79
+
80
+ **Result:**
81
+ ```python
82
+ Stop error - Error 'stop invalid' calling stop finder API for stop ID 20006012345
83
+ ```
60
84
 
61
85
  ### get_trip() parameters
62
86
  Only the first three parameters are mandatory, the rest are optional. All parameters and their defaults are as follows:
@@ -64,6 +88,11 @@ Only the first three parameters are mandatory, the rest are optional. All param
64
88
  .get_trip(origin_stop_id, destination_stop_id, api_key, trip_wait_time = 0, transport_type = 0, strict_transport_type = False, raw_output = False, journeys_to_return = 1, route_filter = '', include_realtime_location = True, include_alerts = 'none', alert_type = 'all', check_stop_ids = True, forced_gtfs_uri = [])
65
89
  ```
66
90
 
91
+ ```trip_wait_time``` is how many minutes from now the departure should be
92
+ If you specify a ```transport_type``` then only journeys with at least **one** leg of the journey including that transport type are included, unless ```strict_transport_type``` is ```True```, in which case the **first** leg must be of the requested type to be returned.
93
+ If ```route_filter``` has a value then only journeys with that value in either the ```origin_line_name``` or ```origin_line_name_short``` fields are included - it's a caseless wildcard search so ```north``` would include ```T1 North Shore & Western Line``` journeys
94
+ ```raw_output``` means that function returns whatever came back from the API call as-is
95
+
67
96
  Transport types:
68
97
  ```
69
98
  1: Train
@@ -97,27 +126,7 @@ stopBlocking: Alerts relating to stop closures
97
126
  bannerInfo: Alerts potentially relating to network-wide impacts
98
127
  ```
99
128
 
100
- 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.
101
-
102
- ### Sample Code - bus journey, no alerts included
103
-
104
- The following example returns the next trip that starts from a bus stop in St. Ives (207537) at least five minutes from now, to Central Station's general stop ID (200060):
105
-
106
- **Code:**
107
- ```python
108
- from TransportNSWv2 import TransportNSWv2
109
- tnsw = TransportNSWv2()
110
- journey = tnsw.get_trip('207537', '200060', 'YOUR_API_KEY', journey_wait_time = 5, transport_type = 5)
111
- print(journey)
112
- ```
113
- **Result:**
114
- ```python
115
- {"journeys_to_return": 1, "journeys_with_data": 1, "journeys": [
116
- {"due": 22, "origin_stop_id": "207537", "origin_name": "Mona Vale Rd at Shinfield Ave, St Ives", "departure_time": "2024-09-10T06:34:24Z", "destination_stop_id": "207235", "destination_name": "Gordon Station, Stand C, Gordon", "arrival_time": "2024-09-
117
- 10T06:40:36Z", "origin_transport_type": "Bus", "origin_transport_name": "Sydney Buses Network", "origin_line_name": "195", "origin_line_name_short": "195", "changes": 0, "occupancy": "FEW_SEATS", "real_time_trip_id": "2197645", "latitude": -33.728271484375,
118
- "longitude": 151.1637420654297, "alerts": []
119
- }]}
120
- ```
129
+ 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.
121
130
 
122
131
  ### Sample Code - train journey, all stop-related alerts normal priority or higher included
123
132
 
@@ -133,23 +142,23 @@ print(journey)
133
142
  **Result:**
134
143
  ```python
135
144
  {"journeys_to_return": 2, "journeys_with_data": 2, "journeys":[
136
- {"due": 8, "origin_stop_id": "207262", "origin_name": "Gordon Station, Platform 2, Gordon", "departure_time": "2024-09-10T05:18:00Z", "destination_stop_id": "2000338", "destination_name": "Central Station, Platform 18, Sydney", "arrival_time": "2024-09-
137
- 10T05:54:00Z", "origin_transport_type": "Train", "origin_transport_name": "Sydney Trains Network", "origin_line_name": "T1 North Shore & Western Line", "origin_line_name_short": "T1", "changes": 0, "occupancy": "unknown", "real_time_trip_id":
145
+ {"due": 8, "origin_stop_id": "207262", "origin_name": "Gordon Station, Platform 2, Gordon", "departure_time": "2024-09-10T05:18:00Z", "destination_stop_id": "2000338", "destination_name": "Central Station, Platform 18, Sydney", "arrival_time": "2024-09-
146
+ 10T05:54:00Z", "origin_transport_type": "Train", "origin_transport_name": "Sydney Trains Network", "origin_line_name": "T1 North Shore & Western Line", "origin_line_name_short": "T1", "changes": 0, "occupancy": "unknown", "real_time_trip_id":
138
147
  "171L.1915.100.8.A.8.83064399", "latitude": -33.755828857421875, "longitude": 151.1542205810547, "alerts": [
139
- {"priority": "normal", "id": "ems-39380", "version": 3, "type": "stopInfo", "infoLinks": [{"urlText": "Central Station Lift 20 between Central Walk and Platform 20/21 is not available", "url": "https://transportnsw.info/alerts/details#/ems-39380", "content":
140
- "At Central Station Lift 20 between Central Walk and Platform 20/21 is temporarily out of service.\n\nIf you need help, ask staff or phone 02 9379 1777.", "subtitle": "Central Station Lift 20 between Central Walk and Platform 20/21 is not available",
141
- "smsText": "Central Station Lift 20 between Central Walk and Platform 20/21 is not available", "speechText": "At Central Station Lift 20 between Central Walk and Platform 20/21 is temporarily out of service.\n\nIf you need help, ask staff or phone 02 9379
148
+ {"priority": "normal", "id": "ems-39380", "version": 3, "type": "stopInfo", "infoLinks": [{"urlText": "Central Station Lift 20 between Central Walk and Platform 20/21 is not available", "url": "https://transportnsw.info/alerts/details#/ems-39380", "content":
149
+ "At Central Station Lift 20 between Central Walk and Platform 20/21 is temporarily out of service.\n\nIf you need help, ask staff or phone 02 9379 1777.", "subtitle": "Central Station Lift 20 between Central Walk and Platform 20/21 is not available",
150
+ "smsText": "Central Station Lift 20 between Central Walk and Platform 20/21 is not available", "speechText": "At Central Station Lift 20 between Central Walk and Platform 20/21 is temporarily out of service.\n\nIf you need help, ask staff or phone 02 9379
142
151
  1777.", "properties": {"publisher": "ems.comm.addinfo", "infoType": "stopInfo", "appliesTo": "departingArriving", "stopIDglobalID": "200060:2000340,2000341"}}
143
152
  ]}
144
153
  ]},
145
- {"due": 11, "origin_stop_id": "207261", "origin_name": "Gordon Station, Platform 1, Gordon", "departure_time": "2024-09-10T05:21:00Z", "destination_stop_id": "2067141", "destination_name": "Chatswood Station, Platform 1, Chatswood", "arrival_time": "2024-09-
146
- 10T05:30:00Z", "origin_transport_type": "Train", "origin_transport_name": "Sydney Trains Network", "origin_line_name": "T1 North Shore & Western Line", "origin_line_name_short": "T1", "changes": 0, "occupancy": "unknown", "real_time_trip_id":
154
+ {"due": 11, "origin_stop_id": "207261", "origin_name": "Gordon Station, Platform 1, Gordon", "departure_time": "2024-09-10T05:21:00Z", "destination_stop_id": "2067141", "destination_name": "Chatswood Station, Platform 1, Chatswood", "arrival_time": "2024-09-
155
+ 10T05:30:00Z", "origin_transport_type": "Train", "origin_transport_name": "Sydney Trains Network", "origin_line_name": "T1 North Shore & Western Line", "origin_line_name_short": "T1", "changes": 0, "occupancy": "unknown", "real_time_trip_id":
147
156
  "281G.1915.100.12.H.8.83062682", "latitude": -33.709938049316406, "longitude": 151.10427856445312, "alerts": [
148
- {"priority": "normal", "id": "ems-38565", "version": 145217, "type": "lineInfo", "infoLinks": [{"urlText": "Metro services temporarily end by 10.30pmMonday to Thursday evenings between Chatswood and Sydenham, please check service times and plan your trip",
149
- "url": "https://transportnsw.info/alerts/details#/ems-38565", "content": "<div>\n<div>For the first four weeks after opening, there are reduced operating hours from Monday to Thursday evenings in the City section between Chatswood and Sydenham to support
150
- essential engineering and maintenance works during the early phases of operations.</div>\n<div>&nbsp;</div>\n<div>This is temporary and only affects services between Chatswood and Sydenham.&nbsp;Following the first four weeks, metro services will operate
151
- between Tallawong and Sydenham on the normal timetable.</div>\n</div>", "subtitle": "Metro services temporarily end by 10.30pm Monday to Thursday evenings between Chatswood and Sydenham, please check service times and plan your trip", "smsText": "Metro
152
- services temporarily end by 10.30pm Monday to Thursday evenings between Chatswood and Sydenham, please check service times and plan your trip", "speechText": "There are reduced operating hours from Monday to Thursday evenings in the City section between
157
+ {"priority": "normal", "id": "ems-38565", "version": 145217, "type": "lineInfo", "infoLinks": [{"urlText": "Metro services temporarily end by 10.30pmMonday to Thursday evenings between Chatswood and Sydenham, please check service times and plan your trip",
158
+ "url": "https://transportnsw.info/alerts/details#/ems-38565", "content": "<div>\n<div>For the first four weeks after opening, there are reduced operating hours from Monday to Thursday evenings in the City section between Chatswood and Sydenham to support
159
+ essential engineering and maintenance works during the early phases of operations.</div>\n<div>&nbsp;</div>\n<div>This is temporary and only affects services between Chatswood and Sydenham.&nbsp;Following the first four weeks, metro services will operate
160
+ between Tallawong and Sydenham on the normal timetable.</div>\n</div>", "subtitle": "Metro services temporarily end by 10.30pm Monday to Thursday evenings between Chatswood and Sydenham, please check service times and plan your trip", "smsText": "Metro
161
+ services temporarily end by 10.30pm Monday to Thursday evenings between Chatswood and Sydenham, please check service times and plan your trip", "speechText": "There are reduced operating hours from Monday to Thursday evenings in the City section between
153
162
  Chatswood and Sydenham to support essential engineering and maintenance works during the early phases of operations.", "properties": {"publisher": "ems.comm.addinfo", "infoType": "lineInfo"}}
154
163
  ]}
155
164
  ]}
@@ -177,13 +186,16 @@ print(journey)
177
186
  ### Notes ###
178
187
  Requesting multiple journeys to be returned doesn't always return that exact number of journeys. The API only ever returns five or six, and if you have any filters applied then that might further reduce the number of 'valid' journeys.
179
188
 
180
- ```origin_line_name``` and ```origin_line_name_short``` are the fields that have the route filter applied, if present. For buses they are usually the same, but for trains and ferries they generally show the full and short line names. Both fields are checked and if either meet the filter then the journey is returned.
181
-
182
- Note also that the origin and destination details are 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.
189
+ Note that the origin and destination details are just that - information about the first and last stops on the journey at the time the request was made. The output doesn't include 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, or parse the raw output by adding ```raw_output = True``` to your call.
183
190
 
184
- Also note that the ```transport_type``` filter, if present, only makes sure that at least **one** leg of the journey includes that transport type unless ```strict_transport_type``` is True, in which case the **first** leg must be of the requested type to be returned.
191
+ ## Exceptions ##
192
+ The latest release of TransportNSWv2 now uses custom Exceptions when things go wrong, instead of returning None - I think that's probably more 'Pythonic'. The Exceptions that can be imported are as follows:
193
+ * InvalidAPIKey - API key-related issues
194
+ * APIRateLimitExceeded - API rate-limit issues
195
+ * StopError - stop ID issues, usually when checking that a stop ID is valid
196
+ * TripError - trip-related issues, including no journeys being returned when calling ```.get_trip()```
185
197
 
186
- ### Rate limits ###
198
+ ## Rate limits ##
187
199
  By default the TransportNSW API allows each API key to make 60,000 calls in a day and up to 5 calls per second. When requesting real-time location information some services required me to brute-force up to 12 (!) URIs until I found the right one which sometimes resulted in an API rate limt breach. From version 0.8.7 I found a TransportNSW-maintained CSV that contains mappings of bus agency IDs to URIs so I'm using that, plus adding in a 0.75 second delay between API calls. Alternatively, if you're confident that the origin and destination IDs are correct you can reduce your API calls by adding ```check_trip_ids = False``` in the parameters. Additionally there's a final option ```forced_gtfs_uri``` which, if you're super-confident you know what the GTFS URI is for your particular journey, will again reduce the API calls per trip query... although I'd use this one with caution! ```forced_gtfs_uri``` needs to be a single-item list, here's an example:
188
200
 
189
201
  ```forced_gtfs_uri = ["/lightrail/innerwest"]```
@@ -31,10 +31,11 @@ ATTR_CHANGES = 'changes'
31
31
 
32
32
  ATTR_OCCUPANCY = 'occupancy'
33
33
 
34
+ ATTR_AVMS_TRIP_ID = 'avms_trip_id'
34
35
  ATTR_REAL_TIME_TRIP_ID = 'real_time_trip_id'
35
36
  ATTR_LATITUDE = 'latitude'
36
37
  ATTR_LONGITUDE = 'longitude'
37
-
38
+ ATTR_GTFS_URI = 'gtfs_uri'
38
39
  ATTR_ALERTS = 'alerts'
39
40
 
40
41
  logger = logging.getLogger(__name__)
@@ -48,14 +49,6 @@ class TransportNSWv2(object):
48
49
 
49
50
  def __init__(self):
50
51
  """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
52
  self.info = {
60
53
  ATTR_DUE_IN : 'n/a',
61
54
  ATTR_ORIGIN_STOP_ID : 'n/a',
@@ -78,7 +71,7 @@ class TransportNSWv2(object):
78
71
  }
79
72
 
80
73
 
81
- def check_stops(self, api_key, stops):
74
+ def check_stops(self, api_key, stops, home_assistant = False):
82
75
  # Check the list of stops and return a JSON array of the stop details, plus if all the checked stops existed
83
76
  # Return a JSON array of the results
84
77
 
@@ -120,43 +113,29 @@ class TransportNSWv2(object):
120
113
  stop_detail = []
121
114
 
122
115
  if response.status_code == 401:
123
- # API key issue - log that, and don't bother with the other calls as they will all fail
124
- skip_api_calls = True
125
- all_stops_valid = False
126
- logger.error(f"Error {str(response.status_code)} calling /v1/tp/stop_finder API; invalid API key")
116
+ raise InvalidAPIKey("Invalid API key")
127
117
 
128
118
  elif response.status_code == 403 or response.status_code == 429:
129
- # We've exceeded the rate limit but that doesn't mean the stop isn't valid
130
- # So let's assume that the stop ID is probably ok but we'll still raise a warning
131
- logger.warn(f"Error {str(response.status_code)} calling /v1/tp/stop_finder API; rate limit exceeded - assuming stop is valid")
119
+ raise APIRateLimitExceeded("API rate limit exceeded")
132
120
 
133
121
  else:
134
- # A misc error - log it
135
- all_stops_valid = False
136
- logger.error(f"Error {str(response.status_code)} calling /v1/tp/stop_finder API")
122
+ raise StopError("Unknown")
123
+
137
124
  else:
138
125
  # Parse the result as a JSON object
139
126
  stop_detail = response.json()
140
127
 
141
128
  # Just a quick check - the presence of systemMessages signifies an error, otherwise we assume it's ok
142
129
  if 'systemMessages' in stop_detail:
143
- all_stops_valid = False
144
- error_code = stop_detail['systemMessages'][0]['code']
145
130
  error_text = stop_detail['systemMessages'][0]['text']
146
- stop_detail = []
147
-
148
- logger.error(f"Error {error_code} calling /v1/tp/stop_finder API; {error_text} for Stop ID {stop}")
131
+ raise StopError(f"{error_text}", stop)
149
132
 
150
133
  # Put in a pause here to try and make sure we stay under the 5 API calls/second limit
151
134
  # Not usually an issue but if multiple processes are running multiple calls we might hit it
152
135
  time.sleep(1.0)
153
136
 
154
137
  except Exception as ex:
155
- # Some other kind of error, we should assume that the stop is invalid
156
- error_code = 999
157
- stop_detail = []
158
-
159
- logger.error(f"Error {str(ex)} calling /v1/tp/stop_finder API; assuming stop is invalid")
138
+ raise StopError(f"Error '{ex}' calling stop finder API for stop ID {stop}", stop)
160
139
 
161
140
  finally:
162
141
  # Append the results to the JSON output - only return the 'isBest' location entry if there's more than one
@@ -165,11 +144,11 @@ class TransportNSWv2(object):
165
144
  for location in stop_detail['locations']:
166
145
  if location['isBest']:
167
146
  stop_detail = location
168
-
169
147
  break
170
148
 
171
149
  else:
172
150
  stop_valid = False
151
+ all_stops_valid = False
173
152
 
174
153
  #Add it to the list
175
154
  data = {"stop_id": stop, "valid": stop_valid, "error_code": error_code, "stop_detail": stop_detail}
@@ -183,33 +162,24 @@ class TransportNSWv2(object):
183
162
 
184
163
  def get_trip(self, name_origin, name_destination , api_key, journey_wait_time = 0, transport_type = 0, \
185
164
  strict_transport_type = False, raw_output = False, journeys_to_return = 1, route_filter = '', \
186
- include_realtime_location = True, include_alerts = 'none', alert_type = 'all', check_stop_ids = True, forced_gtfs_uri = []):
165
+ include_realtime_location = True, include_alerts = 'none', alert_type = 'all', check_stop_ids = True, forced_gtfs_uri = [],
166
+ home_assistant = False):
187
167
 
188
168
  """Get the latest data from Transport NSW."""
189
169
  fmt = '%Y-%m-%dT%H:%M:%SZ'
190
170
 
191
- self.name_origin = name_origin
192
- self.destination = name_destination
193
- self.api_key = api_key
194
- self.journey_wait_time = journey_wait_time
195
- self.transport_type = transport_type
196
- self.strict_transport_type = strict_transport_type
197
- self.raw_output = raw_output
198
- self.journeys_to_return = journeys_to_return
199
- self.route_filter = route_filter.lower()
200
- self.include_realtime_location = include_realtime_location
201
- self.include_alerts = include_alerts.lower()
202
- self.alert_type = alert_type.lower()
171
+ route_filter = route_filter.lower()
172
+ include_alerts = include_alerts.lower()
173
+ alert_type = alert_type.lower()
203
174
 
204
175
  # This query always uses the current date and time - but add in any 'journey_wait_time' minutes
205
176
  now_plus_wait = datetime.now() + timedelta(minutes = journey_wait_time)
206
177
  itdDate = now_plus_wait.strftime('%Y%m%d')
207
178
  itdTime = now_plus_wait.strftime('%H%M')
208
179
 
209
- auth = 'apikey ' + self.api_key
180
+ auth = 'apikey ' + api_key
210
181
  header = {'Accept': 'application/json', 'Authorization': auth}
211
182
 
212
-
213
183
  # First, check if the source and dest stops are valid unless we've been told not to
214
184
  if check_stop_ids:
215
185
  stop_list = [name_origin, name_destination]
@@ -222,9 +192,9 @@ class TransportNSWv2(object):
222
192
  if not stop['valid']:
223
193
  stop_error += stop['stop_id']+ ", "
224
194
 
225
- logger.error(f"Stop ID(s) {stop_error[:-2]} do not exist - exiting")
226
- return None
227
-
195
+ #logger.error(f"Stop ID(s) {stop_error[:-2]} do not exist - exiting")
196
+ raise StopError (f"Stop ID(s) {stop_error[:-2]} do not exist", stop_error)
197
+ #return None
228
198
 
229
199
  # 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
230
200
  # 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
@@ -234,31 +204,32 @@ class TransportNSWv2(object):
234
204
  'https://api.transport.nsw.gov.au/v1/tp/trip?' \
235
205
  'outputFormat=rapidJSON&coordOutputFormat=EPSG%3A4326' \
236
206
  '&depArrMacro=dep&itdDate=' + itdDate + '&itdTime=' + itdTime + \
237
- '&type_origin=any&name_origin=' + self.name_origin + \
238
- '&type_destination=any&name_destination=' + self.destination + \
207
+ '&type_origin=any&name_origin=' + name_origin + \
208
+ '&type_destination=any&name_destination=' + name_destination + \
239
209
  '&TfNSWTR=true'
240
- # '&calcNumberOfTrips=' + str(journeys_to_retrieve) + \
241
210
 
242
211
  # Send the query and return an error if something goes wrong
243
212
  # Otherwise store the response for the next steps
244
213
  try:
245
214
  response = requests.get(url, headers=header, timeout=10)
246
215
 
247
-
248
216
  except Exception as ex:
249
- logger.error(f"Error {str(ex)} calling /v1/tp/trip API")
250
- return None
217
+ raise TripError (f"Error '{str(ex)}' calling trip API for journey {name_origin} to {name_destination}")
251
218
 
252
219
  # If we get bad status code, log error and return with n/a or an empty string
253
220
  if response.status_code != 200:
254
- if response.status_code == 429:
255
- logger.error(f"Error {str(response.status_code)} calling /v1/tp/trip API; rate limit exceeded")
256
- else:
257
- logger.error(f"Error {str(response.status_code)} calling /v1/tp/trip API; check API key")
221
+ if response.status_code == 401:
222
+ # API key issue
223
+ raise InvalidAPIKey("Error 'Invalid API key' calling trip API for journey {name_origin} to {name_destination}")
224
+
225
+ elif response.status_code == 403 or response.status_code == 429:
226
+ raise APIRateLimitExceeded("Error 'API rate limit exceeded' calling trip API for journey {name_origin} to {name_destination}")
258
227
 
259
- return None
228
+ else:
229
+ raise TripError(f"Error '{str(response.status_cude)}' calling trip API for journey {name_origin} to {name_destination}")
230
+
231
+ return None
260
232
 
261
- # Parse the result as a JSON object
262
233
  result = response.json()
263
234
 
264
235
  # The API will always return a valid trip, so it's just a case of grabbing the metadata that we need...
@@ -276,10 +247,7 @@ class TransportNSWv2(object):
276
247
  retrieved_journeys = len(result['journeys'])
277
248
 
278
249
  except:
279
- # Looks like an empty response
280
- logger.error(f"Error {(str(err))} calling /v1/tp/trip API")
281
-
282
- return None
250
+ raise TripError(f"Error 'no journeys returned' calling trip API for journey {name_origin} to {name_destination}")
283
251
 
284
252
  # Loop through the results applying filters where required, and generate the appropriate JSON output including an array of in-scope trips
285
253
  json_output=''
@@ -335,6 +303,11 @@ class TransportNSWv2(object):
335
303
  # We're also going to need the agency_id if it's a bus journey
336
304
  agencyid = transportation['operator']['id']
337
305
 
306
+ # AVMSTripID is for Home Assistant, if needed
307
+ avmstripid = 'n/a'
308
+ if 'properties' in transportation and 'AVMSTripID' in transportation['properties']:
309
+ avmstripid = transportation['properties']['AVMSTripID']
310
+
338
311
  # Line info
339
312
  origin_line_name_short = "unknown"
340
313
  if 'disassembledName' in transportation:
@@ -350,15 +323,16 @@ class TransportNSWv2(object):
350
323
  occupancy = first_stop['properties']['occupancy']
351
324
 
352
325
  alerts = "[]"
353
- if self.include_alerts != 'none':
326
+ if include_alerts != 'none':
354
327
  # We'll be adding these to the returned JSON string as an array
355
328
  # Only include alerts of the specified priority or greater, and of the specified type
356
- alerts = self._find_alerts(legs, self.include_alerts, self.alert_type)
329
+ alerts = self._find_alerts(legs, include_alerts, alert_type)
357
330
 
358
331
  latitude = 'n/a'
359
332
  longitude = 'n/a'
333
+ mode_url = 'n/a'
360
334
 
361
- if self.include_realtime_location and realtimetripid != 'n/a':
335
+ if include_realtime_location and realtimetripid != 'n/a':
362
336
  # See if we can get the latitute and longitude via the Realtime Vehicle Positions API
363
337
  # Build the URL(s) - some modes have multiple GTFS sources, unforunately
364
338
  # Some travel modes require brute-forcing the API call a few times, so if we're sure of the URI,
@@ -397,10 +371,12 @@ class TransportNSWv2(object):
397
371
  break
398
372
  else:
399
373
  # Warn that we didn't get a good return
400
- if response.status_code == 429:
401
- logger.error(f"Error {str(response.status_code)} calling {url} API; rate limit exceeded")
374
+ if response.status_code == 401:
375
+ logger.error(f"Error 'Invalid API key' calling {url} API")
376
+ elif response.status_code == 403 or response.status_code == 429:
377
+ logger.error(f"Error 'API rate limit exceded' calling {url} API")
402
378
  else:
403
- logger.error(f"Error {str(response.status_code)} calling {url} API; check API key")
379
+ logger.error(f"Error '{str(response.status_code)}' calling {url} API")
404
380
 
405
381
  if bFoundTripID == True:
406
382
  # No need to look any further
@@ -426,11 +402,17 @@ class TransportNSWv2(object):
426
402
  ATTR_CHANGES: changes,
427
403
  ATTR_OCCUPANCY : occupancy,
428
404
  ATTR_REAL_TIME_TRIP_ID : realtimetripid,
405
+ ATTR_GTFS_URI: mode_url,
429
406
  ATTR_LATITUDE : latitude,
430
407
  ATTR_LONGITUDE : longitude,
431
408
  ATTR_ALERTS: json.loads(alerts)
432
409
  }
433
410
 
411
+ if home_assistant:
412
+ self.info.update (
413
+ {ATTR_AVMS_TRIP_ID: avmstripid}
414
+ )
415
+
434
416
  found_journeys = found_journeys + 1
435
417
 
436
418
  # Add to the return array
@@ -447,7 +429,7 @@ class TransportNSWv2(object):
447
429
 
448
430
  current_journey_index = next_journey_index
449
431
 
450
- json_output='{"journeys_to_return": ' + str(self.journeys_to_return) + ', "journeys_with_data": ' + str(found_journeys) + ', "journeys": [' + json_output + ']}'
432
+ json_output='{"journeys_to_return": ' + str(journeys_to_return) + ', "journeys_with_data": ' + str(found_journeys) + ', "journeys": [' + json_output + ']}'
451
433
  return json_output
452
434
 
453
435
 
@@ -629,15 +611,17 @@ class TransportNSWv2(object):
629
611
  try:
630
612
  response = requests.get(url, timeout=5)
631
613
  except Exception as ex:
632
- logger.error("Error " + str(ex) + " querying GTFS URL datastore")
614
+ logger.error(f"Error '{str(ex)}' querying GTFS URL datastore")
633
615
  return None
634
616
 
635
617
  # If we get bad status code, log error and return with None
636
618
  if response.status_code != 200:
637
- if response.status_code == 429:
638
- logger.error("Error " + str(response.status_code) + " calling /v1/tp/stop_finder API; rate limit exceeded")
619
+ if response.status_code == 401:
620
+ logger.error (f"Error 'Invalid API key' calling GTFS API url {url}")
621
+ elif response.status_code == 403 or response.status_code == 429:
622
+ logger.error(f"Error 'API rate limit exceeded' calling GTFS API url {url}")
639
623
  else:
640
- logger.error("Error " + str(response.status_code) + " calling /v1/tp/stop_finder API; check API key")
624
+ logger.error(f"Error '{str(response.status_code)}' calling GTFS API url {url}")
641
625
 
642
626
  return None
643
627
 
@@ -668,3 +652,23 @@ class TransportNSWv2(object):
668
652
  if estimated > datetime.utcnow():
669
653
  due = round((estimated - datetime.utcnow()).seconds / 60)
670
654
  return due
655
+
656
+ # Exceptions
657
+ class InvalidAPIKey(Exception):
658
+ """ API key error """
659
+
660
+ class APIRateLimitExceeded(Exception):
661
+ """ API rate limit exceeded """
662
+
663
+ class StopError(Exception):
664
+ """ Stop-finder related error """
665
+ def __init__(self, message = "", stop_detail = ""):
666
+ super().__init__(message)
667
+ self.stop_detail = stop_detail
668
+
669
+ # def __str__(self):
670
+ # return f"{self.message}"
671
+
672
+ class TripError(Exception):
673
+ """" Trip-finder related error """
674
+
@@ -5,7 +5,7 @@ with open("README.md", "r") as fh:
5
5
 
6
6
  setuptools.setup(
7
7
  name="PyTransportNSWv2",
8
- version="0.9.2",
8
+ version="1.0.1",
9
9
  author="andystewart999",
10
10
  author_email="andy.stewart@live.com",
11
11
  description="Get detailed per-trip transport information from TransportNSW",