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.
- {mbtaclient-0.2.5 → mbtaclient-0.2.7}/PKG-INFO +6 -1
- {mbtaclient-0.2.5 → mbtaclient-0.2.7}/pyproject.toml +9 -2
- {mbtaclient-0.2.5 → mbtaclient-0.2.7}/src/MBTAclient.egg-info/PKG-INFO +6 -1
- mbtaclient-0.2.7/src/mbtaclient/base_handler.py +353 -0
- {mbtaclient-0.2.5 → mbtaclient-0.2.7}/src/mbtaclient/journey.py +8 -9
- {mbtaclient-0.2.5 → mbtaclient-0.2.7}/src/mbtaclient/journey_stop.py +2 -2
- mbtaclient-0.2.7/src/mbtaclient/journeys_handler.py +108 -0
- {mbtaclient-0.2.5 → mbtaclient-0.2.7}/src/mbtaclient/mbta_alert.py +1 -1
- {mbtaclient-0.2.5 → mbtaclient-0.2.7}/src/mbtaclient/mbta_client.py +23 -10
- {mbtaclient-0.2.5 → mbtaclient-0.2.7}/src/mbtaclient/mbta_prediction.py +1 -1
- mbtaclient-0.2.7/src/mbtaclient/mbta_utils.py +105 -0
- mbtaclient-0.2.7/src/mbtaclient/trip_handler.py +115 -0
- mbtaclient-0.2.5/src/mbtaclient/base_handler.py +0 -335
- mbtaclient-0.2.5/src/mbtaclient/journeys_handler.py +0 -89
- mbtaclient-0.2.5/src/mbtaclient/mbta_utils.py +0 -50
- mbtaclient-0.2.5/src/mbtaclient/trip_handler.py +0 -85
- {mbtaclient-0.2.5 → mbtaclient-0.2.7}/LICENSE +0 -0
- {mbtaclient-0.2.5 → mbtaclient-0.2.7}/README.md +0 -0
- {mbtaclient-0.2.5 → mbtaclient-0.2.7}/setup.cfg +0 -0
- {mbtaclient-0.2.5 → mbtaclient-0.2.7}/src/MBTAclient.egg-info/SOURCES.txt +0 -0
- {mbtaclient-0.2.5 → mbtaclient-0.2.7}/src/MBTAclient.egg-info/dependency_links.txt +0 -0
- {mbtaclient-0.2.5 → mbtaclient-0.2.7}/src/MBTAclient.egg-info/requires.txt +0 -0
- {mbtaclient-0.2.5 → mbtaclient-0.2.7}/src/MBTAclient.egg-info/top_level.txt +0 -0
- {mbtaclient-0.2.5 → mbtaclient-0.2.7}/src/mbtaclient/__init__.py +0 -0
- {mbtaclient-0.2.5 → mbtaclient-0.2.7}/src/mbtaclient/__version__.py +0 -0
- {mbtaclient-0.2.5 → mbtaclient-0.2.7}/src/mbtaclient/mbta_route.py +0 -0
- {mbtaclient-0.2.5 → mbtaclient-0.2.7}/src/mbtaclient/mbta_schedule.py +0 -0
- {mbtaclient-0.2.5 → mbtaclient-0.2.7}/src/mbtaclient/mbta_stop.py +0 -0
- {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.
|
|
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.
|
|
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.
|
|
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]:
|
|
@@ -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.
|
|
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
|
-
|
|
6
|
-
from
|
|
7
|
-
from
|
|
8
|
-
from
|
|
9
|
-
from
|
|
10
|
-
from
|
|
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.
|
|
29
|
-
self.
|
|
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)
|