MBTAclient 0.2.5__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.
Files changed (29) hide show
  1. {mbtaclient-0.2.5 → mbtaclient-0.2.7}/PKG-INFO +6 -1
  2. {mbtaclient-0.2.5 → mbtaclient-0.2.7}/pyproject.toml +9 -2
  3. {mbtaclient-0.2.5 → mbtaclient-0.2.7}/src/MBTAclient.egg-info/PKG-INFO +6 -1
  4. mbtaclient-0.2.7/src/mbtaclient/base_handler.py +353 -0
  5. {mbtaclient-0.2.5 → mbtaclient-0.2.7}/src/mbtaclient/journey.py +8 -9
  6. {mbtaclient-0.2.5 → mbtaclient-0.2.7}/src/mbtaclient/journey_stop.py +2 -2
  7. mbtaclient-0.2.7/src/mbtaclient/journeys_handler.py +108 -0
  8. {mbtaclient-0.2.5 → mbtaclient-0.2.7}/src/mbtaclient/mbta_alert.py +1 -1
  9. {mbtaclient-0.2.5 → mbtaclient-0.2.7}/src/mbtaclient/mbta_client.py +23 -10
  10. {mbtaclient-0.2.5 → mbtaclient-0.2.7}/src/mbtaclient/mbta_prediction.py +1 -1
  11. mbtaclient-0.2.7/src/mbtaclient/mbta_utils.py +105 -0
  12. mbtaclient-0.2.7/src/mbtaclient/trip_handler.py +115 -0
  13. mbtaclient-0.2.5/src/mbtaclient/base_handler.py +0 -335
  14. mbtaclient-0.2.5/src/mbtaclient/journeys_handler.py +0 -89
  15. mbtaclient-0.2.5/src/mbtaclient/mbta_utils.py +0 -50
  16. mbtaclient-0.2.5/src/mbtaclient/trip_handler.py +0 -85
  17. {mbtaclient-0.2.5 → mbtaclient-0.2.7}/LICENSE +0 -0
  18. {mbtaclient-0.2.5 → mbtaclient-0.2.7}/README.md +0 -0
  19. {mbtaclient-0.2.5 → mbtaclient-0.2.7}/setup.cfg +0 -0
  20. {mbtaclient-0.2.5 → mbtaclient-0.2.7}/src/MBTAclient.egg-info/SOURCES.txt +0 -0
  21. {mbtaclient-0.2.5 → mbtaclient-0.2.7}/src/MBTAclient.egg-info/dependency_links.txt +0 -0
  22. {mbtaclient-0.2.5 → mbtaclient-0.2.7}/src/MBTAclient.egg-info/requires.txt +0 -0
  23. {mbtaclient-0.2.5 → mbtaclient-0.2.7}/src/MBTAclient.egg-info/top_level.txt +0 -0
  24. {mbtaclient-0.2.5 → mbtaclient-0.2.7}/src/mbtaclient/__init__.py +0 -0
  25. {mbtaclient-0.2.5 → mbtaclient-0.2.7}/src/mbtaclient/__version__.py +0 -0
  26. {mbtaclient-0.2.5 → mbtaclient-0.2.7}/src/mbtaclient/mbta_route.py +0 -0
  27. {mbtaclient-0.2.5 → mbtaclient-0.2.7}/src/mbtaclient/mbta_schedule.py +0 -0
  28. {mbtaclient-0.2.5 → mbtaclient-0.2.7}/src/mbtaclient/mbta_stop.py +0 -0
  29. {mbtaclient-0.2.5 → mbtaclient-0.2.7}/src/mbtaclient/mbta_trip.py +0 -0
@@ -1,10 +1,15 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: MBTAclient
3
- Version: 0.2.5
3
+ Version: 0.2.7
4
4
  Summary: A Python client for interacting with the MBTA API
5
5
  Author-email: Luca Chiabrera <luca.chiabrera@gmail.com>
6
+ License: MIT
6
7
  Project-URL: Homepage, https://github.com/chiabre/MBTAclient
7
8
  Project-URL: Issues, https://github.com/chiabre/MBTAclient/issues
9
+ Classifier: Programming Language :: Python :: 3
10
+ Classifier: Programming Language :: Python :: 3.9
11
+ Classifier: License :: OSI Approved :: MIT License
12
+ Classifier: Operating System :: OS Independent
8
13
  Requires-Python: >=3.12
9
14
  Description-Content-Type: text/markdown
