MBTAclient 0.2.4__tar.gz → 0.2.6__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 (28) hide show
  1. {mbtaclient-0.2.4 → mbtaclient-0.2.6}/PKG-INFO +6 -1
  2. {mbtaclient-0.2.4 → mbtaclient-0.2.6}/pyproject.toml +9 -2
  3. {mbtaclient-0.2.4 → mbtaclient-0.2.6}/src/MBTAclient.egg-info/PKG-INFO +6 -1
  4. {mbtaclient-0.2.4 → mbtaclient-0.2.6}/src/MBTAclient.egg-info/SOURCES.txt +1 -0
  5. mbtaclient-0.2.6/src/mbtaclient/__init__.py +31 -0
  6. mbtaclient-0.2.6/src/mbtaclient/__version__.py +1 -0
  7. {mbtaclient-0.2.4 → mbtaclient-0.2.6}/src/mbtaclient/base_handler.py +140 -146
  8. {mbtaclient-0.2.4 → mbtaclient-0.2.6}/src/mbtaclient/journey.py +8 -9
  9. {mbtaclient-0.2.4 → mbtaclient-0.2.6}/src/mbtaclient/journey_stop.py +2 -2
  10. {mbtaclient-0.2.4 → mbtaclient-0.2.6}/src/mbtaclient/journeys_handler.py +7 -7
  11. {mbtaclient-0.2.4 → mbtaclient-0.2.6}/src/mbtaclient/mbta_alert.py +1 -1
  12. {mbtaclient-0.2.4 → mbtaclient-0.2.6}/src/mbtaclient/mbta_client.py +11 -10
  13. {mbtaclient-0.2.4 → mbtaclient-0.2.6}/src/mbtaclient/mbta_prediction.py +1 -1
  14. mbtaclient-0.2.6/src/mbtaclient/mbta_utils.py +100 -0
  15. mbtaclient-0.2.6/src/mbtaclient/trip_handler.py +124 -0
  16. mbtaclient-0.2.4/src/mbtaclient/__init__.py +0 -0
  17. mbtaclient-0.2.4/src/mbtaclient/mbta_utils.py +0 -50
  18. mbtaclient-0.2.4/src/mbtaclient/trip_handler.py +0 -85
  19. {mbtaclient-0.2.4 → mbtaclient-0.2.6}/LICENSE +0 -0
  20. {mbtaclient-0.2.4 → mbtaclient-0.2.6}/README.md +0 -0
  21. {mbtaclient-0.2.4 → mbtaclient-0.2.6}/setup.cfg +0 -0
  22. {mbtaclient-0.2.4 → mbtaclient-0.2.6}/src/MBTAclient.egg-info/dependency_links.txt +0 -0
  23. {mbtaclient-0.2.4 → mbtaclient-0.2.6}/src/MBTAclient.egg-info/requires.txt +0 -0
  24. {mbtaclient-0.2.4 → mbtaclient-0.2.6}/src/MBTAclient.egg-info/top_level.txt +0 -0
  25. {mbtaclient-0.2.4 → mbtaclient-0.2.6}/src/mbtaclient/mbta_route.py +0 -0
  26. {mbtaclient-0.2.4 → mbtaclient-0.2.6}/src/mbtaclient/mbta_schedule.py +0 -0
  27. {mbtaclient-0.2.4 → mbtaclient-0.2.6}/src/mbtaclient/mbta_stop.py +0 -0
  28. {mbtaclient-0.2.4 → mbtaclient-0.2.6}/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.4
3
+ Version: 0.2.6
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.4"
7
+ version = "0.2.6"
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.4
3
+ Version: 0.2.6
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
@@ -7,6 +7,7 @@ src/MBTAclient.egg-info/dependency_links.txt
7
7
  src/MBTAclient.egg-info/requires.txt
8
8
  src/MBTAclient.egg-info/top_level.txt
9
9
  src/mbtaclient/__init__.py
10
+ src/mbtaclient/__version__.py
10
11
  src/mbtaclient/base_handler.py
11
12
  src/mbtaclient/journey.py
12
13
  src/mbtaclient/journey_stop.py
