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.
- {mbtaclient-0.2.8 → mbtaclient-0.2.9}/PKG-INFO +3 -3
- {mbtaclient-0.2.8 → mbtaclient-0.2.9}/pyproject.toml +2 -2
- {mbtaclient-0.2.8 → mbtaclient-0.2.9}/src/MBTAclient.egg-info/PKG-INFO +3 -3
- {mbtaclient-0.2.8 → mbtaclient-0.2.9}/src/MBTAclient.egg-info/SOURCES.txt +9 -9
- mbtaclient-0.2.9/src/MBTAclient.egg-info/requires.txt +1 -0
- {mbtaclient-0.2.8 → mbtaclient-0.2.9}/src/mbtaclient/__init__.py +7 -7
- {mbtaclient-0.2.8 → mbtaclient-0.2.9}/src/mbtaclient/base_handler.py +9 -9
- mbtaclient-0.2.9/src/mbtaclient/client.py +236 -0
- {mbtaclient-0.2.8 → mbtaclient-0.2.9}/src/mbtaclient/journey.py +8 -8
- {mbtaclient-0.2.8 → mbtaclient-0.2.9}/src/mbtaclient/journey_stop.py +5 -5
- {mbtaclient-0.2.8 → mbtaclient-0.2.9}/src/mbtaclient/journeys_handler.py +3 -3
- mbtaclient-0.2.8/src/mbtaclient/mbta_prediction.py → mbtaclient-0.2.9/src/mbtaclient/prediction.py +1 -1
- {mbtaclient-0.2.8 → mbtaclient-0.2.9}/src/mbtaclient/trip_handler.py +3 -3
- mbtaclient-0.2.8/src/mbtaclient/mbta_utils.py → mbtaclient-0.2.9/src/mbtaclient/utils.py +4 -4
- mbtaclient-0.2.9/tests/test_journey.py +95 -0
- {mbtaclient-0.2.8 → mbtaclient-0.2.9}/tests/test_journey_stop.py +2 -2
- mbtaclient-0.2.9/tests/test_mbta_alert.py +43 -0
- mbtaclient-0.2.9/tests/test_mbta_client.py +200 -0
- mbtaclient-0.2.9/tests/test_mbta_prediction.py +40 -0
- mbtaclient-0.2.9/tests/test_mbta_route.py +27 -0
- mbtaclient-0.2.9/tests/test_mbta_schedule.py +26 -0
- mbtaclient-0.2.9/tests/test_mbta_stop.py +30 -0
- mbtaclient-0.2.9/tests/test_mbta_trip.py +23 -0
- {mbtaclient-0.2.8 → mbtaclient-0.2.9}/tests/test_mbta_utils.py +53 -38
- mbtaclient-0.2.8/src/MBTAclient.egg-info/requires.txt +0 -1
- mbtaclient-0.2.8/src/mbtaclient/__version__.py +0 -1
- mbtaclient-0.2.8/src/mbtaclient/mbta_client.py +0 -132
- mbtaclient-0.2.8/tests/test_mbta_alert.py +0 -139
- mbtaclient-0.2.8/tests/test_mbta_client.py +0 -109
- mbtaclient-0.2.8/tests/test_mbta_prediction.py +0 -97
- mbtaclient-0.2.8/tests/test_mbta_route.py +0 -58
- mbtaclient-0.2.8/tests/test_mbta_schedule.py +0 -88
- mbtaclient-0.2.8/tests/test_mbta_stop.py +0 -68
- mbtaclient-0.2.8/tests/test_mbta_trip.py +0 -68
- {mbtaclient-0.2.8 → mbtaclient-0.2.9}/LICENSE +0 -0
- {mbtaclient-0.2.8 → mbtaclient-0.2.9}/README.md +0 -0
- {mbtaclient-0.2.8 → mbtaclient-0.2.9}/setup.cfg +0 -0
- {mbtaclient-0.2.8 → mbtaclient-0.2.9}/src/MBTAclient.egg-info/dependency_links.txt +0 -0
- {mbtaclient-0.2.8 → mbtaclient-0.2.9}/src/MBTAclient.egg-info/top_level.txt +0 -0
- /mbtaclient-0.2.8/src/mbtaclient/mbta_alert.py → /mbtaclient-0.2.9/src/mbtaclient/alert.py +0 -0
- /mbtaclient-0.2.8/src/mbtaclient/mbta_route.py → /mbtaclient-0.2.9/src/mbtaclient/route.py +0 -0
- /mbtaclient-0.2.8/src/mbtaclient/mbta_schedule.py → /mbtaclient-0.2.9/src/mbtaclient/schedule.py +0 -0
- /mbtaclient-0.2.8/src/mbtaclient/mbta_stop.py → /mbtaclient-0.2.9/src/mbtaclient/stop.py +0 -0
- /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
|
+
Metadata-Version: 2.2
|
|
2
2
|
Name: MBTAclient
|
|
3
|
-
Version: 0.2.
|
|
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
|
|
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.
|
|
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
|
|
12
|
+
"aiohttp"
|
|
13
13
|
]
|
|
14
14
|
|
|
15
15
|
license = { text = "MIT" }
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
Metadata-Version: 2.
|
|
1
|
+
Metadata-Version: 2.2
|
|
2
2
|
Name: MBTAclient
|
|
3
|
-
Version: 0.2.
|
|
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
|
|
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/
|
|
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/
|
|
16
|
-
src/mbtaclient/
|
|
17
|
-
src/mbtaclient/
|
|
18
|
-
src/mbtaclient/
|
|
19
|
-
src/mbtaclient/
|
|
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 .
|
|
8
|
-
from .
|
|
9
|
-
from .
|
|
10
|
-
from .
|
|
11
|
-
from .
|
|
12
|
-
from .
|
|
13
|
-
from .
|
|
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 .
|
|
6
|
+
from .client import MBTAClient
|
|
7
7
|
from .journey import Journey
|
|
8
|
-
from .
|
|
9
|
-
from .
|
|
10
|
-
from .
|
|
11
|
-
from .
|
|
12
|
-
from .
|
|
13
|
-
from .
|
|
14
|
-
from .
|
|
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 .
|
|
6
|
-
from .
|
|
7
|
-
from .
|
|
8
|
-
from .
|
|
9
|
-
from .
|
|
10
|
-
from .
|
|
11
|
-
from .
|
|
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 .
|
|
5
|
-
from .
|
|
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[
|
|
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 .
|
|
8
|
-
from .
|
|
9
|
-
from .
|
|
7
|
+
from .route import MBTARoute
|
|
8
|
+
from .trip import MBTATrip
|
|
9
|
+
from .schedule import MBTASchedule
|
|
10
10
|
|
|
11
11
|
|
|
12
12
|
class JourneysHandler(BaseHandler):
|
|
@@ -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 .
|
|
8
|
-
from .
|
|
9
|
-
from .
|
|
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[
|
|
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[
|
|
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.
|
|
4
|
-
from src.mbtaclient.
|
|
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"}
|