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.
Files changed (36) hide show
  1. {mbtaclient-0.2.6 → mbtaclient-0.2.8}/PKG-INFO +2 -2
  2. {mbtaclient-0.2.6 → mbtaclient-0.2.8}/pyproject.toml +2 -2
  3. {mbtaclient-0.2.6 → mbtaclient-0.2.8}/src/MBTAclient.egg-info/PKG-INFO +2 -2
  4. {mbtaclient-0.2.6 → mbtaclient-0.2.8}/src/MBTAclient.egg-info/SOURCES.txt +10 -1
  5. {mbtaclient-0.2.6 → mbtaclient-0.2.8}/src/mbtaclient/base_handler.py +69 -45
  6. {mbtaclient-0.2.6 → mbtaclient-0.2.8}/src/mbtaclient/journey_stop.py +1 -1
  7. mbtaclient-0.2.8/src/mbtaclient/journeys_handler.py +108 -0
  8. {mbtaclient-0.2.6 → mbtaclient-0.2.8}/src/mbtaclient/mbta_client.py +14 -2
  9. {mbtaclient-0.2.6 → mbtaclient-0.2.8}/src/mbtaclient/mbta_utils.py +27 -12
  10. mbtaclient-0.2.8/src/mbtaclient/trip_handler.py +115 -0
  11. mbtaclient-0.2.8/tests/test_journey_stop.py +167 -0
  12. mbtaclient-0.2.8/tests/test_mbta_alert.py +139 -0
  13. mbtaclient-0.2.8/tests/test_mbta_client.py +109 -0
  14. mbtaclient-0.2.8/tests/test_mbta_prediction.py +97 -0
  15. mbtaclient-0.2.8/tests/test_mbta_route.py +58 -0
  16. mbtaclient-0.2.8/tests/test_mbta_schedule.py +88 -0
  17. mbtaclient-0.2.8/tests/test_mbta_stop.py +68 -0
  18. mbtaclient-0.2.8/tests/test_mbta_trip.py +68 -0
  19. mbtaclient-0.2.8/tests/test_mbta_utils.py +126 -0
  20. mbtaclient-0.2.6/src/mbtaclient/journeys_handler.py +0 -89
  21. mbtaclient-0.2.6/src/mbtaclient/trip_handler.py +0 -124
  22. {mbtaclient-0.2.6 → mbtaclient-0.2.8}/LICENSE +0 -0
  23. {mbtaclient-0.2.6 → mbtaclient-0.2.8}/README.md +0 -0
  24. {mbtaclient-0.2.6 → mbtaclient-0.2.8}/setup.cfg +0 -0
  25. {mbtaclient-0.2.6 → mbtaclient-0.2.8}/src/MBTAclient.egg-info/dependency_links.txt +0 -0
  26. {mbtaclient-0.2.6 → mbtaclient-0.2.8}/src/MBTAclient.egg-info/requires.txt +0 -0
  27. {mbtaclient-0.2.6 → mbtaclient-0.2.8}/src/MBTAclient.egg-info/top_level.txt +0 -0
  28. {mbtaclient-0.2.6 → mbtaclient-0.2.8}/src/mbtaclient/__init__.py +0 -0
  29. {mbtaclient-0.2.6 → mbtaclient-0.2.8}/src/mbtaclient/__version__.py +0 -0
  30. {mbtaclient-0.2.6 → mbtaclient-0.2.8}/src/mbtaclient/journey.py +0 -0
  31. {mbtaclient-0.2.6 → mbtaclient-0.2.8}/src/mbtaclient/mbta_alert.py +0 -0
  32. {mbtaclient-0.2.6 → mbtaclient-0.2.8}/src/mbtaclient/mbta_prediction.py +0 -0
  33. {mbtaclient-0.2.6 → mbtaclient-0.2.8}/src/mbtaclient/mbta_route.py +0 -0
  34. {mbtaclient-0.2.6 → mbtaclient-0.2.8}/src/mbtaclient/mbta_schedule.py +0 -0
  35. {mbtaclient-0.2.6 → mbtaclient-0.2.8}/src/mbtaclient/mbta_stop.py +0 -0
  36. {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.6
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.9
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.6"
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.9",
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.6
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.9
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 (f"BaseHandler(depart_from_name={self.depart_from['name']}, arrive_at_name={self.arrive_at['name']})")
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
- """Retrive stops """
51
- self.logger.debug("Retriving MBTA stops")
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(f"Error retriving MBTA stops: {e}")
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(f"Unexpected data type for stop: {type(stop)}")
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(f"Error processing MBTA stop data for {self.depart_from['name']}")
84
- raise MBTAStopError(f"Invalid stop name: {self.depart_from['name']}")
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(f"Error processing MBTA stop data for {self.arrive_at['name']}")
88
- raise MBTAStopError(f"Invalid stop name: {self.arrive_at['name']}")
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 self.depart_from['ids'] + self.arrive_at['ids']
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
- """Retrive MBTA schedules"""
114
- self.logger.debug("Retriving MBTA schedules")
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(f"Error retriving MBTA schedules: {e}")
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(f"Invalid schedule data: {schedule}")
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(f"Stop {schedule.stop_id} of schedule {schedule.id} doesn't belong to the journey stop ids")
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(f"Stop ID {schedule.stop_id} is not categorized as departure or arrival for schedule: {schedule}")
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
- """Retrive MBTA predictions based on the provided stop IDs"""
165
- self.logger.debug("Retriving MBTA predictions")
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(f"Error retriving MBTA predictions: {e}")
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(f"Invalid prediction data: {prediction}")
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(f"Invalid stop ID: {prediction.stop_id} for prediction: {prediction}")
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(f"Stop ID {prediction.stop_id} is not categorized as departure or arrival for prediction: {prediction}")
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
- """Retrive MBTA alerts"""
220
- self.logger.debug("Retriving MBTA alerts")
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(f"Error retriving MBTA alerts: {e}")
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(f"Invalid alert data: {alert}")
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
- # Check if the alert is already associated by comparing IDs
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(f"Error processing MBTA alert {alert.id}: {e}")
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(f"Retriving MBTA trip: {trip_id} ")
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(f"Error fetching trip {trip_id}: {e}")
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
- """Retrive MBTA route based on route_id."""
305
- self.logger.debug(f"Retriving MBTA route: {route_id} ")
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(f"Error retriving MBTA route {route_id}: {e}")
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
- """Retrive MBTA trips"""
316
- self.logger.debug("Retriving MBTA trips")
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(f"Error retriving MBTA route: {e}")
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("Unexpected response format")
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
- _LOGGER = logging.getLogger(__name__)
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
- return datetime.fromisoformat(time_str)
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
- _LOGGER.debug(f"Cache hit for {func.__name__} with arguments {cache_key} at {current_time}")
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
- _LOGGER.debug(f"Cache hit for {func.__name__} with arguments {cache_key} at {current_time}")
101
+ logger.debug(f"Cache hit for {func.__name__} with arguments {cache_key} at {current_time}")
91
102
  return cached_result
92
103
 
93
- _LOGGER.debug(f"Cache miss for {func.__name__} with arguments {cache_key} at {current_time}")
94
- result = await func(*args)
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
- _LOGGER.debug(f"Cache updated for key: {cache_key} at {current_time}")
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