@@ -0,0 +1,31 @@
1
+ # mbtaclient/__init__.py
2
+
3
+
4
+ from .journey_stop import JourneyStop
5
+ from .journey import Journey
6
+ from .journeys_handler import JourneysHandler
7
+ from .mbta_alert import MBTAAlert
8
+ from .mbta_client import MBTAClient
9
+ from .mbta_prediction import MBTAPrediction
10
+ from .mbta_route import MBTARoute
11
+ from .mbta_schedule import MBTASchedule
12
+ from .mbta_stop import MBTAStop
13
+ from .mbta_trip import MBTATrip
14
+ from .trip_handler import TripHandler
15
+ from .__version__ import __version__
16
+
17
+ __all__ = [
18
+ "JourneyStop",
19
+ "Journey",
20
+ "JourneysHandler",
21
+ "MBTAAlert",
22
+ "MBTAClient",
23
+ "MBTARoute",
24
+ "MBTATrip",
25
+ "MBTAStop",
26
+ "MBTASchedule",
27
+ "MBTAPrediction",
28
+ "TripHandler",
29
+ ]
30
+
31
+ __version__ = __version__
@@ -0,0 +1 @@
1
+ __version__ = "0.2.5"
@@ -1,25 +1,23 @@
1
1
  import logging
2
- import traceback
3
2
  import aiohttp
4
3
 
5
4
  from typing import Optional
6
- from datetime import date
7
5
 
8
-
9
- from mbta_client import MBTAClient
10
- from journey import Journey
11
- from mbta_stop import MBTAStop
12
- from mbta_route import MBTARoute
13
- from mbta_schedule import MBTASchedule
14
- from mbta_prediction import MBTAPrediction
15
- from mbta_trip import MBTATrip
16
- from mbta_alert import MBTAAlert
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
17
15
 
18
16
 
19
17
  class BaseHandler:
20
18
  """Base class for handling MBTA journeys."""
21
19
 
22
- def __init__(self, session: aiohttp.ClientSession, logger: logging.Logger, depart_from_name: str , arrive_at_name: str, api_key: str = None) -> None:
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:
23
21
 
24
22
  self.depart_from = {
25
23
  'name' : depart_from_name,
@@ -31,20 +29,14 @@ class BaseHandler:
31
29
  'stops' : None,
32
30
  'ids' : None
33
31
  }
34
- self.mbta_client = MBTAClient(session,logger, api_key=api_key)
32
+
33
+ client_session = session or aiohttp.ClientSession()
34
+ self.mbta_client = MBTAClient(client_session,logger,api_key)
35
35
 
36
36
  self.journeys: dict[str, Journey] = {}
37
-
38
- # Caches
39
- self._stops_cache: Optional[list[MBTAStop]] = None
40
- self._schedules_cache: Optional[list[MBTASchedule]] = None
41
- self._schedules_cache_date: Optional[date] = None
42
- self._trip_cache: dict[str, MBTATrip] = {}
43
- self._route_cache: dict[str, MBTARoute] = {}
37
+
38
+ self.logger: logging.Logger = logger or logging.getLogger(__name__)
44
39
 
45
- # Logger
46
- self.logger: logging.Logger = logger
47
-
48
40
  def __repr__(self) -> str:
49
41
  return (f"BaseHandler(depart_from_name={self.depart_from['name']}, arrive_at_name={self.arrive_at['name']})")
50
42
 
@@ -53,56 +45,52 @@ class BaseHandler:
53
45
  stops = await self.__fetch_stops()
54
46
  self.__process_stops(stops)
55
47
 
48
+ @memoize_async()
56
49
  async def __fetch_stops(self, params: dict = None) -> list[MBTAStop]:
57
- """Retrieve and process stops with a non-expiring cache."""
58
- self.logger.debug("Fetching MBTA stops")
59
-
60
- # Check if stops are already cached
61
- if self._stops_cache is not None:
62
- return self._stops_cache
63
-
64
- # Cache is empty, so we fetch the stops from the API
50
+ """Retrive stops """
51
+ self.logger.debug("Retriving MBTA stops")
65
52
  base_params = {'filter[location_type]': '0'}
66
-
67
53
  if params is not None:
68
54
  base_params.update(params)
69
-
70
55
  try:
71
56
  stops: list[MBTAStop] = await self.mbta_client.list_stops(base_params)
72
- self.logger.debug("Updating cached stops")
73
- self._stops_cache = stops
74
57
  return stops
75
-
76
58
  except Exception as e:
77
- self.logger.error(f"Error fetching stops: {e}")
78
- traceback.print_exc()
59
+ self.logger.error(f"Error retriving MBTA stops: {e}")
79
60
  return []
