MBTAclient 0.2.8__tar.gz → 0.2.9__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 (44) hide show
  1. {mbtaclient-0.2.8 → mbtaclient-0.2.9}/PKG-INFO +3 -3
  2. {mbtaclient-0.2.8 → mbtaclient-0.2.9}/pyproject.toml +2 -2
  3. {mbtaclient-0.2.8 → mbtaclient-0.2.9}/src/MBTAclient.egg-info/PKG-INFO +3 -3
  4. {mbtaclient-0.2.8 → mbtaclient-0.2.9}/src/MBTAclient.egg-info/SOURCES.txt +9 -9
  5. mbtaclient-0.2.9/src/MBTAclient.egg-info/requires.txt +1 -0
  6. {mbtaclient-0.2.8 → mbtaclient-0.2.9}/src/mbtaclient/__init__.py +7 -7
  7. {mbtaclient-0.2.8 → mbtaclient-0.2.9}/src/mbtaclient/base_handler.py +9 -9
  8. mbtaclient-0.2.9/src/mbtaclient/client.py +236 -0
  9. {mbtaclient-0.2.8 → mbtaclient-0.2.9}/src/mbtaclient/journey.py +8 -8
  10. {mbtaclient-0.2.8 → mbtaclient-0.2.9}/src/mbtaclient/journey_stop.py +5 -5
  11. {mbtaclient-0.2.8 → mbtaclient-0.2.9}/src/mbtaclient/journeys_handler.py +3 -3
  12. mbtaclient-0.2.8/src/mbtaclient/mbta_prediction.py → mbtaclient-0.2.9/src/mbtaclient/prediction.py +1 -1
  13. {mbtaclient-0.2.8 → mbtaclient-0.2.9}/src/mbtaclient/trip_handler.py +3 -3
  14. mbtaclient-0.2.8/src/mbtaclient/mbta_utils.py → mbtaclient-0.2.9/src/mbtaclient/utils.py +4 -4
  15. mbtaclient-0.2.9/tests/test_journey.py +95 -0
  16. {mbtaclient-0.2.8 → mbtaclient-0.2.9}/tests/test_journey_stop.py +2 -2
  17. mbtaclient-0.2.9/tests/test_mbta_alert.py +43 -0
  18. mbtaclient-0.2.9/tests/test_mbta_client.py +200 -0
  19. mbtaclient-0.2.9/tests/test_mbta_prediction.py +40 -0
  20. mbtaclient-0.2.9/tests/test_mbta_route.py +27 -0
  21. mbtaclient-0.2.9/tests/test_mbta_schedule.py +26 -0
  22. mbtaclient-0.2.9/tests/test_mbta_stop.py +30 -0
  23. mbtaclient-0.2.9/tests/test_mbta_trip.py +23 -0
  24. {mbtaclient-0.2.8 → mbtaclient-0.2.9}/tests/test_mbta_utils.py +53 -38
  25. mbtaclient-0.2.8/src/MBTAclient.egg-info/requires.txt +0 -1
  26. mbtaclient-0.2.8/src/mbtaclient/__version__.py +0 -1
  27. mbtaclient-0.2.8/src/mbtaclient/mbta_client.py +0 -132
  28. mbtaclient-0.2.8/tests/test_mbta_alert.py +0 -139
  29. mbtaclient-0.2.8/tests/test_mbta_client.py +0 -109
  30. mbtaclient-0.2.8/tests/test_mbta_prediction.py +0 -97
  31. mbtaclient-0.2.8/tests/test_mbta_route.py +0 -58
  32. mbtaclient-0.2.8/tests/test_mbta_schedule.py +0 -88
  33. mbtaclient-0.2.8/tests/test_mbta_stop.py +0 -68
  34. mbtaclient-0.2.8/tests/test_mbta_trip.py +0 -68
  35. {mbtaclient-0.2.8 → mbtaclient-0.2.9}/LICENSE +0 -0
  36. {mbtaclient-0.2.8 → mbtaclient-0.2.9}/README.md +0 -0
  37. {mbtaclient-0.2.8 → mbtaclient-0.2.9}/setup.cfg +0 -0
  38. {mbtaclient-0.2.8 → mbtaclient-0.2.9}/src/MBTAclient.egg-info/dependency_links.txt +0 -0
  39. {mbtaclient-0.2.8 → mbtaclient-0.2.9}/src/MBTAclient.egg-info/top_level.txt +0 -0
  40. /mbtaclient-0.2.8/src/mbtaclient/mbta_alert.py → /mbtaclient-0.2.9/src/mbtaclient/alert.py +0 -0
  41. /mbtaclient-0.2.8/src/mbtaclient/mbta_route.py → /mbtaclient-0.2.9/src/mbtaclient/route.py +0 -0
  42. /mbtaclient-0.2.8/src/mbtaclient/mbta_schedule.py → /mbtaclient-0.2.9/src/mbtaclient/schedule.py +0 -0
  43. /mbtaclient-0.2.8/src/mbtaclient/mbta_stop.py → /mbtaclient-0.2.9/src/mbtaclient/stop.py +0 -0
  44. /mbtaclient-0.2.8/src/mbtaclient/mbta_trip.py → /mbtaclient-0.2.9/src/mbtaclient/trip.py +0 -0
