MBTAclient 0.2.6__tar.gz → 0.2.8__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.
- {mbtaclient-0.2.6 → mbtaclient-0.2.8}/PKG-INFO +2 -2
- {mbtaclient-0.2.6 → mbtaclient-0.2.8}/pyproject.toml +2 -2
- {mbtaclient-0.2.6 → mbtaclient-0.2.8}/src/MBTAclient.egg-info/PKG-INFO +2 -2
- {mbtaclient-0.2.6 → mbtaclient-0.2.8}/src/MBTAclient.egg-info/SOURCES.txt +10 -1
- {mbtaclient-0.2.6 → mbtaclient-0.2.8}/src/mbtaclient/base_handler.py +69 -45
- {mbtaclient-0.2.6 → mbtaclient-0.2.8}/src/mbtaclient/journey_stop.py +1 -1
- mbtaclient-0.2.8/src/mbtaclient/journeys_handler.py +108 -0
- {mbtaclient-0.2.6 → mbtaclient-0.2.8}/src/mbtaclient/mbta_client.py +14 -2
- {mbtaclient-0.2.6 → mbtaclient-0.2.8}/src/mbtaclient/mbta_utils.py +27 -12
- mbtaclient-0.2.8/src/mbtaclient/trip_handler.py +115 -0
- mbtaclient-0.2.8/tests/test_journey_stop.py +167 -0
- mbtaclient-0.2.8/tests/test_mbta_alert.py +139 -0
- mbtaclient-0.2.8/tests/test_mbta_client.py +109 -0
- mbtaclient-0.2.8/tests/test_mbta_prediction.py +97 -0
- mbtaclient-0.2.8/tests/test_mbta_route.py +58 -0
- mbtaclient-0.2.8/tests/test_mbta_schedule.py +88 -0
- mbtaclient-0.2.8/tests/test_mbta_stop.py +68 -0
- mbtaclient-0.2.8/tests/test_mbta_trip.py +68 -0
- mbtaclient-0.2.8/tests/test_mbta_utils.py +126 -0
- mbtaclient-0.2.6/src/mbtaclient/journeys_handler.py +0 -89
- mbtaclient-0.2.6/src/mbtaclient/trip_handler.py +0 -124
- {mbtaclient-0.2.6 → mbtaclient-0.2.8}/LICENSE +0 -0
- {mbtaclient-0.2.6 → mbtaclient-0.2.8}/README.md +0 -0
- {mbtaclient-0.2.6 → mbtaclient-0.2.8}/setup.cfg +0 -0
- {mbtaclient-0.2.6 → mbtaclient-0.2.8}/src/MBTAclient.egg-info/dependency_links.txt +0 -0
- {mbtaclient-0.2.6 → mbtaclient-0.2.8}/src/MBTAclient.egg-info/requires.txt +0 -0
- {mbtaclient-0.2.6 → mbtaclient-0.2.8}/src/MBTAclient.egg-info/top_level.txt +0 -0
- {mbtaclient-0.2.6 → mbtaclient-0.2.8}/src/mbtaclient/__init__.py +0 -0
- {mbtaclient-0.2.6 → mbtaclient-0.2.8}/src/mbtaclient/__version__.py +0 -0
- {mbtaclient-0.2.6 → mbtaclient-0.2.8}/src/mbtaclient/journey.py +0 -0
- {mbtaclient-0.2.6 → mbtaclient-0.2.8}/src/mbtaclient/mbta_alert.py +0 -0
- {mbtaclient-0.2.6 → mbtaclient-0.2.8}/src/mbtaclient/mbta_prediction.py +0 -0
- {mbtaclient-0.2.6 → mbtaclient-0.2.8}/src/mbtaclient/mbta_route.py +0 -0
- {mbtaclient-0.2.6 → mbtaclient-0.2.8}/src/mbtaclient/mbta_schedule.py +0 -0
- {mbtaclient-0.2.6 → mbtaclient-0.2.8}/src/mbtaclient/mbta_stop.py +0 -0
- {mbtaclient-0.2.6 → mbtaclient-0.2.8}/src/mbtaclient/mbta_trip.py +0 -0
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: MBTAclient
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.8
|
|
4
4
|
Summary: A Python client for interacting with the MBTA API
|
|
5
5
|
Author-email: Luca Chiabrera <luca.chiabrera@gmail.com>
|
|
6
6
|
License: MIT
|
|
7
7
|
Project-URL: Homepage, https://github.com/chiabre/MBTAclient
|
|
8
8
|
Project-URL: Issues, https://github.com/chiabre/MBTAclient/issues
|
|
9
9
|
Classifier: Programming Language :: Python :: 3
|
|
10
|
-
Classifier: Programming Language :: Python :: 3.
|
|
10
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
11
11
|
Classifier: License :: OSI Approved :: MIT License
|
|
12
12
|
Classifier: Operating System :: OS Independent
|
|
13
13
|
Requires-Python: >=3.12
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "MBTAclient"
|
|
7
|
-
version = "0.2.
|
|
7
|
+
version = "0.2.8"
|
|
8
8
|
description = "A Python client for interacting with the MBTA API"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
requires-python = ">=3.12"
|
|
@@ -15,7 +15,7 @@ dependencies = [
|
|
|
15
15
|
license = { text = "MIT" }
|
|
16
16
|
classifiers = [
|
|
17
17
|
"Programming Language :: Python :: 3",
|
|
18
|
-
"Programming Language :: Python :: 3.
|
|
18
|
+
"Programming Language :: Python :: 3.12",
|
|
19
19
|
"License :: OSI Approved :: MIT License",
|
|
20
20
|
"Operating System :: OS Independent"
|
|
21
21
|
]
|
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: MBTAclient
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.8
|
|
4
4
|
Summary: A Python client for interacting with the MBTA API
|
|
5
5
|
Author-email: Luca Chiabrera <luca.chiabrera@gmail.com>
|
|
6
6
|
License: MIT
|
|
7
7
|
Project-URL: Homepage, https://github.com/chiabre/MBTAclient
|
|
8
8
|
Project-URL: Issues, https://github.com/chiabre/MBTAclient/issues
|
|
9
9
|
Classifier: Programming Language :: Python :: 3
|
|
10
|
-
Classifier: Programming Language :: Python :: 3.
|
|
10
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
11
11
|
Classifier: License :: OSI Approved :: MIT License
|
|
12
12
|
Classifier: Operating System :: OS Independent
|
|
13
13
|
Requires-Python: >=3.12
|
|
@@ -20,4 +20,13 @@ src/mbtaclient/mbta_schedule.py
|
|
|
20
20
|
src/mbtaclient/mbta_stop.py
|
|
21
21
|
src/mbtaclient/mbta_trip.py
|
|
22
22
|
src/mbtaclient/mbta_utils.py
|
|
23
|
-
src/mbtaclient/trip_handler.py
|
|
23
|
+
src/mbtaclient/trip_handler.py
|
|
24
|
+
tests/test_journey_stop.py
|
|
25
|
+
tests/test_mbta_alert.py
|
|
26
|
+
tests/test_mbta_client.py
|
|
27
|
+
tests/test_mbta_prediction.py
|
|
28
|
+
tests/test_mbta_route.py
|
|
29
|
+
tests/test_mbta_schedule.py
|
|
30
|
+
tests/test_mbta_stop.py
|
|
31
|
+
tests/test_mbta_trip.py
|
|
32
|
+
tests/test_mbta_utils.py
|
|
@@ -31,15 +31,23 @@ class BaseHandler:
|
|
|
31
31
|
}
|
|
32
32
|
|
|
33
33
|
client_session = session or aiohttp.ClientSession()
|
|
34
|
-
self.mbta_client = MBTAClient(client_session,logger,api_key)
|
|
34
|
+
self.mbta_client = MBTAClient(client_session, logger, api_key)
|
|
35
35
|
|
|
36
36
|
self.journeys: dict[str, Journey] = {}
|
|
37
37
|
|
|
38
38
|
self.logger: logging.Logger = logger or logging.getLogger(__name__)
|
|
39
39
|
|
|
40
40
|
def __repr__(self) -> str:
|
|
41
|
-
return
|
|
41
|
+
return "BaseHandler(depart_from_name={}, arrive_at_name={})".format(self.depart_from['name'], self.arrive_at['name'])
|
|
42
42
|
|
|
43
|
+
async def __aenter__(self):
|
|
44
|
+
# Entering context, initialize and return the handler
|
|
45
|
+
await self._async_init()
|
|
46
|
+
return self
|
|
47
|
+
|
|
48
|
+
async def __aexit__(self, exc_type, exc, tb):
|
|
49
|
+
# Exit context, clean up resources if necessary
|
|
50
|
+
await self.mbta_client.close()
|
|
43
51
|
|
|
44
52
|
async def _async_init(self):
|
|
45
53
|
stops = await self.__fetch_stops()
|
|
@@ -47,16 +55,19 @@ class BaseHandler:
|
|
|
47
55
|
|
|
48
56
|
@memoize_async()
|
|
49
57
|
async def __fetch_stops(self, params: dict = None) -> list[MBTAStop]:
|
|
50
|
-
"""
|
|
51
|
-
self.logger.debug("
|
|
58
|
+
"""Retrieve stops."""
|
|
59
|
+
self.logger.debug("Retrieving MBTA stops")
|
|
52
60
|
base_params = {'filter[location_type]': '0'}
|
|
53
61
|
if params is not None:
|
|
54
62
|
base_params.update(params)
|
|
55
63
|
try:
|
|
56
64
|
stops: list[MBTAStop] = await self.mbta_client.list_stops(base_params)
|
|
57
65
|
return stops
|
|
66
|
+
except aiohttp.ClientError as e:
|
|
67
|
+
self.logger.error("HTTP error occurred while retrieving MBTA stops: {}".format(e))
|
|
68
|
+
return []
|
|
58
69
|
except Exception as e:
|
|
59
|
-
self.logger.error(
|
|
70
|
+
self.logger.error("Error retrieving MBTA stops: {}".format(e))
|
|
60
71
|
return []
|
|
61
72
|
|
|
62
73
|
def __process_stops(self, stops: list[MBTAStop]):
|
|
@@ -68,7 +79,7 @@ class BaseHandler:
|
|
|
68
79
|
|
|
69
80
|
for stop in stops:
|
|
70
81
|
if not isinstance(stop, MBTAStop): # Validate data type
|
|
71
|
-
self.logger.warning(
|
|
82
|
+
self.logger.warning("Unexpected data type for stop: {}".format(type(stop)))
|
|
72
83
|
continue # Skip invalid data
|
|
73
84
|
|
|
74
85
|
if stop.name.lower() == self.depart_from['name'].lower():
|
|
@@ -80,12 +91,12 @@ class BaseHandler:
|
|
|
80
91
|
arrive_at_ids.append(stop.id)
|
|
81
92
|
|
|
82
93
|
if len(depart_from_stops) == 0:
|
|
83
|
-
self.logger.error(
|
|
84
|
-
raise MBTAStopError(
|
|
94
|
+
self.logger.error("Error processing MBTA stop data for {}".format(self.depart_from['name']))
|
|
95
|
+
raise MBTAStopError("Invalid stop name: {}".format(self.depart_from['name']))
|
|
85
96
|
|
|
86
97
|
if len(arrive_at_stops) == 0:
|
|
87
|
-
self.logger.error(
|
|
88
|
-
raise MBTAStopError(
|
|
98
|
+
self.logger.error("Error processing MBTA stop data for {}".format(self.arrive_at['name']))
|
|
99
|
+
raise MBTAStopError("Invalid stop name: {}".format(self.arrive_at['name']))
|
|
89
100
|
|
|
90
101
|
self.depart_from['stops'] = depart_from_stops
|
|
91
102
|
self.depart_from['ids'] = depart_from_ids
|
|
@@ -99,19 +110,19 @@ class BaseHandler:
|
|
|
99
110
|
return None
|
|
100
111
|
|
|
101
112
|
def _get_stops_ids(self) -> list[str]:
|
|
102
|
-
return
|
|
113
|
+
return self.depart_from['ids'] + self.arrive_at['ids']
|
|
103
114
|
|
|
104
115
|
def __get_stops_ids_by_stop_type(self, stop_type: str) -> Optional[list[str]]:
|
|
105
116
|
if stop_type == 'departure':
|
|
106
117
|
return self.depart_from['ids']
|
|
107
118
|
elif stop_type == 'arrival':
|
|
108
119
|
return self.arrive_at['ids']
|
|
109
|
-
return None
|
|
120
|
+
return None
|
|
110
121
|
|
|
111
122
|
@memoize_async(expire_at_end_of_day=True)
|
|
112
123
|
async def _fetch_schedules(self, params: Optional[dict] = None) -> list[MBTASchedule]:
|
|
113
|
-
"""
|
|
114
|
-
self.logger.debug("
|
|
124
|
+
"""Retrieve MBTA schedules"""
|
|
125
|
+
self.logger.debug("Retrieving MBTA schedules")
|
|
115
126
|
base_params = {
|
|
116
127
|
'filter[stop]': ','.join(self._get_stops_ids()),
|
|
117
128
|
'sort': 'departure_time'
|
|
@@ -121,8 +132,11 @@ class BaseHandler:
|
|
|
121
132
|
try:
|
|
122
133
|
schedules: list[MBTASchedule] = await self.mbta_client.list_schedules(params)
|
|
123
134
|
return schedules
|
|
135
|
+
except aiohttp.ClientError as e:
|
|
136
|
+
self.logger.error("HTTP error occurred while retrieving MBTA schedules: {}".format(e))
|
|
137
|
+
return []
|
|
124
138
|
except Exception as e:
|
|
125
|
-
self.logger.error(
|
|
139
|
+
self.logger.error("Error retrieving MBTA schedules: {}".format(e))
|
|
126
140
|
return []
|
|
127
141
|
|
|
128
142
|
async def _process_schedules(self, schedules: list[MBTASchedule]):
|
|
@@ -131,7 +145,7 @@ class BaseHandler:
|
|
|
131
145
|
for schedule in schedules:
|
|
132
146
|
# Validate schedule data
|
|
133
147
|
if not schedule.trip_id or not schedule.stop_id:
|
|
134
|
-
self.logger.error(
|
|
148
|
+
self.logger.error("Invalid schedule data: {}".format(schedule))
|
|
135
149
|
continue # Skip to the next schedule
|
|
136
150
|
|
|
137
151
|
# If the schedule trip_id is not in the journeys
|
|
@@ -144,7 +158,7 @@ class BaseHandler:
|
|
|
144
158
|
# Validate stop
|
|
145
159
|
stop = self.__get_stop_by_id(schedule.stop_id)
|
|
146
160
|
if not stop:
|
|
147
|
-
self.logger.debug(
|
|
161
|
+
self.logger.debug("Stop {} of schedule {} doesn't belong to the journey stop ids".format(schedule.stop_id, schedule.id))
|
|
148
162
|
continue # Skip to the next schedule
|
|
149
163
|
|
|
150
164
|
departure_stops_ids = self.__get_stops_ids_by_stop_type('departure')
|
|
@@ -156,13 +170,11 @@ class BaseHandler:
|
|
|
156
170
|
elif schedule.stop_id in arrival_stops_ids:
|
|
157
171
|
self.journeys[schedule.trip_id].add_stop('arrival', schedule, stop, 'SCHEDULED')
|
|
158
172
|
else:
|
|
159
|
-
self.logger.warning(
|
|
160
|
-
|
|
161
|
-
|
|
173
|
+
self.logger.warning("Stop ID {} is not categorized as departure or arrival for schedule: {}".format(schedule.stop_id, schedule))
|
|
162
174
|
|
|
163
175
|
async def _fetch_predictions(self, params: str = None) -> list[MBTAPrediction]:
|
|
164
|
-
"""
|
|
165
|
-
self.logger.debug("
|
|
176
|
+
"""Retrieve MBTA predictions based on the provided stop IDs"""
|
|
177
|
+
self.logger.debug("Retrieving MBTA predictions")
|
|
166
178
|
base_params = {
|
|
167
179
|
'filter[stop]': ','.join(self._get_stops_ids()),
|
|
168
180
|
'filter[revenue]': 'REVENUE',
|
|
@@ -173,17 +185,19 @@ class BaseHandler:
|
|
|
173
185
|
try:
|
|
174
186
|
predictions: list[MBTAPrediction] = await self.mbta_client.list_predictions(base_params)
|
|
175
187
|
return predictions
|
|
188
|
+
except aiohttp.ClientError as e:
|
|
189
|
+
self.logger.error("HTTP error occurred while retrieving MBTA predictions: {}".format(e))
|
|
190
|
+
return []
|
|
176
191
|
except Exception as e:
|
|
177
|
-
self.logger.error(
|
|
192
|
+
self.logger.error("Error retrieving MBTA predictions: {}".format(e))
|
|
178
193
|
|
|
179
|
-
|
|
180
194
|
async def _process_predictions(self, predictions: list[MBTAPrediction]):
|
|
181
195
|
self.logger.debug("Processing MBTA predictions")
|
|
182
196
|
|
|
183
197
|
for prediction in predictions:
|
|
184
198
|
# Validate prediction data
|
|
185
199
|
if not prediction.trip_id or not prediction.stop_id:
|
|
186
|
-
self.logger.error(
|
|
200
|
+
self.logger.error("Invalid prediction data: {}".format(prediction))
|
|
187
201
|
continue # Skip to the next prediction
|
|
188
202
|
|
|
189
203
|
# If the trip of the prediction is not in the journeys dict
|
|
@@ -196,7 +210,7 @@ class BaseHandler:
|
|
|
196
210
|
# Validate stop
|
|
197
211
|
stop = self.__get_stop_by_id(prediction.stop_id)
|
|
198
212
|
if not stop:
|
|
199
|
-
self.logger.error(
|
|
213
|
+
self.logger.error("Invalid stop ID: {} for prediction: {}".format(prediction.stop_id, prediction))
|
|
200
214
|
continue # Skip to the next prediction
|
|
201
215
|
|
|
202
216
|
departure_stops_ids = self.__get_stops_ids_by_stop_type('departure')
|
|
@@ -212,12 +226,11 @@ class BaseHandler:
|
|
|
212
226
|
elif prediction.stop_id in arrival_stops_ids:
|
|
213
227
|
self.journeys[prediction.trip_id].add_stop('arrival', prediction, stop, prediction.schedule_relationship)
|
|
214
228
|
else:
|
|
215
|
-
self.logger.warning(
|
|
216
|
-
|
|
229
|
+
self.logger.warning("Stop ID {} is not categorized as departure or arrival for prediction: {}".format(prediction.stop_id, prediction))
|
|
217
230
|
|
|
218
|
-
async def _fetch_alerts(self,params: str = None) -> list[MBTAAlert]:
|
|
219
|
-
"""
|
|
220
|
-
self.logger.debug("
|
|
231
|
+
async def _fetch_alerts(self, params: str = None) -> list[MBTAAlert]:
|
|
232
|
+
"""Retrieve MBTA alerts"""
|
|
233
|
+
self.logger.debug("Retrieving MBTA alerts")
|
|
221
234
|
|
|
222
235
|
# Prepare filter parameters
|
|
223
236
|
base_params = {
|
|
@@ -232,9 +245,12 @@ class BaseHandler:
|
|
|
232
245
|
try:
|
|
233
246
|
alerts: list[MBTAAlert] = await self.mbta_client.list_alerts(base_params)
|
|
234
247
|
return alerts
|
|
248
|
+
except aiohttp.ClientError as e:
|
|
249
|
+
self.logger.error("HTTP error occurred while retrieving MBTA alerts: {}".format(e))
|
|
250
|
+
return []
|
|
235
251
|
except Exception as e:
|
|
236
|
-
self.logger.error(
|
|
237
|
-
|
|
252
|
+
self.logger.error("Error retrieving MBTA alerts: {}".format(e))
|
|
253
|
+
return []
|
|
238
254
|
|
|
239
255
|
def _process_alerts(self, alerts: list[MBTAAlert]):
|
|
240
256
|
self.logger.debug("Processing MBTA alerts")
|
|
@@ -242,12 +258,12 @@ class BaseHandler:
|
|
|
242
258
|
for alert in alerts:
|
|
243
259
|
# Validate alert data
|
|
244
260
|
if not alert.id or not alert.effect:
|
|
245
|
-
self.logger.error(
|
|
261
|
+
self.logger.error("Invalid alert data: {}".format(alert))
|
|
246
262
|
continue # Skip to the next alert
|
|
247
263
|
|
|
248
264
|
# Iterate through each journey and associate relevant alerts
|
|
249
265
|
for journey in self.journeys.values():
|
|
250
|
-
|
|
266
|
+
# Check if the alert is already associated by comparing IDs
|
|
251
267
|
if any(existing_alert.id == alert.id for existing_alert in journey.alerts):
|
|
252
268
|
continue # Skip if alert is already associated
|
|
253
269
|
|
|
@@ -256,7 +272,7 @@ class BaseHandler:
|
|
|
256
272
|
if self.__is_alert_relevant(alert, journey):
|
|
257
273
|
journey.alerts.append(alert)
|
|
258
274
|
except Exception as e:
|
|
259
|
-
self.logger.error(
|
|
275
|
+
self.logger.error("Error processing MBTA alert {}: {}".format(alert.id, e))
|
|
260
276
|
continue # Skip to the next journey if an error occurs
|
|
261
277
|
|
|
262
278
|
def __is_alert_relevant(self, alert: MBTAAlert, journey: Journey) -> bool:
|
|
@@ -291,36 +307,44 @@ class BaseHandler:
|
|
|
291
307
|
@memoize_async()
|
|
292
308
|
async def _fetch_trip(self, trip_id: str, params: dict = None) -> Optional[MBTATrip]:
|
|
293
309
|
"""Retrieve MBTA trip based on trip_id."""
|
|
294
|
-
self.logger.debug(
|
|
310
|
+
self.logger.debug("Retrieving MBTA trip: {}".format(trip_id))
|
|
295
311
|
try:
|
|
296
312
|
trip: MBTATrip = await self.mbta_client.get_trip(trip_id, params)
|
|
297
313
|
return trip
|
|
314
|
+
except aiohttp.ClientError as e:
|
|
315
|
+
self.logger.error("HTTP error occurred while fetching trip {}: {}".format(trip_id, e))
|
|
316
|
+
return None
|
|
298
317
|
except Exception as e:
|
|
299
|
-
self.logger.error(
|
|
318
|
+
self.logger.error("Error fetching trip {}: {}".format(trip_id, e))
|
|
300
319
|
return None
|
|
301
320
|
|
|
302
321
|
@memoize_async()
|
|
303
322
|
async def _fetch_route(self, route_id: str, params: dict = None) -> Optional[MBTARoute]:
|
|
304
|
-
"""
|
|
305
|
-
self.logger.debug(
|
|
323
|
+
"""Retrieve MBTA route based on route_id."""
|
|
324
|
+
self.logger.debug("Retrieving MBTA route: {}".format(route_id))
|
|
306
325
|
try:
|
|
307
326
|
route: MBTARoute = await self.mbta_client.get_route(route_id, params)
|
|
308
327
|
return route
|
|
328
|
+
except aiohttp.ClientError as e:
|
|
329
|
+
self.logger.error("HTTP error occurred while retrieving MBTA route {}: {}".format(route_id, e))
|
|
330
|
+
return None
|
|
309
331
|
except Exception as e:
|
|
310
|
-
self.logger.error(
|
|
332
|
+
self.logger.error("Error retrieving MBTA route {}: {}".format(route_id, e))
|
|
311
333
|
return None
|
|
312
334
|
|
|
313
335
|
@memoize_async()
|
|
314
336
|
async def _fetch_trips(self, params: dict = None) -> Optional[MBTARoute]:
|
|
315
|
-
"""
|
|
316
|
-
self.logger.debug("
|
|
337
|
+
"""Retrieve MBTA trips"""
|
|
338
|
+
self.logger.debug("Retrieving MBTA trips")
|
|
317
339
|
try:
|
|
318
340
|
trips: list[MBTATrip] = await self.mbta_client.list_trips(params)
|
|
319
341
|
return trips
|
|
342
|
+
except aiohttp.ClientError as e:
|
|
343
|
+
self.logger.error("HTTP error occurred while retrieving MBTA trips: {}".format(e))
|
|
344
|
+
return None
|
|
320
345
|
except Exception as e:
|
|
321
|
-
self.logger.error(
|
|
346
|
+
self.logger.error("Error retrieving MBTA trips: {}".format(e))
|
|
322
347
|
return None
|
|
323
|
-
|
|
324
348
|
|
|
325
349
|
class MBTAStopError(Exception):
|
|
326
350
|
pass
|
|
@@ -50,7 +50,7 @@ class JourneyStop:
|
|
|
50
50
|
self.real_departure_time = MBTAUtils.parse_datetime(departure_time)
|
|
51
51
|
if self.departure_time is not None:
|
|
52
52
|
self.departure_delay = MBTAUtils.calculate_time_difference(self.real_departure_time, self.departure_time)
|
|
53
|
-
|
|
53
|
+
|
|
54
54
|
def get_time(self) -> Optional[datetime]:
|
|
55
55
|
"""Return the most relevant time for the stop."""
|
|
56
56
|
if self.real_arrival_time is not None:
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import aiohttp
|
|
2
|
+
import logging
|
|
3
|
+
from datetime import datetime
|
|
4
|
+
|
|
5
|
+
from .base_handler import BaseHandler
|
|
6
|
+
from .journey import Journey
|
|
7
|
+
from .mbta_route import MBTARoute
|
|
8
|
+
from .mbta_trip import MBTATrip
|
|
9
|
+
from .mbta_schedule import MBTASchedule
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class JourneysHandler(BaseHandler):
|
|
13
|
+
"""Handler for managing a specific journey."""
|
|
14
|
+
|
|
15
|
+
def __init__(self, depart_from_name: str, arrive_at_name: str, max_journeys: int = 4, api_key: str = None, session: aiohttp.ClientSession = None, logger: logging.Logger = None):
|
|
16
|
+
super().__init__(depart_from_name=depart_from_name, arrive_at_name=arrive_at_name, api_key=api_key, session=session, logger=logger)
|
|
17
|
+
self.max_journeys = max_journeys
|
|
18
|
+
self.logger: logging.Logger = logger or logging.getLogger(__name__)
|
|
19
|
+
|
|
20
|
+
async def async_init(self):
|
|
21
|
+
try:
|
|
22
|
+
await super()._async_init()
|
|
23
|
+
except Exception as e:
|
|
24
|
+
self.logger.error("Error during async initialization: {}".format(e))
|
|
25
|
+
raise
|
|
26
|
+
|
|
27
|
+
async def update(self) -> list[Journey]:
|
|
28
|
+
try:
|
|
29
|
+
schedules = await self.__fetch_schedules()
|
|
30
|
+
await super()._process_schedules(schedules)
|
|
31
|
+
|
|
32
|
+
predictions = await self._fetch_predictions()
|
|
33
|
+
await super()._process_predictions(predictions)
|
|
34
|
+
|
|
35
|
+
self.__sort_and_clean()
|
|
36
|
+
|
|
37
|
+
await self.__fetch_trips()
|
|
38
|
+
|
|
39
|
+
await self.__fetch_routes()
|
|
40
|
+
|
|
41
|
+
alerts = await self._fetch_alerts()
|
|
42
|
+
super()._process_alerts(alerts)
|
|
43
|
+
|
|
44
|
+
return list(self.journeys.values())
|
|
45
|
+
except Exception as e:
|
|
46
|
+
self.logger.error("Error during update: {}".format(e))
|
|
47
|
+
raise
|
|
48
|
+
|
|
49
|
+
async def __fetch_schedules(self) -> list[MBTASchedule]:
|
|
50
|
+
try:
|
|
51
|
+
now = datetime.now().astimezone()
|
|
52
|
+
|
|
53
|
+
params = {
|
|
54
|
+
'filter[stop]': ','.join(super()._get_stops_ids()),
|
|
55
|
+
'filter[min_time]': now.strftime('%H:%M'),
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
schedules = await super()._fetch_schedules(params)
|
|
59
|
+
return schedules
|
|
60
|
+
except Exception as e:
|
|
61
|
+
self.logger.error("Error fetching schedules: {}".format(e))
|
|
62
|
+
raise
|
|
63
|
+
|
|
64
|
+
def __sort_and_clean(self):
|
|
65
|
+
try:
|
|
66
|
+
now = datetime.now().astimezone()
|
|
67
|
+
|
|
68
|
+
processed_journeys = {
|
|
69
|
+
trip_id: journey
|
|
70
|
+
for trip_id, journey in self.journeys.items()
|
|
71
|
+
if journey.stops['departure']
|
|
72
|
+
and journey.stops['arrival']
|
|
73
|
+
and journey.stops['departure'].stop_sequence < journey.stops['arrival'].stop_sequence
|
|
74
|
+
#and journey.stops['departure'].get_time() is not None
|
|
75
|
+
and journey.stops['departure'].get_time() >= now
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
sorted_journeys = dict(
|
|
79
|
+
sorted(
|
|
80
|
+
processed_journeys.items(),
|
|
81
|
+
key=lambda item: item[1].stops['departure'].get_time()
|
|
82
|
+
)
|
|
83
|
+
)
|
|
84
|
+
|
|
85
|
+
self.journeys = dict(list(sorted_journeys.items())[:self.max_journeys] if self.max_journeys > 0 else sorted_journeys)
|
|
86
|
+
|
|
87
|
+
except Exception as e:
|
|
88
|
+
self.logger.error("Error sorting and cleaning journeys: {}".format(e))
|
|
89
|
+
raise
|
|
90
|
+
|
|
91
|
+
async def __fetch_trips(self):
|
|
92
|
+
try:
|
|
93
|
+
for trip_id, journey in self.journeys.items():
|
|
94
|
+
trip: MBTATrip = await super()._fetch_trip(trip_id)
|
|
95
|
+
journey.trip = trip
|
|
96
|
+
except Exception as e:
|
|
97
|
+
self.logger.error("Error fetching trips: {}".format(e))
|
|
98
|
+
raise
|
|
99
|
+
|
|
100
|
+
async def __fetch_routes(self):
|
|
101
|
+
try:
|
|
102
|
+
for journey in self.journeys.values():
|
|
103
|
+
if journey.trip and journey.trip.route_id:
|
|
104
|
+
route: MBTARoute = await super()._fetch_route(journey.trip.route_id)
|
|
105
|
+
journey.route = route
|
|
106
|
+
except Exception as e:
|
|
107
|
+
self.logger.error("Error fetching routes: {}".format(e))
|
|
108
|
+
raise
|
|
@@ -29,6 +29,18 @@ class MBTAClient:
|
|
|
29
29
|
self.logger: logging.Logger = logger or logging.getLogger(__name__)
|
|
30
30
|
self._api_key: str = api_key
|
|
31
31
|
|
|
32
|
+
async def __aenter__(self):
|
|
33
|
+
"""Enter the context and return the client."""
|
|
34
|
+
return self
|
|
35
|
+
|
|
36
|
+
async def __aexit__(self, exc_type, exc, tb):
|
|
37
|
+
"""Exit the context and close the session."""
|
|
38
|
+
await self.close()
|
|
39
|
+
|
|
40
|
+
async def close(self) -> None:
|
|
41
|
+
"""Close the session manually."""
|
|
42
|
+
await self._session.close()
|
|
43
|
+
|
|
32
44
|
async def get_route(self, id: str, params: Optional[dict[str, Any]] = None) -> MBTARoute:
|
|
33
45
|
"""Get a route by its ID."""
|
|
34
46
|
route_data = await self._fetch_data(f'{ENDPOINTS["ROUTES"]}/{id}', params)
|
|
@@ -80,12 +92,12 @@ class MBTAClient:
|
|
|
80
92
|
response = await self.request("get", endpoint, params)
|
|
81
93
|
data = await response.json()
|
|
82
94
|
if 'data' not in data:
|
|
83
|
-
raise ValueError("
|
|
95
|
+
raise ValueError("missing 'data'")
|
|
84
96
|
return data
|
|
85
97
|
except Exception as error:
|
|
86
98
|
self.logger.error(f"Error fetching data: {error}")
|
|
87
99
|
raise
|
|
88
|
-
|
|
100
|
+
|
|
89
101
|
async def request(
|
|
90
102
|
self, method: str, path: str, params: Optional[dict[str, Any]] = None) -> aiohttp.ClientResponse:
|
|
91
103
|
"""Make an HTTP request with Optional query parameters and JSON body."""
|
|
@@ -1,12 +1,9 @@
|
|
|
1
1
|
from datetime import datetime, timedelta
|
|
2
|
-
|
|
3
2
|
from typing import Optional
|
|
4
|
-
from collections.abc import Hashable
|
|
5
3
|
import logging
|
|
6
4
|
|
|
7
5
|
# logging.basicConfig(level=logging.DEBUG)
|
|
8
|
-
|
|
9
|
-
|
|
6
|
+
logger = logging.getLogger(__name__)
|
|
10
7
|
|
|
11
8
|
class MBTAUtils:
|
|
12
9
|
|
|
@@ -40,7 +37,18 @@ class MBTAUtils:
|
|
|
40
37
|
@staticmethod
|
|
41
38
|
def time_to(time: Optional[datetime], now: datetime) -> Optional[float]:
|
|
42
39
|
if time is None:
|
|
40
|
+
logger.warning("time_to: Provided 'time' is None.")
|
|
43
41
|
return None
|
|
42
|
+
# Check if both datetime objects have timezone info
|
|
43
|
+
if time.tzinfo != now.tzinfo:
|
|
44
|
+
# Make both datetimes the same type: either both naive or both aware
|
|
45
|
+
if time.tzinfo is None and now.tzinfo is not None:
|
|
46
|
+
# Convert time to aware by using the timezone of `now`
|
|
47
|
+
time = time.replace(tzinfo=now.tzinfo)
|
|
48
|
+
elif time.tzinfo is not None and now.tzinfo is None:
|
|
49
|
+
# Convert now to naive by stripping timezone info
|
|
50
|
+
now = now.replace(tzinfo=None)
|
|
51
|
+
# Now perform the calculation
|
|
44
52
|
return (time - now).total_seconds()
|
|
45
53
|
|
|
46
54
|
@staticmethod
|
|
@@ -54,8 +62,11 @@ class MBTAUtils:
|
|
|
54
62
|
"""Parse a string in ISO 8601 format to a datetime object."""
|
|
55
63
|
if not isinstance(time_str, str):
|
|
56
64
|
return None
|
|
57
|
-
|
|
58
|
-
|
|
65
|
+
try:
|
|
66
|
+
return datetime.fromisoformat(time_str)
|
|
67
|
+
except ValueError as e:
|
|
68
|
+
logger.error(f"Error parsing datetime string: {e}")
|
|
69
|
+
return None
|
|
59
70
|
|
|
60
71
|
|
|
61
72
|
from datetime import datetime, timedelta
|
|
@@ -83,18 +94,22 @@ def memoize_async(expire_at_end_of_day=False):
|
|
|
83
94
|
|
|
84
95
|
if expire_at_end_of_day:
|
|
85
96
|
if timestamp.date() == current_time.date():
|
|
86
|
-
|
|
97
|
+
logger.debug(f"Cache hit for {func.__name__} with arguments {cache_key} at {current_time}")
|
|
87
98
|
return cached_result
|
|
88
99
|
else: # Expiration based on 30 days
|
|
89
100
|
if current_time - timestamp < timedelta(days=30):
|
|
90
|
-
|
|
101
|
+
logger.debug(f"Cache hit for {func.__name__} with arguments {cache_key} at {current_time}")
|
|
91
102
|
return cached_result
|
|
92
103
|
|
|
93
|
-
|
|
94
|
-
|
|
104
|
+
logger.debug(f"Cache miss for {func.__name__} with arguments {cache_key} at {current_time}")
|
|
105
|
+
try:
|
|
106
|
+
result = await func(*args)
|
|
107
|
+
except Exception as e:
|
|
108
|
+
logger.error(f"Error occurred while executing {func.__name__} with arguments {args}: {e}")
|
|
109
|
+
raise
|
|
95
110
|
cache[cache_key] = (result, current_time)
|
|
96
|
-
|
|
111
|
+
logger.debug(f"Cache updated for key: {cache_key} at {current_time}")
|
|
97
112
|
return result
|
|
98
113
|
|
|
99
114
|
return wrapper
|
|
100
|
-
return decorator
|
|
115
|
+
return decorator
|