80
61
 
81
- def __process_stops(self, stops: list[MBTAStop]):
62
+ def __process_stops(self, stops: list[MBTAStop]):
63
+ self.logger.debug("Processing MBTA stops")
82
64
  depart_from_stops = []
83
65
  depart_from_ids = []
84
66
  arrive_at_stops = []
85
67
  arrive_at_ids = []
68
+
86
69
  for stop in stops:
70
+ if not isinstance(stop, MBTAStop): # Validate data type
71
+ self.logger.warning(f"Unexpected data type for stop: {type(stop)}")
72
+ continue # Skip invalid data
73
+
87
74
  if stop.name.lower() == self.depart_from['name'].lower():
88
75
  depart_from_stops.append(stop)
89
76
  depart_from_ids.append(stop.id)
77
+
90
78
  if stop.name.lower() == self.arrive_at['name'].lower():
91
79
  arrive_at_stops.append(stop)
92
80
  arrive_at_ids.append(stop.id)
93
-
81
+
94
82
  if len(depart_from_stops) == 0:
95
- self.logger.error(f"Error fetching MBTA stop data for {self.depart_from['name']}")
96
- raise ValueError(f"Invalid stop name: {self.depart_from['name']}")
97
-
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']}")
85
+
98
86
  if len(arrive_at_stops) == 0:
99
- self.logger.error(f"Error fetching MBTA stop data for {self.arrive_at['name']}")
100
- raise ValueError(f"Invalid stop name: {self.arrive_at['name']}")
101
- else:
102
- self.depart_from['stops'] = depart_from_stops
103
- self.depart_from['ids'] = depart_from_ids
104
- self.arrive_at['stops'] = arrive_at_stops
105
- self.arrive_at['ids'] = arrive_at_ids
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']}")
89
+
90
+ self.depart_from['stops'] = depart_from_stops
91
+ self.depart_from['ids'] = depart_from_ids
92
+ self.arrive_at['stops'] = arrive_at_stops
93
+ self.arrive_at['ids'] = arrive_at_ids
106
94
 
107
95
  def __get_stop_by_id(self, stop_id: str) -> Optional[MBTAStop]:
108
96
  for stop in (self.depart_from['stops'] + self.arrive_at['stops']):
@@ -120,106 +108,116 @@ class BaseHandler:
120
108
  return self.arrive_at['ids']
121
109
  return None
122
110
 
111
+ @memoize_async(expire_at_end_of_day=True)
123
112
  async def _fetch_schedules(self, params: Optional[dict] = None) -> list[MBTASchedule]:
124
- """Retrieve and process schedules for today."""
125
- self.logger.debug("Fetching MBTA schedules")
126
-
127
- # Check if the cache is outdated
128
- if self._schedules_cache_date is not None and self._schedules_cache_date == date.today():
129
- self.logger.debug("Returning cached schedules")
130
- return self._schedules_cache
131
-
113
+ """Retrive MBTA schedules"""
114
+ self.logger.debug("Retriving MBTA schedules")
132
115
  base_params = {
133
116
  'filter[stop]': ','.join(self._get_stops_ids()),
134
117
  'sort': 'departure_time'
135
118
  }
136
119
  if params is not None:
137
120
  base_params.update(params)
138
-
139
121
  try:
140
- schedules: list[MBTASchedule] = await self.mbta_client.list_schedules(base_params)
141
- # Update the cache with new data and timestamp
142
- self.logger.debug("Updating cached schedules")
143
- self._schedules_cache = schedules
144
- self._schedules_cache_date = date.today()
122
+ schedules: list[MBTASchedule] = await self.mbta_client.list_schedules(params)
145
123
  return schedules
146
124
  except Exception as e:
147
- self.logger.error(f"Error fetching schedules: {e}")
148
- traceback.print_exc()
125
+ self.logger.error(f"Error retriving MBTA schedules: {e}")
149
126
  return []
150
127
 
151
128
  async def _process_schedules(self, schedules: list[MBTASchedule]):