@@ -1,6 +1,6 @@
1
- Metadata-Version: 2.1
1
+ Metadata-Version: 2.2
2
2
  Name: MBTAclient
3
- Version: 0.2.8
3
+ Version: 0.2.9
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
@@ -13,7 +13,7 @@ Classifier: Operating System :: OS Independent
13
13
  Requires-Python: >=3.12
14
14
  Description-Content-Type: text/markdown
15
15
  License-File: LICENSE
16
- Requires-Dist: aiohttp>=3.9.5
16
+ Requires-Dist: aiohttp
17
17
 
18
18
  # MBTAclient
19
19
 
@@ -4,12 +4,12 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "MBTAclient"
7
- version = "0.2.8"
7
+ version = "0.2.9"
8
8
  description = "A Python client for interacting with the MBTA API"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.12"
11
11
  dependencies = [
12
- "aiohttp>=3.9.5"
12
+ "aiohttp"
13
13
  ]
14
14
 
15
15
  license = { text = "MIT" }
@@ -1,6 +1,6 @@
1
- Metadata-Version: 2.1
1
+ Metadata-Version: 2.2
2
2
  Name: MBTAclient
3
- Version: 0.2.8
3
+ Version: 0.2.9
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
@@ -13,7 +13,7 @@ Classifier: Operating System :: OS Independent
13
13
  Requires-Python: >=3.12
14
14
  Description-Content-Type: text/markdown
15
15
  License-File: LICENSE
16
- Requires-Dist: aiohttp>=3.9.5
16
+ Requires-Dist: aiohttp
17
17
 
18
18
  # MBTAclient
19
19
 
@@ -7,20 +7,20 @@ src/MBTAclient.egg-info/dependency_links.txt
7
7
  src/MBTAclient.egg-info/requires.txt
8
8
  src/MBTAclient.egg-info/top_level.txt
9
9
  src/mbtaclient/__init__.py
10
- src/mbtaclient/__version__.py
10
+ src/mbtaclient/alert.py
11
11
  src/mbtaclient/base_handler.py
12
+ src/mbtaclient/client.py
12
13
  src/mbtaclient/journey.py
13
14
  src/mbtaclient/journey_stop.py
14
15
  src/mbtaclient/journeys_handler.py
15
- src/mbtaclient/mbta_alert.py
16
- src/mbtaclient/mbta_client.py
17
- src/mbtaclient/mbta_prediction.py
18
- src/mbtaclient/mbta_route.py
19
- src/mbtaclient/mbta_schedule.py
20
- src/mbtaclient/mbta_stop.py
21
- src/mbtaclient/mbta_trip.py
22
- src/mbtaclient/mbta_utils.py
16
+ src/mbtaclient/prediction.py
17
+ src/mbtaclient/route.py
18
+ src/mbtaclient/schedule.py
19
+ src/mbtaclient/stop.py
20
+ src/mbtaclient/trip.py
23
21
  src/mbtaclient/trip_handler.py
22
+ src/mbtaclient/utils.py
23
+ tests/test_journey.py
24
24
  tests/test_journey_stop.py
25
25
  tests/test_mbta_alert.py
26
26
  tests/test_mbta_client.py
@@ -0,0 +1 @@
1
+ aiohttp
@@ -4,13 +4,13 @@
4
4
  from .journey_stop import JourneyStop
5
5
  from .journey import Journey