10
15
  License-File: LICENSE
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "MBTAclient"
7
- version = "0.2.5"
7
+ version = "0.2.7"
8
8
  description = "A Python client for interacting with the MBTA API"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.12"
@@ -12,6 +12,14 @@ dependencies = [
12
12
  "aiohttp>=3.9.5"
13
13
  ]
14
14
 
15
+ license = { text = "MIT" }
16
+ classifiers = [
17
+ "Programming Language :: Python :: 3",
18
+ "Programming Language :: Python :: 3.9",
19
+ "License :: OSI Approved :: MIT License",
20
+ "Operating System :: OS Independent"
21
+ ]
22
+
15
23
  [[project.authors]]
16
24
  name = "Luca Chiabrera"
17
25
  email = "luca.chiabrera@gmail.com"
@@ -20,6 +28,5 @@ email = "luca.chiabrera@gmail.com"
20
28
  Homepage = "https://github.com/chiabre/MBTAclient"
21
29
  Issues = "https://github.com/chiabre/MBTAclient/issues"
22
30
 
23
-
24
31
  [tool.setuptools.packages.find]
25
32
  where = ["src"]
@@ -1,10 +1,15 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: MBTAclient
3
- Version: 0.2.5
3
+ Version: 0.2.7
4
4
  Summary: A Python client for interacting with the MBTA API
5
5
  Author-email: Luca Chiabrera <luca.chiabrera@gmail.com>
6
+ License: MIT
6
7
  Project-URL: Homepage, https://github.com/chiabre/MBTAclient
7
8
  Project-URL: Issues, https://github.com/chiabre/MBTAclient/issues
9
+ Classifier: Programming Language :: Python :: 3
10
+ Classifier: Programming Language :: Python :: 3.9
11
+ Classifier: License :: OSI Approved :: MIT License
12
+ Classifier: Operating System :: OS Independent
8
13
  Requires-Python: >=3.12
9
14
  Description-Content-Type: text/markdown
10
15
  License-File: LICENSE