152
- self.logger.debug("Processing schedules")
153
-
154
- for schedule in schedules:
155
-
156
- # if the schedule trip id not in the journeys
129
+ self.logger.debug("Processing MBTA schedules")
130
+
131
+ for schedule in schedules:
132
+ # Validate schedule data
133
+ if not schedule.trip_id or not schedule.stop_id:
134
+ self.logger.error(f"Invalid schedule data: {schedule}")
135
+ continue # Skip to the next schedule
136
+
137
+ # If the schedule trip_id is not in the journeys
157
138
  if schedule.trip_id not in self.journeys:
158
- # create the journey
139
+ # Create the journey
159
140
  journey = Journey()
160
- # add the journey to the journeys dict using the trip_id as key
141
+ # Add the journey to the journeys dict using the trip_id as key
161
142
  self.journeys[schedule.trip_id] = journey
162
-
163
- stop: MBTAStop = self.__get_stop_by_id(schedule.stop_id)
143
+
144
+ # Validate stop
145
+ stop = self.__get_stop_by_id(schedule.stop_id)
146
+ if not stop:
147
+ self.logger.debug(f"Stop {schedule.stop_id} of schedule {schedule.id} doesn't belong to the journey stop ids")
148
+ continue # Skip to the next schedule
149
+
164
150
  departure_stops_ids = self.__get_stops_ids_by_stop_type('departure')
165
151
  arrival_stops_ids = self.__get_stops_ids_by_stop_type('arrival')
166
-
152
+
153
+ # Check if the stop_id is in the departure or arrival stops lists
167
154
  if schedule.stop_id in departure_stops_ids:
168
- self.journeys[schedule.trip_id].add_stop('departure',schedule,stop,'SCHEDULED')
155
+ self.journeys[schedule.trip_id].add_stop('departure', schedule, stop, 'SCHEDULED')
169
156
  elif schedule.stop_id in arrival_stops_ids:
170
- self.journeys[schedule.trip_id].add_stop('arrival',schedule, stop,'SCHEDULED')
157
+ self.journeys[schedule.trip_id].add_stop('arrival', schedule, stop, 'SCHEDULED')
158
+ else:
159
+ self.logger.warning(f"Stop ID {schedule.stop_id} is not categorized as departure or arrival for schedule: {schedule}")
160
+
171
161
 
172
162
 
173
163
  async def _fetch_predictions(self, params: str = None) -> list[MBTAPrediction]:
174
- """Retrieve and process predictions based on the provided stop IDs and route ID."""
175
- self.logger.debug("Fetching MBTA predictions")
176
-
164
+ """Retrive MBTA predictions based on the provided stop IDs"""
165
+ self.logger.debug("Retriving MBTA predictions")
177
166
  base_params = {
178
167
  'filter[stop]': ','.join(self._get_stops_ids()),
179
168
  'filter[revenue]': 'REVENUE',
180
169
  'sort': 'departure_time'
181
170
  }
182
-
183
171
  if params is not None:
184
172
  base_params.update(params)
185
-
186
173
  try:
187
174
  predictions: list[MBTAPrediction] = await self.mbta_client.list_predictions(base_params)
188
175
  return predictions
189
-
190
176
  except Exception as e:
191
- self.logger.error(f"Error fetching predictions: {e}")
192
- traceback.print_exc()
177
+ self.logger.error(f"Error retriving MBTA predictions: {e}")
178
+
193
179
 
194
- async def _process_predictions (self, predictions: list[MBTAPrediction]):
195
- self.logger.debug("Processing predictions")
196
-
180
+ async def _process_predictions(self, predictions: list[MBTAPrediction]):
181
+ self.logger.debug("Processing MBTA predictions")
182
+
197
183
  for prediction in predictions:
198
-
199
- # if the trip of the prediciton is not in the journeys dict
200
- if prediction.trip_id not in self.journeys:
201
- # create the journey
184
+ # Validate prediction data
185
+ if not prediction.trip_id or not prediction.stop_id:
186
+ self.logger.error(f"Invalid prediction data: {prediction}")
187
+ continue # Skip to the next prediction
188
+
189
+ # If the trip of the prediction is not in the journeys dict
190
+ if prediction.trip_id not in self.journeys:
191
+ # Create the journey
202
192
  journey = Journey()
203
- # add the journey to the journeys dict using the trip_id as key
193
+ # Add the journey to the journeys dict using the trip_id as key
204
194
  self.journeys[prediction.trip_id] = journey