6
6
  from .journeys_handler import JourneysHandler
7
- from .mbta_alert import MBTAAlert
8
- from .mbta_client import MBTAClient
9
- from .mbta_prediction import MBTAPrediction
10
- from .mbta_route import MBTARoute
11
- from .mbta_schedule import MBTASchedule
12
- from .mbta_stop import MBTAStop
13
- from .mbta_trip import MBTATrip
7
+ from .alert import MBTAAlert
8
+ from .client import MBTAClient
9
+ from .prediction import MBTAPrediction
10
+ from .route import MBTARoute
11
+ from .schedule import MBTASchedule
12
+ from .stop import MBTAStop
13
+ from .trip import MBTATrip
14
14
  from .trip_handler import TripHandler
15
15
  from .__version__ import __version__
16
16
 
@@ -3,15 +3,15 @@ import aiohttp
3
3
 
4
4
  from typing import Optional
5
5
 
6
- from .mbta_client import MBTAClient
6
+ from .client import MBTAClient
7
7
  from .journey import Journey
8
- from .mbta_stop import MBTAStop
9
- from .mbta_route import MBTARoute
10
- from .mbta_schedule import MBTASchedule
11
- from .mbta_prediction import MBTAPrediction
12
- from .mbta_trip import MBTATrip
13
- from .mbta_alert import MBTAAlert
14
- from .mbta_utils import memoize_async
8
+ from .stop import MBTAStop
9
+ from .route import MBTARoute
10
+ from .schedule import MBTASchedule
11
+ from .prediction import MBTAPrediction
12
+ from .trip import MBTATrip
13
+ from .alert import MBTAAlert
14
+ from .utils import memoize_async
15
15
 
16
16
 
17
17
  class BaseHandler:
@@ -32,7 +32,7 @@ class BaseHandler:
32
32
 
33
33
  client_session = session or aiohttp.ClientSession()
34
34
  self.mbta_client = MBTAClient(client_session, logger, api_key)
35
-
35
+
36
36
  self.journeys: dict[str, Journey] = {}
37
37
 
38
38
  self.logger: logging.Logger = logger or logging.getLogger(__name__)
