PyTransportNSWv2 0.9.2__tar.gz → 1.0.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,32 +1,31 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: PyTransportNSWv2
3
- Version: 0.9.2
3
+ Version: 1.0.0
4
4
  Summary: Get detailed per-trip transport information from TransportNSW
5
5
  Home-page: https://github.com/andystewart999/TransportNSW
6
6
  Author: andystewart999
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.0
4
4
  Summary: Get detailed per-trip transport information from TransportNSW
5
5
  Home-page: https://github.com/andystewart999/TransportNSW
6
6
  Author: andystewart999
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,6 +31,7 @@ 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'
@@ -121,31 +122,36 @@ class TransportNSWv2(object):
121
122
 
122
123
  if response.status_code == 401:
123
124
  # 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")
125
+ #skip_api_calls = True
126
+ #all_stops_valid = False
127
+ #logger.error(f"Error {str(response.status_code)} calling /v1/tp/stop_finder API; invalid API key")
128
+ raise InvalidAPIKey("Invalid API key")
127
129
 
128
130
  elif response.status_code == 403 or response.status_code == 429:
129
131
  # 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")
132
+ # So raise an API rate limit exception and let the user decide how to handle it
133
+ #logger.warn(f"Error {str(response.status_code)} calling /v1/tp/stop_finder API; rate limit exceeded - assuming stop is valid")
134
+ raise APIRateLimitExceeded("API rate limit exceeded")
132
135
 
133
136
  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")
137
+ # Raise a generic error exception
138
+ #all_stops_valid = False
139
+ #logger.error(f"Error {str(response.status_code)} calling /v1/tp/stop_finder API")
140
+ raise StopError("Unknown")
141
+
137
142
  else:
138
143
  # Parse the result as a JSON object
139
144
  stop_detail = response.json()
140
145
 
141
146
  # Just a quick check - the presence of systemMessages signifies an error, otherwise we assume it's ok
142
147
  if 'systemMessages' in stop_detail:
143
- all_stops_valid = False
144
- error_code = stop_detail['systemMessages'][0]['code']
148
+ #all_stops_valid = False
149
+ #error_code = stop_detail['systemMessages'][0]['code']
145
150
  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}")
151
+ #stop_detail = []
152
+ #logger.error(f"Error {error_code} calling /v1/tp/stop_finder API; {error_text} for Stop ID {stop}")
153
+ #raise StopError(f"Error {str(error_code)} ({str(error_text)}) calling stop finder API for stop ID {stop}")
154
+ raise StopError(f"{error_text}")
149
155
 
150
156
  # Put in a pause here to try and make sure we stay under the 5 API calls/second limit
151
157
  # Not usually an issue but if multiple processes are running multiple calls we might hit it
@@ -153,10 +159,11 @@ class TransportNSWv2(object):
153
159
 
154
160
  except Exception as ex:
155
161
  # Some other kind of error, we should assume that the stop is invalid
156
- error_code = 999
157
- stop_detail = []
162
+ #error_code = 999
163
+ #stop_detail = []
158
164
 
159
- logger.error(f"Error {str(ex)} calling /v1/tp/stop_finder API; assuming stop is invalid")
165
+ #logger.error(f"Error {str(ex)} calling /v1/tp/stop_finder API; assuming stop is invalid")
166
+ raise StopError(f"Error '{ex}' calling stop finder API for stop ID {stop}")
160
167
 
161
168
  finally:
162
169
  # Append the results to the JSON output - only return the 'isBest' location entry if there's more than one
@@ -170,6 +177,7 @@ class TransportNSWv2(object):
170
177
 
171
178
  else:
172
179
  stop_valid = False
180
+ all_stops_valid = False
173
181
 
174
182
  #Add it to the list
175
183
  data = {"stop_id": stop, "valid": stop_valid, "error_code": error_code, "stop_detail": stop_detail}
@@ -183,33 +191,35 @@ class TransportNSWv2(object):
183
191
 
184
192
  def get_trip(self, name_origin, name_destination , api_key, journey_wait_time = 0, transport_type = 0, \
185
193
  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 = []):
194
+ include_realtime_location = True, include_alerts = 'none', alert_type = 'all', check_stop_ids = True, forced_gtfs_uri = [],
195
+ home_assistant = False):
187
196
 
188
197
  """Get the latest data from Transport NSW."""
189
198
  fmt = '%Y-%m-%dT%H:%M:%SZ'