205
-
206
- stop: MBTAStop = self.__get_stop_by_id(prediction.stop_id)
195
+
196
+ # Validate stop
197
+ stop = self.__get_stop_by_id(prediction.stop_id)
198
+ if not stop:
199
+ self.logger.error(f"Invalid stop ID: {prediction.stop_id} for prediction: {prediction}")
200
+ continue # Skip to the next prediction
201
+
207
202
  departure_stops_ids = self.__get_stops_ids_by_stop_type('departure')
208
203
  arrival_stops_ids = self.__get_stops_ids_by_stop_type('arrival')
209
-
204
+
205
+ # Default schedule relationship to 'PREDICTED' if not set
210
206
  if prediction.schedule_relationship is None:
211
- prediction.schedule_relationship = 'PREDICTED'
212
-
213
- # if the prediction stop id is in the departure stop ids
207
+ prediction.schedule_relationship = 'PREDICTED'
208
+
209
+ # Check if the prediction stop_id is in the departure or arrival stops lists
214
210
  if prediction.stop_id in departure_stops_ids:
215
- self.journeys[prediction.trip_id].add_stop('departure',prediction,stop,prediction.schedule_relationship)
216
- # if the prediction stop id is in the arrival stop ids
217
- if prediction.stop_id in arrival_stops_ids:
218
- self.journeys[prediction.trip_id].add_stop('arrival',prediction,stop,prediction.schedule_relationship)
211
+ self.journeys[prediction.trip_id].add_stop('departure', prediction, stop, prediction.schedule_relationship)
212
+ elif prediction.stop_id in arrival_stops_ids:
213
+ self.journeys[prediction.trip_id].add_stop('arrival', prediction, stop, prediction.schedule_relationship)
214
+ else:
215
+ self.logger.warning(f"Stop ID {prediction.stop_id} is not categorized as departure or arrival for prediction: {prediction}")
216
+
219
217
 
220
218
  async def _fetch_alerts(self,params: str = None) -> list[MBTAAlert]:
221
- """Retrieve and associate alerts with the relevant journeys."""
222
- self.logger.debug("Fetching MBTA alerts")
219
+ """Retrive MBTA alerts"""
220
+ self.logger.debug("Retriving MBTA alerts")
223
221
 
224
222
  # Prepare filter parameters