@@ -0,0 +1,236 @@
1
+ import aiohttp
2
+ import asyncio
3
+ import logging
4
+ import time
5
+ from aiohttp import ClientConnectionError, ClientResponseError
6
+ from typing import Optional, Any, Dict, List, Type
7
+
8
+ from .route import MBTARoute
9
+ from .stop import MBTAStop
10
+ from .schedule import MBTASchedule
11
+ from .prediction import MBTAPrediction
12
+ from .trip import MBTATrip
13
+ from .alert import MBTAAlert
14
+
15
+ MBTA_DEFAULT_HOST = "api-v3.mbta.com"
16
+
17
+ ENDPOINTS = {
18
+ 'STOPS': 'stops',
19
+ 'ROUTES': 'routes',
20
+ 'PREDICTIONS': 'predictions',
21
+ 'SCHEDULES': 'schedules',
22
+ 'TRIPS': 'trips',
23
+ 'ALERTS': 'alerts',
24
+ }
25
+
26
+ MAX_CONCURRENT_REQUESTS = 10
27
+
28
+ class MBTAAuthenticationError(Exception):
29
+ """Custom exception for MBTA authentication errors."""
30
+
31
+ class MBTAClientError(Exception):
32
+ """Custom exception class for MBTA API errors."""
33
+
34
+ class MBTAClient:
35
+ """Class to interact with the MBTA v3 API."""
36
+
37
+ def __init__(self, session: aiohttp.ClientSession = None, logger: logging.Logger = None, api_key: Optional[str] = None, max_concurrent_requests: int = MAX_CONCURRENT_REQUESTS):
38
+ self._session = session
39
+ self._api_key: Optional[str] = api_key
40
+ self._max_concurrent_requests = max_concurrent_requests
41
+
42
+ if self._session:
43
+ # If an external session is provided, pass it to SessionManager
44
+ SessionManager.configure(self._max_concurrent_requests, self._session, logger=logger)
45
+ else:
46
+ # If no session is provided, the SessionManager will manage it
47
+ SessionManager.configure(self._max_concurrent_requests, logger=logger)
48
+
49
+ self._logger: logging.Logger = logger or logging.getLogger(__name__)
50
+
51
+ async def __aenter__(self):
52
+ """Enter the context and return the client."""
53
+ if not self._session:
54
+ # If session is not passed, get it from SessionManager
55
+ self._session = await SessionManager.get_session()
56
+ return self
57
+
58
+ async def __aexit__(self, exc_type, exc, tb):
59
+ """Exit the context."""
60
+ await SessionManager.close_session()
61
+ SessionManager.cleanup()
62
+
63
+ # Generic fetch method for list operations
64
+ async def fetch_list(
65
+ self, endpoint: str, params: Optional[Dict[str, Any]], obj_class: Type
66
+ ) -> List[Any]:
67
+ """Fetch a list of objects from the MBTA API."""
68
+ self._logger.debug(f"Fetching list from endpoint: {endpoint} with params: {params}")
69
+ data = await self._fetch_data(endpoint, params)
70
+ # Generalize by ensuring each object is created dynamically
71
+ return [obj_class(item) for item in data["data"]]
72
+
73
+ # Fetch data helper with retries
74
+ async def _fetch_data(
75
+ self, path: str, params: Optional[Dict[str, Any]] = None
76
+ ) -> Dict[str, Any]:
77
+ """Helper method to fetch data from the MBTA API."""
78
+ self._logger.debug(f"Fetching data from https://{MBTA_DEFAULT_HOST}/{path} with params: {params}")
79
+ try:
80
+ response = await self.request("GET", path, params)
81
+ data = await response.json()
82
+ if "data" not in data:
83
+ self._logger.error(f"Response missing 'data': {data}")
84
+ raise MBTAClientError(f"Invalid response from API: {data}")
85
+ return data
86
+ except MBTAClientError as error:
87
+ self._logger.error(f"MBTAClientError occurred: {error}")
88
+ raise
89
+ except Exception as error:
90
+ self._logger.error(f"Unexpected error while fetching data: {error}")
91
+ raise
92
+
93
+ async def request(self, method: str, path: str, params: Optional[Dict[str, Any]] = None) -> aiohttp.ClientResponse:
94
+ """
95
+ Make an HTTP request with optional query parameters and JSON body.
96
+
97
+ Adds retry logic and configurable timeouts.
98
+
99
+ Raises:
100
+ MBTAAuthenticationError: For 403 Forbidden errors (invalid API key).
101
+ MBTAClientError: For other HTTP or unexpected errors.
102
+ """
103
+ params = params or {}
104
+ if self._api_key:
105
+ params["api_key"] = self._api_key
106
+
107
+ url = f"https://{MBTA_DEFAULT_HOST}/{path}"
108
+ self._logger.debug(f"Making {method} request to {url} with params: {params}")
109
+
110
+ retries = 3
111
+ timeout = aiohttp.ClientTimeout(total=10) # 10 seconds timeout
112
+
113
+ for attempt in range(retries):
114
+ try:
115
+ async with SessionManager._semaphore:
116
+ response: aiohttp.ClientResponse = await self._session.request(method, url, params=params, timeout=timeout)
117
+ self._logger.debug(f"Received response {response.status} for {url}")
118
+ response.raise_for_status() # Raise HTTP errors
119
+ return response
120
+
121
+ except (ClientResponseError, ClientConnectionError) as error:
122
+ self._logger.error(f"Error on attempt {attempt + 1}/{retries}: {error}")
123
+ if attempt == retries - 1:
124
+ self._logger.error(f"Final attempt failed: {error}")
125
+ raise MBTAClientError(f"Request failed: {error}") from error
126
+ await asyncio.sleep(2) # Wait before retrying
127
+
128
+ except Exception as error:
129
+ self._logger.error(f"Unexpected error during {method} request to {url}: {error}", exc_info=True)
130
+ raise MBTAClientError(f"Unexpected error: {error}") from error
131
+
132
+ # Specific API methods
133
+ async def get_route(self, id: str, params: Optional[Dict[str, Any]] = None) -> MBTARoute:
134
+ """Get a route by its ID."""
135
+ data = await self._fetch_data(f"{ENDPOINTS['ROUTES']}/{id}", params)
136
+ return MBTARoute(data["data"])
137
+
138
+ async def get_trip(self, id: str, params: Optional[Dict[str, Any]] = None) -> MBTATrip:
139
+ """Get a trip by its ID."""
140
+ data = await self._fetch_data(f"{ENDPOINTS['TRIPS']}/{id}", params)
141
+ return MBTATrip(data["data"])
142
+
143
+ async def get_stop(self, id: str, params: Optional[Dict[str, Any]] = None) -> MBTAStop:
144
+ """Get a stop by its ID."""
145
+ data = await self._fetch_data(f'{ENDPOINTS["STOPS"]}/{id}', params)
146
+ return MBTAStop(data['data'])
147
+
148
+ async def list_routes(self, params: Optional[Dict[str, Any]] = None) -> List[MBTARoute]:
149
+ """List all routes."""
150
+ return await self.fetch_list(ENDPOINTS["ROUTES"], params, MBTARoute)
151
+
152
+ async def list_trips(self, params: Optional[Dict[str, Any]] = None) -> List[MBTATrip]:
153
+ """List all trips."""
154
+ return await self.fetch_list(ENDPOINTS["TRIPS"], params, MBTATrip)
155
+
156
+ async def list_stops(self, params: Optional[Dict[str, Any]] = None) -> List[MBTAStop]:
157
+ """List all stops."""
158
+ data = await self._fetch_data(ENDPOINTS['STOPS'], params)
159
+ return [MBTAStop(item) for item in data["data"]]
160
+
161
+ async def list_schedules(self, params: Optional[Dict[str, Any]] = None) -> List[MBTASchedule]:
162
+ """List all schedules."""
163
+ data = await self._fetch_data(ENDPOINTS['SCHEDULES'], params)
164
+ return [MBTASchedule(item) for item in data["data"]]
165
+
166
+ async def list_predictions(self, params: Optional[Dict[str, Any]] = None) -> List[MBTAPrediction]:
167
+ """List all predictions."""
168
+ data = await self._fetch_data(ENDPOINTS['PREDICTIONS'], params)
169
+ return [MBTAPrediction(item) for item in data["data"]]
170
+
171
+ async def list_alerts(self, params: Optional[Dict[str, Any]] = None) -> List[MBTAAlert]:
172
+ """List all alerts."""
173
+ data = await self._fetch_data(ENDPOINTS['ALERTS'], params)
174
+ return [MBTAAlert(item) for item in data["data"]]
175
+
176
+ import logging
177
+ import aiohttp
178
+ import asyncio
179
+
180
+ class SessionManager:
181
+ """Singleton class to manage a shared aiohttp.ClientSession."""
182
+
183
+ _session: Optional[aiohttp.ClientSession] = None
184
+ _semaphore: Optional[asyncio.Semaphore] = None
185
+ _max_concurrent_requests: int = 10 # Default maximum concurrent requests
186
+ _logger: logging.Logger = None
187
+
188
+ @classmethod
189
+ def configure(cls, max_concurrent_requests: int = 10, session: Optional[aiohttp.ClientSession] = None, logger: logging.Logger = None):
190
+ """
191
+ Configure the SessionManager with the maximum number of concurrent requests and optionally an external session.
192
+
193
+ Args:
194
+ max_concurrent_requests (int): The number of concurrent requests allowed.
195
+ session (aiohttp.ClientSession, optional): An external session to use.
196
+ """
197
+ cls._logger: logging.Logger = logger or logging.getLogger(__name__)
198
+ cls._max_concurrent_requests = max_concurrent_requests
199
+ cls._semaphore = asyncio.Semaphore(max_concurrent_requests)
200
+ if session:
201
+ cls._session = session
202
+ cls._logger.debug(f"Using provided external session: {session}")
203
+ else:
204
+ cls._logger.debug(f"Creating a new session with max concurrent requests: {max_concurrent_requests}")
205
+
206
+ @classmethod
207
+ async def get_session(cls) -> aiohttp.ClientSession:
208
+ """
209
+ Get the shared aiohttp.ClientSession instance, creating it if necessary.
210
+
211
+ Returns:
212
+ aiohttp.ClientSession: The shared session instance.
213
+ """
214
+ if cls._session is None or cls._session.closed:
215
+ cls._logger.debug("No active session found, creating a new one.")
216
+ cls._session = aiohttp.ClientSession()
217
+ else:
218
+ cls._logger.debug("Returning existing session.")
219
+ return cls._session
220
+
221
+ @classmethod
222
+ async def close_session(cls):
223
+ """Close the shared aiohttp.ClientSession."""
224
+ if cls._session and not cls._session.closed:
225
+ cls._logger.debug("Closing the shared session.")
226
+ await cls._session.close()
227
+ cls._session = None
228
+ else:
229
+ cls._logger.debug("No session to close or already closed.")
230
+
231
+ @classmethod
232
+ async def cleanup(cls):
233
+ """Clean up resources when shutting down."""
234
+ cls._logger.debug("Cleaning up resources and closing session.")
235
+ await cls.close_session()
236
+ cls._semaphore = None
@@ -2,13 +2,13 @@ from typing import Union, Optional
2
2
  from datetime import datetime
