MBTAclient 0.1.0__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.1.0/LICENSE +21 -0
- mbtaclient-0.1.0/PKG-INFO +31 -0
- mbtaclient-0.1.0/README.md +17 -0
- mbtaclient-0.1.0/pyproject.toml +18 -0
- mbtaclient-0.1.0/setup.cfg +4 -0
- mbtaclient-0.1.0/src/MBTAclient.egg-info/PKG-INFO +31 -0
- mbtaclient-0.1.0/src/MBTAclient.egg-info/SOURCES.txt +19 -0
- mbtaclient-0.1.0/src/MBTAclient.egg-info/dependency_links.txt +1 -0
- mbtaclient-0.1.0/src/MBTAclient.egg-info/top_level.txt +12 -0
- mbtaclient-0.1.0/src/__init__.py +0 -0
- mbtaclient-0.1.0/src/main.py +198 -0
- mbtaclient-0.1.0/src/mbta_alert.py +54 -0
- mbtaclient-0.1.0/src/mbta_auth.py +45 -0
- mbtaclient-0.1.0/src/mbta_client.py +88 -0
- mbtaclient-0.1.0/src/mbta_journey.py +143 -0
- mbtaclient-0.1.0/src/mbta_journeys.py +402 -0
- mbtaclient-0.1.0/src/mbta_prediction.py +40 -0
- mbtaclient-0.1.0/src/mbta_route.py +38 -0
- mbtaclient-0.1.0/src/mbta_schedule.py +28 -0
- mbtaclient-0.1.0/src/mbta_stop.py +38 -0
- mbtaclient-0.1.0/src/mbta_trip.py +30 -0
mbtaclient-0.1.0/LICENSE
ADDED
|
@@ -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
|
+
# MBTAclient
|
|
2
|
+
|
|
3
|
+
**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.
|
|
4
|
+
|
|
5
|
+
## Features
|
|
6
|
+
|
|
7
|
+
- Provide simplified access to MBTA routes, stops, trips, schedules, predictions, and alerts data
|
|
8
|
+
- Organize the above information into journeys, collections of trips from stop A to stop B
|
|
9
|
+
- Easily integrate with Home Assistant or other Python-based systems
|
|
10
|
+
|
|
11
|
+
## Contributing
|
|
12
|
+
|
|
13
|
+
Contributions are welcome! If you would like to contribute to this project, please fork the repository and submit a pull request.
|
|
14
|
+
|
|
15
|
+
## License
|
|
16
|
+
|
|
17
|
+
This project is licensed under the MIT License.
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "MBTAclient"
|
|
3
|
+
version = "0.1.0"
|
|
4
|
+
authors = [
|
|
5
|
+
{ name="Luca Chiabrera", email="luca.chiabrera@gmail.com" },
|
|
6
|
+
]
|
|
7
|
+
description = "A Python client for interacting with the MBTA API"
|
|
8
|
+
readme = "README.md"
|
|
9
|
+
requires-python = ">=3.8"
|
|
10
|
+
classifiers = [
|
|
11
|
+
"Programming Language :: Python :: 3",
|
|
12
|
+
"License :: OSI Approved :: MIT License",
|
|
13
|
+
"Operating System :: OS Independent",
|
|
14
|
+
]
|
|
15
|
+
|
|
16
|
+
[project.urls]
|
|
17
|
+
Homepage = "https://github.com/chiabre/MBTAclient"
|
|
18
|
+
Issues = "https://github.com/chiabre/MBTAclient/issues"
|
|
@@ -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,19 @@
|
|
|
1
|
+
LICENSE
|
|
2
|
+
README.md
|
|
3
|
+
pyproject.toml
|
|
4
|
+
src/__init__.py
|
|
5
|
+
src/main.py
|
|
6
|
+
src/mbta_alert.py
|
|
7
|
+
src/mbta_auth.py
|
|
8
|
+
src/mbta_client.py
|
|
9
|
+
src/mbta_journey.py
|
|
10
|
+
src/mbta_journeys.py
|
|
11
|
+
src/mbta_prediction.py
|
|
12
|
+
src/mbta_route.py
|
|
13
|
+
src/mbta_schedule.py
|
|
14
|
+
src/mbta_stop.py
|
|
15
|
+
src/mbta_trip.py
|
|
16
|
+
src/MBTAclient.egg-info/PKG-INFO
|
|
17
|
+
src/MBTAclient.egg-info/SOURCES.txt
|
|
18
|
+
src/MBTAclient.egg-info/dependency_links.txt
|
|
19
|
+
src/MBTAclient.egg-info/top_level.txt
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
File without changes
|
|
@@ -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())
|
|
@@ -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')})
|
|
@@ -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
|
+
|
|
@@ -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
|
+
|
|
@@ -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
|
+
|
|
@@ -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
|
+
|
|
@@ -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')
|
|
@@ -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')
|
|
@@ -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
|
+
|
|
@@ -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
|
|
@@ -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
|
+
|