190
199
 
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()
203
-
200
+ #self.name_origin = name_origin
201
+ #self.destination = name_destination
202
+ #self.api_key = api_key
203
+ #self.journey_wait_time = journey_wait_time
204
+ #self.transport_type = transport_type
205
+ #self.strict_transport_type = strict_transport_type
206
+ #self.raw_output = raw_output
207
+ #self.journeys_to_return = journeys_to_return
208
+ #self.route_filter = route_filter.lower()
209
+ #self.include_realtime_location = include_realtime_location
210
+ #self.include_alerts = include_alerts.lower()
211
+ #self.alert_type = alert_type.lower()
212
+ route_filter = route_filter.lower()
213
+ include_alerts = include_alerts.lower()
214
+ alert_type = alert_type.lower()
204
215
  # This query always uses the current date and time - but add in any 'journey_wait_time' minutes
205
216
  now_plus_wait = datetime.now() + timedelta(minutes = journey_wait_time)
206
217
  itdDate = now_plus_wait.strftime('%Y%m%d')
207
218
  itdTime = now_plus_wait.strftime('%H%M')
208
219
 
209
- auth = 'apikey ' + self.api_key
220
+ auth = 'apikey ' + api_key
210
221
  header = {'Accept': 'application/json', 'Authorization': auth}
211
222
 
212
-
213
223
  # First, check if the source and dest stops are valid unless we've been told not to
214
224
  if check_stop_ids:
215
225
  stop_list = [name_origin, name_destination]
@@ -222,9 +232,9 @@ class TransportNSWv2(object):
222
232
  if not stop['valid']:
223
233
  stop_error += stop['stop_id']+ ", "
224
234
 
225
- logger.error(f"Stop ID(s) {stop_error[:-2]} do not exist - exiting")
226
- return None
227
-
235
+ #logger.error(f"Stop ID(s) {stop_error[:-2]} do not exist - exiting")
236
+ raise StopError (f"Stop ID(s) {stop_error[:-2]} do not exist")
237
+ #return None
228
238
 
229
239
  # 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
240
  # 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,8 +244,8 @@ class TransportNSWv2(object):
234
244
  'https://api.transport.nsw.gov.au/v1/tp/trip?' \
235
245
  'outputFormat=rapidJSON&coordOutputFormat=EPSG%3A4326' \
236
246
  '&depArrMacro=dep&itdDate=' + itdDate + '&itdTime=' + itdTime + \
237
- '&type_origin=any&name_origin=' + self.name_origin + \
238
- '&type_destination=any&name_destination=' + self.destination + \
247
+ '&type_origin=any&name_origin=' + name_origin + \
248
+ '&type_destination=any&name_destination=' + destination + \
239
249
  '&TfNSWTR=true'
240
250
  # '&calcNumberOfTrips=' + str(journeys_to_retrieve) + \
241
251
 
@@ -244,21 +254,27 @@ class TransportNSWv2(object):
244
254
  try:
245
255
  response = requests.get(url, headers=header, timeout=10)
246
256
 
247
-
248
257
  except Exception as ex:
249
- logger.error(f"Error {str(ex)} calling /v1/tp/trip API")
250
- return None
258
+ #logger.error(f"Error {str(ex)} calling /v1/tp/trip API")
259
+ #return None
260
+ raise TripError (f"Error '{str(ex)}' calling trip API for journey {self.name_origin} to {self.destination}")
251
261
 
252
262
  # If we get bad status code, log error and return with n/a or an empty string
253
263
  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")
264
+ if response.status_code == 401:
265
+ # API key issue
266
+ raise InvalidAPIKey("Error 'Invalid API key' calling trip API for journey {self.name_origin} to {self.destination}")
258
267
 
259
- return None
268
+ elif response.status_code == 403 or response.status_code == 429:
269
+ raise APIRateLimitExceeded("Error 'API rate limit exceeded' calling trip API for journey {self.name_origin} to {self.destination}")
270
+ #logger.error(f"Error {str(response.status_code)} calling /v1/tp/trip API; rate limit exceeded")
271
+
272
+ else:
273
+ raise TripError(f"Error '{str(response.status_cude)}' calling trip API for journey {self.name_origin} to {self.destination}")
274
+ #logger.error(f"Error {str(response.status_code)} calling /v1/tp/trip API; check API key")
275
+
276
+ return None
260
277
 
261
- # Parse the result as a JSON object
262
278
  result = response.json()
263
279
 
