MBTAclient 0.1.0__py3-none-any.whl

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.
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2024 Luca Chiabrera
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,31 @@
1
+ Metadata-Version: 2.1
2
+ Name: MBTAclient
3
+ Version: 0.1.0
4
+ Summary: A Python client for interacting with the MBTA API
5
+ Author-email: Luca Chiabrera <luca.chiabrera@gmail.com>
6
+ Project-URL: Homepage, https://github.com/chiabre/MBTAclient
7
+ Project-URL: Issues, https://github.com/chiabre/MBTAclient/issues
8
+ Classifier: Programming Language :: Python :: 3
9
+ Classifier: License :: OSI Approved :: MIT License
10
+ Classifier: Operating System :: OS Independent
11
+ Requires-Python: >=3.8
12
+ Description-Content-Type: text/markdown
13
+ License-File: LICENSE
14
+
15
+ # MBTAclient
16
+
17
+ **MBTAclient** is a Python client library for interacting with the Massachusetts Bay Transportation Authority (MBTA) API. This library provides easy access to MBTA data, including routes, predictions, schedules, and more.
18
+
19
+ ## Features
20
+
21
+ - Provide simplified access to MBTA routes, stops, trips, schedules, predictions, and alerts data
22
+ - Organize the above information into journeys, collections of trips from stop A to stop B
23
+ - Easily integrate with Home Assistant or other Python-based systems
24
+
25
+ ## Contributing
26
+
27
+ Contributions are welcome! If you would like to contribute to this project, please fork the repository and submit a pull request.
28
+
29
+ ## License
30
+
31
+ This project is licensed under the MIT License.
@@ -0,0 +1,17 @@
1
+ __init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
2
+ main.py,sha256=Gm5ERtQVS-MCdMItJF_SQUDu-oxc5crtYg_Zt10j__0,8185
3
+ mbta_alert.py,sha256=QZjSOj9UqsLQqBhdUcuzVApQYSNrlvzVVZCHdxmfp2M,2786
4
+ mbta_auth.py,sha256=usnhR3qIJRUsCaqAAxTfnMrpjKT_0G-W6AuLXm8ld8s,1526
5
+ mbta_client.py,sha256=W17rdU5nAjklJ_Ne4uWZhKntWsoBzNIWQrkpUaLg6yM,3639
6
+ mbta_journey.py,sha256=9zJpG7MdJQ4E6vSWfeS_CK_mBVNhlgiJynqMx6uN1K4,6686
7
+ mbta_journeys.py,sha256=2DALtaEPtGebsn2atStTk3JocLLhPO44M5w5BoZBUYc,18946
8
+ mbta_prediction.py,sha256=_4W7NQ2au2BXmOHzx65ijDD6aSROxEVQC3h8A5TIz1M,2169
9
+ mbta_route.py,sha256=4et8yDqasfrbR0Q-Q4AnqHhLwkglEyZCmN0LvHl9304,1480
10
+ mbta_schedule.py,sha256=l-Stq-5zap8jn6HlRq6dtFa90cHVAYJOQc22uVB5NCw,1355
11
+ mbta_stop.py,sha256=dQ1CdaJO-DX-ztf3Ct9mwTqCrjOgDZ6ah1SnBKyxrlI,1940
12
+ mbta_trip.py,sha256=sljtTN55HJHpJeflAsP3rwelhViG11rpdeP1tRETgxg,1237
13
+ MBTAclient-0.1.0.dist-info/LICENSE,sha256=N_b-0MB8CsrVnmzC8Zkh90oQjHHuk_m7Y_h104W-KHk,1070
14
+ MBTAclient-0.1.0.dist-info/METADATA,sha256=stUxGhiFHZnURFwVVdMqaFHDpT5AL8VgR0BvLuMtu_Q,1245
15
+ MBTAclient-0.1.0.dist-info/WHEEL,sha256=HiCZjzuy6Dw0hdX5R3LCFPDmFS4BWl8H-8W39XfmgX4,91
16
+ MBTAclient-0.1.0.dist-info/top_level.txt,sha256=62nQxZk1J0LSilns91C4qTb71SQTbcY3YuYDW-K2W9E,135
17
+ MBTAclient-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (72.2.0)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,12 @@
1
+ __init__
2
+ main
3
+ mbta_alert
4
+ mbta_auth
5
+ mbta_client
6
+ mbta_journey
7
+ mbta_journeys
8
+ mbta_prediction
9
+ mbta_route
10
+ mbta_schedule
11
+ mbta_stop
12
+ mbta_trip
__init__.py ADDED
File without changes
main.py ADDED
@@ -0,0 +1,198 @@
1
+ import aiohttp
2
+
3
+ from mbta_client import MBTAClient
4
+ from mbta_stop import MBTAStop
5
+ from mbta_route import MBTARoute
6
+ from mbta_journeys import MBTAJourneys
7
+
8
+
9
+ from datetime import datetime
10
+ from typing import Dict, List, Any
11
+
12
+ API_KEY = None
13
+ MAX_JOURNEYS = 2
14
+
15
+ # ROUTE = 'Framingham/Worcester Line'
16
+ # ARRIVE_AT = 'Wellesley Square'
17
+ # DEPART_FROM = 'South Station'
18
+
19
+ # ROUTE = 'Framingham/Worcester Line'
20
+ # DEPART_FROM = 'Wellesley Square'
21
+ # ARRIVE_AT = 'South Station'
22
+
23
+ # ROUTE = 'Red'
24
+ # DEPART_FROM = 'South Station'
25
+ # ARRIVE_AT = 'Alewife'
26
+
27
+ ROUTE = None
28
+ DEPART_FROM = 'Copley'
29
+ ARRIVE_AT = 'Park Street'
30
+
31
+ # ROUTE = None
32
+ # DEPART_FROM = 'North Station'
33
+ # ARRIVE_AT = 'Swampscott'
34
+
35
+ # ROUTE = 'Wakefield Avenue & Truman Parkway - Ashmont Station'
36
+ # DEPART_FROM = 'Dorchester Ave @ Valley Rd'
37
+ # ARRIVE_AT = 'River St @ Standard St'
38
+
39
+ # ROUTE = 'Forest Hills Station - Back Bay Station'
40
+ # DEPART_FROM = 'Back Bay'
41
+ # ARRIVE_AT = 'Huntington Ave @ Opera Pl'
42
+
43
+ # DEPART_FROM = 'Charlestown Navy Yard'
44
+ # ARRIVE_AT = 'Long Wharf (South)'
45
+ # ROUTE = 'Charlestown Ferry'
46
+
47
+ # ROUTE = None
48
+ # DEPART_FROM = 'North Billerica'
49
+ # ARRIVE_AT = 'North Station'
50
+
51
+ # ROUTE = None
52
+ # DEPART_FROM = 'Back Bay'
53
+ # ARRIVE_AT = 'South Station'
54
+
55
+ # ROUTE = None
56
+ # DEPART_FROM = 'Pemberton Point'
57
+ # ARRIVE_AT = 'Summer St from Cushing Way to Water St (FLAG)'
58
+
59
+
60
+
61
+ async def main():
62
+ async with aiohttp.ClientSession() as session:
63
+
64
+ if API_KEY:
65
+ mbta_client = MBTAClient(session, API_KEY)
66
+ else:
67
+ mbta_client = MBTAClient(session)
68
+
69
+
70
+ params = {
71
+ 'filter[location_type]' :'0'
72
+ }
73
+
74
+ stops = await mbta_client.list_stops(params)
75
+
76
+
77
+ depart_from_stops = MBTAStop.get_stops_by_name(stops,DEPART_FROM )
78
+ arrive_at_stops = MBTAStop.get_stops_by_name(stops,ARRIVE_AT )
79
+
80
+ del stops
81
+
82
+ if ROUTE:
83
+ routes = await mbta_client.list_routes()
84
+ journey_route = None
85
+
86
+ for route in routes:
87
+ if route.long_name == ROUTE:
88
+ journey_route: MBTARoute = route
89
+ break # Found the route, no need to continue the loop
90
+
91
+ if journey_route:
92
+ journeys = MBTAJourneys(mbta_client, MAX_JOURNEYS, depart_from_stops, arrive_at_stops, journey_route)
93
+ del route
94
+ else:
95
+ journeys = MBTAJourneys(mbta_client, MAX_JOURNEYS, depart_from_stops, arrive_at_stops)
96
+ else:
97
+ journeys = MBTAJourneys(mbta_client, MAX_JOURNEYS, depart_from_stops, arrive_at_stops)
98
+
99
+ await journeys.populate()
100
+
101
+ for journey in journeys.journeys.values():
102
+
103
+ route_type = journeys.get_route_type(journey)
104
+
105
+ # if subway or ferry
106
+ if route_type == 0 or route_type == 1 or route_type == 4:
107
+
108
+ print("###########")
109
+ print("Line:", journeys.get_route_long_name(journey))
110
+ print("Type:", journeys.get_route_description(journey))
111
+ print("Color:", journeys.get_route_color(journey))
112
+ print()
113
+ print("Direction:", journeys.get_trip_direction(journey)+" to "+journeys.get_trip_destination(journey))
114
+ print("Destination:", journeys.get_trip_headsign(journey))
115
+ print()
116
+ for i in range(len(journey.journey_stops)):
117
+ print("Station:", journeys.get_stop_name(journey, i))
118
+ print("Platform:", journeys.get_platform_name(journey, i))
119
+ print("Time:", journeys.get_stop_time(journey, i))
120
+ print("Delay:", journeys.get_stop_delay(journey, i))
121
+ print("Time To:", journeys.get_stop_time_to(journey, i))
122
+ print()
123
+ for j in range(len(journey.alerts)):
124
+ print("Alert:" , journeys.get_alert_header(journey, j))
125
+ print()
126
+
127
+ # if train
128
+ elif route_type == 2:
129
+
130
+ print("###########")
131
+ print("Line:", journeys.get_route_long_name(journey))
132
+ print("Type:", journeys.get_route_description(journey))
133
+ print("Color:", journeys.get_route_color(journey))
134
+ print()
135
+ print("Train Number:", journeys.get_trip_name(journey))
136
+ print("Direction:", journeys.get_trip_direction(journey)+" to "+journeys.get_trip_destination(journey))
137
+ print("Destination:", journeys.get_trip_headsign(journey))
138
+ print()
139
+ for i in range(len(journey.journey_stops)):
140
+ print("Station:", journeys.get_stop_name(journey, i))
141
+ print("Platform:", journeys.get_platform_name(journey, i))
142
+ print("Time:", journeys.get_stop_time(journey, i))
143
+ print("Delay:", journeys.get_stop_delay(journey, i))
144
+ print("Time To:", journeys.get_stop_time_to(journey, i))
145
+ print()
146
+
147
+ for j in range(len(journey.alerts)):
148
+ print("Alert:" , journeys.get_alert_header(journey, j))
149
+ print()
150
+
151
+ #if bus
152
+ elif route_type == 3:
153
+
154
+ print("###########")
155
+ print("Line:", journeys.get_route_short_name(journey))
156
+ print("Type:", journeys.get_route_description(journey))
157
+ print("Color:", journeys.get_route_color(journey))
158
+ print()
159
+ print("Direction:", journeys.get_trip_direction(journey)+" to "+journeys.get_trip_destination(journey))
160
+ print("Destination:", journeys.get_trip_headsign(journey))
161
+ print()
162
+ for i in range(len(journey.journey_stops)):
163
+ print("Stop:", journeys.get_stop_name(journey, i))
164
+ print("Time:", journeys.get_stop_time(journey, i))
165
+ print("Delay:", journeys.get_stop_delay(journey, i))
166
+ print("Time To:", journeys.get_stop_time_to(journey, i))
167
+ print()
168
+
169
+ for j in range(len(journey.alerts)):
170
+ print("Alert:" , journeys.get_alert_header(journey, j))
171
+ print()
172
+
173
+ # elif journeys.get_route_type(journey) == 4:
174
+
175
+ # print("###########")
176
+ # print("Line:", journeys.get_route_long_name(journey))
177
+ # print("Type:", journeys.get_route_description(journey))
178
+ # print("Color:", journeys.get_route_color(journey))
179
+ # print()
180
+ # print("Direction:", journeys.get_trip_direction(journey)+" to "+journeys.get_trip_destination(journey))
181
+ # print("Destination:", journeys.get_trip_headsign(journey))
182
+ # print()
183
+ # for i in range(len(journey.journey_stops)):
184
+ # print("Stop:", journeys.get_stop_name(journey, i))
185
+ # print("Time:", journeys.get_stop_time(journey, i))
186
+ # print("Delay:", journeys.get_stop_delay(journey, i))
187
+ # print("Time To:", journeys.get_stop_time_to(journey, i))
188
+ # print()
189
+ # for j in range(len(journey.alerts)):
190
+ # print("Alert:" , journeys.get_alert_header(journey, j))
191
+ # print()
192
+ else:
193
+
194
+ print('ARGH!')
195
+
196
+ # Run the main function
197
+ import asyncio
198
+ asyncio.run(main())
mbta_alert.py ADDED
@@ -0,0 +1,54 @@
1
+ import typing
2
+ from typing import Any, Dict, List, Optional
3
+
4
+ class MBTAAlert:
5
+ """An alert object to hold information about an MBTA alert."""
6
+
7
+ def __init__(self, alert: Dict[str, Any]) -> None:
8
+ attributes = alert.get('attributes', {})
9
+
10
+ # Basic attributes
11
+ self.alert_id: str = alert.get('id', '')
12
+ self.active_period_start: Optional[str] = attributes.get('active_period', [{}])[0].get('start', None)
13
+ self.active_period_end: Optional[str] = attributes.get('active_period', [{}])[0].get('end', None)
14
+ self.cause: str = attributes.get('cause', '')
15
+ self.effect: str = attributes.get('effect', '')
16
+ self.header_text: str = attributes.get('header', '')
17
+ self.description_text: Optional[str] = attributes.get('description', None)
18
+ self.severity: int = attributes.get('severity', 0)
19
+ self.created_at: str = attributes.get('created_at', '')
20
+ self.updated_at: str = attributes.get('updated_at', '')
21
+
22
+ # Informed entities
23
+ self.informed_entities: List[Dict[str, Any]] = [
24
+ {
25
+ "activities": entity.get('activities', []),
26
+ "route": entity.get('route', ''),
27
+ "route_type": entity.get('route_type', 0),
28
+ "stop": entity.get('stop', ''),
29
+ "trip": entity.get('trip', ''),
30
+ "facility": entity.get('facility', '')
31
+ }
32
+ for entity in attributes.get('informed_entity', [])
33
+ ]
34
+
35
+ def __repr__(self) -> str:
36
+ return (f"MBTAalert(id={self.alert_id}, active_period_start={self.alert_active_period_start}, active_period_end={self.alert_active_period_end}, "
37
+ f"cause={self.alert_cause}, effect={self.alert_effect}, header_text={self.alert_header_text}, description_text={self.alert_description_text}, "
38
+ f"severity={self.alert_severity}, created_at={self.alert_created_at}, updated_at={self.alert_updated_at}, "
39
+ f"informed_entities={self.informed_entities})")
40
+
41
+ def __str__(self) -> str:
42
+ return f"Alert {self.alert_id}: {self.alert_header_text}"
43
+
44
+ def get_informed_stops(self) -> List[str]:
45
+ """Retrieve a list of unique stops from informed entities."""
46
+ return list({entity['stop'] for entity in self.informed_entities if entity.get('stop')})
47
+
48
+ def get_informed_trips(self) -> List[str]:
49
+ """Retrieve a list of unique trips from informed entities."""
50
+ return list({entity['trip'] for entity in self.informed_entities if entity.get('trip')})
51
+
52
+ def get_informed_routes(self) -> List[str]:
53
+ """Retrieve a list of unique routes from informed entities."""
54
+ return list({entity['route'] for entity in self.informed_entities if entity.get('route')})
mbta_auth.py ADDED
@@ -0,0 +1,45 @@
1
+ import logging
2
+
3
+ from aiohttp import ClientConnectionError, ClientResponse, ClientResponseError, ClientSession
4
+ from typing import Optional, Dict, Any
5
+
6
+ class MBTAAuth:
7
+ """Class to make authenticated requests"""
8
+
9
+ def __init__(self, session: ClientSession, host: str , api_key: Optional[str] = None) -> None:
10
+ """Initialize the auth."""
11
+ self._session = session
12
+ self._api_key = api_key
13
+ self._host = host
14
+
15
+ async def request(
16
+ self, method: str, path: str, params: Optional[Dict[str, Any]] = None) -> ClientResponse:
17
+ """Make an HTTP request with optional query parameters and JSON body."""
18
+
19
+ if params is None:
20
+ params = {}
21
+ if self._api_key:
22
+ params['api_key'] = self._api_key
23
+
24
+ try:
25
+ response = await self._session.request(
26
+ method,
27
+ f'https://{self._host}/{path}',
28
+ params=params
29
+ )
30
+
31
+ # Ensure response has a valid status code
32
+ response.raise_for_status()
33
+
34
+ return response
35
+
36
+ except ClientConnectionError as error:
37
+ logging.error(f"Connection error: {error}")
38
+ raise
39
+ except ClientResponseError as error:
40
+ logging.error(f"Client response error: {error.status} - {str(error)}")
41
+ raise
42
+ except Exception as error:
43
+ logging.error(f"An unexpected error occurred: {error}")
44
+ raise
45
+
mbta_client.py ADDED
@@ -0,0 +1,88 @@
1
+ import aiohttp
2
+ import logging
3
+
4
+ from typing import List, Optional, Dict, Any
5
+
6
+ from mbta_auth import MBTAAuth
7
+ from mbta_route import MBTARoute
8
+ from mbta_stop import MBTAStop
9
+ from mbta_schedule import MBTASchedule
10
+ from mbta_prediction import MBTAPrediction
11
+ from mbta_trip import MBTATrip
12
+ from mbta_alert import MBTAAlert
13
+
14
+ MBTA_DEFAULT_HOST = "api-v3.mbta.com"
15
+
16
+ ENDPOINTS = {
17
+ 'STOPS': 'stops',
18
+ 'ROUTES': 'routes',
19
+ 'PREDICTIONS': 'predictions',
20
+ 'SCHEDULES': 'schedules',
21
+ 'TRIPS': 'trips',
22
+ 'ALERTS': 'alerts'
23
+ }
24
+
25
+ class MBTAClient:
26
+ """Class to interact with the MBTA v3 API."""
27
+
28
+ def __init__(self, session: aiohttp.ClientSession, api_key: Optional[str] = None) -> None:
29
+ """Initialize the MBTA client with authentication and optional route details."""
30
+ if api_key:
31
+ self.auth = MBTAAuth(session, MBTA_DEFAULT_HOST, api_key)
32
+ else:
33
+ self.auth = MBTAAuth(session, MBTA_DEFAULT_HOST)
34
+
35
+
36
+ async def get_route(self, id: str, params: Optional[Dict[str, Any]] = None) -> MBTARoute:
37
+ """Get a route by its ID."""
38
+ route_data = await self._fetch_data(f'{ENDPOINTS["ROUTES"]}/{id}', params)
39
+ return MBTARoute(route_data['data'])
40
+
41
+ async def get_stop(self, id: str, params: Optional[Dict[str, Any]] = None) -> MBTAStop:
42
+ """Get a stop by its ID."""
43
+ stop_data = await self._fetch_data(f'{ENDPOINTS["STOPS"]}/{id}', params)
44
+ return MBTAStop(stop_data['data'])
45
+
46
+ async def get_trip(self, id: str, params: Optional[Dict[str, Any]] = None) -> MBTATrip:
47
+ """Get a trip by its ID."""
48
+ trip_data = await self._fetch_data(f'{ENDPOINTS["TRIPS"]}/{id}', params)
49
+ return MBTATrip(trip_data['data'])
50
+
51
+ async def list_routes(self, params: Optional[Dict[str, Any]] = None) -> List[MBTARoute]:
52
+ """List all routes."""
53
+ route_data = await self._fetch_data(ENDPOINTS['ROUTES'], params)
54
+ return [MBTARoute(item) for item in route_data['data']]
55
+
56
+ async def list_stops(self, params: Optional[Dict[str, Any]] = None) -> List[MBTAStop]:
57
+ """List all stops."""
58
+ stop_data = await self._fetch_data(ENDPOINTS['STOPS'], params)
59
+ return [MBTAStop(item) for item in stop_data['data']]
60
+
61
+ async def list_schedules(self, params: Optional[Dict[str, Any]] = None) -> List[MBTASchedule]:
62
+ """List all schedules."""
63
+ schedule_data = await self._fetch_data(ENDPOINTS['SCHEDULES'], params)
64
+ return [MBTASchedule(item) for item in schedule_data['data']]
65
+
66
+ async def list_predictions(self, params: Optional[Dict[str, Any]] = None) -> List[MBTAPrediction]:
67
+ """List all predictions."""
68
+ prediction_data = await self._fetch_data(ENDPOINTS['PREDICTIONS'], params)
69
+ return [MBTAPrediction(item) for item in prediction_data['data']]
70
+
71
+ async def list_alerts(self, params: Optional[Dict[str, Any]] = None) -> List[MBTAAlert]:
72
+ """List all predictions."""
73
+ alert_data = await self._fetch_data(ENDPOINTS['ALERTS'], params)
74
+ return [MBTAAlert(item) for item in alert_data['data']]
75
+
76
+ async def _fetch_data(self, endpoint: str, params: Optional[Dict[str, Any]] = None) -> Dict[str, Any]:
77
+ """Helper method to fetch data from API."""
78
+ response = await self.auth.request("get", endpoint, params)
79
+ try:
80
+ data = await response.json()
81
+ if 'data' not in data:
82
+ raise ValueError("Unexpected response format")
83
+ return data
84
+ except ValueError as error:
85
+ logging.error(f"Error processing JSON response: {error}")
86
+ raise
87
+
88
+
mbta_journey.py ADDED
@@ -0,0 +1,143 @@
1
+ from typing import Optional, List
2
+ from datetime import datetime
3
+ from mbta_stop import MBTAStop
4
+ from mbta_route import MBTARoute
5
+ from mbta_trip import MBTATrip
6
+ from mbta_alert import MBTAAlert
7
+ from mbta_prediction import MBTAPrediction
8
+ import logging
9
+
10
+ class MBTAJourney:
11
+ """A class to manage a journey with multiple stops."""
12
+
13
+ def __init__(self) -> None:
14
+
15
+ self.route: Optional[MBTARoute] = None
16
+ self.trip: Optional[MBTATrip] = None
17
+ self.alerts: List[MBTAAlert] = []
18
+ self.journey_stops: List[MBTAJourneyStop] = []
19
+
20
+
21
+ def __repr__(self) -> str:
22
+ stops_repr = ', '.join([repr(stop) for stop in self.journey_stops])
23
+ return f"MBTAjourney(route={self.route}, trip={self.trip}, stops=[{stops_repr}])"
24
+
25
+ def add_stop(self, stop: 'MBTAJourneyStop') -> None:
26
+ """Add a stop to the journey."""
27
+ self.journey_stops.append(stop)
28
+
29
+ def get_stop_ids(self) -> List[str]:
30
+ """Return a list of stop IDs for all stops in the journey."""
31
+ return [journey_stop.stop.id for journey_stop in self.journey_stops]
32
+
33
+ def find_jounrey_stop_by_id(self, stop_id: str) -> Optional['MBTAJourneyStop']:
34
+ """Return the MBTAjourneyStop with the given stop_id, or None if not found."""
35
+ for journey_stop in self.journey_stops:
36
+ if journey_stop.stop.id == stop_id:
37
+ return journey_stop
38
+ return None
39
+
40
+ def update_journey_stop(self, stop_index: int, stop: MBTAStop, arrival_time: str, departure_time: str, stop_sequence: int = None, arrival_uncertainty: Optional[str] = None, departure_uncertainty: Optional[str] = None):
41
+
42
+ if (stop_index == 0 and len(self.journey_stops ) == 0) or (stop_index == 1 and len(self.journey_stops ) == 1):
43
+ journey_stop = MBTAJourneyStop(stop, arrival_time, departure_time, stop_sequence, arrival_uncertainty, departure_uncertainty)
44
+ self.journey_stops.append(journey_stop)
45
+ else:
46
+ self.journey_stops[stop_index].update_stop(stop, arrival_time, departure_time, stop_sequence, arrival_uncertainty, departure_uncertainty)
47
+
48
+ class MBTAJourneyStop:
49
+ """A journey stop object to hold and manage arrival and departure details."""
50
+
51
+ def __init__(self, stop: MBTAStop, arrival_time: str, departure_time: str, stop_sequence: int = None, arrival_uncertainty: Optional[str] = None, departure_uncertainty: Optional[str] = None) -> None:
52
+ now = datetime.now().astimezone()
53
+
54
+ self.stop = stop
55
+ self.arrival_time = self.__parse_datetime(arrival_time)
56
+ self.real_arrival_time = None
57
+ self.arrival_uncertainty = MBTAPrediction.get_uncertainty_description(arrival_uncertainty)
58
+ self.arrival_delay = None
59
+ self.time_to_arrival = self.__time_to(self.arrival_time, now)
60
+
61
+ self.departure_time = self.__parse_datetime(departure_time)
62
+ self.real_departure_time = None
63
+ self.departure_uncertainty = MBTAPrediction.get_uncertainty_description(departure_uncertainty)
64
+ self.departure_delay = None
65
+ self.time_to_departure = self.__time_to(self.departure_time, now)
66
+
67
+ self.stop_sequence = stop_sequence
68
+
69
+ def __repr__(self) -> str:
70
+ return (f"MBTAjourneyStop(stop={repr(self.stop)}")
71
+
72
+ def update_stop(self, stop: MBTAStop, arrival_time: str, departure_time: str, stop_sequence: int = None, arrival_uncertainty: Optional[str] = None, departure_uncertainty: Optional[str] = None) -> None:
73
+ """Update the stop details, including real arrival and departure times, uncertainties, and delays."""
74
+ self.stop = stop
75
+ self.stop_sequence = stop_sequence
76
+ if arrival_time is not None:
77
+ self.real_arrival_time = self.__parse_datetime(arrival_time)
78
+ if self.arrival_time is not None:
79
+ self.arrival_delay = self.__compute_delay(self.real_arrival_time, self.arrival_time)
80
+ self.time_to_arrival = self.__time_to(self.real_arrival_time, datetime.now().astimezone())
81
+ if departure_time is not None:
82
+ self.real_departure_time = self.__parse_datetime(departure_time)
83
+ if self.departure_time is not None:
84
+ self.departure_delay = self.__compute_delay(self.real_departure_time, self.departure_time)
85
+ self.time_to_departure = self.__time_to(self.real_departure_time, datetime.now().astimezone())
86
+ if arrival_uncertainty is not None:
87
+ self.arrival_uncertainty = arrival_uncertainty
88
+ if departure_uncertainty is not None:
89
+ self.departure_uncertainty = departure_uncertainty
90
+
91
+ def get_time(self) -> Optional[datetime]:
92
+ """Return the most relevant time for the stop."""
93
+ if self.real_departure_time is not None:
94
+ return self.real_departure_time
95
+ if self.departure_time is not None:
96
+ return self.departure_time
97
+ if self.real_arrival_time is not None:
98
+ return self.real_arrival_time
99
+ if self.arrival_time is not None:
100
+ return self.arrival_time
101
+ return None
102
+
103
+ def get_delay(self) -> Optional[float]:
104
+ """Return the most relevant delay for the stop."""
105
+ if self.departure_delay is None and self.arrival_delay is None:
106
+ return None
107
+ if self.departure_delay is not None:
108
+ return self.departure_delay
109
+ if self.arrival_delay is not None:
110
+ return self.arrival_delay
111
+ return None
112
+
113
+ def get_time_to(self) -> Optional[float]:
114
+ """Return the most relevant time to for the stop."""
115
+ return self.time_to_arrival or self.time_to_departure
116
+
117
+ def get_uncertainty(self) -> Optional[str]:
118
+ """Return the most relevant time to for the stop."""
119
+ return self.arrival_uncertainty or self.departure_uncertainty
120
+
121
+ @staticmethod
122
+ def __time_to(time: Optional[datetime], now: datetime) -> Optional[float]:
123
+ if time is None:
124
+ return None
125
+ return (time - now).total_seconds()
126
+
127
+ @staticmethod
128
+ def __compute_delay(real_time: Optional[datetime], time: Optional[datetime]) -> Optional[float]:
129
+ if real_time is None or time is None:
130
+ return None
131
+ return (real_time - time).total_seconds()
132
+
133
+ @staticmethod
134
+ def __parse_datetime(time_str: Optional[str]) -> Optional[datetime]:
135
+ """Parse a string in ISO 8601 format to a datetime object."""
136
+ if time_str is None:
137
+ return None
138
+ try:
139
+ return datetime.fromisoformat(time_str)
140
+ except ValueError as e:
141
+ logging.error(f"Error parsing datetime: {e}")
142
+ return None
143
+
mbta_journeys.py ADDED
@@ -0,0 +1,402 @@
1
+ import logging
2
+ import traceback
3
+ from typing import Any, Dict, Optional, List
4
+ from datetime import datetime
5
+ from mbta_client import MBTAClient
6
+ from mbta_stop import MBTAStop
7
+ from mbta_route import MBTARoute
8
+ from mbta_trip import MBTATrip
9
+ from mbta_alert import MBTAAlert
10
+ from mbta_schedule import MBTASchedule
11
+ from mbta_prediction import MBTAPrediction
12
+ from mbta_journey import MBTAJourney, MBTAJourneyStop
13
+
14
+ class MBTAJourneys:
15
+ """A class to manage a journey on a route from/to stops."""
16
+
17
+ def __init__(self, mbta_client: MBTAClient, max_journeys: int, depart_from_stops: List[MBTAStop], arrive_at_stops: List[MBTAStop], route: MBTARoute = None) -> None:
18
+ self.mbta_client = mbta_client
19
+ self.max_journeys = max_journeys
20
+ self.depart_from_stops = depart_from_stops
21
+ self.arrive_at_stops = arrive_at_stops
22
+ self.route = route
23
+ self.journeys: Dict[str, MBTAJourney] = {}
24
+
25
+ async def populate(self):
26
+ """Populate the journeys with schedules, predictions, trips, routes, and alerts."""
27
+ try:
28
+ logging.debug("Starting to populate journeys...")
29
+ await self.__schedules()
30
+ await self.__predictions()
31
+ self.__finalize_journeys()
32
+ await self.__trips()
33
+ await self.__routes()
34
+ await self.__alerts()
35
+ logging.debug("Finished populating journeys.")
36
+ except Exception as e:
37
+ logging.error(f"Error populating journeys: {e}")
38
+ traceback.print_exc() # This will print the full traceback to the console
39
+ print()
40
+
41
+ async def __schedules(self):
42
+ """Retrieve and process schedules based on the provided stop IDs and route ID."""
43
+ now = datetime.now()
44
+ params = {
45
+ 'filter[stop]': ','.join(self._get_stop_ids_from_stops(self.depart_from_stops + self.arrive_at_stops)),
46
+ 'filter[min_time]': now.strftime('%H:%M'),
47
+ 'filter[date]': now.strftime('%Y-%m-%d'),
48
+ 'sort': 'departure_time'
49
+ }
50
+ if self.route:
51
+ params['filter[route]'] = self.route.id
52
+
53
+ schedules: List[MBTASchedule] = await self.mbta_client.list_schedules(params)
54
+
55
+ for schedule in schedules:
56
+ # if the schedule trip id not in the journeys
57
+ if schedule.trip_id not in self.journeys:
58
+ # journey stops are ordered by departure time
59
+ # if the first schedule stop is not in the depart stops ( = it's an arrival)
60
+ if schedule.stop_id not in self._get_stop_ids_from_stops(self.depart_from_stops):
61
+ # skip the schedule
62
+ continue
63
+ # create the journey
64
+ journey = MBTAJourney()
65
+ # add the journey to the journeys Dict using the trip_id as key
66
+ self.journeys[schedule.trip_id] = journey
67
+
68
+ # create the stop
69
+ journey_stop = MBTAJourneyStop(
70
+ stop = self._get_stop_by_id((self.depart_from_stops + self.arrive_at_stops), schedule.stop_id),
71
+ arrival_time=schedule.arrival_time,
72
+ departure_time=schedule.departure_time,
73
+ stop_sequence=schedule.stop_sequence
74
+ )
75
+ # add the stop to the journey
76
+ self.journeys[schedule.trip_id].add_stop(journey_stop)
77
+
78
+ # get the journey stops
79
+ stops = self.journeys[schedule.trip_id].journey_stops
80
+ # if there are 2 stops and
81
+ # the departure stop (stops[0]) id is not in the departure stop ids OR the arrival stop (stops[1]) id is not in the arrival stop ids
82
+ # ( = the trip is in the wrong direction)
83
+ if len(stops) == 2 and (stops[0].stop.id not in self._get_stop_ids_from_stops(self.depart_from_stops) or stops[1].stop.id not in self._get_stop_ids_from_stops(self.arrive_at_stops)):
84
+ # delete the yourney from the journeys Dict
85
+ del self.journeys[schedule.trip_id]
86
+
87
+ async def __predictions(self):
88
+ """Retrieve and process predictions based on the provided stop IDs and route ID."""
89
+
90
+ now = datetime.now().astimezone()
91
+
92
+ journey_stops = self.depart_from_stops + self.arrive_at_stops
93
+ journey_stops_ids = self._get_stop_ids_from_stops(self.depart_from_stops + self.arrive_at_stops)
94
+ depart_stop_ids = self._get_stop_ids_from_stops(self.depart_from_stops)
95
+ arrival_stop_ids = self._get_stop_ids_from_stops(self.arrive_at_stops)
96
+
97
+ params = {
98
+ 'filter[stop]': ','.join(journey_stops_ids),
99
+ 'sort': 'departure_time'
100
+ }
101
+ if self.route:
102
+ params['filter[route]'] = self.route.id
103
+
104
+ predictions: List[MBTAPrediction] = await self.mbta_client.list_predictions(params)
105
+
106
+ for prediction in predictions:
107
+
108
+ is_cancelled_trip = prediction.schedule_relationship in ['CANCELLED', 'SKIPPED']
109
+ is_past_trip = prediction.arrival_time and datetime.fromisoformat(prediction.arrival_time) < now
110
+ is_departure_stop = prediction.stop_id in depart_stop_ids
111
+ is_arrival_stop = prediction.stop_id in arrival_stop_ids
112
+
113
+ # If the trip of the prediction is cancelled/skipped
114
+ if is_cancelled_trip:
115
+ # remove the journey on the same trip_id from the journeys Dict
116
+ self.journeys.pop(prediction.trip_id, None)
117
+ continue
118
+
119
+ # If the trip of the prediction is in the past remove it
120
+ if is_past_trip:
121
+ # remove the journey on the same trip_id from the journeys Dict
122
+ self.journeys.pop(prediction.trip_id, None)
123
+ continue
124
+
125
+ # if the trip of the prediciton is not in the journeys Dict
126
+ if prediction.trip_id not in self.journeys:
127
+ # if the first stop is not a departure stop
128
+ if is_departure_stop:
129
+ # skipp the prediction
130
+ continue
131
+
132
+ # create the journey
133
+ journey = MBTAJourney()
134
+ # add the journey to the journeys Dict using the trip_id as key
135
+ self.journeys[prediction.trip_id] = journey
136
+
137
+ # add (smart update) the stop to the journey in position 0 (departure)
138
+ journey.update_journey_stop(
139
+ 0,
140
+ stop=self._get_stop_by_id(journey_stops, prediction.stop_id),
141
+ arrival_time=prediction.arrival_time,
142
+ departure_time=prediction.departure_time,
143
+ stop_sequence=prediction.stop_sequence,
144
+ arrival_uncertainty=prediction.arrival_uncertainty,
145
+ departure_uncertainty=prediction.departure_uncertainty
146
+ )
147
+
148
+ # if the prediciton trip is in the journeys
149
+ else:
150
+ # get the journey
151
+ journey: MBTAJourney = self.journeys[prediction.trip_id]
152
+
153
+ # if the prediction stop id is in the departure stop ids
154
+ if is_departure_stop:
155
+
156
+ # add (smart update) the stop to the journey in position 0 (departure)
157
+ journey.update_journey_stop(
158
+ 0,
159
+ stop=self._get_stop_by_id(journey_stops, prediction.stop_id),
160
+ arrival_time=prediction.arrival_time,
161
+ departure_time=prediction.departure_time,
162
+ stop_sequence=prediction.stop_sequence,
163
+ arrival_uncertainty=prediction.arrival_uncertainty,
164
+ departure_uncertainty=prediction.departure_uncertainty
165
+ )
166
+
167
+ # if the prediction stop id is in the arrival stop ids
168
+ elif is_arrival_stop:
169
+
170
+ # add (smart update) the stop to the journey in position 1 (arrival)
171
+ journey.update_journey_stop(
172
+ 1,
173
+ stop=self._get_stop_by_id(journey_stops, prediction.stop_id),
174
+ arrival_time=prediction.arrival_time,
175
+ departure_time=prediction.departure_time,
176
+ stop_sequence=prediction.stop_sequence,
177
+ arrival_uncertainty=prediction.arrival_uncertainty,
178
+ departure_uncertainty=prediction.departure_uncertainty
179
+ )
180
+
181
+ def __finalize_journeys(self):
182
+ """Clean up and sort valid journeys."""
183
+ processed_journeys = {}
184
+
185
+ for trip_id, journey in self.journeys.items():
186
+ # remove journey with 1 stop or with wrong stop sequence
187
+ stops = journey.journey_stops
188
+ if len(stops) < 2 or stops[0].stop_sequence > stops[1].stop_sequence:
189
+ continue
190
+ processed_journeys[trip_id] = journey
191
+
192
+ # Sort journeys based on departure time
193
+ sorted_journeys = dict(
194
+ sorted(
195
+ processed_journeys.items(),
196
+ key=lambda item: self._get_first_stop_departure_time(item[1])
197
+ )
198
+ )
199
+
200
+ # Limit the number of journeys to `self.max_journeys`
201
+ self.journeys = dict(list(sorted_journeys.items())[:self.max_journeys])
202
+
203
+ async def __trips(self):
204
+ """Retrieve trip details for each journey."""
205
+ for trip_id, journey in self.journeys.items():
206
+ try:
207
+ trip: MBTATrip = await self.mbta_client.get_trip(trip_id)
208
+ journey.trip = trip
209
+ except Exception as e:
210
+ logging.error(f"Error retrieving trip {trip_id}: {e}")
211
+
212
+ async def __routes(self):
213
+ """Retrieve route details for each journey."""
214
+
215
+ routes: List[MBTARoute] = []
216
+
217
+ if self.route is not None:
218
+ routes.append(self.route)
219
+ else:
220
+ route_ids: List[str] = []
221
+ for journey in self.journeys.values():
222
+ if journey.trip and journey.trip.route_id and journey.trip.route_id not in route_ids:
223
+ route_ids.append(journey.trip.route_id)
224
+
225
+ # Fetch route details
226
+ for route_id in route_ids:
227
+ try:
228
+ route: MBTARoute = await self.mbta_client.get_route(route_id)
229
+ routes.append(route)
230
+ except Exception as e:
231
+ logging.error(f"Error retrieving route {route_id}: {e}")
232
+
233
+ route_dict = {route.id: route for route in routes}
234
+
235
+ for journey in self.journeys.values():
236
+ if journey.trip and journey.trip.route_id in route_dict:
237
+ journey.route = route_dict[journey.trip.route_id]
238
+
239
+ async def __alerts(self):
240
+ """Retrieve and associate alerts with the relevant journeys."""
241
+ params = {
242
+ 'filter[stop]': ','.join(self._get_all_stop_ids()),
243
+ 'filter[trip]': ','.join(self._get_all_trip_ids()),
244
+ 'filter[route]': ','.join(self._get_all_route_ids()),
245
+ 'filter[activity]': 'BOARD,EXIT,RIDE'
246
+ }
247
+
248
+ alerts: List[MBTAAlert] = await self.mbta_client.list_alerts(params)
249
+
250
+ now = datetime.now().astimezone()
251
+
252
+ for alert in alerts:
253
+
254
+ if datetime.fromisoformat(alert.active_period_start) > now and datetime.fromisoformat(alert.active_period_end) < now:
255
+
256
+ for informed_entity in alert.informed_entities:
257
+ for journey in self.journeys.values():
258
+
259
+ # if the alert is already in the journey
260
+ if alert in journey.alerts:
261
+ # skip the journey
262
+ continue
263
+ # if informed entity stop is not null and the stop id is in not in the journey stop id
264
+ if informed_entity.get('stop') != '' and informed_entity['stop'] not in journey.get_stop_ids():
265
+ # skip the journey
266
+ continue
267
+ # if informed entity trip is not null and the trip id is not in the journey trip id
268
+ if informed_entity.get('trip') != '' and informed_entity['trip'] != journey.trip.id:
269
+ # skip the journey
270
+ continue
271
+ # if informed entity route is not null and the route id is not in the journey route id
272
+ if informed_entity.get('route') != '' and informed_entity['route'] != journey.route.id:
273
+ # skip the journey
274
+ continue
275
+ # If the informed entity stop is a departure and the informed entity activities don't include BOARD or RIDE
276
+ if informed_entity['stop'] == journey.journey_stops[0].stop.id and not any(activity in informed_entity.get('activities', []) for activity in ['BOARD', 'RIDE']):
277
+ # Skip the journey
278
+ continue
279
+ # If the informed entity stop is an arrival and the informed entity activities don't include EXIT or RIDE
280
+ if informed_entity['stop'] == journey.journey_stops[1].stop.id and not any(activity in informed_entity.get('activities', []) for activity in ['EXIT', 'RIDE']):
281
+ # Skip the journey
282
+ continue
283
+ # add the alert to the journy
284
+ journey.alerts.append(alert)
285
+
286
+ def _get_stop_ids_from_stops(self, stops: List[MBTAStop]) -> List[str]:
287
+ """Extract stop IDs from a list of MBTAstop objects."""
288
+ stop_ids = [stop.id for stop in stops]
289
+ return stop_ids
290
+
291
+ def _get_stop_by_id(self, stops: List[MBTAStop], stop_id: str) -> Optional[MBTAStop]:
292
+ """Retrieve a stop from the list of MBTAstop objects based on the stop ID."""
293
+ for stop in stops:
294
+ if stop.id == stop_id:
295
+ return stop
296
+ return None
297
+
298
+ def _get_first_stop_departure_time(self, journey: MBTAJourney) -> datetime:
299
+ """Get the departure time of the first stop in a journey."""
300
+ departure_stop = journey.journey_stops[0]
301
+ return departure_stop.get_time()
302
+
303
+ def _get_all_stop_ids(self) -> List[str]:
304
+ """Retrieve a list of all unique stop IDs from the journeys."""
305
+ stop_ids = set()
306
+ for journey in self.journeys.values():
307
+ stop_ids.update(journey.get_stop_ids())
308
+ return sorted(list(stop_ids))
309
+
310
+ def _get_all_trip_ids(self) -> List[str]:
311
+ """Retrieve a list of all trip IDs from the journeys."""
312
+ return list(self.journeys.keys())
313
+
314
+ def _get_all_route_ids(self) -> List[str]:
315
+ """Retrieve a list of all unique route IDs from the journeys."""
316
+ route_ids = set()
317
+ for journey in self.journeys.values():
318
+ if journey.trip and journey.trip.route_id:
319
+ route_ids.add(journey.trip.route_id)
320
+ return sorted(list(route_ids))
321
+
322
+ def get_route_short_name(self, journey: MBTAJourney) -> Optional[str]:
323
+ """Get the long name of the route for a given journey."""
324
+ if journey.route:
325
+ return journey.route.short_name
326
+ return None
327
+
328
+ def get_route_long_name(self, journey: MBTAJourney) -> Optional[str]:
329
+ """Get the long name of the route for a given journey."""
330
+ if journey.route:
331
+ return journey.route.long_name
332
+ return None
333
+
334
+ def get_route_color(self, journey: MBTAJourney) -> Optional[str]:
335
+ """Get the color of the route for a given journey."""
336
+ if journey.route:
337
+ return journey.route.color
338
+ return None
339
+
340
+ def get_route_description(self, journey: MBTAJourney) -> Optional[str]:
341
+ """Get the description of the route for a given journey."""
342
+ if journey.route:
343
+ return MBTARoute.get_route_type_desc_by_type_id(journey.route.type)
344
+ return None
345
+
346
+ def get_route_type(self, journey: MBTAJourney) -> Optional[str]:
347
+ """Get the description of the route for a given journey."""
348
+ if journey.route:
349
+ return journey.route.type
350
+ return None
351
+
352
+ def get_trip_headsign(self, journey: MBTAJourney) -> Optional[str]:
353
+ """Get the headsign of the trip for a given journey."""
354
+ if journey.trip:
355
+ return journey.trip.headsign
356
+ return None
357
+
358
+ def get_trip_name(self, journey: MBTAJourney) -> Optional[str]:
359
+ if journey.trip:
360
+ return journey.trip.name
361
+ return None
362
+
363
+ def get_trip_destination(self, journey: MBTAJourney) -> Optional[str]:
364
+ if journey.trip and journey.route:
365
+ trip_direction = journey.trip.direction_id
366
+ return journey.route.direction_destinations[trip_direction]
367
+ return None
368
+
369
+ def get_trip_direction(self, journey: MBTAJourney) -> Optional[str]:
370
+ if journey.trip and journey.route:
371
+ trip_direction = journey.trip.direction_id
372
+ return journey.route.direction_names[trip_direction]
373
+ return None
374
+
375
+ def get_stop_name(self, journey: MBTAJourney, stop_index: int) -> Optional[str]:
376
+ journey_stop = journey.journey_stops[stop_index]
377
+ return journey_stop.stop.name
378
+
379
+ def get_platform_name(self, journey: MBTAJourney, stop_index: int) -> Optional[str]:
380
+ journey_stop = journey.journey_stops[stop_index]
381
+ return journey_stop.stop.platform_name
382
+
383
+ def get_stop_time(self, journey: MBTAJourney, stop_index: int) -> Optional[datetime]:
384
+ journey_stop = journey.journey_stops[stop_index]
385
+ return journey_stop.get_time()
386
+
387
+ def get_stop_delay(self, journey: MBTAJourney, stop_index: int) -> Optional[float]:
388
+ journey_stop = journey.journey_stops[stop_index]
389
+ return journey_stop.get_delay()
390
+
391
+ def get_stop_time_to(self, journey: MBTAJourney, stop_index: int) -> Optional[float]:
392
+ journey_stop = journey.journey_stops[stop_index]
393
+ return journey_stop.get_time_to()
394
+
395
+ def get_stop_uncertainty(self, journey: MBTAJourney, stop_index: int) -> Optional[str]:
396
+ journey_stop = journey.journey_stops[stop_index]
397
+ return journey_stop.get_uncertainty()
398
+
399
+ def get_alert_header(self, journey: MBTAJourney, alert_index: int) -> Optional[str]:
400
+ alert = journey.alerts[alert_index]
401
+ return alert.header_text
402
+
mbta_prediction.py ADDED
@@ -0,0 +1,40 @@
1
+ import typing
2
+ from typing import Any, Dict, Optional
3
+
4
+ class MBTAPrediction:
5
+ """A prediction object to hold information about a prediction."""
6
+
7
+ UNCERTAINTY = {
8
+ '60': 'Trip that has already started',
9
+ '120': 'Trip not started and a vehicle is awaiting departure at the origin',
10
+ '300': 'Vehicle has not yet been assigned to the trip',
11
+ '301': 'Vehicle appears to be stalled or significantly delayed',
12
+ '360': 'Trip not started and a vehicle is completing a previous trip'
13
+ }
14
+
15
+ def __init__(self, prediction: Dict[str, Any]) -> None:
16
+ attributes = prediction.get('attributes', {})
17
+
18
+ self.id: str = prediction.get('id', '')
19
+ self.arrival_time: str = attributes.get('arrival_time', '')
20
+ self.arrival_uncertainty: str = self.get_uncertainty_description(attributes.get('arrival_uncertainty', ''))
21
+ self.departure_time: str = attributes.get('departure_time', '')
22
+ self.departure_uncertainty: str = self.get_uncertainty_description(attributes.get('departure_uncertainty', ''))
23
+ self.direction_id: int = attributes.get('direction_id', 0)
24
+ self.last_trip: Optional[bool] = attributes.get('last_trip')
25
+ self.revenue: Optional[bool] = attributes.get('revenue')
26
+ self.schedule_relationship: str = attributes.get('schedule_relationship', '')
27
+ self.status: str = attributes.get('status', '')
28
+ self.stop_sequence: int = attributes.get('stop_sequence', 0)
29
+ self.update_type: str = attributes.get('update_type', '')
30
+
31
+ self.route_id: str = prediction.get('relationships', {}).get('route', {}).get('data', {}).get('id', '')
32
+ self.stop_id: str = prediction.get('relationships', {}).get('stop', {}).get('data', {}).get('id', '')
33
+ self.trip_id: str = prediction.get('relationships', {}).get('trip', {}).get('data', {}).get('id', '')
34
+
35
+ def __repr__(self) -> str:
36
+ return (f"MBTAprediction(id={self.id}, route_id={self.route_id}, stop_id={self.stop_id}, trip_id={self.trip_id})")
37
+
38
+ @staticmethod
39
+ def get_uncertainty_description(key: str) -> str:
40
+ return MBTAPrediction.UNCERTAINTY.get(key, 'None')
mbta_route.py ADDED
@@ -0,0 +1,38 @@
1
+ import typing
2
+ from typing import Any, Dict, List
3
+
4
+ class MBTARoute:
5
+ """A route object to hold information about a route."""
6
+
7
+ ROUTE_TYPES= {
8
+ # 0: 'Light Rail', # Example: Green Line
9
+ # 1: 'Heavy Rail', # Example: Red Line
10
+ 0: 'Subway',
11
+ 1: 'Subway',
12
+ 2: 'Commuter Rail',
13
+ 3: 'Bus',
14
+ 4: 'Ferry'
15
+ }
16
+
17
+ def __init__(self, route: Dict[str, Any]) -> None:
18
+ attributes = route.get('attributes', {})
19
+
20
+ self.id: str = route.get('id', '')
21
+ self.color: str = attributes.get('color', '')
22
+ self.description: str = attributes.get('description', '')
23
+ self.direction_destinations: List[str] = attributes.get('direction_destinations', [])
24
+ self.direction_names: List[str] = attributes.get('direction_names', [])
25
+ self.fare_class: str = attributes.get('fare_class', '')
26
+ self.long_name: str = attributes.get('long_name', '')
27
+ self.short_name: str = attributes.get('short_name', '')
28
+ self.sort_order: int = attributes.get('sort_order', 0)
29
+ self.text_color: str = attributes.get('text_color', '')
30
+ self.type: str = attributes.get('type', '')
31
+
32
+ def __repr__(self) -> str:
33
+ return (f"MBTAroute(id={self.id}, short_name={self.short_name})")
34
+
35
+ @staticmethod
36
+ def get_route_type_desc_by_type_id(route_type: int) -> str:
37
+ """Get a description of the route type."""
38
+ return MBTARoute.ROUTE_TYPES.get(route_type, 'Unknown')
mbta_schedule.py ADDED
@@ -0,0 +1,28 @@
1
+ import typing
2
+ from typing import Any, Dict
3
+ from datetime import datetime, timezone
4
+
5
+ class MBTASchedule:
6
+ """A schedule object to hold information about a schedule."""
7
+
8
+ def __init__(self, schedule: Dict[str, Any]) -> None:
9
+ attributes = schedule.get('attributes', {})
10
+
11
+ self.id: str = schedule.get('id', '')
12
+ self.arrival_time: str = attributes.get('arrival_time', '')
13
+ self.departure_time: str = attributes.get('departure_time', '')
14
+ self.direction_id: int = attributes.get('direction_id', 0)
15
+ self.drop_off_type: str = attributes.get('drop_off_type', '')
16
+ self.pickup_type: str = attributes.get('pickup_type', '')
17
+ self.stop_headsign: str = attributes.get('stop_headsign', '')
18
+ self.stop_sequence: int = attributes.get('stop_sequence', 0)
19
+ self.timepoint: bool = attributes.get('timepoint', False)
20
+
21
+ relationships = schedule.get('relationships', {})
22
+ self.route_id: str = relationships.get('route', {}).get('data', {}).get('id', '')
23
+ self.stop_id: str = relationships.get('stop', {}).get('data', {}).get('id', '')
24
+ self.trip_id: str = relationships.get('trip', {}).get('data', {}).get('id', '')
25
+
26
+ def __repr__(self) -> str:
27
+ return (f"MBTAschedule(id={self.id}, route_id={self.route_id}, stop_id={self.stop_id}, trip_id={self.trip_id})")
28
+
mbta_stop.py ADDED
@@ -0,0 +1,38 @@
1
+ import typing
2
+ from typing import Any, Dict, Optional, List
3
+
4
+ class MBTAStop:
5
+ """A stop object to hold information about a stop."""
6
+
7
+ def __init__(self, stop: Dict[str, Any]) -> None:
8
+ attributes = stop.get('attributes', {})
9
+
10
+ self.id: str = stop.get('id', '')
11
+ self.address: str = attributes.get('address', '')
12
+ self.at_street: str = attributes.get('at_street', '')
13
+ self.description: str = attributes.get('description', '')
14
+ self.latitude: float = attributes.get('latitude', 0.0)
15
+ self.location_type: int = attributes.get('location_type', 0)
16
+ self.longitude: float = attributes.get('longitude', 0.0)
17
+ self.municipality: str = attributes.get('municipality', '')
18
+ self.name: str = attributes.get('name', '')
19
+ self.on_street: str = attributes.get('on_street', '')
20
+ self.platform_code: str = attributes.get('platform_code', '')
21
+ self.platform_name: str = attributes.get('platform_name', '')
22
+ self.vehicle_type: int = attributes.get('vehicle_type', 0)
23
+ self.wheelchair_boarding: int = attributes.get('wheelchair_boarding', 0)
24
+
25
+ def __repr__(self) -> str:
26
+ return (f"MBTAstop(id={self.id}, name={self.name})")
27
+
28
+ @classmethod
29
+ def get_stop_ids_by_name(cls, stops: List['MBTAStop'], stop_name: str) -> List[str]:
30
+ """Given a list of MBTAstop objects and a stop name, return a list of stop ids that match the stop name."""
31
+ matching_stop_ids = [stop.id for stop in stops if stop.name.lower() == stop_name.lower()]
32
+ return matching_stop_ids
33
+
34
+ @classmethod
35
+ def get_stops_by_name(cls, stops: List['MBTAStop'], stop_name: str) -> List['MBTAStop']:
36
+ """Given a list of MBTAstop objects and a stop name, return a list of MBTAstop objects that match the stop name."""
37
+ matching_stops = [stop for stop in stops if stop.name.lower() == stop_name.lower()]
38
+ return matching_stops
mbta_trip.py ADDED
@@ -0,0 +1,30 @@
1
+ import typing
2
+ from typing import Any, Dict, Optional
3
+
4
+ class MBTATrip:
5
+ """A trip object to hold information about a trip."""
6
+
7
+ def __init__(self, trip: Dict[str, Any]) -> None:
8
+ attributes = trip.get('attributes', {})
9
+
10
+ self.id: str = trip.get('id', '')
11
+ self.name: str = attributes.get('name', '')
12
+ self.headsign: str = attributes.get('headsign', '')
13
+ self.direction_id: int = attributes.get('direction_id', 0)
14
+ self.block_id: str = attributes.get('block_id', '')
15
+ self.shape_id: str = attributes.get('shape_id', '')
16
+ self.wheelchair_accessible: Optional[bool] = attributes.get('wheelchair_accessible')
17
+ self.bikes_allowed: Optional[bool] = attributes.get('bikes_allowed')
18
+ self.schedule_relationship: str = attributes.get('schedule_relationship', '')
19
+
20
+ self.route_id: str = trip.get('relationships', {}).get('route', {}).get('data', {}).get('id', '')
21
+
22
+ service_data = trip.get('relationships', {}).get('service', {}).get('data', {})
23
+ self.service_id: str = service_data.get('id', '') if service_data else ''
24
+
25
+
26
+ def __repr__(self) -> str:
27
+ return (f"MBTAtrip(id={self.id}, route_id={self.route_id})")
28
+
29
+
30
+