225
223
  base_params = {
@@ -235,22 +233,31 @@ class BaseHandler:
235
233
  alerts: list[MBTAAlert] = await self.mbta_client.list_alerts(base_params)
236
234
  return alerts
237
235
  except Exception as e:
238
- self.logger.error(f"Error fetching alerts: {e}")
239
- traceback.print_exc()
236
+ self.logger.error(f"Error retriving MBTA alerts: {e}")
237
+
240
238
 
241
239
  def _process_alerts(self, alerts: list[MBTAAlert]):
242
- self.logger.debug("Processing alerts")
240
+ self.logger.debug("Processing MBTA alerts")
243
241
 
244
242
  for alert in alerts:
245
-
243
+ # Validate alert data
244
+ if not alert.id or not alert.effect:
245
+ self.logger.error(f"Invalid alert data: {alert}")
246
+ continue # Skip to the next alert
247
+
246
248
  # Iterate through each journey and associate relevant alerts
247
249
  for journey in self.journeys.values():
248
- if alert in journey.alerts:
250
+ # Check if the alert is already associated by comparing IDs
251
+ if any(existing_alert.id == alert.id for existing_alert in journey.alerts):
249
252
  continue # Skip if alert is already associated
250
253
 
251
254
  # Check if the alert is relevant to the journey
252
- if self.__is_alert_relevant(alert, journey):
253
- journey.alerts.append(alert)
255
+ try:
256
+ if self.__is_alert_relevant(alert, journey):
257
+ journey.alerts.append(alert)
258
+ except Exception as e:
259
+ self.logger.error(f"Error processing MBTA alert {alert.id}: {e}")
260
+ continue # Skip to the next journey if an error occurs
254
261
 
255
262
  def __is_alert_relevant(self, alert: MBTAAlert, journey: Journey) -> bool:
256
263
  """Check if an alert is relevant to a given journey."""
@@ -281,55 +288,42 @@ class BaseHandler:
281
288
  return False
282
289
  return True
283
290
 
291
+ @memoize_async()
284
292
  async def _fetch_trip(self, trip_id: str, params: dict = None) -> Optional[MBTATrip]:
285
- """Retrieve and process a trip with a non-expiring cache based on trip_id."""
286
- self.logger.debug(f"Fetching MBTA trip: {trip_id} ")
287
-
288
- # Check if the trip is already cached
289
- if trip_id in self._trip_cache:
290
- self.logger.debug(f"Returning cached trip: {trip_id}")
291
- return self._trip_cache[trip_id]
292
-
293
- # Trip is not in the cache, so fetch it from the API
293
+ """Retrieve MBTA trip based on trip_id."""
294
+ self.logger.debug(f"Retriving MBTA trip: {trip_id} ")
294
295
  try:
295
296
  trip: MBTATrip = await self.mbta_client.get_trip(trip_id, params)
296
- self.logger.debug(f"Updating cached trip: {trip_id}")
297
- self._trip_cache[trip_id] = trip
298
297
  return trip
299
-
300
298
  except Exception as e:
301
- self.logger.error(f"Error fetching trip: {e}")
302
- traceback.print_exc()
299
+ self.logger.error(f"Error fetching trip {trip_id}: {e}")
303
300
  return None
304
-
301
+
302
+ @memoize_async()
305
303
  async def _fetch_route(self, route_id: str, params: dict = None) -> Optional[MBTARoute]:
306
- """Retrieve and process a route with a non-expiring cache based on route_id."""
307
- self.logger.debug(f"Fetching MBTA route: {route_id} ")
308
-
309
- # Check if the trip is already cached
310
- if route_id in self._route_cache:
311
- return self._route_cache[route_id]
312
-
313
- # Trip is not in the cache, so fetch it from the API
304
+ """Retrive MBTA route based on route_id."""
305
+ self.logger.debug(f"Retriving MBTA route: {route_id} ")
314
306
  try:
315
307
  route: MBTARoute = await self.mbta_client.get_route(route_id, params)
316
- # Update the cache
317
- self.logger.debug(f"Updating cached route: {route_id}")
318
- self._route_cache[route_id] = route
319
308
  return route
320
-
321
309
  except Exception as e:
322
- self.logger.error(f"Error fetching route: {e}")
323
- traceback.print_exc()
310
+ self.logger.error(f"Error retriving MBTA route {route_id}: {e}")
324
311
  return None
325
-
312
+
313
+ @memoize_async()
326
314
  async def _fetch_trips(self, params: dict = None) -> Optional[MBTARoute]:
327
- self.logger.debug("Fetching MBTA trips")
315
+ """Retrive MBTA trips"""
316
+ self.logger.debug("Retriving MBTA trips")
328
317
  try:
329
318
  trips: list[MBTATrip] = await self.mbta_client.list_trips(params)
330
319
  return trips
331
320
  except Exception as e:
332
- self.logger.error(f"Error fetching route: {e}")
333
- traceback.print_exc()
321
+ self.logger.error(f"Error retriving MBTA route: {e}")
334
322
  return None
335
323
 
324
+
325
+ class MBTAStopError(Exception):
326
+ pass
327
+
328
+ class MBTATripError(Exception):
329
+ 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:
@@ -3,18 +3,18 @@ import logging
3
3
 
4
4
  from datetime import datetime
5
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
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
11
 
12
12
 
13
13
  class JourneysHandler(BaseHandler):
14
14
  """Handler for managing a specific journey."""
15
15
 
16
- def __init__(self, session: aiohttp.ClientSession, logger: logging.Logger, depart_from_name: str, arrive_at_name: str, max_journeys: int, api_key:str = None) :
17
- super().__init__(session, logger, depart_from_name,arrive_at_name, api_key)
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
18
  self.max_journeys = max_journeys
19
19
 
20
20
  async def async_init(self):
@@ -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,10 +24,10 @@ 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
30
31
 
31
32
  async def get_route(self, id: str, params: Optional[dict[str, Any]] = None) -> MBTARoute:
32
33
  """Get a route by its ID."""
@@ -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."""
@@ -0,0 +1,100 @@
1
+ from datetime import datetime, timedelta
2
+
3
+ from typing import Optional
4
+ from collections.abc import Hashable
5
+ import logging
6
+
7
+ # logging.basicConfig(level=logging.DEBUG)
8
+ _LOGGER = logging.getLogger(__name__)
9
+
10
+
11
+ class MBTAUtils:
12
+
13
+ ROUTE_TYPES= {
14
+ # 0: 'Light Rail', # Example: Green Line
15
+ # 1: 'Heavy Rail', # Example: Red Line
16
+ 0: 'Subway',
17
+ 1: 'Subway',
18
+ 2: 'Commuter Rail',
19
+ 3: 'Bus',
20
+ 4: 'Ferry'
21
+ }
22
+
23
+ UNCERTAINTY = {
24
+ '60': 'Trip that has already started',
25
+ '120': 'Trip not started and a vehicle is awaiting departure at the origin',
26
+ '300': 'Vehicle has not yet been assigned to the trip',
27
+ '301': 'Vehicle appears to be stalled or significantly delayed',
28
+ '360': 'Trip not started and a vehicle is completing a previous trip'
29
+ }
30
+
31
+ @staticmethod
32
+ def get_route_type_desc_by_type_id(route_type: int) -> str:
33
+ """Get a description of the route type."""
34
+ return MBTAUtils.ROUTE_TYPES.get(route_type, 'Unknown')
35
+
36
+ @staticmethod
37
+ def get_uncertainty_description(key: str) -> str:
38
+ return MBTAUtils.UNCERTAINTY.get(key, 'None')
39
+
40
+ @staticmethod
41
+ def time_to(time: Optional[datetime], now: datetime) -> Optional[float]:
42
+ if time is None:
43
+ return None
44
+ return (time - now).total_seconds()
45
+
46
+ @staticmethod
47
+ def calculate_time_difference(real_time: Optional[datetime], time: Optional[datetime]) -> Optional[float]:
48
+ if real_time is None or time is None:
49
+ return None
50
+ return (real_time - time).total_seconds()
51
+
52
+ @staticmethod
53
+ def parse_datetime(time_str: str) -> Optional[datetime]:
54
+ """Parse a string in ISO 8601 format to a datetime object."""
55
+ if not isinstance(time_str, str):
56
+ return None
57
+ return datetime.fromisoformat(time_str)
58
+
59
+
60
+
61
+ from datetime import datetime, timedelta
62
+ import logging
63
+
64
+ logger = logging.getLogger(__name__)
65
+
66
+ def memoize_async(expire_at_end_of_day=False):
67
+ def decorator(func):
68
+ cache = {}
69
+
70
+ def make_hashable(item):
71
+ if isinstance(item, dict):
72
+ # Exclude the 'filter[min_time]' key from the dictionary
73
+ item = {k: v for k, v in item.items() if k != 'filter[min_time]'}
74
+ return frozenset((make_hashable(k), make_hashable(v)) for k, v in item.items())
75
+ return str(item) # Convert non-dict items to string
76
+
77
+ async def wrapper(*args):
78
+ current_time = datetime.now()
79
+ cache_key = tuple(make_hashable(arg) for arg in args)
80
+
81
+ if cache_key in cache:
82
+ cached_result, timestamp = cache[cache_key]
83
+
84
+ if expire_at_end_of_day:
85
+ if timestamp.date() == current_time.date():
86
+ _LOGGER.debug(f"Cache hit for {func.__name__} with arguments {cache_key} at {current_time}")
87
+ return cached_result
88
+ else: # Expiration based on 30 days
89
+ if current_time - timestamp < timedelta(days=30):
90
+ _LOGGER.debug(f"Cache hit for {func.__name__} with arguments {cache_key} at {current_time}")
91
+ return cached_result
92
+
93
+ _LOGGER.debug(f"Cache miss for {func.__name__} with arguments {cache_key} at {current_time}")
94
+ result = await func(*args)
95
+ cache[cache_key] = (result, current_time)
96
+ _LOGGER.debug(f"Cache updated for key: {cache_key} at {current_time}")
97
+ return result
98
+
99
+ return wrapper
100
+ return decorator
@@ -0,0 +1,124 @@
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
@@ -1,50 +0,0 @@
1
- from datetime import datetime
2
- from typing import Optional
3
-
4
- class MBTAUtils:
5
-
6
- ROUTE_TYPES= {
7
- # 0: 'Light Rail', # Example: Green Line
8
- # 1: 'Heavy Rail', # Example: Red Line
9
- 0: 'Subway',
10
- 1: 'Subway',
11
- 2: 'Commuter Rail',
12
- 3: 'Bus',
13
- 4: 'Ferry'
14
- }
15
-
16
- UNCERTAINTY = {
17
- '60': 'Trip that has already started',
18
- '120': 'Trip not started and a vehicle is awaiting departure at the origin',
19
- '300': 'Vehicle has not yet been assigned to the trip',
20
- '301': 'Vehicle appears to be stalled or significantly delayed',
21
- '360': 'Trip not started and a vehicle is completing a previous trip'
22
- }
23
-
24
- @staticmethod
25
- def get_route_type_desc_by_type_id(route_type: int) -> str:
26
- """Get a description of the route type."""
27
- return MBTAUtils.ROUTE_TYPES.get(route_type, 'Unknown')
28
-
29
- @staticmethod
30
- def get_uncertainty_description(key: str) -> str:
31
- return MBTAUtils.UNCERTAINTY.get(key, 'None')
32
-
33
- @staticmethod
34
- def time_to(time: Optional[datetime], now: datetime) -> Optional[float]:
35
- if time is None:
36
- return None
37
- return (time - now).total_seconds()
38
-
39
- @staticmethod
40
- def calculate_time_difference(real_time: Optional[datetime], time: Optional[datetime]) -> Optional[float]:
41
- if real_time is None or time is None:
42
- return None
43
- return (real_time - time).total_seconds()
44
-
45
- @staticmethod
46
- def parse_datetime(time_str: str) -> Optional[datetime]:
47
- """Parse a string in ISO 8601 format to a datetime object."""
48
- if not isinstance(time_str, str):
49
- return None
50
- return datetime.fromisoformat(time_str)
@@ -1,85 +0,0 @@
1
- import aiohttp
2
- import logging
3
-
4
- from base_handler import BaseHandler
5
- from journey import Journey
6
- from mbta_route import MBTARoute
7
- from mbta_trip import MBTATrip
8
- from mbta_schedule import MBTASchedule
9
- from mbta_prediction import MBTAPrediction
10
-
11
- class TripHandler(BaseHandler):
12
- """Handler for managing a specific trip."""
13
-
14
- def __init__(self, session: aiohttp.ClientSession, logger: logging.Logger, depart_from_name: str, arrive_at_name: str, trip_name: str, api_key:str = None ) :
15
- super().__init__(session, logger, depart_from_name, arrive_at_name, api_key)
16
- self.trip_name = trip_name
17
-
18
-
19
- async def async_init(self):
20
- await super()._async_init()
21
-
22
- params = {
23
- 'filter[revenue]' :'REVENUE',
24
- 'filter[name]' : self.trip_name
25
- }
26
- trips: list[MBTATrip] = await super()._fetch_trips(params)
27
-
28
- journey = Journey()
29
- journey.trip = trips[0]
30
-
31
- route: MBTARoute = await super()._fetch_route(journey.trip.route_id)
32
-
33
- journey.route = route
34
- self.journeys[trips[0].id] = journey
35
-
36
- async def update(self) -> list[Journey]:
37
-
38
- schedules = await self.__fetch_schedules()
39
- await super()._process_schedules(schedules)
40
-
41
- predictions = await self.__fetch_predictions()
42
- await super()._process_predictions(predictions)
43
-
44
- alerts = await self.__fetch_alerts()
45
- super()._process_alerts(alerts)
46
- return list(self.journeys.values())
47
-
48
-
49
- async def __fetch_schedules(self) -> list[MBTASchedule]:
50
-
51
- jounrey = next(iter(self.journeys.values()))
52
- jounrey.trip.id
53
-
54
- params = {
55
- 'filter[trip]': jounrey.trip.id,
56
- }
57
-
58
- schedules = await super()._fetch_schedules(params)
59
- return schedules
60
-
61
-
62
- async def __fetch_predictions(self) -> list[MBTAPrediction]:
63
-
64
- jounrey = next(iter(self.journeys.values()))
65
- jounrey.trip.id
66
-
67
- params = {
68
- 'filter[trip]': jounrey.trip.id,
69
- }
70
-
71
- predictions = await super()._fetch_predictions(params)
72
- return predictions
73
-
74
-
75
- async def __fetch_alerts(self) -> list[MBTAPrediction]:
76
-
77
- jounrey = next(iter(self.journeys.values()))
78
- jounrey.trip.id
79
-
80
- params = {
81
- 'filter[trip]': jounrey.trip.id,
82
- }
83
-
84
- alerts = await super()._fetch_alerts(params)
85
- return alerts
File without changes
File without changes
File without changes