@@ -0,0 +1,353 @@
1
+ import logging
2
+ import aiohttp
3
+
4
+ from typing import Optional
5
+
6
+ from .mbta_client import MBTAClient
7
+ from .journey import Journey
8
+ from .mbta_stop import MBTAStop
9
+ from .mbta_route import MBTARoute
10
+ from .mbta_schedule import MBTASchedule
11
+ from .mbta_prediction import MBTAPrediction
12
+ from .mbta_trip import MBTATrip
13
+ from .mbta_alert import MBTAAlert
14
+ from .mbta_utils import memoize_async
15
+
16
+
17
+ class BaseHandler:
18
+ """Base class for handling MBTA journeys."""
19
+
20
+ def __init__(self, depart_from_name: str , arrive_at_name: str, api_key: str = None, session: aiohttp.ClientSession = None, logger: logging.Logger = None) -> None:
21
+
22
+ self.depart_from = {
23
+ 'name' : depart_from_name,
24
+ 'stops' : None,
25
+ 'ids' : None
26
+ }
27
+ self.arrive_at = {
28
+ 'name' : arrive_at_name,
29
+ 'stops' : None,
30
+ 'ids' : None
31
+ }
32
+
33
+ client_session = session or aiohttp.ClientSession()
34
+ self.mbta_client = MBTAClient(client_session, logger, api_key)
35
+
36
+ self.journeys: dict[str, Journey] = {}
37
+
38
+ self.logger: logging.Logger = logger or logging.getLogger(__name__)
39
+
40
+ def __repr__(self) -> str:
41
+ return "BaseHandler(depart_from_name={}, arrive_at_name={})".format(self.depart_from['name'], self.arrive_at['name'])
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()
51
+
52
+ async def _async_init(self):
53
+ stops = await self.__fetch_stops()
54
+ self.__process_stops(stops)
55
+
56
+ @memoize_async()
57
+ async def __fetch_stops(self, params: dict = None) -> list[MBTAStop]:
58
+ """Retrieve stops."""
59
+ self.logger.debug("Retrieving MBTA stops")
60
+ base_params = {'filter[location_type]': '0'}
61
+ if params is not None:
62
+ base_params.update(params)
63
+ try:
64
+ stops: list[MBTAStop] = await self.mbta_client.list_stops(base_params)
65
+ return stops
66
+ except aiohttp.ClientError as e:
67
+ self.logger.error("HTTP error occurred while retrieving MBTA stops: {}".format(e))
68
+ return []
69
+ except Exception as e:
70
+ self.logger.error("Error retrieving MBTA stops: {}".format(e))
71
+ return []
72
+
73
+ def __process_stops(self, stops: list[MBTAStop]):
74
+ self.logger.debug("Processing MBTA stops")
75
+ depart_from_stops = []
76
+ depart_from_ids = []
77
+ arrive_at_stops = []
78
+ arrive_at_ids = []
79
+
80
+ for stop in stops:
81
+ if not isinstance(stop, MBTAStop): # Validate data type
82
+ self.logger.warning("Unexpected data type for stop: {}".format(type(stop)))
83
+ continue # Skip invalid data
84
+
85
+ if stop.name.lower() == self.depart_from['name'].lower():
86
+ depart_from_stops.append(stop)
87
+ depart_from_ids.append(stop.id)
88
+
89
+ if stop.name.lower() == self.arrive_at['name'].lower():
90
+ arrive_at_stops.append(stop)
91
+ arrive_at_ids.append(stop.id)
92
+
93
+ if len(depart_from_stops) == 0:
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']))
96
+
97
+ if len(arrive_at_stops) == 0:
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']))
100
+
101
+ self.depart_from['stops'] = depart_from_stops
102
+ self.depart_from['ids'] = depart_from_ids
103
+ self.arrive_at['stops'] = arrive_at_stops
104
+ self.arrive_at['ids'] = arrive_at_ids
105
+
106
+ def __get_stop_by_id(self, stop_id: str) -> Optional[MBTAStop]:
107
+ for stop in (self.depart_from['stops'] + self.arrive_at['stops']):
108
+ if stop.id == stop_id:
109
+ return stop
110
+ return None
111
+
112
+ def _get_stops_ids(self) -> list[str]:
113
+ return self.depart_from['ids'] + self.arrive_at['ids']
114
+
115
+ def __get_stops_ids_by_stop_type(self, stop_type: str) -> Optional[list[str]]:
116
+ if stop_type == 'departure':
117
+ return self.depart_from['ids']
118
+ elif stop_type == 'arrival':
119
+ return self.arrive_at['ids']
120
+ return None
121
+
122
+ @memoize_async(expire_at_end_of_day=True)
123
+ async def _fetch_schedules(self, params: Optional[dict] = None) -> list[MBTASchedule]:
124
+ """Retrieve MBTA schedules"""
125
+ self.logger.debug("Retrieving MBTA schedules")
126
+ base_params = {
127
+ 'filter[stop]': ','.join(self._get_stops_ids()),
128
+ 'sort': 'departure_time'
129
+ }
130
+ if params is not None:
131
+ base_params.update(params)
132
+ try:
133
+ schedules: list[MBTASchedule] = await self.mbta_client.list_schedules(params)
134
+ return schedules
135
+ except aiohttp.ClientError as e:
136
+ self.logger.error("HTTP error occurred while retrieving MBTA schedules: {}".format(e))
137
+ return []
138
+ except Exception as e:
139
+ self.logger.error("Error retrieving MBTA schedules: {}".format(e))
140
+ return []
141
+
142
+ async def _process_schedules(self, schedules: list[MBTASchedule]):
143
+ self.logger.debug("Processing MBTA schedules")
144
+
145
+ for schedule in schedules:
146
+ # Validate schedule data
147
+ if not schedule.trip_id or not schedule.stop_id:
148
+ self.logger.error("Invalid schedule data: {}".format(schedule))
149
+ continue # Skip to the next schedule
150
+
151
+ # If the schedule trip_id is not in the journeys
152
+ if schedule.trip_id not in self.journeys:
153
+ # Create the journey
154
+ journey = Journey()
155
+ # Add the journey to the journeys dict using the trip_id as key
156
+ self.journeys[schedule.trip_id] = journey
157
+
158
+ # Validate stop
159
+ stop = self.__get_stop_by_id(schedule.stop_id)
160
+ if not stop:
161
+ self.logger.debug("Stop {} of schedule {} doesn't belong to the journey stop ids".format(schedule.stop_id, schedule.id))
162
+ continue # Skip to the next schedule
163
+
164
+ departure_stops_ids = self.__get_stops_ids_by_stop_type('departure')
165
+ arrival_stops_ids = self.__get_stops_ids_by_stop_type('arrival')
166
+
167
+ # Check if the stop_id is in the departure or arrival stops lists
168
+ if schedule.stop_id in departure_stops_ids:
169
+ self.journeys[schedule.trip_id].add_stop('departure', schedule, stop, 'SCHEDULED')
170
+ elif schedule.stop_id in arrival_stops_ids:
171
+ self.journeys[schedule.trip_id].add_stop('arrival', schedule, stop, 'SCHEDULED')
172
+ else:
173
+ self.logger.warning("Stop ID {} is not categorized as departure or arrival for schedule: {}".format(schedule.stop_id, schedule))
174
+
175
+ async def _fetch_predictions(self, params: str = None) -> list[MBTAPrediction]:
176
+ """Retrieve MBTA predictions based on the provided stop IDs"""
177
+ self.logger.debug("Retrieving MBTA predictions")
178
+ base_params = {
179
+ 'filter[stop]': ','.join(self._get_stops_ids()),
180
+ 'filter[revenue]': 'REVENUE',
181
+ 'sort': 'departure_time'
182
+ }
183
+ if params is not None:
184
+ base_params.update(params)
185
+ try:
186
+ predictions: list[MBTAPrediction] = await self.mbta_client.list_predictions(base_params)
187
+ return predictions
188
+ except aiohttp.ClientError as e:
189
+ self.logger.error("HTTP error occurred while retrieving MBTA predictions: {}".format(e))
190
+ return []
191
+ except Exception as e:
192
+ self.logger.error("Error retrieving MBTA predictions: {}".format(e))
193
+
194
+ async def _process_predictions(self, predictions: list[MBTAPrediction]):
195
+ self.logger.debug("Processing MBTA predictions")
196
+
197
+ for prediction in predictions:
198
+ # Validate prediction data
199
+ if not prediction.trip_id or not prediction.stop_id:
200
+ self.logger.error("Invalid prediction data: {}".format(prediction))
201
+ continue # Skip to the next prediction
202
+
203
+ # If the trip of the prediction is not in the journeys dict
204
+ if prediction.trip_id not in self.journeys:
205
+ # Create the journey
206
+ journey = Journey()
207
+ # Add the journey to the journeys dict using the trip_id as key
208
+ self.journeys[prediction.trip_id] = journey
209
+
210
+ # Validate stop
211
+ stop = self.__get_stop_by_id(prediction.stop_id)
212
+ if not stop:
213
+ self.logger.error("Invalid stop ID: {} for prediction: {}".format(prediction.stop_id, prediction))
214
+ continue # Skip to the next prediction
215
+
216
+ departure_stops_ids = self.__get_stops_ids_by_stop_type('departure')
217
+ arrival_stops_ids = self.__get_stops_ids_by_stop_type('arrival')
218
+
219
+ # Default schedule relationship to 'PREDICTED' if not set
220
+ if prediction.schedule_relationship is None:
221
+ prediction.schedule_relationship = 'PREDICTED'
222
+
223
+ # Check if the prediction stop_id is in the departure or arrival stops lists
224
+ if prediction.stop_id in departure_stops_ids:
225
+ self.journeys[prediction.trip_id].add_stop('departure', prediction, stop, prediction.schedule_relationship)
226
+ elif prediction.stop_id in arrival_stops_ids:
227
+ self.journeys[prediction.trip_id].add_stop('arrival', prediction, stop, prediction.schedule_relationship)
228
+ else:
229
+ self.logger.warning("Stop ID {} is not categorized as departure or arrival for prediction: {}".format(prediction.stop_id, prediction))
230
+
231
+ async def _fetch_alerts(self, params: str = None) -> list[MBTAAlert]:
232
+ """Retrieve MBTA alerts"""
233
+ self.logger.debug("Retrieving MBTA alerts")
234
+
235
+ # Prepare filter parameters
236
+ base_params = {
237
+ 'filter[stop]': ','.join(self._get_stops_ids()),
238
+ 'filter[activity]': 'BOARD,EXIT,RIDE',
239
+ 'filter[datetime]': 'NOW'
240
+ }
241
+
242
+ if params is not None:
243
+ base_params.update(params)
244
+
245
+ try:
246
+ alerts: list[MBTAAlert] = await self.mbta_client.list_alerts(base_params)
247
+ return alerts
248
+ except aiohttp.ClientError as e:
249
+ self.logger.error("HTTP error occurred while retrieving MBTA alerts: {}".format(e))
250
+ return []
251
+ except Exception as e:
252
+ self.logger.error("Error retrieving MBTA alerts: {}".format(e))
253
+ return []
254
+
255
+ def _process_alerts(self, alerts: list[MBTAAlert]):
256
+ self.logger.debug("Processing MBTA alerts")
257
+
258
+ for alert in alerts:
259
+ # Validate alert data
260
+ if not alert.id or not alert.effect:
261
+ self.logger.error("Invalid alert data: {}".format(alert))
262
+ continue # Skip to the next alert
263
+
264
+ # Iterate through each journey and associate relevant alerts
265
+ for journey in self.journeys.values():
266
+ # Check if the alert is already associated by comparing IDs
267
+ if any(existing_alert.id == alert.id for existing_alert in journey.alerts):
268
+ continue # Skip if alert is already associated
269
+
270
+ # Check if the alert is relevant to the journey
271
+ try:
272
+ if self.__is_alert_relevant(alert, journey):
273
+ journey.alerts.append(alert)
274
+ except Exception as e:
275
+ self.logger.error("Error processing MBTA alert {}: {}".format(alert.id, e))
276
+ continue # Skip to the next journey if an error occurs
277
+
278
+ def __is_alert_relevant(self, alert: MBTAAlert, journey: Journey) -> bool:
279
+ """Check if an alert is relevant to a given journey."""
280
+ for informed_entity in alert.informed_entities:
281
+ # Check informed entity stop relevance
282
+ if informed_entity.get('stop') and informed_entity['stop'] not in journey.get_stops_ids():
283
+ continue
284
+ # Check informed entity trip relevance
285
+ if informed_entity.get('trip') and informed_entity['trip'] != journey.trip.id:
286
+ continue
287
+ # Check informed entity route relevance
288
+ if informed_entity.get('route') and informed_entity['route'] != journey.route.id:
289
+ continue
290
+ # Check activities relevance based on departure or arrival
291
+ if not self.__is_alert_activity_relevant(informed_entity, journey):
292
+ continue
293
+ return True # Alert is relevant if all checks pass
294
+ return False # Alert is not relevant
295
+
296
+ def __is_alert_activity_relevant(self, informed_entity: dict, journey: Journey) -> bool:
297
+ """Check if the activities of the informed entity are relevant to the journey."""
298
+ departure_stop_id = journey.get_stop_id('departure')
299
+ arrival_stop_id = journey.get_stop_id('arrival')
300
+
301
+ if informed_entity['stop'] == departure_stop_id and not any(activity in informed_entity.get('activities', []) for activity in ['BOARD', 'RIDE']):
302
+ return False
303
+ if informed_entity['stop'] == arrival_stop_id and not any(activity in informed_entity.get('activities', []) for activity in ['EXIT', 'RIDE']):
304
+ return False
305
+ return True
306
+
307
+ @memoize_async()
308
+ async def _fetch_trip(self, trip_id: str, params: dict = None) -> Optional[MBTATrip]:
309
+ """Retrieve MBTA trip based on trip_id."""
310
+ self.logger.debug("Retrieving MBTA trip: {}".format(trip_id))
311
+ try:
312
+ trip: MBTATrip = await self.mbta_client.get_trip(trip_id, params)
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
317
+ except Exception as e:
318
+ self.logger.error("Error fetching trip {}: {}".format(trip_id, e))
319
+ return None
320
+
321
+ @memoize_async()
322
+ async def _fetch_route(self, route_id: str, params: dict = None) -> Optional[MBTARoute]:
323
+ """Retrieve MBTA route based on route_id."""
324
+ self.logger.debug("Retrieving MBTA route: {}".format(route_id))
325
+ try:
326
+ route: MBTARoute = await self.mbta_client.get_route(route_id, params)
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
331
+ except Exception as e:
332
+ self.logger.error("Error retrieving MBTA route {}: {}".format(route_id, e))
333
+ return None
334
+
335
+ @memoize_async()
336
+ async def _fetch_trips(self, params: dict = None) -> Optional[MBTARoute]:
337
+ """Retrieve MBTA trips"""
338
+ self.logger.debug("Retrieving MBTA trips")
339
+ try:
340
+ trips: list[MBTATrip] = await self.mbta_client.list_trips(params)
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
345
+ except Exception as e:
346
+ self.logger.error("Error retrieving MBTA trips: {}".format(e))
347
+ return None
348
+
349
+ class MBTAStopError(Exception):
350
+ pass
351
+
352
+ class MBTATripError(Exception):
353
+ pass
@@ -1,14 +1,14 @@
1
1
  from typing import Union, Optional