3
3
 
4
4
  from .journey_stop import JourneyStop
5
- from .mbta_schedule import MBTASchedule
6
- from .mbta_prediction import MBTAPrediction
7
- from .mbta_stop import MBTAStop
8
- from .mbta_route import MBTARoute
9
- from .mbta_trip import MBTATrip
10
- from .mbta_alert import MBTAAlert
11
- from .mbta_utils import MBTAUtils
5
+ from .schedule import MBTASchedule
6
+ from .prediction import MBTAPrediction
7
+ from .stop import MBTAStop
8
+ from .route import MBTARoute
9
+ from .trip import MBTATrip
10
+ from .alert import MBTAAlert
11
+ from .utils import MBTAUtils
12
12
 
13
13
  class Journey:
14
14
  """A class to manage a journey with multiple stops."""
@@ -115,7 +115,7 @@ class Journey:
115
115
 
116
116
  def get_trip_duration(self) -> Optional[str]:
117
117
  if self.duration:
118
- return self.duration
118
+ return round(self.duration,0)
119
119
  return None
120
120
 
121
121
  def get_stop_name(self, stop_type: str) -> Optional[str]:
@@ -1,8 +1,8 @@
1
1
  from typing import Optional
2
2
  from datetime import datetime
3
3
 
4
- from .mbta_stop import MBTAStop
5
- from .mbta_utils import MBTAUtils
4
+ from .stop import MBTAStop
5
+ from .utils import MBTAUtils
6
6
 
