MBTAclient 0.2.7__tar.gz → 0.2.8__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {mbtaclient-0.2.7 → mbtaclient-0.2.8}/PKG-INFO +2 -2
- {mbtaclient-0.2.7 → mbtaclient-0.2.8}/pyproject.toml +2 -2
- {mbtaclient-0.2.7 → mbtaclient-0.2.8}/src/MBTAclient.egg-info/PKG-INFO +2 -2
- {mbtaclient-0.2.7 → mbtaclient-0.2.8}/src/MBTAclient.egg-info/SOURCES.txt +10 -1
- {mbtaclient-0.2.7 → mbtaclient-0.2.8}/src/mbtaclient/journey_stop.py +1 -1
- {mbtaclient-0.2.7 → mbtaclient-0.2.8}/src/mbtaclient/mbta_client.py +2 -2
- {mbtaclient-0.2.7 → mbtaclient-0.2.8}/src/mbtaclient/mbta_utils.py +10 -0
- mbtaclient-0.2.8/tests/test_journey_stop.py +167 -0
- mbtaclient-0.2.8/tests/test_mbta_alert.py +139 -0
- mbtaclient-0.2.8/tests/test_mbta_client.py +109 -0
- mbtaclient-0.2.8/tests/test_mbta_prediction.py +97 -0
- mbtaclient-0.2.8/tests/test_mbta_route.py +58 -0
- mbtaclient-0.2.8/tests/test_mbta_schedule.py +88 -0
- mbtaclient-0.2.8/tests/test_mbta_stop.py +68 -0
- mbtaclient-0.2.8/tests/test_mbta_trip.py +68 -0
- mbtaclient-0.2.8/tests/test_mbta_utils.py +126 -0
- {mbtaclient-0.2.7 → mbtaclient-0.2.8}/LICENSE +0 -0
- {mbtaclient-0.2.7 → mbtaclient-0.2.8}/README.md +0 -0
- {mbtaclient-0.2.7 → mbtaclient-0.2.8}/setup.cfg +0 -0
- {mbtaclient-0.2.7 → mbtaclient-0.2.8}/src/MBTAclient.egg-info/dependency_links.txt +0 -0
- {mbtaclient-0.2.7 → mbtaclient-0.2.8}/src/MBTAclient.egg-info/requires.txt +0 -0
- {mbtaclient-0.2.7 → mbtaclient-0.2.8}/src/MBTAclient.egg-info/top_level.txt +0 -0
- {mbtaclient-0.2.7 → mbtaclient-0.2.8}/src/mbtaclient/__init__.py +0 -0
- {mbtaclient-0.2.7 → mbtaclient-0.2.8}/src/mbtaclient/__version__.py +0 -0
- {mbtaclient-0.2.7 → mbtaclient-0.2.8}/src/mbtaclient/base_handler.py +0 -0
- {mbtaclient-0.2.7 → mbtaclient-0.2.8}/src/mbtaclient/journey.py +0 -0
- {mbtaclient-0.2.7 → mbtaclient-0.2.8}/src/mbtaclient/journeys_handler.py +0 -0
- {mbtaclient-0.2.7 → mbtaclient-0.2.8}/src/mbtaclient/mbta_alert.py +0 -0
- {mbtaclient-0.2.7 → mbtaclient-0.2.8}/src/mbtaclient/mbta_prediction.py +0 -0
- {mbtaclient-0.2.7 → mbtaclient-0.2.8}/src/mbtaclient/mbta_route.py +0 -0
- {mbtaclient-0.2.7 → mbtaclient-0.2.8}/src/mbtaclient/mbta_schedule.py +0 -0
- {mbtaclient-0.2.7 → mbtaclient-0.2.8}/src/mbtaclient/mbta_stop.py +0 -0
- {mbtaclient-0.2.7 → mbtaclient-0.2.8}/src/mbtaclient/mbta_trip.py +0 -0
- {mbtaclient-0.2.7 → mbtaclient-0.2.8}/src/mbtaclient/trip_handler.py +0 -0
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: MBTAclient
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.8
|
|
4
4
|
Summary: A Python client for interacting with the MBTA API
|
|
5
5
|
Author-email: Luca Chiabrera <luca.chiabrera@gmail.com>
|
|
6
6
|
License: MIT
|
|
7
7
|
Project-URL: Homepage, https://github.com/chiabre/MBTAclient
|
|
8
8
|
Project-URL: Issues, https://github.com/chiabre/MBTAclient/issues
|
|
9
9
|
Classifier: Programming Language :: Python :: 3
|
|
10
|
-
Classifier: Programming Language :: Python :: 3.
|
|
10
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
11
11
|
Classifier: License :: OSI Approved :: MIT License
|
|
12
12
|
Classifier: Operating System :: OS Independent
|
|
13
13
|
Requires-Python: >=3.12
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "MBTAclient"
|
|
7
|
-
version = "0.2.
|
|
7
|
+
version = "0.2.8"
|
|
8
8
|
description = "A Python client for interacting with the MBTA API"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
requires-python = ">=3.12"
|
|
@@ -15,7 +15,7 @@ dependencies = [
|
|
|
15
15
|
license = { text = "MIT" }
|
|
16
16
|
classifiers = [
|
|
17
17
|
"Programming Language :: Python :: 3",
|
|
18
|
-
"Programming Language :: Python :: 3.
|
|
18
|
+
"Programming Language :: Python :: 3.12",
|
|
19
19
|
"License :: OSI Approved :: MIT License",
|
|
20
20
|
"Operating System :: OS Independent"
|
|
21
21
|
]
|
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: MBTAclient
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.8
|
|
4
4
|
Summary: A Python client for interacting with the MBTA API
|
|
5
5
|
Author-email: Luca Chiabrera <luca.chiabrera@gmail.com>
|
|
6
6
|
License: MIT
|
|
7
7
|
Project-URL: Homepage, https://github.com/chiabre/MBTAclient
|
|
8
8
|
Project-URL: Issues, https://github.com/chiabre/MBTAclient/issues
|
|
9
9
|
Classifier: Programming Language :: Python :: 3
|
|
10
|
-
Classifier: Programming Language :: Python :: 3.
|
|
10
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
11
11
|
Classifier: License :: OSI Approved :: MIT License
|
|
12
12
|
Classifier: Operating System :: OS Independent
|
|
13
13
|
Requires-Python: >=3.12
|
|
@@ -20,4 +20,13 @@ src/mbtaclient/mbta_schedule.py
|
|
|
20
20
|
src/mbtaclient/mbta_stop.py
|
|
21
21
|
src/mbtaclient/mbta_trip.py
|
|
22
22
|
src/mbtaclient/mbta_utils.py
|
|
23
|
-
src/mbtaclient/trip_handler.py
|
|
23
|
+
src/mbtaclient/trip_handler.py
|
|
24
|
+
tests/test_journey_stop.py
|
|
25
|
+
tests/test_mbta_alert.py
|
|
26
|
+
tests/test_mbta_client.py
|
|
27
|
+
tests/test_mbta_prediction.py
|
|
28
|
+
tests/test_mbta_route.py
|
|
29
|
+
tests/test_mbta_schedule.py
|
|
30
|
+
tests/test_mbta_stop.py
|
|
31
|
+
tests/test_mbta_trip.py
|
|
32
|
+
tests/test_mbta_utils.py
|
|
@@ -50,7 +50,7 @@ class JourneyStop:
|
|
|
50
50
|
self.real_departure_time = MBTAUtils.parse_datetime(departure_time)
|
|
51
51
|
if self.departure_time is not None:
|
|
52
52
|
self.departure_delay = MBTAUtils.calculate_time_difference(self.real_departure_time, self.departure_time)
|
|
53
|
-
|
|
53
|
+
|
|
54
54
|
def get_time(self) -> Optional[datetime]:
|
|
55
55
|
"""Return the most relevant time for the stop."""
|
|
56
56
|
if self.real_arrival_time is not None:
|
|
@@ -92,12 +92,12 @@ class MBTAClient:
|
|
|
92
92
|
response = await self.request("get", endpoint, params)
|
|
93
93
|
data = await response.json()
|
|
94
94
|
if 'data' not in data:
|
|
95
|
-
raise ValueError("
|
|
95
|
+
raise ValueError("missing 'data'")
|
|
96
96
|
return data
|
|
97
97
|
except Exception as error:
|
|
98
98
|
self.logger.error(f"Error fetching data: {error}")
|
|
99
99
|
raise
|
|
100
|
-
|
|
100
|
+
|
|
101
101
|
async def request(
|
|
102
102
|
self, method: str, path: str, params: Optional[dict[str, Any]] = None) -> aiohttp.ClientResponse:
|
|
103
103
|
"""Make an HTTP request with Optional query parameters and JSON body."""
|
|
@@ -39,6 +39,16 @@ class MBTAUtils:
|
|
|
39
39
|
if time is None:
|
|
40
40
|
logger.warning("time_to: Provided 'time' is None.")
|
|
41
41
|
return None
|
|
42
|
+
# Check if both datetime objects have timezone info
|
|
43
|
+
if time.tzinfo != now.tzinfo:
|
|
44
|
+
# Make both datetimes the same type: either both naive or both aware
|
|
45
|
+
if time.tzinfo is None and now.tzinfo is not None:
|
|
46
|
+
# Convert time to aware by using the timezone of `now`
|
|
47
|
+
time = time.replace(tzinfo=now.tzinfo)
|
|
48
|
+
elif time.tzinfo is not None and now.tzinfo is None:
|
|
49
|
+
# Convert now to naive by stripping timezone info
|
|
50
|
+
now = now.replace(tzinfo=None)
|
|
51
|
+
# Now perform the calculation
|
|
42
52
|
return (time - now).total_seconds()
|
|
43
53
|
|
|
44
54
|
@staticmethod
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
import pytest
|
|
2
|
+
from datetime import datetime, timedelta
|
|
3
|
+
from src.mbtaclient.mbta_stop import MBTAStop
|
|
4
|
+
from src.mbtaclient.mbta_utils import MBTAUtils
|
|
5
|
+
from src.mbtaclient.journey_stop import JourneyStop
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def test_journey_stop_init():
|
|
9
|
+
"""Test initialization of JourneyStop object."""
|
|
10
|
+
stop = MBTAStop(stop={"id": "test_stop_id", "name": "Test Stop"})
|
|
11
|
+
future_time = datetime.now() + timedelta(minutes=30)
|
|
12
|
+
|
|
13
|
+
journey_stop = JourneyStop(
|
|
14
|
+
stop,
|
|
15
|
+
arrival_time=future_time.isoformat(),
|
|
16
|
+
departure_time=future_time.isoformat(),
|
|
17
|
+
stop_sequence=1,
|
|
18
|
+
status="Scheduled",
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
assert journey_stop.stop == stop
|
|
22
|
+
assert journey_stop.arrival_time == future_time
|
|
23
|
+
assert journey_stop.real_arrival_time is None
|
|
24
|
+
assert journey_stop.arrival_delay is None
|
|
25
|
+
assert journey_stop.departure_time == future_time
|
|
26
|
+
assert journey_stop.real_departure_time is None
|
|
27
|
+
assert journey_stop.departure_delay is None
|
|
28
|
+
assert journey_stop.status == "Scheduled"
|
|
29
|
+
assert journey_stop.stop_sequence == 1
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def test_journey_stop_update_stop_with_real_times():
|
|
33
|
+
"""Test update_stop with real arrival and departure times."""
|
|
34
|
+
stop = MBTAStop(stop={"id": "test_stop_id", "name": "Test Stop"})
|
|
35
|
+
future_time = datetime.now() + timedelta(minutes=30)
|
|
36
|
+
real_arrival_time_str = (future_time + timedelta(minutes=5)).isoformat()
|
|
37
|
+
real_departure_time_str = (future_time + timedelta(minutes=10)).isoformat()
|
|
38
|
+
|
|
39
|
+
journey_stop = JourneyStop(
|
|
40
|
+
stop,
|
|
41
|
+
arrival_time=future_time.isoformat(),
|
|
42
|
+
departure_time=future_time.isoformat(),
|
|
43
|
+
stop_sequence=1,
|
|
44
|
+
status="Scheduled",
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
journey_stop.update_stop(
|
|
48
|
+
stop, real_arrival_time_str, real_departure_time_str, 1, "On Time"
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
assert journey_stop.real_arrival_time == future_time + timedelta(minutes=5)
|
|
52
|
+
assert journey_stop.arrival_delay == timedelta(minutes=5).total_seconds()
|
|
53
|
+
assert journey_stop.real_departure_time == future_time + timedelta(minutes=10)
|
|
54
|
+
assert journey_stop.departure_delay == timedelta(minutes=10).total_seconds()
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def test_journey_stop_update_stop_with_none_times():
|
|
58
|
+
"""Test update_stop with None times."""
|
|
59
|
+
stop = MBTAStop(stop={"id": "test_stop_id", "name": "Test Stop"})
|
|
60
|
+
future_time = datetime.now() + timedelta(minutes=30)
|
|
61
|
+
|
|
62
|
+
journey_stop = JourneyStop(
|
|
63
|
+
stop,
|
|
64
|
+
arrival_time=future_time.isoformat(),
|
|
65
|
+
departure_time=future_time.isoformat(),
|
|
66
|
+
stop_sequence=1,
|
|
67
|
+
status="Scheduled",
|
|
68
|
+
)
|
|
69
|
+
|
|
70
|
+
journey_stop.update_stop(stop, None, None, 1, "Cancelled")
|
|
71
|
+
|
|
72
|
+
assert journey_stop.real_arrival_time is None
|
|
73
|
+
assert journey_stop.arrival_delay is None
|
|
74
|
+
assert journey_stop.real_departure_time is None
|
|
75
|
+
assert journey_stop.departure_delay is None
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def test_journey_stop_get_time():
|
|
79
|
+
"""Test get_time method."""
|
|
80
|
+
stop = MBTAStop(stop={"id": "test_stop_id", "name": "Test Stop"})
|
|
81
|
+
future_time = datetime.now() + timedelta(minutes=30)
|
|
82
|
+
past_time = datetime.now() - timedelta(minutes=10)
|
|
83
|
+
|
|
84
|
+
journey_stop = JourneyStop(
|
|
85
|
+
stop, arrival_time=future_time.isoformat(), departure_time=None, stop_sequence=1, status="Scheduled"
|
|
86
|
+
)
|
|
87
|
+
assert journey_stop.get_time() == future_time
|
|
88
|
+
|
|
89
|
+
journey_stop.update_stop(stop, None, future_time.isoformat(), 1, "Scheduled")
|
|
90
|
+
assert journey_stop.get_time() == future_time
|
|
91
|
+
|
|
92
|
+
journey_stop.update_stop(stop, past_time.isoformat(), None, 1, "Scheduled")
|
|
93
|
+
assert journey_stop.get_time() == past_time
|
|
94
|
+
|
|
95
|
+
journey_stop.update_stop(stop, None, None, 1, "Cancelled")
|
|
96
|
+
assert journey_stop.get_time() is None
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def test_journey_stop_get_delay():
|
|
100
|
+
"""Test get_delay method."""
|
|
101
|
+
stop = MBTAStop(stop={"id": "test_stop_id", "name": "Test Stop"})
|
|
102
|
+
future_time = datetime.now() + timedelta(minutes=30)
|
|
103
|
+
|
|
104
|
+
journey_stop = JourneyStop(
|
|
105
|
+
stop,
|
|
106
|
+
arrival_time=future_time.isoformat(),
|
|
107
|
+
departure_time=future_time.isoformat(),
|
|
108
|
+
stop_sequence=1,
|
|
109
|
+
status="Scheduled",
|
|
110
|
+
)
|
|
111
|
+
|
|
112
|
+
# First, test with departure time set (arrival time is None)
|
|
113
|
+
journey_stop.update_stop(
|
|
114
|
+
stop,
|
|
115
|
+
None,
|
|
116
|
+
(future_time + timedelta(minutes=5)).isoformat(), # 5 minutes late departure
|
|
117
|
+
1,
|
|
118
|
+
"Scheduled",
|
|
119
|
+
)
|
|
120
|
+
# Assert that departure delay is returned
|
|
121
|
+
assert journey_stop.get_delay() == timedelta(minutes=5).total_seconds()
|
|
122
|
+
|
|
123
|
+
# Now update with arrival time (set arrival delay)
|
|
124
|
+
journey_stop.update_stop(
|
|
125
|
+
stop,
|
|
126
|
+
(future_time + timedelta(minutes=5)).isoformat(), # 5 minutes late arrival
|
|
127
|
+
None,
|
|
128
|
+
1,
|
|
129
|
+
"Scheduled",
|
|
130
|
+
)
|
|
131
|
+
# Assert that arrival delay is returned (since arrival delay is more relevant)
|
|
132
|
+
assert journey_stop.get_delay() == timedelta(minutes=5).total_seconds()
|
|
133
|
+
|
|
134
|
+
# Now update with departure time (set departure delay)
|
|
135
|
+
journey_stop.update_stop(
|
|
136
|
+
stop,
|
|
137
|
+
None,
|
|
138
|
+
(future_time + timedelta(minutes=10)).isoformat(), # 10 minutes late departure again
|
|
139
|
+
1,
|
|
140
|
+
"Scheduled",
|
|
141
|
+
)
|
|
142
|
+
# Assert that arrival delay is still returned (since arrival delay is more relevant)
|
|
143
|
+
assert journey_stop.get_delay() == timedelta(minutes=5).total_seconds()
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
def test_journey_stop_get_time_to():
|
|
147
|
+
"""Test get_time_to method."""
|
|
148
|
+
stop = MBTAStop(stop={"id": "test_stop_id", "name": "Test Stop"})
|
|
149
|
+
future_time = datetime.now() + timedelta(minutes=30)
|
|
150
|
+
past_time = datetime.now() - timedelta(minutes=10)
|
|
151
|
+
|
|
152
|
+
journey_stop = JourneyStop(
|
|
153
|
+
stop, arrival_time=future_time.isoformat(), departure_time=None, stop_sequence=1, status="Scheduled"
|
|
154
|
+
)
|
|
155
|
+
time_to = journey_stop.get_time_to()
|
|
156
|
+
assert time_to >= 0
|
|
157
|
+
|
|
158
|
+
journey_stop.update_stop(stop, None, future_time.isoformat(), 1, "Scheduled")
|
|
159
|
+
time_to = journey_stop.get_time_to()
|
|
160
|
+
assert time_to >= 0
|
|
161
|
+
|
|
162
|
+
journey_stop.update_stop(stop, past_time.isoformat(), None, 1, "Scheduled")
|
|
163
|
+
time_to = journey_stop.get_time_to()
|
|
164
|
+
assert time_to <= 0
|
|
165
|
+
|
|
166
|
+
journey_stop.update_stop(stop, None, None, 1, "Cancelled")
|
|
167
|
+
assert journey_stop.get_time_to() is None
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
import pytest
|
|
2
|
+
from src.mbtaclient.mbta_alert import MBTAAlert
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
def test_mbta_alert_init():
|
|
6
|
+
"""Test initialization of MBTAAlert object."""
|
|
7
|
+
alert_data = {
|
|
8
|
+
"id": "12345",
|
|
9
|
+
"attributes": {
|
|
10
|
+
"active_period": [
|
|
11
|
+
{
|
|
12
|
+
"start": "2024-07-04T10:00:00-04:00",
|
|
13
|
+
"end": "2024-07-04T12:00:00-04:00"
|
|
14
|
+
}
|
|
15
|
+
],
|
|
16
|
+
"cause": "Accident",
|
|
17
|
+
"effect": "Delays",
|
|
18
|
+
"header": "Green Line Delays",
|
|
19
|
+
"description": "Accident on the tracks near Park Street station.",
|
|
20
|
+
"severity": 1,
|
|
21
|
+
"informed_entity": [
|
|
22
|
+
{
|
|
23
|
+
"route": "Green",
|
|
24
|
+
"route_type": 13
|
|
25
|
+
},
|
|
26
|
+
{
|
|
27
|
+
"stop": "place-park"
|
|
28
|
+
}
|
|
29
|
+
]
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
alert = MBTAAlert(alert_data)
|
|
34
|
+
|
|
35
|
+
assert alert.id == "12345"
|
|
36
|
+
assert alert.cause == "Accident"
|
|
37
|
+
assert alert.effect == "Delays"
|
|
38
|
+
assert alert.header_text == "Green Line Delays"
|
|
39
|
+
assert alert.severity == 1
|
|
40
|
+
assert len(alert.informed_entities) == 2
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def test_mbta_alert_get_informed_stops():
|
|
44
|
+
"""Test get_informed_stops method."""
|
|
45
|
+
alert_data = {
|
|
46
|
+
"id": "12345",
|
|
47
|
+
"attributes": {
|
|
48
|
+
"active_period": [
|
|
49
|
+
{
|
|
50
|
+
"start": "2024-07-04T10:00:00-04:00",
|
|
51
|
+
"end": "2024-07-04T12:00:00-04:00"
|
|
52
|
+
}
|
|
53
|
+
],
|
|
54
|
+
"cause": "Accident",
|
|
55
|
+
"effect": "Delays",
|
|
56
|
+
"header": "Green Line Delays",
|
|
57
|
+
"description": "Accident on the tracks near Park Street station.",
|
|
58
|
+
"severity": 1,
|
|
59
|
+
"informed_entity": [
|
|
60
|
+
{
|
|
61
|
+
"route": "Green",
|
|
62
|
+
"route_type": 13
|
|
63
|
+
},
|
|
64
|
+
{
|
|
65
|
+
"stop": "place-park"
|
|
66
|
+
}
|
|
67
|
+
]
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
alert = MBTAAlert(alert_data)
|
|
72
|
+
|
|
73
|
+
assert alert.get_informed_stops() == ["place-park"]
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def test_mbta_alert_get_informed_trips():
|
|
77
|
+
"""Test get_informed_trips method."""
|
|
78
|
+
alert_data = {
|
|
79
|
+
"id": "12345",
|
|
80
|
+
"attributes": {
|
|
81
|
+
"active_period": [
|
|
82
|
+
{
|
|
83
|
+
"start": "2024-07-04T10:00:00-04:00",
|
|
84
|
+
"end": "2024-07-04T12:00:00-04:00"
|
|
85
|
+
}
|
|
86
|
+
],
|
|
87
|
+
"cause": "Accident",
|
|
88
|
+
"effect": "Delays",
|
|
89
|
+
"header": "Green Line Delays",
|
|
90
|
+
"description": "Accident on the tracks near Park Street station.",
|
|
91
|
+
"severity": 1,
|
|
92
|
+
"informed_entity": [
|
|
93
|
+
{
|
|
94
|
+
"route": "Green",
|
|
95
|
+
"route_type": 13
|
|
96
|
+
},
|
|
97
|
+
{
|
|
98
|
+
"stop": "place-park"
|
|
99
|
+
}
|
|
100
|
+
]
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
alert = MBTAAlert(alert_data)
|
|
105
|
+
|
|
106
|
+
assert alert.get_informed_trips() == []
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def test_mbta_alert_get_informed_routes():
|
|
110
|
+
"""Test get_informed_routes method."""
|
|
111
|
+
alert_data = {
|
|
112
|
+
"id": "12345",
|
|
113
|
+
"attributes": {
|
|
114
|
+
"active_period": [
|
|
115
|
+
{
|
|
116
|
+
"start": "2024-07-04T10:00:00-04:00",
|
|
117
|
+
"end": "2024-07-04T12:00:00-04:00"
|
|
118
|
+
}
|
|
119
|
+
],
|
|
120
|
+
"cause": "Accident",
|
|
121
|
+
"effect": "Delays",
|
|
122
|
+
"header": "Green Line Delays",
|
|
123
|
+
"description": "Accident on the tracks near Park Street station.",
|
|
124
|
+
"severity": 1,
|
|
125
|
+
"informed_entity": [
|
|
126
|
+
{
|
|
127
|
+
"route": "Green",
|
|
128
|
+
"route_type": 13
|
|
129
|
+
},
|
|
130
|
+
{
|
|
131
|
+
"stop": "place-park"
|
|
132
|
+
}
|
|
133
|
+
]
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
alert = MBTAAlert(alert_data)
|
|
138
|
+
|
|
139
|
+
assert alert.get_informed_routes() == ["Green"]
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import pytest
|
|
2
|
+
|
|
3
|
+
from unittest.mock import AsyncMock, MagicMock, patch
|
|
4
|
+
from aiohttp import ClientConnectionError, ClientResponseError, RequestInfo
|
|
5
|
+
from yarl import URL
|
|
6
|
+
|
|
7
|
+
from src.mbtaclient.mbta_client import MBTAClient, MBTA_DEFAULT_HOST, ENDPOINTS
|
|
8
|
+
from src.mbtaclient.mbta_route import MBTARoute
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
@pytest.mark.asyncio
|
|
12
|
+
async def test_get_route():
|
|
13
|
+
async def mock_fetch_data(url, params):
|
|
14
|
+
return {'data': {'id': 'route-xyz'}}
|
|
15
|
+
|
|
16
|
+
client = MBTAClient()
|
|
17
|
+
client._fetch_data = AsyncMock(side_effect=mock_fetch_data)
|
|
18
|
+
route = await client.get_route('route-xyz')
|
|
19
|
+
assert route.id == 'route-xyz'
|
|
20
|
+
client._fetch_data.assert_called_once_with(f'{ENDPOINTS["ROUTES"]}/route-xyz', None)
|
|
21
|
+
await client.close()
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
@pytest.mark.asyncio
|
|
25
|
+
async def test_get_route_error():
|
|
26
|
+
async def mock_request(method, url, params=None):
|
|
27
|
+
return MagicMock(json=AsyncMock(return_value={}))
|
|
28
|
+
|
|
29
|
+
client = MBTAClient()
|
|
30
|
+
client.request = AsyncMock(side_effect=mock_request)
|
|
31
|
+
with patch.object(client, 'logger', MagicMock()) as mock_logger:
|
|
32
|
+
with pytest.raises(ValueError) as excinfo:
|
|
33
|
+
await client.get_route('route-xyz')
|
|
34
|
+
assert str(excinfo.value) == "missing 'data'"
|
|
35
|
+
mock_logger.error.assert_called_once_with("Error fetching data: missing 'data'")
|
|
36
|
+
await client.close()
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
@pytest.mark.asyncio
|
|
40
|
+
async def test_list_routes():
|
|
41
|
+
async def mock_fetch_data(url, params):
|
|
42
|
+
return {'data': [{'id': 'route-1'}, {'id': 'route-2'}]}
|
|
43
|
+
|
|
44
|
+
client = MBTAClient()
|
|
45
|
+
client._fetch_data = AsyncMock(side_effect=mock_fetch_data)
|
|
46
|
+
routes = await client.list_routes()
|
|
47
|
+
assert len(routes) == 2
|
|
48
|
+
assert isinstance(routes[0], MBTARoute)
|
|
49
|
+
client._fetch_data.assert_called_once_with(ENDPOINTS['ROUTES'], None)
|
|
50
|
+
await client.close()
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
@pytest.mark.asyncio
|
|
54
|
+
async def test_request_connection_error():
|
|
55
|
+
async def mock_request(*args, **kwargs):
|
|
56
|
+
raise ClientConnectionError('Connection error')
|
|
57
|
+
|
|
58
|
+
client = MBTAClient()
|
|
59
|
+
client._session.request = AsyncMock(side_effect=mock_request)
|
|
60
|
+
with patch.object(client, 'logger', MagicMock()) as mock_logger:
|
|
61
|
+
with pytest.raises(ClientConnectionError):
|
|
62
|
+
await client.request('get', '/test')
|
|
63
|
+
mock_logger.error.assert_called_once_with('Connection error: Connection error')
|
|
64
|
+
await client.close()
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
@pytest.mark.asyncio
|
|
68
|
+
async def test_request_client_response_error():
|
|
69
|
+
request_info = RequestInfo(
|
|
70
|
+
url=URL("https://api-v3.mbta.com/test"),
|
|
71
|
+
method="GET",
|
|
72
|
+
headers={},
|
|
73
|
+
)
|
|
74
|
+
|
|
75
|
+
async def mock_request(*args, **kwargs):
|
|
76
|
+
raise ClientResponseError(
|
|
77
|
+
request_info=request_info,
|
|
78
|
+
history=None,
|
|
79
|
+
status=404,
|
|
80
|
+
message="Not Found",
|
|
81
|
+
headers=None,
|
|
82
|
+
)
|
|
83
|
+
|
|
84
|
+
client = MBTAClient()
|
|
85
|
+
client._session.request = AsyncMock(side_effect=mock_request)
|
|
86
|
+
with patch.object(client, 'logger', MagicMock()) as mock_logger:
|
|
87
|
+
with pytest.raises(ClientResponseError):
|
|
88
|
+
await client.request('get', '/test')
|
|
89
|
+
mock_logger.error.assert_called_once_with(
|
|
90
|
+
'Client response error: 404 - 404, message=\'Not Found\', url=\'https://api-v3.mbta.com/test\''
|
|
91
|
+
)
|
|
92
|
+
await client.close()
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
@pytest.mark.asyncio
|
|
96
|
+
async def test_request_success():
|
|
97
|
+
async def mock_request(*args, **kwargs):
|
|
98
|
+
return MagicMock(status=200, json=AsyncMock(return_value={}))
|
|
99
|
+
|
|
100
|
+
client = MBTAClient()
|
|
101
|
+
client._session.request = AsyncMock(side_effect=mock_request)
|
|
102
|
+
response = await client.request('get', 'test')
|
|
103
|
+
assert response.status == 200
|
|
104
|
+
client._session.request.assert_called_once_with(
|
|
105
|
+
'get',
|
|
106
|
+
f'https://{MBTA_DEFAULT_HOST}/test',
|
|
107
|
+
params={},
|
|
108
|
+
)
|
|
109
|
+
await client.close()
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import pytest
|
|
2
|
+
from typing import Dict, Optional
|
|
3
|
+
|
|
4
|
+
from src.mbtaclient.mbta_prediction import MBTAPrediction
|
|
5
|
+
from src.mbtaclient.mbta_utils import MBTAUtils
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
@pytest.mark.parametrize(
|
|
9
|
+
"prediction_data",
|
|
10
|
+
[
|
|
11
|
+
{
|
|
12
|
+
"id": "prediction-456",
|
|
13
|
+
"attributes": {
|
|
14
|
+
"arrival_time": "2023-01-08T10:00:00-05:00",
|
|
15
|
+
"arrival_uncertainty": "120",
|
|
16
|
+
"departure_time": "2023-01-08T10:01:00-05:00",
|
|
17
|
+
"departure_uncertainty": "300",
|
|
18
|
+
"direction_id": 1,
|
|
19
|
+
"last_trip": True,
|
|
20
|
+
"revenue": True,
|
|
21
|
+
"schedule_relationship": "SCHEDULED",
|
|
22
|
+
"status": "ACTIVE",
|
|
23
|
+
"stop_sequence": 5,
|
|
24
|
+
"update_type": "PREDICTED",
|
|
25
|
+
},
|
|
26
|
+
"relationships": {
|
|
27
|
+
"route": {"data": {"id": "route-xyz"}},
|
|
28
|
+
"stop": {"data": {"id": "stop-abc"}},
|
|
29
|
+
"trip": {"data": {"id": "trip-def"}},
|
|
30
|
+
},
|
|
31
|
+
},
|
|
32
|
+
# Test case with missing data
|
|
33
|
+
{"id": "prediction-456"},
|
|
34
|
+
],
|
|
35
|
+
)
|
|
36
|
+
def test_init(prediction_data):
|
|
37
|
+
"""Tests that MBTAPrediction is initialized correctly with or without data."""
|
|
38
|
+
|
|
39
|
+
prediction = MBTAPrediction(prediction_data)
|
|
40
|
+
|
|
41
|
+
# Test expected attributes
|
|
42
|
+
assert prediction.id == prediction_data["id"]
|
|
43
|
+
assert prediction.arrival_time == prediction_data.get("attributes", {}).get(
|
|
44
|
+
"arrival_time", ""
|
|
45
|
+
)
|
|
46
|
+
assert prediction.arrival_uncertainty == MBTAUtils.get_uncertainty_description(
|
|
47
|
+
prediction_data.get("attributes", {}).get("arrival_uncertainty", "")
|
|
48
|
+
)
|
|
49
|
+
assert prediction.departure_time == prediction_data.get("attributes", {}).get(
|
|
50
|
+
"departure_time", ""
|
|
51
|
+
)
|
|
52
|
+
assert prediction.departure_uncertainty == MBTAUtils.get_uncertainty_description(
|
|
53
|
+
prediction_data.get("attributes", {}).get("departure_uncertainty", "")
|
|
54
|
+
)
|
|
55
|
+
assert prediction.direction_id == prediction_data.get("attributes", {}).get(
|
|
56
|
+
"direction_id", 0
|
|
57
|
+
)
|
|
58
|
+
assert prediction.last_trip is prediction_data.get("attributes", {}).get(
|
|
59
|
+
"last_trip"
|
|
60
|
+
)
|
|
61
|
+
assert prediction.revenue is prediction_data.get("attributes", {}).get("revenue")
|
|
62
|
+
assert prediction.schedule_relationship == prediction_data.get("attributes", {}).get(
|
|
63
|
+
"schedule_relationship", ""
|
|
64
|
+
)
|
|
65
|
+
assert prediction.status == prediction_data.get("attributes", {}).get("status", "")
|
|
66
|
+
assert prediction.stop_sequence == prediction_data.get("attributes", {}).get(
|
|
67
|
+
"stop_sequence", 0
|
|
68
|
+
)
|
|
69
|
+
assert prediction.update_type == prediction_data.get("attributes", {}).get(
|
|
70
|
+
"update_type", ""
|
|
71
|
+
)
|
|
72
|
+
|
|
73
|
+
# Test relationships
|
|
74
|
+
assert prediction.route_id == (
|
|
75
|
+
prediction_data.get("relationships", {}).get("route", {}).get("data", {}).get(
|
|
76
|
+
"id", ""
|
|
77
|
+
)
|
|
78
|
+
)
|
|
79
|
+
assert prediction.stop_id == (
|
|
80
|
+
prediction_data.get("relationships", {}).get("stop", {}).get("data", {}).get(
|
|
81
|
+
"id", ""
|
|
82
|
+
)
|
|
83
|
+
)
|
|
84
|
+
assert prediction.trip_id == (
|
|
85
|
+
prediction_data.get("relationships", {}).get("trip", {}).get("data", {}).get(
|
|
86
|
+
"id", ""
|
|
87
|
+
)
|
|
88
|
+
)
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def test_repr():
|
|
92
|
+
"""Tests that the __repr__ method returns a string representation."""
|
|
93
|
+
|
|
94
|
+
prediction_data = {"id": "prediction-456"}
|
|
95
|
+
prediction = MBTAPrediction(prediction_data)
|
|
96
|
+
|
|
97
|
+
assert repr(prediction) == "MBTAprediction(id=prediction-456)"
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import pytest
|
|
2
|
+
from typing import Dict
|
|
3
|
+
|
|
4
|
+
from src.mbtaclient.mbta_route import MBTARoute
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
@pytest.mark.parametrize(
|
|
8
|
+
"route_data",
|
|
9
|
+
[
|
|
10
|
+
{
|
|
11
|
+
"id": "route-123",
|
|
12
|
+
"attributes": {
|
|
13
|
+
"color": "#0099CC",
|
|
14
|
+
"description": "Green Line B",
|
|
15
|
+
"direction_destinations": ["Bowdoin", "Cleaveland Circle"],
|
|
16
|
+
"direction_names": ["Outbound", "Inbound"],
|
|
17
|
+
"fare_class": "1",
|
|
18
|
+
"long_name": "Green Line B",
|
|
19
|
+
"short_name": "B",
|
|
20
|
+
"sort_order": 3,
|
|
21
|
+
"text_color": "#FFFFFF",
|
|
22
|
+
"type": "light_rail",
|
|
23
|
+
},
|
|
24
|
+
},
|
|
25
|
+
# Test case with missing data
|
|
26
|
+
{"id": "route-123"},
|
|
27
|
+
],
|
|
28
|
+
)
|
|
29
|
+
def test_init(route_data):
|
|
30
|
+
"""Tests that MBTARoute is initialized correctly with or without data."""
|
|
31
|
+
|
|
32
|
+
route = MBTARoute(route_data)
|
|
33
|
+
|
|
34
|
+
# Test expected attributes
|
|
35
|
+
assert route.id == route_data["id"]
|
|
36
|
+
assert route.color == route_data.get("attributes", {}).get("color", "")
|
|
37
|
+
assert route.description == route_data.get("attributes", {}).get("description", "")
|
|
38
|
+
assert route.direction_destinations == route_data.get(
|
|
39
|
+
"attributes", {}
|
|
40
|
+
).get("direction_destinations", [])
|
|
41
|
+
assert route.direction_names == route_data.get("attributes", {}).get(
|
|
42
|
+
"direction_names", []
|
|
43
|
+
)
|
|
44
|
+
assert route.fare_class == route_data.get("attributes", {}).get("fare_class", "")
|
|
45
|
+
assert route.long_name == route_data.get("attributes", {}).get("long_name", "")
|
|
46
|
+
assert route.short_name == route_data.get("attributes", {}).get("short_name", "")
|
|
47
|
+
assert route.sort_order == route_data.get("attributes", {}).get("sort_order", 0)
|
|
48
|
+
assert route.text_color == route_data.get("attributes", {}).get("text_color", "")
|
|
49
|
+
assert route.type == route_data.get("attributes", {}).get("type", "")
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def test_repr():
|
|
53
|
+
"""Tests that the __repr__ method returns a string representation."""
|
|
54
|
+
|
|
55
|
+
route_data = {"id": "route-123"}
|
|
56
|
+
route = MBTARoute(route_data)
|
|
57
|
+
|
|
58
|
+
assert repr(route) == "MBTAroute(id=route-123)"
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import pytest
|
|
2
|
+
from typing import Dict
|
|
3
|
+
|
|
4
|
+
from src.mbtaclient.mbta_schedule import MBTASchedule
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
@pytest.mark.parametrize(
|
|
8
|
+
"schedule_data",
|
|
9
|
+
[
|
|
10
|
+
{
|
|
11
|
+
"id": "sched_1234",
|
|
12
|
+
"attributes": {
|
|
13
|
+
"arrival_time": "2023-01-08T09:00:00-05:00",
|
|
14
|
+
"departure_time": "2023-01-08T09:01:00-05:00",
|
|
15
|
+
"direction_id": 1,
|
|
16
|
+
"drop_off_type": "2",
|
|
17
|
+
"pickup_type": "1",
|
|
18
|
+
"stop_headsign": "Riverside",
|
|
19
|
+
"stop_sequence": 3,
|
|
20
|
+
"timepoint": True,
|
|
21
|
+
},
|
|
22
|
+
"relationships": {
|
|
23
|
+
"route": {"data": {"id": "route-xyz"}},
|
|
24
|
+
"stop": {"data": {"id": "stop-abc"}},
|
|
25
|
+
"trip": {"data": {"id": "trip-def"}},
|
|
26
|
+
},
|
|
27
|
+
},
|
|
28
|
+
# Test case with missing data
|
|
29
|
+
{"id": "sched_1234"},
|
|
30
|
+
],
|
|
31
|
+
)
|
|
32
|
+
def test_init(schedule_data):
|
|
33
|
+
"""Tests that MBTASchedule is initialized correctly with or without data."""
|
|
34
|
+
|
|
35
|
+
schedule = MBTASchedule(schedule_data)
|
|
36
|
+
|
|
37
|
+
# Test expected attributes
|
|
38
|
+
assert schedule.id == schedule_data["id"]
|
|
39
|
+
assert schedule.arrival_time == schedule_data.get("attributes", {}).get(
|
|
40
|
+
"arrival_time", ""
|
|
41
|
+
)
|
|
42
|
+
assert schedule.departure_time == schedule_data.get("attributes", {}).get(
|
|
43
|
+
"departure_time", ""
|
|
44
|
+
)
|
|
45
|
+
assert schedule.direction_id == schedule_data.get("attributes", {}).get(
|
|
46
|
+
"direction_id", 0
|
|
47
|
+
)
|
|
48
|
+
assert schedule.drop_off_type == schedule_data.get("attributes", {}).get(
|
|
49
|
+
"drop_off_type", ""
|
|
50
|
+
)
|
|
51
|
+
assert schedule.pickup_type == schedule_data.get("attributes", {}).get(
|
|
52
|
+
"pickup_type", ""
|
|
53
|
+
)
|
|
54
|
+
assert schedule.stop_headsign == schedule_data.get("attributes", {}).get(
|
|
55
|
+
"stop_headsign", ""
|
|
56
|
+
)
|
|
57
|
+
assert schedule.stop_sequence == schedule_data.get("attributes", {}).get(
|
|
58
|
+
"stop_sequence", 0
|
|
59
|
+
)
|
|
60
|
+
assert schedule.timepoint is schedule_data.get("attributes", {}).get(
|
|
61
|
+
"timepoint", False
|
|
62
|
+
)
|
|
63
|
+
|
|
64
|
+
# Test relationships
|
|
65
|
+
assert schedule.route_id == (
|
|
66
|
+
schedule_data.get("relationships", {}).get("route", {}).get("data", {}).get(
|
|
67
|
+
"id", ""
|
|
68
|
+
)
|
|
69
|
+
)
|
|
70
|
+
assert schedule.stop_id == (
|
|
71
|
+
schedule_data.get("relationships", {}).get("stop", {}).get("data", {}).get(
|
|
72
|
+
"id", ""
|
|
73
|
+
)
|
|
74
|
+
)
|
|
75
|
+
assert schedule.trip_id == (
|
|
76
|
+
schedule_data.get("relationships", {}).get("trip", {}).get("data", {}).get(
|
|
77
|
+
"id", ""
|
|
78
|
+
)
|
|
79
|
+
)
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def test_repr():
|
|
83
|
+
"""Tests that the __repr__ method returns a string representation."""
|
|
84
|
+
|
|
85
|
+
schedule_data = {"id": "sched_1234"}
|
|
86
|
+
schedule = MBTASchedule(schedule_data)
|
|
87
|
+
|
|
88
|
+
assert repr(schedule) == "MBTAschedule(id=sched_1234)"
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import pytest
|
|
2
|
+
from typing import Dict
|
|
3
|
+
|
|
4
|
+
from src.mbtaclient.mbta_stop import MBTAStop
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
@pytest.mark.parametrize(
|
|
8
|
+
"stop_data",
|
|
9
|
+
[
|
|
10
|
+
{
|
|
11
|
+
"id": "place-dit9",
|
|
12
|
+
"attributes": {
|
|
13
|
+
"address": "100 Mystic Ave, Medford, MA 02155",
|
|
14
|
+
"at_street": "Mystic Ave",
|
|
15
|
+
"description": "Sullivan Square",
|
|
16
|
+
"latitude": 42.408733,
|
|
17
|
+
"location_type": 1,
|
|
18
|
+
"longitude": -71.060088,
|
|
19
|
+
"municipality": "Medford",
|
|
20
|
+
"name": "Sullivan Square",
|
|
21
|
+
"on_street": "Mystic Ave",
|
|
22
|
+
"platform_code": "",
|
|
23
|
+
"platform_name": "",
|
|
24
|
+
"vehicle_type": 0,
|
|
25
|
+
"wheelchair_boarding": 1,
|
|
26
|
+
},
|
|
27
|
+
},
|
|
28
|
+
# Test case with missing data
|
|
29
|
+
{"id": "place-dit9"},
|
|
30
|
+
],
|
|
31
|
+
)
|
|
32
|
+
def test_init(stop_data):
|
|
33
|
+
"""Tests that MBTAStop is initialized correctly with or without data."""
|
|
34
|
+
|
|
35
|
+
stop = MBTAStop(stop_data)
|
|
36
|
+
|
|
37
|
+
# Test expected attributes
|
|
38
|
+
assert stop.id == stop_data["id"]
|
|
39
|
+
assert stop.address == stop_data.get("attributes", {}).get("address", "")
|
|
40
|
+
assert stop.at_street == stop_data.get("attributes", {}).get("at_street", "")
|
|
41
|
+
assert stop.description == stop_data.get("attributes", {}).get("description", "")
|
|
42
|
+
assert stop.location_type == stop_data.get("attributes", {}).get("location_type", 0)
|
|
43
|
+
assert stop.municipality == stop_data.get("attributes", {}).get("municipality", "")
|
|
44
|
+
assert stop.name == stop_data.get("attributes", {}).get("name", "")
|
|
45
|
+
assert stop.on_street == stop_data.get("attributes", {}).get("on_street", "")
|
|
46
|
+
assert stop.platform_code == stop_data.get("attributes", {}).get("platform_code", "")
|
|
47
|
+
assert stop.platform_name == stop_data.get("attributes", {}).get("platform_name", "")
|
|
48
|
+
assert stop.vehicle_type == stop_data.get("attributes", {}).get("vehicle_type", 0)
|
|
49
|
+
assert stop.wheelchair_boarding == stop_data.get("attributes", {}).get(
|
|
50
|
+
"wheelchair_boarding", 0
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
# Use pytest.approx for floating-point comparisons
|
|
54
|
+
assert pytest.approx(stop.latitude) == stop_data.get("attributes", {}).get(
|
|
55
|
+
"latitude", 0.0
|
|
56
|
+
)
|
|
57
|
+
assert pytest.approx(stop.longitude) == stop_data.get("attributes", {}).get(
|
|
58
|
+
"longitude", 0.0
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def test_repr():
|
|
63
|
+
"""Tests that the __repr__ method returns a string representation."""
|
|
64
|
+
|
|
65
|
+
stop_data = {"id": "place-dit9"}
|
|
66
|
+
stop = MBTAStop(stop_data)
|
|
67
|
+
|
|
68
|
+
assert repr(stop) == "MBTAstop(id=place-dit9)"
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import pytest
|
|
2
|
+
from typing import Dict
|
|
3
|
+
|
|
4
|
+
from src.mbtaclient.mbta_trip import MBTATrip
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
@pytest.mark.parametrize(
|
|
8
|
+
"trip_data",
|
|
9
|
+
[
|
|
10
|
+
{
|
|
11
|
+
"id": "1234",
|
|
12
|
+
"attributes": {
|
|
13
|
+
"name": "Green Line B",
|
|
14
|
+
"headsign": "Cleveland Circle",
|
|
15
|
+
"direction_id": 1,
|
|
16
|
+
"block_id": "b123",
|
|
17
|
+
"shape_id": "s456",
|
|
18
|
+
"wheelchair_accessible": True,
|
|
19
|
+
"bikes_allowed": False,
|
|
20
|
+
"schedule_relationship": "weekday",
|
|
21
|
+
},
|
|
22
|
+
"relationships": {
|
|
23
|
+
"route": {"data": {"id": "route_id_1"}},
|
|
24
|
+
"service": {"data": {"id": "service_id_1"}},
|
|
25
|
+
},
|
|
26
|
+
},
|
|
27
|
+
# Test case with missing data
|
|
28
|
+
{
|
|
29
|
+
"id": "5678",
|
|
30
|
+
"attributes": {"name": "Red Line"},
|
|
31
|
+
"relationships": {"route": {"data": {}}},
|
|
32
|
+
},
|
|
33
|
+
],
|
|
34
|
+
)
|
|
35
|
+
def test_init(trip_data):
|
|
36
|
+
"""Tests that MBTATrip is initialized correctly with or without data."""
|
|
37
|
+
|
|
38
|
+
trip = MBTATrip(trip_data)
|
|
39
|
+
|
|
40
|
+
# Test expected attributes
|
|
41
|
+
assert trip.id == trip_data["id"]
|
|
42
|
+
assert trip.name == trip_data.get("attributes", {}).get("name", "")
|
|
43
|
+
assert trip.headsign == trip_data.get("attributes", {}).get("headsign", "")
|
|
44
|
+
assert trip.direction_id == trip_data.get("attributes", {}).get("direction_id", 0)
|
|
45
|
+
assert trip.block_id == trip_data.get("attributes", {}).get("block_id", "")
|
|
46
|
+
assert trip.shape_id == trip_data.get("attributes", {}).get("shape_id", "")
|
|
47
|
+
assert trip.wheelchair_accessible is trip_data.get(
|
|
48
|
+
"attributes", {}
|
|
49
|
+
).get("wheelchair_accessible")
|
|
50
|
+
assert trip.bikes_allowed is trip_data.get("attributes", {}).get("bikes_allowed")
|
|
51
|
+
assert trip.schedule_relationship == trip_data.get(
|
|
52
|
+
"attributes", {}
|
|
53
|
+
).get("schedule_relationship", "")
|
|
54
|
+
|
|
55
|
+
# Test relationships
|
|
56
|
+
assert trip.route_id == (
|
|
57
|
+
trip_data.get("relationships", {}).get("route", {}).get("data", {}).get(
|
|
58
|
+
"id", ""
|
|
59
|
+
)
|
|
60
|
+
)
|
|
61
|
+
assert trip.service_id == (
|
|
62
|
+
trip_data.get("relationships", {}).get("service", {}).get("data", {}).get(
|
|
63
|
+
"id", ""
|
|
64
|
+
)
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
# Add more test cases for different scenarios (e.g., invalid data types, edge cases)
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
import pytest
|
|
2
|
+
from datetime import datetime, timedelta
|
|
3
|
+
from unittest.mock import patch, MagicMock
|
|
4
|
+
from zoneinfo import ZoneInfo
|
|
5
|
+
|
|
6
|
+
from src.mbtaclient.mbta_utils import MBTAUtils, memoize_async
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class TestMBTAUtils:
|
|
10
|
+
@pytest.fixture
|
|
11
|
+
def now(self):
|
|
12
|
+
return datetime.now()
|
|
13
|
+
|
|
14
|
+
@staticmethod
|
|
15
|
+
def make_hashable(item):
|
|
16
|
+
if isinstance(item, dict):
|
|
17
|
+
return frozenset((TestMBTAUtils.make_hashable(k), TestMBTAUtils.make_hashable(v)) for k, v in item.items())
|
|
18
|
+
return str(item)
|
|
19
|
+
|
|
20
|
+
def test_get_route_type_desc_by_type_id(self):
|
|
21
|
+
assert MBTAUtils.get_route_type_desc_by_type_id(0) == "Subway"
|
|
22
|
+
assert MBTAUtils.get_route_type_desc_by_type_id(1) == "Subway"
|
|
23
|
+
assert MBTAUtils.get_route_type_desc_by_type_id(2) == "Commuter Rail"
|
|
24
|
+
assert MBTAUtils.get_route_type_desc_by_type_id(3) == "Bus"
|
|
25
|
+
assert MBTAUtils.get_route_type_desc_by_type_id(4) == "Ferry"
|
|
26
|
+
assert MBTAUtils.get_route_type_desc_by_type_id(5) == "Unknown"
|
|
27
|
+
|
|
28
|
+
def test_get_uncertainty_description(self):
|
|
29
|
+
assert MBTAUtils.get_uncertainty_description("60") == "Trip that has already started"
|
|
30
|
+
assert MBTAUtils.get_uncertainty_description("120") == (
|
|
31
|
+
"Trip not started and a vehicle is awaiting departure at the origin"
|
|
32
|
+
)
|
|
33
|
+
assert MBTAUtils.get_uncertainty_description("300") == "Vehicle has not yet been assigned to the trip"
|
|
34
|
+
assert MBTAUtils.get_uncertainty_description("301") == (
|
|
35
|
+
"Vehicle appears to be stalled or significantly delayed"
|
|
36
|
+
)
|
|
37
|
+
assert MBTAUtils.get_uncertainty_description("360") == (
|
|
38
|
+
"Trip not started and a vehicle is completing a previous trip"
|
|
39
|
+
)
|
|
40
|
+
assert MBTAUtils.get_uncertainty_description("invalid") == "None"
|
|
41
|
+
|
|
42
|
+
def test_time_to(self, now):
|
|
43
|
+
future_time = now + timedelta(minutes=10)
|
|
44
|
+
past_time = now - timedelta(minutes=5)
|
|
45
|
+
|
|
46
|
+
# Test with valid time
|
|
47
|
+
assert pytest.approx(MBTAUtils.time_to(future_time, now)) == 600, 1
|
|
48
|
+
|
|
49
|
+
# Test with past time
|
|
50
|
+
assert pytest.approx(MBTAUtils.time_to(past_time, now)) == -300, 1
|
|
51
|
+
|
|
52
|
+
# Test with None time
|
|
53
|
+
assert MBTAUtils.time_to(None, now) is None
|
|
54
|
+
|
|
55
|
+
# Test with different timezones
|
|
56
|
+
aware_time1 = now.replace(tzinfo=ZoneInfo("America/Los_Angeles"))
|
|
57
|
+
aware_time2 = now.replace(tzinfo=ZoneInfo("Europe/Berlin"))
|
|
58
|
+
assert pytest.approx(MBTAUtils.time_to(aware_time1, aware_time2)) == 32400.0, 1
|
|
59
|
+
|
|
60
|
+
def test_calculate_time_difference(self, now):
|
|
61
|
+
real_time = now + timedelta(minutes=5)
|
|
62
|
+
scheduled_time = now
|
|
63
|
+
|
|
64
|
+
# Test with valid times
|
|
65
|
+
assert pytest.approx(MBTAUtils.calculate_time_difference(real_time, scheduled_time)) == 300, 1
|
|
66
|
+
|
|
67
|
+
# Test with None times
|
|
68
|
+
assert MBTAUtils.calculate_time_difference(None, scheduled_time) is None
|
|
69
|
+
assert MBTAUtils.calculate_time_difference(real_time, None) is None
|
|
70
|
+
assert MBTAUtils.calculate_time_difference(None, None) is None
|
|
71
|
+
|
|
72
|
+
def test_parse_datetime(self):
|
|
73
|
+
time_str = "2023-11-20T10:30:00+00:00"
|
|
74
|
+
|
|
75
|
+
# Test with valid ISO 8601 string
|
|
76
|
+
parsed_time = MBTAUtils.parse_datetime(time_str)
|
|
77
|
+
assert parsed_time is not None
|
|
78
|
+
assert parsed_time.isoformat() == time_str
|
|
79
|
+
|
|
80
|
+
# Test with invalid string
|
|
81
|
+
time_str = "invalid_time"
|
|
82
|
+
parsed_time = MBTAUtils.parse_datetime(time_str)
|
|
83
|
+
assert parsed_time is None
|
|
84
|
+
|
|
85
|
+
# Test with non-string input
|
|
86
|
+
parsed_time = MBTAUtils.parse_datetime(123)
|
|
87
|
+
assert parsed_time is None
|
|
88
|
+
|
|
89
|
+
@pytest.mark.asyncio
|
|
90
|
+
@patch('src.mbtaclient.mbta_utils.logger.debug')
|
|
91
|
+
@patch('src.mbtaclient.mbta_utils.logger.error')
|
|
92
|
+
async def test_memoize_async(self, mock_error, mock_debug):
|
|
93
|
+
@memoize_async()
|
|
94
|
+
async def my_func(arg1, arg2):
|
|
95
|
+
return arg1 + arg2
|
|
96
|
+
|
|
97
|
+
# First call should miss the cache
|
|
98
|
+
result1 = await my_func(1, 2)
|
|
99
|
+
assert result1 == 3
|
|
100
|
+
|
|
101
|
+
# Assert that both the 'Cache miss' and 'Cache updated' logs are called
|
|
102
|
+
mock_debug.assert_any_call(f"Cache miss for my_func with arguments {(TestMBTAUtils.make_hashable(1), TestMBTAUtils.make_hashable(2))} at {datetime.now().isoformat()}")
|
|
103
|
+
mock_debug.assert_any_call(f"Cache updated for key: {(TestMBTAUtils.make_hashable(1), TestMBTAUtils.make_hashable(2))} at {datetime.now().isoformat()}")
|
|
104
|
+
|
|
105
|
+
# Second call should hit the cache
|
|
106
|
+
result2 = await my_func(1, 2)
|
|
107
|
+
assert result2 == 3
|
|
108
|
+
|
|
109
|
+
# Check if cache hit log is called
|
|
110
|
+
mock_debug.assert_any_call(f"Cache hit for my_func with arguments {(TestMBTAUtils.make_hashable(1), TestMBTAUtils.make_hashable(2))} at {datetime.now().isoformat()}")
|
|
111
|
+
|
|
112
|
+
# Call with different arguments should miss the cache
|
|
113
|
+
result3 = await my_func(1, 3)
|
|
114
|
+
assert result3 == 4
|
|
115
|
+
mock_debug.assert_any_call(f"Cache miss for my_func with arguments {(TestMBTAUtils.make_hashable(1), TestMBTAUtils.make_hashable(3))} at {datetime.now().isoformat()}")
|
|
116
|
+
mock_debug.assert_any_call(f"Cache updated for key: {(TestMBTAUtils.make_hashable(1), TestMBTAUtils.make_hashable(3))} at {datetime.now().isoformat()}")
|
|
117
|
+
|
|
118
|
+
# Test error handling
|
|
119
|
+
@memoize_async()
|
|
120
|
+
async def my_func_error(arg):
|
|
121
|
+
raise ValueError("Test error")
|
|
122
|
+
|
|
123
|
+
with pytest.raises(ValueError):
|
|
124
|
+
await my_func_error(1)
|
|
125
|
+
mock_error.assert_called_with(f"Error occurred while executing my_func_error with arguments (1,): Test error")
|
|
126
|
+
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|