2
2
  from datetime import datetime
3
3
 
4
- from journey_stop import JourneyStop
5
- from mbta_schedule import MBTASchedule
6
- from mbta_prediction import MBTAPrediction
7
- from mbta_stop import MBTAStop
8
- from mbta_route import MBTARoute
9
- from mbta_trip import MBTATrip
10
- from mbta_alert import MBTAAlert
11
- from mbta_utils import MBTAUtils
4
+ from .journey_stop import JourneyStop
5
+ from .mbta_schedule import MBTASchedule
6
+ from .mbta_prediction import MBTAPrediction
7
+ from .mbta_stop import MBTAStop
8
+ from .mbta_route import MBTARoute
9
+ from .mbta_trip import MBTATrip
10
+ from .mbta_alert import MBTAAlert
11
+ from .mbta_utils import MBTAUtils
12
12
 
13
13
  class Journey:
14
14
  """A class to manage a journey with multiple stops."""
@@ -90,7 +90,6 @@ class Journey:
90
90
  return self.route.color if self.route else None
91
91
 
92
92
  def get_route_description(self) -> Optional[str]:
93
- from mbta_utils import MBTAUtils
94
93
  return MBTAUtils.get_route_type_desc_by_type_id(self.route.type) if self.route else None
95
94
 
96
95
  def get_route_type(self) -> Optional[str]:
@@ -1,8 +1,8 @@
1
1
  from typing import Optional
2
2
  from datetime import datetime
3
3
 
4
- from mbta_stop import MBTAStop
5
- from mbta_utils import MBTAUtils
4
+ from .mbta_stop import MBTAStop
5
+ from .mbta_utils import MBTAUtils
6
6
 
7
7
 
8
8
  class JourneyStop:
@@ -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
@@ -7,7 +7,7 @@ class MBTAAlert:
7
7
  attributes = alert.get('attributes', {})
8
8
 
9
9
  # Basic attributes
10
- self.alert_id: str = alert.get('id', '')
10
+ self.id: str = alert.get('id', '')
11
11
  self.active_period_start: Optional[str] = attributes.get('active_period', [{}])[0].get('start', None)
12
12
  self.active_period_end: Optional[str] = attributes.get('active_period', [{}])[0].get('end', None)
13
13
  self.cause: str = attributes.get('cause', '')
@@ -2,12 +2,13 @@ import aiohttp
2
2
  import logging
3
3
  from aiohttp import ClientConnectionError, ClientResponseError
4
4
  from typing import Optional, Any
5
- from mbta_route import MBTARoute
6
- from mbta_stop import MBTAStop
7
- from mbta_schedule import MBTASchedule
8
- from mbta_prediction import MBTAPrediction
9
- from mbta_trip import MBTATrip
10
- from mbta_alert import MBTAAlert
5
+
6
+ from .mbta_route import MBTARoute
7
+ from .mbta_stop import MBTAStop
8
+ from .mbta_schedule import MBTASchedule
9
+ from .mbta_prediction import MBTAPrediction
10
+ from .mbta_trip import MBTATrip
11
+ from .mbta_alert import MBTAAlert
11
12
 