7
7
 
8
8
  class JourneyStop:
@@ -63,12 +63,12 @@ class JourneyStop:
63
63
  return self.departure_time
64
64
  return None
65
65
 
66
- def get_delay(self) -> Optional[float]:
66
+ def get_delay(self) -> Optional[int]:
67
67
  """Return the most relevant delay for the stop."""
68
68
  if self.arrival_delay is not None:
69
- return self.arrival_delay
69
+ return int(round(self.arrival_delay,0))
70
70
  if self.departure_delay is not None:
71
- return self.departure_delay
71
+ return int(round(self.departure_delay,0))
72
72
  return None
73
73
 
74
74
  def get_time_to(self) -> float:
@@ -4,9 +4,9 @@ from datetime import datetime
4
4
 
5
5
  from .base_handler import BaseHandler
6
6
  from .journey import Journey
7
- from .mbta_route import MBTARoute
8
- from .mbta_trip import MBTATrip
9
- from .mbta_schedule import MBTASchedule
7
+ from .route import MBTARoute
8
+ from .trip import MBTATrip
9
+ from .schedule import MBTASchedule
10
10
 
11
11
 
12
12
  class JourneysHandler(BaseHandler):
@@ -1,6 +1,6 @@
1
1
 
2
2
  from typing import Any, Optional
3
- from .mbta_utils import MBTAUtils
3
+ from .utils import MBTAUtils
4
4
 
5
5
  class MBTAPrediction:
6
6
  """A prediction object to hold information about a prediction."""
@@ -4,9 +4,9 @@ import logging
4
4
  from datetime import datetime, timedelta
5
5
  from .base_handler import BaseHandler, MBTATripError
6
6
  from .journey import Journey
7
- from .mbta_route import MBTARoute
8
- from .mbta_trip import MBTATrip
9
- from .mbta_schedule import MBTASchedule
7
+ from .route import MBTARoute
8
+ from .trip import MBTATrip
9
+ from .schedule import MBTASchedule
10
10
 
11
11
  class TripHandler(BaseHandler):
12
12
  """Handler for managing a specific trip."""
