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.
Files changed (34) hide show
  1. {mbtaclient-0.2.7 → mbtaclient-0.2.8}/PKG-INFO +2 -2
  2. {mbtaclient-0.2.7 → mbtaclient-0.2.8}/pyproject.toml +2 -2
  3. {mbtaclient-0.2.7 → mbtaclient-0.2.8}/src/MBTAclient.egg-info/PKG-INFO +2 -2
  4. {mbtaclient-0.2.7 → mbtaclient-0.2.8}/src/MBTAclient.egg-info/SOURCES.txt +10 -1
  5. {mbtaclient-0.2.7 → mbtaclient-0.2.8}/src/mbtaclient/journey_stop.py +1 -1
  6. {mbtaclient-0.2.7 → mbtaclient-0.2.8}/src/mbtaclient/mbta_client.py +2 -2
  7. {mbtaclient-0.2.7 → mbtaclient-0.2.8}/src/mbtaclient/mbta_utils.py +10 -0
  8. mbtaclient-0.2.8/tests/test_journey_stop.py +167 -0
  9. mbtaclient-0.2.8/tests/test_mbta_alert.py +139 -0
  10. mbtaclient-0.2.8/tests/test_mbta_client.py +109 -0
  11. mbtaclient-0.2.8/tests/test_mbta_prediction.py +97 -0
  12. mbtaclient-0.2.8/tests/test_mbta_route.py +58 -0
  13. mbtaclient-0.2.8/tests/test_mbta_schedule.py +88 -0
  14. mbtaclient-0.2.8/tests/test_mbta_stop.py +68 -0
  15. mbtaclient-0.2.8/tests/test_mbta_trip.py +68 -0
  16. mbtaclient-0.2.8/tests/test_mbta_utils.py +126 -0
  17. {mbtaclient-0.2.7 → mbtaclient-0.2.8}/LICENSE +0 -0
  18. {mbtaclient-0.2.7 → mbtaclient-0.2.8}/README.md +0 -0
  19. {mbtaclient-0.2.7 → mbtaclient-0.2.8}/setup.cfg +0 -0
  20. {mbtaclient-0.2.7 → mbtaclient-0.2.8}/src/MBTAclient.egg-info/dependency_links.txt +0 -0
  21. {mbtaclient-0.2.7 → mbtaclient-0.2.8}/src/MBTAclient.egg-info/requires.txt +0 -0
  22. {mbtaclient-0.2.7 → mbtaclient-0.2.8}/src/MBTAclient.egg-info/top_level.txt +0 -0
  23. {mbtaclient-0.2.7 → mbtaclient-0.2.8}/src/mbtaclient/__init__.py +0 -0
  24. {mbtaclient-0.2.7 → mbtaclient-0.2.8}/src/mbtaclient/__version__.py +0 -0
  25. {mbtaclient-0.2.7 → mbtaclient-0.2.8}/src/mbtaclient/base_handler.py +0 -0
  26. {mbtaclient-0.2.7 → mbtaclient-0.2.8}/src/mbtaclient/journey.py +0 -0
  27. {mbtaclient-0.2.7 → mbtaclient-0.2.8}/src/mbtaclient/journeys_handler.py +0 -0
  28. {mbtaclient-0.2.7 → mbtaclient-0.2.8}/src/mbtaclient/mbta_alert.py +0 -0
  29. {mbtaclient-0.2.7 → mbtaclient-0.2.8}/src/mbtaclient/mbta_prediction.py +0 -0
  30. {mbtaclient-0.2.7 → mbtaclient-0.2.8}/src/mbtaclient/mbta_route.py +0 -0
  31. {mbtaclient-0.2.7 → mbtaclient-0.2.8}/src/mbtaclient/mbta_schedule.py +0 -0
  32. {mbtaclient-0.2.7 → mbtaclient-0.2.8}/src/mbtaclient/mbta_stop.py +0 -0
  33. {mbtaclient-0.2.7 → mbtaclient-0.2.8}/src/mbtaclient/mbta_trip.py +0 -0
  34. {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.7
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.9
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"
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.9",
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.7
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.9
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("Unexpected response format")
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