12
13
  MBTA_DEFAULT_HOST = "api-v3.mbta.com"
13
14
 
@@ -23,11 +24,23 @@ ENDPOINTS = {
23
24
  class MBTAClient:
24
25
  """Class to interact with the MBTA v3 API."""
25
26
 
26
- def __init__(self, session: aiohttp.ClientSession, logger: logging.Logger, api_key: Optional[str] = None)-> None:
27
- self._session = session
28
- self._api_key = api_key
29
- self.logger: logging.Logger = logger
27
+ def __init__(self, session: aiohttp.ClientSession = None, logger: logging.Logger = None, api_key: Optional[str] = None)-> None:
28
+ self._session = session or aiohttp.ClientSession()
29
+ self.logger: logging.Logger = logger or logging.getLogger(__name__)
30
+ self._api_key: str = api_key
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()
30
39
 
40
+ async def close(self) -> None:
41
+ """Close the session manually."""
42
+ await self._session.close()
43
+
31
44
  async def get_route(self, id: str, params: Optional[dict[str, Any]] = None) -> MBTARoute:
32
45
  """Get a route by its ID."""
33
46
  route_data = await self._fetch_data(f'{ENDPOINTS["ROUTES"]}/{id}', params)
@@ -1,6 +1,6 @@
1
1
 
2
2
  from typing import Any, Optional
3
- from mbta_utils import MBTAUtils
3
+ from .mbta_utils import MBTAUtils
4
4
 
5
5
  class MBTAPrediction:
6
6
  """A prediction object to hold information about a prediction."""