MBTAclient 0.2.6__tar.gz → 0.2.7__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.7}/PKG-INFO +1 -1
- {mbtaclient-0.2.6 → mbtaclient-0.2.7}/pyproject.toml +1 -1
- {mbtaclient-0.2.6 → mbtaclient-0.2.7}/src/MBTAclient.egg-info/PKG-INFO +1 -1
- {mbtaclient-0.2.6 → mbtaclient-0.2.7}/src/mbtaclient/base_handler.py +69 -45
- mbtaclient-0.2.7/src/mbtaclient/journeys_handler.py +108 -0
- {mbtaclient-0.2.6 → mbtaclient-0.2.7}/src/mbtaclient/mbta_client.py +12 -0
- {mbtaclient-0.2.6 → mbtaclient-0.2.7}/src/mbtaclient/mbta_utils.py +17 -12
- mbtaclient-0.2.7/src/mbtaclient/trip_handler.py +115 -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.7}/LICENSE +0 -0
- {mbtaclient-0.2.6 → mbtaclient-0.2.7}/README.md +0 -0
- {mbtaclient-0.2.6 → mbtaclient-0.2.7}/setup.cfg +0 -0
- {mbtaclient-0.2.6 → mbtaclient-0.2.7}/src/MBTAclient.egg-info/SOURCES.txt +0 -0
- {mbtaclient-0.2.6 → mbtaclient-0.2.7}/src/MBTAclient.egg-info/dependency_links.txt +0 -0
- {mbtaclient-0.2.6 → mbtaclient-0.2.7}/src/MBTAclient.egg-info/requires.txt +0 -0
- {mbtaclient-0.2.6 → mbtaclient-0.2.7}/src/MBTAclient.egg-info/top_level.txt +0 -0
- {mbtaclient-0.2.6 → mbtaclient-0.2.7}/src/mbtaclient/__init__.py +0 -0
- {mbtaclient-0.2.6 → mbtaclient-0.2.7}/src/mbtaclient/__version__.py +0 -0
- {mbtaclient-0.2.6 → mbtaclient-0.2.7}/src/mbtaclient/journey.py +0 -0
- {mbtaclient-0.2.6 → mbtaclient-0.2.7}/src/mbtaclient/journey_stop.py +0 -0
- {mbtaclient-0.2.6 → mbtaclient-0.2.7}/src/mbtaclient/mbta_alert.py +0 -0
- {mbtaclient-0.2.6 → mbtaclient-0.2.7}/src/mbtaclient/mbta_prediction.py +0 -0
- {mbtaclient-0.2.6 → mbtaclient-0.2.7}/src/mbtaclient/mbta_route.py +0 -0
- {mbtaclient-0.2.6 → mbtaclient-0.2.7}/src/mbtaclient/mbta_schedule.py +0 -0
- {mbtaclient-0.2.6 → mbtaclient-0.2.7}/src/mbtaclient/mbta_stop.py +0 -0
- {mbtaclient-0.2.6 → mbtaclient-0.2.7}/src/mbtaclient/mbta_trip.py +0 -0
|
@@ -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
|
|
@@ -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)
|
|
@@ -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,6 +37,7 @@ 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
|
|
44
42
|
return (time - now).total_seconds()
|
|
45
43
|
|
|
@@ -54,8 +52,11 @@ class MBTAUtils:
|
|
|
54
52
|
"""Parse a string in ISO 8601 format to a datetime object."""
|
|
55
53
|
if not isinstance(time_str, str):
|
|
56
54
|
return None
|
|
57
|
-
|
|
58
|
-
|
|
55
|
+
try:
|
|
56
|
+
return datetime.fromisoformat(time_str)
|
|
57
|
+
except ValueError as e:
|
|
58
|
+
logger.error(f"Error parsing datetime string: {e}")
|
|
59
|
+
return None
|
|
59
60
|
|
|
60
61
|
|
|
61
62
|
from datetime import datetime, timedelta
|
|
@@ -83,18 +84,22 @@ def memoize_async(expire_at_end_of_day=False):
|
|
|
83
84
|
|
|
84
85
|
if expire_at_end_of_day:
|
|
85
86
|
if timestamp.date() == current_time.date():
|
|
86
|
-
|
|
87
|
+
logger.debug(f"Cache hit for {func.__name__} with arguments {cache_key} at {current_time}")
|
|
87
88
|
return cached_result
|
|
88
89
|
else: # Expiration based on 30 days
|
|
89
90
|
if current_time - timestamp < timedelta(days=30):
|
|
90
|
-
|
|
91
|
+
logger.debug(f"Cache hit for {func.__name__} with arguments {cache_key} at {current_time}")
|
|
91
92
|
return cached_result
|
|
92
93
|
|
|
93
|
-
|
|
94
|
-
|
|
94
|
+
logger.debug(f"Cache miss for {func.__name__} with arguments {cache_key} at {current_time}")
|
|
95
|
+
try:
|
|
96
|
+
result = await func(*args)
|
|
97
|
+
except Exception as e:
|
|
98
|
+
logger.error(f"Error occurred while executing {func.__name__} with arguments {args}: {e}")
|
|
99
|
+
raise
|
|
95
100
|
cache[cache_key] = (result, current_time)
|
|
96
|
-
|
|
101
|
+
logger.debug(f"Cache updated for key: {cache_key} at {current_time}")
|
|
97
102
|
return result
|
|
98
103
|
|
|
99
104
|
return wrapper
|
|
100
|
-
return decorator
|
|
105
|
+
return decorator
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import aiohttp
|
|
2
|
+
import logging
|
|
3
|
+
|
|
4
|
+
from datetime import datetime, timedelta
|
|
5
|
+
from .base_handler import BaseHandler, MBTATripError
|
|
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
|
+
class TripHandler(BaseHandler):
|
|
12
|
+
"""Handler for managing a specific trip."""
|
|
13
|
+
|
|
14
|
+
def __init__(self, depart_from_name: str, arrive_at_name: str, trip_name: str, api_key:str = None, session: aiohttp.ClientSession = None, logger: logging.Logger = None):
|
|
15
|
+
super().__init__(depart_from_name=depart_from_name, arrive_at_name=arrive_at_name, api_key=api_key, session=session, logger=logger)
|
|
16
|
+
self.trip_name = trip_name
|
|
17
|
+
self.logger: logging.Logger = logger or logging.getLogger(__name__)
|
|
18
|
+
|
|
19
|
+
async def async_init(self):
|
|
20
|
+
self.logger.debug("Initializing TripHandler")
|
|
21
|
+
try:
|
|
22
|
+
await super()._async_init()
|
|
23
|
+
|
|
24
|
+
self.logger.debug("Retrieving MBTA trip for {}".format(self.trip_name))
|
|
25
|
+
params = {
|
|
26
|
+
'filter[revenue]': 'REVENUE',
|
|
27
|
+
'filter[name]': self.trip_name
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
# Fetch trips and validate the response
|
|
31
|
+
trips: list[MBTATrip] = await super()._fetch_trips(params)
|
|
32
|
+
if not trips or not isinstance(trips, list) or not trips[0]:
|
|
33
|
+
self.logger.error("Error retrieving MBTA trip {}: Invalid trip name or response".format(self.trip_name))
|
|
34
|
+
raise MBTATripError("Invalid trip name")
|
|
35
|
+
|
|
36
|
+
# Create a new journey and assign the first trip
|
|
37
|
+
journey = Journey()
|
|
38
|
+
journey.trip = trips[0]
|
|
39
|
+
|
|
40
|
+
self.logger.debug("Retrieving MBTA route for trip {}".format(self.trip_name))
|
|
41
|
+
route: MBTARoute = await super()._fetch_route(journey.trip.route_id)
|
|
42
|
+
if route is None:
|
|
43
|
+
self.logger.error("Error retrieving MBTA route for trip {}".format(self.trip_name))
|
|
44
|
+
raise MBTATripError("Invalid route")
|
|
45
|
+
|
|
46
|
+
journey.route = route
|
|
47
|
+
self.journeys[trips[0].id] = journey
|
|
48
|
+
self.logger.info("Trip {} initialized successfully with route {}".format(self.trip_name, journey.route.id))
|
|
49
|
+
|
|
50
|
+
except Exception as e:
|
|
51
|
+
self.logger.error("Error during TripHandler initialization: {}".format(e))
|
|
52
|
+
|
|
53
|
+
async def update(self) -> list[Journey]:
|
|
54
|
+
now = datetime.now().astimezone()
|
|
55
|
+
self.logger.debug("Updating trips for {}".format(self.trip_name))
|
|
56
|
+
|
|
57
|
+
try:
|
|
58
|
+
for i in range(7):
|
|
59
|
+
params = {}
|
|
60
|
+
date_to_try = (now + timedelta(days=i)).strftime('%Y-%m-%d')
|
|
61
|
+
params['filter[date]'] = date_to_try
|
|
62
|
+
if i == 0:
|
|
63
|
+
params['filter[min_time]'] = now.strftime('%H:%M')
|
|
64
|
+
|
|
65
|
+
self.logger.debug("Fetching schedules for {} for trip {}".format(date_to_try, self.trip_name))
|
|
66
|
+
schedules = await self.__fetch_schedules(params)
|
|
67
|
+
await super()._process_schedules(schedules)
|
|
68
|
+
|
|
69
|
+
# Check for valid schedules
|
|
70
|
+
if next(iter(self.journeys.values())).get_stop_time_to('arrival') is not None:
|
|
71
|
+
self.logger.info("Valid schedule found for trip {} on {}".format(self.trip_name, date_to_try))
|
|
72
|
+
break
|
|
73
|
+
|
|
74
|
+
# Log an error if no valid schedules after the final attempt
|
|
75
|
+
if i == 6:
|
|
76
|
+
self.logger.error("Error retrieving scheduling for {} and {} on trip {}".format(self.depart_from['name'], self.arrive_at['name'], self.trip_name))
|
|
77
|
+
raise MBTATripError("Invalid stops for the trip")
|
|
78
|
+
|
|
79
|
+
except MBTATripError as e:
|
|
80
|
+
self.logger.error("{}".format(e))
|
|
81
|
+
|
|
82
|
+
# Fetch predictions and alerts
|
|
83
|
+
try:
|
|
84
|
+
self.logger.debug("Fetching predictions for trip {}".format(self.trip_name))
|
|
85
|
+
predictions = await self.__fetch_predictions()
|
|
86
|
+
await super()._process_predictions(predictions)
|
|
87
|
+
|
|
88
|
+
self.logger.debug("Fetching alerts for trip {}".format(self.trip_name))
|
|
89
|
+
alerts = await self.__fetch_alerts()
|
|
90
|
+
super()._process_alerts(alerts)
|
|
91
|
+
|
|
92
|
+
except Exception as e:
|
|
93
|
+
self.logger.error("Error during predictions/alerts fetching for trip {}: {}".format(self.trip_name, e))
|
|
94
|
+
|
|
95
|
+
return list(self.journeys.values())
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
async def __fetch_schedules(self, params: dict) -> list[MBTASchedule]:
|
|
99
|
+
self.logger.debug("Fetching schedules with params: {}".format(params))
|
|
100
|
+
journey = next(iter(self.journeys.values()))
|
|
101
|
+
trip_id = journey.trip.id
|
|
102
|
+
|
|
103
|
+
base_params = {
|
|
104
|
+
'filter[trip]': trip_id,
|
|
105
|
+
}
|
|
106
|
+
if params is not None:
|
|
107
|
+
base_params.update(params)
|
|
108
|
+
|
|
109
|
+
try:
|
|
110
|
+
schedules = await super()._fetch_schedules(base_params)
|
|
111
|
+
self.logger.debug("Fetched {} schedules for trip {}".format(len(schedules), trip_id))
|
|
112
|
+
return schedules
|
|
113
|
+
except Exception as e:
|
|
114
|
+
self.logger.error("Error fetching schedules for trip {}: {}".format(trip_id, e))
|
|
115
|
+
|
|
@@ -1,89 +0,0 @@
|
|
|
1
|
-
import aiohttp
|
|
2
|
-
import logging
|
|
3
|
-
|
|
4
|
-
from datetime import datetime
|
|
5
|
-
|
|
6
|
-
from .base_handler import BaseHandler
|
|
7
|
-
from .journey import Journey
|
|
8
|
-
from .mbta_route import MBTARoute
|
|
9
|
-
from .mbta_trip import MBTATrip
|
|
10
|
-
from .mbta_schedule import MBTASchedule
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
class JourneysHandler(BaseHandler):
|
|
14
|
-
"""Handler for managing a specific journey."""
|
|
15
|
-
|
|
16
|
-
def __init__(self, depart_from_name: str, arrive_at_name: str, max_journeys: int, api_key:str = None, session: aiohttp.ClientSession = None, logger: logging.Logger = None):
|
|
17
|
-
super().__init__(depart_from_name=depart_from_name, arrive_at_name=arrive_at_name, api_key=api_key, session=session, logger=logger)
|
|
18
|
-
self.max_journeys = max_journeys
|
|
19
|
-
|
|
20
|
-
async def async_init(self):
|
|
21
|
-
await super()._async_init()
|
|
22
|
-
|
|
23
|
-
async def update(self) -> list[Journey]:
|
|
24
|
-
|
|
25
|
-
schedules = await self.__fetch_schedules()
|
|
26
|
-
await super()._process_schedules(schedules)
|
|
27
|
-
|
|
28
|
-
predictions = await self._fetch_predictions()
|
|
29
|
-
await super()._process_predictions(predictions)
|
|
30
|
-
|
|
31
|
-
self.__sort_and_clean()
|
|
32
|
-
|
|
33
|
-
await self.__fetch_trips()
|
|
34
|
-
|
|
35
|
-
await self.__fetch_routes()
|
|
36
|
-
|
|
37
|
-
alerts = await self._fetch_alerts()
|
|
38
|
-
super()._process_alerts(alerts)
|
|
39
|
-
|
|
40
|
-
return list(self.journeys.values())
|
|
41
|
-
|
|
42
|
-
async def __fetch_schedules(self) -> list[MBTASchedule]:
|
|
43
|
-
|
|
44
|
-
now = datetime.now().astimezone()
|
|
45
|
-
|
|
46
|
-
params = {
|
|
47
|
-
'filter[stop]': ','.join(super()._get_stops_ids()),
|
|
48
|
-
'filter[min_time]': now.strftime('%H:%M'),
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
schedules = await super()._fetch_schedules(params)
|
|
52
|
-
|
|
53
|
-
return schedules
|
|
54
|
-
|
|
55
|
-
def __sort_and_clean(self):
|
|
56
|
-
"""Clean up and sort valid journeys."""
|
|
57
|
-
|
|
58
|
-
now = datetime.now().astimezone()
|
|
59
|
-
|
|
60
|
-
processed_journeys = {
|
|
61
|
-
trip_id: journey
|
|
62
|
-
for trip_id, journey in self.journeys.items()
|
|
63
|
-
if journey.stops['departure']
|
|
64
|
-
and journey.stops['arrival']
|
|
65
|
-
and journey.stops['departure'].stop_sequence < journey.stops['arrival'].stop_sequence
|
|
66
|
-
and journey.stops['departure'].get_time() >= now
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
sorted_journeys = dict(
|
|
70
|
-
sorted(
|
|
71
|
-
processed_journeys.items(),
|
|
72
|
-
key=lambda item: item[1].stops['departure'].get_time()
|
|
73
|
-
)
|
|
74
|
-
)
|
|
75
|
-
|
|
76
|
-
self.journeys = dict(list(sorted_journeys.items())[:self.max_journeys] if self.max_journeys > 0 else sorted_journeys)
|
|
77
|
-
|
|
78
|
-
async def __fetch_trips(self):
|
|
79
|
-
"""Retrieve trip details for each journey."""
|
|
80
|
-
for trip_id, journey in self.journeys.items():
|
|
81
|
-
trip: MBTATrip = await super()._fetch_trip(trip_id)
|
|
82
|
-
journey.trip = trip
|
|
83
|
-
|
|
84
|
-
async def __fetch_routes(self):
|
|
85
|
-
"""Retrieve route details for each journey."""
|
|
86
|
-
for journey in self.journeys.values():
|
|
87
|
-
if journey.trip and journey.trip.route_id:
|
|
88
|
-
route: MBTARoute = await super()._fetch_route(journey.trip.route_id)
|
|
89
|
-
journey.route = route
|
|
@@ -1,124 +0,0 @@
|
|
|
1
|
-
import aiohttp
|
|
2
|
-
import logging
|
|
3
|
-
|
|
4
|
-
from datetime import datetime, timedelta
|
|
5
|
-
from .base_handler import BaseHandler, MBTATripError
|
|
6
|
-
from .journey import Journey
|
|
7
|
-
from .mbta_route import MBTARoute
|
|
8
|
-
from .mbta_trip import MBTATrip
|
|
9
|
-
from .mbta_schedule import MBTASchedule
|
|
10
|
-
from .mbta_prediction import MBTAPrediction
|
|
11
|
-
|
|
12
|
-
class TripHandler(BaseHandler):
|
|
13
|
-
"""Handler for managing a specific trip."""
|
|
14
|
-
|
|
15
|
-
def __init__(self, depart_from_name: str, arrive_at_name: str, trip_name: str, 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.trip_name = trip_name
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
async def async_init(self):
|
|
21
|
-
self.logger.debug("Initializing TripHandler")
|
|
22
|
-
try:
|
|
23
|
-
await super()._async_init()
|
|
24
|
-
|
|
25
|
-
self.logger.debug("Retriving MBTA trip")
|
|
26
|
-
params = {
|
|
27
|
-
'filter[revenue]': 'REVENUE',
|
|
28
|
-
'filter[name]': self.trip_name
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
# Fetch trips and validate the response
|
|
32
|
-
trips: list[MBTATrip] = await super()._fetch_trips(params)
|
|
33
|
-
if not trips or not isinstance(trips, list) or not trips[0]:
|
|
34
|
-
self.logger.error(f"Error retriving MBTA trip {self.trip_name}")
|
|
35
|
-
raise MBTATripError("Invalid trip name")
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
# Create a new journey and assign the first trip
|
|
39
|
-
journey = Journey()
|
|
40
|
-
journey.trip = trips[0]
|
|
41
|
-
|
|
42
|
-
# Fetch route and validate the response
|
|
43
|
-
self.logger.debug("Retriving MBTA route")
|
|
44
|
-
route: MBTARoute = await super()._fetch_route(journey.trip.route_id)
|
|
45
|
-
|
|
46
|
-
journey.route = route
|
|
47
|
-
self.journeys[trips[0].id] = journey
|
|
48
|
-
|
|
49
|
-
except Exception as e:
|
|
50
|
-
self.logger.error(f"Error during TripHandler initialization: {e}")
|
|
51
|
-
|
|
52
|
-
async def update(self) -> list[Journey]:
|
|
53
|
-
now = datetime.now().astimezone()
|
|
54
|
-
|
|
55
|
-
try:
|
|
56
|
-
for i in range(7):
|
|
57
|
-
params = {}
|
|
58
|
-
# Calculate the date for each attempt (i days after today)
|
|
59
|
-
date_to_try = (now + timedelta(days=i)).strftime('%Y-%m-%d')
|
|
60
|
-
params['filter[date]'] = date_to_try
|
|
61
|
-
if i == 0:
|
|
62
|
-
params['filter[min_time]'] = now.strftime('%H:%M')
|
|
63
|
-
|
|
64
|
-
# Attempt to get schedules for up to the next 7 days
|
|
65
|
-
schedules = await self.__fetch_schedules(params)
|
|
66
|
-
await super()._process_schedules(schedules)
|
|
67
|
-
if next(iter(self.journeys.values())).get_stop_time_to('arrival') is not None:
|
|
68
|
-
break
|
|
69
|
-
|
|
70
|
-
# If it's the last attempt and no valid schedules were found, log an error and raise an exception
|
|
71
|
-
if i == 6:
|
|
72
|
-
self.logger.error(
|
|
73
|
-
f"Error retrieving scheduling for {self.depart_from['name']} and {self.arrive_at['name']} on trip {self.trip_name}"
|
|
74
|
-
)
|
|
75
|
-
raise MBTATripError("Invalid stops for the trip")
|
|
76
|
-
|
|
77
|
-
except MBTATripError as e:
|
|
78
|
-
# Handle the error here without re-raising it
|
|
79
|
-
self.logger.error(f"{e}")
|
|
80
|
-
# Continue with other operations despite the failure
|
|
81
|
-
|
|
82
|
-
predictions = await self.__fetch_predictions()
|
|
83
|
-
await super()._process_predictions(predictions)
|
|
84
|
-
|
|
85
|
-
alerts = await self.__fetch_alerts()
|
|
86
|
-
super()._process_alerts(alerts)
|
|
87
|
-
return list(self.journeys.values())
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
async def __fetch_schedules(self, params: dict) -> list[MBTASchedule]:
|
|
91
|
-
journey = next(iter(self.journeys.values()))
|
|
92
|
-
trip_id = journey.trip.id
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
base_params = {
|
|
96
|
-
'filter[trip]': trip_id,
|
|
97
|
-
}
|
|
98
|
-
if params is not None:
|
|
99
|
-
base_params.update(params)
|
|
100
|
-
|
|
101
|
-
schedules = await super()._fetch_schedules(base_params)
|
|
102
|
-
return schedules
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
async def __fetch_predictions(self) -> list[MBTAPrediction]:
|
|
106
|
-
jounrey = next(iter(self.journeys.values()))
|
|
107
|
-
jounrey.trip.id
|
|
108
|
-
params = {
|
|
109
|
-
'filter[trip]': jounrey.trip.id,
|
|
110
|
-
}
|
|
111
|
-
predictions = await super()._fetch_predictions(params)
|
|
112
|
-
return predictions
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
async def __fetch_alerts(self) -> list[MBTAPrediction]:
|
|
116
|
-
jounrey = next(iter(self.journeys.values()))
|
|
117
|
-
jounrey.trip.id
|
|
118
|
-
params = {
|
|
119
|
-
'filter[trip]': jounrey.trip.id,
|
|
120
|
-
}
|
|
121
|
-
alerts = await super()._fetch_alerts(params)
|
|
122
|
-
return alerts
|
|
123
|
-
|
|
124
|
-
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|