264
280
  # The API will always return a valid trip, so it's just a case of grabbing the metadata that we need...
@@ -277,9 +293,9 @@ class TransportNSWv2(object):
277
293
 
278
294
  except:
279
295
  # Looks like an empty response
280
- logger.error(f"Error {(str(err))} calling /v1/tp/trip API")
281
-
282
- return None
296
+ #logger.error(f"Error {(str(err))} calling /v1/tp/trip API")
297
+ raise TripError(f"Error 'no journeys returned' calling trip API for journey {self.name_origin} to {self.destination}")
298
+ #return None
283
299
 
284
300
  # Loop through the results applying filters where required, and generate the appropriate JSON output including an array of in-scope trips
285
301
  json_output=''
@@ -335,6 +351,11 @@ class TransportNSWv2(object):
335
351
  # We're also going to need the agency_id if it's a bus journey
336
352
  agencyid = transportation['operator']['id']
337
353
 
354
+ # AVMSTripID is for Home Assistant, if needed
355
+ avmstripid = 'n/a'
356
+ if 'properties' in transportation and 'AVMSTripID' in transportation['properties']:
357
+ avmstripid = transportation['properties']['AVMSTripID']
358
+
338
359
  # Line info
339
360
  origin_line_name_short = "unknown"
340
361
  if 'disassembledName' in transportation:
@@ -397,10 +418,12 @@ class TransportNSWv2(object):
397
418
  break
398
419
  else:
399
420
  # 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")
421
+ if response.status_code == 401:
422
+ logger.error(f"Error 'Invalid API key' calling {url} API")
423
+ elif response.status_code == 403 or response.status_code == 429:
424
+ logger.error(f"Error 'API rate limit exceded' calling {url} API")
402
425
  else:
403
- logger.error(f"Error {str(response.status_code)} calling {url} API; check API key")
426
+ logger.error(f"Error '{str(response.status_code)}' calling {url} API")
404
427
 
405
428
  if bFoundTripID == True:
406
429
  # No need to look any further
@@ -431,6 +454,11 @@ class TransportNSWv2(object):
431
454
  ATTR_ALERTS: json.loads(alerts)
432
455
  }
433
456
 
457
+ if self.home_assistant:
458
+ self.info.update (
459
+ {ATTR_AVMS_TRIP_ID: avmstripid}
460
+ )
461
+
434
462
  found_journeys = found_journeys + 1
435
463
 
436
464
  # Add to the return array
@@ -629,15 +657,17 @@ class TransportNSWv2(object):
629
657
  try:
630
658
  response = requests.get(url, timeout=5)
631
659
  except Exception as ex:
632
- logger.error("Error " + str(ex) + " querying GTFS URL datastore")
660
+ logger.error(f"Error '{str(ex)}' querying GTFS URL datastore")
633
661
  return None
634
662
 
635
663
  # If we get bad status code, log error and return with None
636
664
  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")
665
+ if response.status_code == 401:
666
+ logger.error (f"Error 'Invalid API key' calling GTFS API url {url}")
667
+ elif response.status_code == 403 or response.status_code == 429:
668
+ logger.error(f"Error 'API rate limit exceeded' calling GTFS API url {url}")
639
669
  else:
640
- logger.error("Error " + str(response.status_code) + " calling /v1/tp/stop_finder API; check API key")
670
+ logger.error(f"Error '{str(response.status_code)}' calling GTFS API url {url}")
641
671
 
642
672
  return None
643
673
 
@@ -668,3 +698,20 @@ class TransportNSWv2(object):
668
698
  if estimated > datetime.utcnow():
669
699
  due = round((estimated - datetime.utcnow()).seconds / 60)
670
700
  return due
701
+
702
+ # Exceptions
703
+ class TFNSWAPIError(Exception):
704
+ """ Base error for all exceptions """
705
+
706
+ class InvalidAPIKey(TFNSWAPIError):
707
+ """ API key error """
708
+
709
+ class APIRateLimitExceeded(TFNSWAPIError):
710
+ """ API rate limit exceeded """
711
+
712
+ class StopError(TFNSWAPIError):
713
+ """ Stop-finder related error """
714
+
715
+ class TripError(TFNSWAPIError):
716
+ """" Trip-finder related error """
717
+
@@ -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.0",
9
9
  author="andystewart999",
10
10
  author_email="andy.stewart@live.com",
11
11
  description="Get detailed per-trip transport information from TransportNSW",