@@ -35,7 +35,7 @@ class MBTAUtils:
35
35
  return MBTAUtils.UNCERTAINTY.get(key, 'None')
36
36
 
37
37
  @staticmethod
38
- def time_to(time: Optional[datetime], now: datetime) -> Optional[float]:
38
+ def time_to(time: Optional[datetime], now: datetime) -> Optional[int]:
39
39
  if time is None:
40
40
  logger.warning("time_to: Provided 'time' is None.")
41
41
  return None
@@ -49,13 +49,13 @@ class MBTAUtils:
49
49
  # Convert now to naive by stripping timezone info
50
50
  now = now.replace(tzinfo=None)
51
51
  # Now perform the calculation
52
- return (time - now).total_seconds()
52
+ return int(round((time - now).total_seconds(),0))
53
53
 
54
54
  @staticmethod
55
- def calculate_time_difference(real_time: Optional[datetime], time: Optional[datetime]) -> Optional[float]:
55
+ def calculate_time_difference(real_time: Optional[datetime], time: Optional[datetime]) -> Optional[int]:
56
56
  if real_time is None or time is None:
57
57
  return None
58
- return (real_time - time).total_seconds()
58
+ return int(round((real_time - time).total_seconds(),0))
59
59
 
60
60
  @staticmethod
61
61
  def parse_datetime(time_str: str) -> Optional[datetime]:
@@ -0,0 +1,95 @@
1
+ import pytest
2
+ from unittest.mock import MagicMock
3
+ from datetime import datetime
4
+
5
+ from src.mbtaclient.journey import Journey
6
+ from src.mbtaclient.schedule import MBTASchedule
7
+ from src.mbtaclient.prediction import MBTAPrediction
8
+ from src.mbtaclient.stop import MBTAStop
9
+ from src.mbtaclient.route import MBTARoute
10
+ from src.mbtaclient.alert import MBTAAlert
11
+ from src.mbtaclient.utils import MBTAUtils
12
+ from src.mbtaclient.journey_stop import JourneyStop
13
+ from tests.mock_data import VALID_ROUTE_RESPONSE_DATA, VALID_SCHEDULE_RESPONSE_DATA, VALID_PREDICTION_RESPONSE_DATA, VALID_STOP_RESPONSE_DATA # Direct import
14
+
15
+
16
+ @pytest.fixture
17
+ def mock_route():
18
+ route = MBTARoute(VALID_ROUTE_RESPONSE_DATA)
19
+ return route
20
+
21
+ @pytest.fixture
22
+ def mock_schedule():
23
+ schedule = MBTASchedule(VALID_SCHEDULE_RESPONSE_DATA)
24
+ return schedule
25
+
26
+ @pytest.fixture
27
+ def mock_prediction():
28
+ prediction = MBTAPrediction(VALID_PREDICTION_RESPONSE_DATA)
29
+ return prediction
30
+
31
+ @pytest.fixture
32
+ def mock_stop():
33
+ stop = MBTAStop(VALID_STOP_RESPONSE_DATA)
34
+ return stop
35
+
36
+ @pytest.fixture
37
+ def journey():
38
+ """Fixture to create a Journey instance."""
39
+ return Journey()
40
+
41
+
42
+ def test_add_stop(journey, mock_schedule, mock_stop):
43
+ """Test the add_stop method."""
44
+ journey.add_stop("departure", mock_schedule, mock_stop, "On time")
45
+
46
+ assert journey.stops['departure'] is not None
47
+ assert journey.stops['departure'].stop.name == "South St @ Spalding St"
48
+ assert journey.stops['departure'].arrival_time == None
49
+ assert journey.stops['departure'].departure_time == datetime.fromisoformat("2025-01-07T05:15:00-05:00")
50
+ assert journey.stops['departure'].stop_sequence == 50
51
+
52
+
53
+ def test_get_route_details(journey, mock_route):
54
+ """Test the get_route_details method."""
55
+ journey.route = mock_route
56
+
57
+ assert journey.get_route_short_name() == ""
58
+ assert journey.get_route_long_name() == "Red Line"
59
+ assert journey.get_route_color() == "DA291C"
60
+ assert journey.get_route_type() == 1
61
+ assert journey.get_route_description() == "Subway"
62
+
63
+
64
+ def test_get_stop_details(journey, mock_schedule, mock_stop):
65
+ """Test the get_stop_details method."""
66
+ journey.add_stop("departure", mock_schedule, mock_stop, "On time")
67
+ journey.add_stop("arrival", mock_schedule, mock_stop, "On time")
68
+
69
+ assert journey.get_stop_name("departure") == "South St @ Spalding St"
70
+ assert journey.get_platform_name("departure") == None
71
+ assert journey.get_stop_time("departure") == datetime.fromisoformat("2025-01-07T05:15:00-05:00")
72
+ assert journey.get_stop_delay("departure") is None
73
+ assert journey.get_stop_status("departure") == "On time"
74
+
75
+
76
+ def test_get_stop_id(journey, mock_schedule, mock_stop):
77
+ """Test the get_stop_id method."""
78
+ journey.add_stop("departure", mock_schedule, mock_stop, "On time")
79
+ journey.add_stop("arrival", mock_schedule, mock_stop, "On time")
80
+
81
+ assert journey.get_stop_id("departure") == "1936"
82
+ assert journey.get_stop_id("arrival") == "1936"
83
+
84
+
85
+ def test_find_journey_stop_by_id(journey, mock_schedule, mock_stop):
86
+ """Test the find_journey_stop_by_id method."""
87
+ journey.add_stop("departure", mock_schedule, mock_stop, "On time")
88
+ journey.add_stop("arrival", mock_schedule, mock_stop, "On time")
89
+
90
+ journey_stop = journey.find_journey_stop_by_id("1936")
91
+ assert journey_stop is not None
92
+ assert journey_stop.stop.id == "1936"
93
+
94
+ journey_stop = journey.find_journey_stop_by_id("non-existing-id")
95
+ assert journey_stop is None
@@ -1,7 +1,7 @@
1
1
  import pytest
2
2
  from datetime import datetime, timedelta
3
- from src.mbtaclient.mbta_stop import MBTAStop
4
- from src.mbtaclient.mbta_utils import MBTAUtils
3
+ from src.mbtaclient.stop import MBTAStop
4
+ from src.mbtaclient.utils import MBTAUtils
5
5
  from src.mbtaclient.journey_stop import JourneyStop
6
6
 
7
7
 
@@ -0,0 +1,43 @@
1
+ import pytest
2
+ from src.mbtaclient.alert import MBTAAlert
3
+ from tests.mock_data import VALID_ALERT_RESPONSE_DATA # Direct import from mock data
4
+
5
+ def test_mbta_alert_init():
6
+ """Test initialization of MBTAAlert object."""
7
+ alert = MBTAAlert(VALID_ALERT_RESPONSE_DATA)
8
+
9
+ assert alert.id == "382310"
10
+ assert alert.cause == "CONSTRUCTION"
11
+ assert alert.effect == "STATION_ISSUE"
12
+ assert alert.header_text == (
13
+ "The Quincy Adams parking garage has re-opened with most parking spaces available. "
14
+ "Customers can access the garage via the Route 3 off ramp exit as well as the Burgin Parkway entrance."
15
+ )
16
+ assert alert.severity == 1
17
+ assert len(alert.informed_entities) == 3
18
+ assert alert.informed_entities[0]["stop"] == "70103"
19
+ assert alert.informed_entities[0]["route"] == "Red"
20
+
21
+ def test_mbta_alert_get_informed_stops():
22
+ """Test get_informed_stops method."""
23
+ alert = MBTAAlert(VALID_ALERT_RESPONSE_DATA)
24
+
25
+ # Test for informed stops
26
+ informed_stops = alert.get_informed_stops()
27
+ assert set(informed_stops) == {"70103", "70104", "place-qamnl"}
28
+
29
+ def test_mbta_alert_get_informed_trips():
30
+ """Test get_informed_trips method."""
31
+ alert = MBTAAlert(VALID_ALERT_RESPONSE_DATA)
32
+
33
+ # Test for informed trips (should be empty)
34
+ informed_trips = alert.get_informed_trips()
35
+ assert informed_trips == []
36
+
37
+ def test_mbta_alert_get_informed_routes():
38
+ """Test get_informed_routes method."""
39
+ alert = MBTAAlert(VALID_ALERT_RESPONSE_DATA)
40
+
41
+ # Test for informed routes
42
+ informed_routes = alert.get_informed_routes()
43
+ assert set(informed_routes) == {"Red"}