Skip to content

Commit cd2fba5

Browse files
committed
Improve type hints
1 parent d058f58 commit cd2fba5

File tree

4 files changed

+156
-26
lines changed

4 files changed

+156
-26
lines changed

py_nextbus/client.py

Lines changed: 24 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -10,15 +10,21 @@
1010
import requests
1111
from requests.exceptions import HTTPError
1212

13+
from py_nextbus.models import AgencyInfo
14+
from py_nextbus.models import RouteDetails
15+
from py_nextbus.models import RouteInfo
16+
from py_nextbus.models import StopPrediction
17+
1318
LOG = logging.getLogger()
1419

1520

1621
class NextBusError(Exception):
1722
pass
1823

1924

20-
class NextBusHTTPError(HTTPError, NextBusError):
25+
class NextBusHTTPError(NextBusError):
2126
def __init__(self, message: str, http_err: HTTPError):
27+
super().__init__()
2228
self.__dict__.update(http_err.__dict__)
2329
self.message: str = message
2430

@@ -102,35 +108,32 @@ def rate_limit_percent(self) -> float:
102108

103109
return self.rate_limit_remaining / self.rate_limit * 100
104110

105-
def agencies(self) -> list[dict[str, Any]]:
106-
result = self._get("agencies")
107-
return cast(list[dict[str, Any]], result)
111+
def agencies(self) -> list[AgencyInfo]:
112+
return cast(list[AgencyInfo], self._get("agencies"))
108113

109-
def routes(self, agency_id: str | None = None) -> list[dict[str, Any]]:
114+
def routes(self, agency_id: str | None = None) -> list[RouteInfo]:
110115
if not agency_id:
111116
agency_id = self.agency_id
112117

113-
result = self._get(f"agencies/{agency_id}/routes")
114-
return cast(list[dict[str, Any]], result)
118+
return cast(list[RouteInfo], self._get(f"agencies/{agency_id}/routes"))
115119

116120
def route_details(
117121
self, route_id: str, agency_id: str | None = None
118-
) -> dict[str, Any] | str:
122+
) -> RouteDetails:
119123
"""Includes stops and directions."""
120124
agency_id = agency_id or self.agency_id
121125
if not agency_id:
122126
raise NextBusValidationError("Agency ID is required")
123127

124-
result = self._get(f"agencies/{agency_id}/routes/{route_id}")
125-
return cast(dict[str, Any], result)
128+
return cast(RouteDetails, self._get(f"agencies/{agency_id}/routes/{route_id}"))
126129

127130
def predictions_for_stop(
128131
self,
129132
stop_id: str | int,
130133
route_id: str | None = None,
131134
direction_id: str | None = None,
132135
agency_id: str | None = None,
133-
) -> list[dict[str, Any]]:
136+
) -> list[StopPrediction]:
134137
"""Returns predictions for a stop."""
135138
agency_id = agency_id or self.agency_id
136139
if not agency_id:
@@ -141,13 +144,17 @@ def predictions_for_stop(
141144
raise NextBusValidationError("Direction ID provided without route ID")
142145

143146
if route_id:
144-
result = self._get(
145-
f"agencies/{agency_id}/nstops/{route_id}:{stop_id}/predictions"
147+
predictions = cast(
148+
list[StopPrediction],
149+
self._get(
150+
f"agencies/{agency_id}/nstops/{route_id}:{stop_id}/predictions"
151+
),
146152
)
147153
else:
148-
result = self._get(f"agencies/{agency_id}/stops/{stop_id}/predictions")
149-
150-
predictions = cast(list[dict[str, Any]], result)
154+
predictions = cast(
155+
list[StopPrediction],
156+
self._get(f"agencies/{agency_id}/stops/{stop_id}/predictions"),
157+
)
151158

152159
# If route not provided, return all predictions as the API returned them
153160
if not route_id:
@@ -174,9 +181,7 @@ def predictions_for_stop(
174181

175182
return predictions
176183

177-
def _get(
178-
self, endpoint: str, params: dict[str, Any] | None = None
179-
) -> dict[str, Any] | list[dict[str, Any]]:
184+
def _get(self, endpoint: str, params: dict[str, Any] | None = None) -> Any:
180185
if params is None:
181186
params = {}
182187

py_nextbus/models.py

Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,119 @@
1+
from typing import TypedDict
2+
3+
4+
class AgencyInfo(TypedDict):
5+
id: str
6+
name: str
7+
shortName: str
8+
region: str
9+
website: str
10+
logo: str
11+
nxbs2RedirectUrl: str
12+
13+
14+
class PredictionRouteInfo(TypedDict):
15+
id: str
16+
title: str
17+
description: str
18+
color: str
19+
textColor: str
20+
hidden: bool
21+
22+
23+
class RouteInfo(PredictionRouteInfo):
24+
rev: int
25+
timestamp: str
26+
27+
28+
class RouteBoundingBox(TypedDict):
29+
latMin: float
30+
latMax: float
31+
lonMin: float
32+
lonMax: float
33+
34+
35+
class StopInfo(TypedDict):
36+
id: str
37+
lat: float
38+
lon: float
39+
name: str
40+
code: str
41+
hidden: bool
42+
showDestinationSelector: bool
43+
directions: list[str]
44+
45+
46+
class PredictionStopInfo(TypedDict):
47+
id: str
48+
lat: float
49+
lon: float
50+
name: str
51+
code: str
52+
hidden: bool
53+
showDestinationSelector: bool
54+
route: str
55+
56+
57+
class Point(TypedDict):
58+
lat: float
59+
lon: float
60+
61+
62+
class RoutePath(TypedDict):
63+
id: str
64+
points: list[Point]
65+
66+
67+
class DirectionInfo(TypedDict):
68+
id: str
69+
shortName: str
70+
name: str
71+
useForUi: bool
72+
stops: list[str]
73+
74+
75+
class RouteDetails(TypedDict):
76+
id: str
77+
rev: int
78+
title: str
79+
description: str
80+
color: str
81+
textColor: str
82+
hidden: bool
83+
boundingBox: RouteBoundingBox
84+
stops: list[StopInfo]
85+
directions: list[DirectionInfo]
86+
paths: list[RoutePath]
87+
timestamp: str
88+
89+
90+
class PredictionDirection(TypedDict):
91+
id: str
92+
name: str
93+
destinationName: str
94+
95+
96+
class PredictionValue(TypedDict):
97+
timestamp: int
98+
minutes: int
99+
affectedByLayover: bool
100+
isDeparture: bool
101+
occupancyStatus: int
102+
occupancyDescription: str
103+
vehiclesInConsist: int
104+
linkedVehicleIds: str
105+
vehicleId: str
106+
vehicleType: str | None
107+
direction: PredictionDirection
108+
tripId: str
109+
delay: int
110+
predUsingNavigationTm: bool
111+
departure: bool
112+
113+
114+
class StopPrediction(TypedDict):
115+
serverTimestamp: int
116+
nxbs2RedirectUrl: str
117+
route: PredictionRouteInfo
118+
stop: PredictionStopInfo
119+
values: list[PredictionValue]

tests/client_test.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
from __future__ import annotations
22

33
import unittest.mock
4+
from unittest.mock import MagicMock
45

56
from py_nextbus.client import NextBusClient
67
from tests.helpers.mock_responses import MOCK_PREDICTIONS_RESPONSE_NO_ROUTE
@@ -16,7 +17,7 @@ def setUp(self):
1617
self.client = NextBusClient()
1718

1819
@unittest.mock.patch("py_nextbus.client.NextBusClient._get")
19-
def test_predictions_for_stop_no_route(self, mock_get):
20+
def test_predictions_for_stop_no_route(self, mock_get: MagicMock):
2021
mock_get.return_value = MOCK_PREDICTIONS_RESPONSE_NO_ROUTE
2122

2223
result = self.client.predictions_for_stop(
@@ -32,7 +33,7 @@ def test_predictions_for_stop_no_route(self, mock_get):
3233
)
3334

3435
@unittest.mock.patch("py_nextbus.client.NextBusClient._get")
35-
def test_predictions_for_stop_with_route(self, mock_get):
36+
def test_predictions_for_stop_with_route(self, mock_get: MagicMock):
3637
mock_get.return_value = MOCK_PREDICTIONS_RESPONSE_WITH_ROUTE
3738

3839
result = self.client.predictions_for_stop(

tests/helpers/mock_responses.py

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,14 @@
1+
from py_nextbus.models import AgencyInfo
2+
from py_nextbus.models import RouteDetails
3+
from py_nextbus.models import RouteInfo
4+
from py_nextbus.models import StopPrediction
5+
16
TEST_AGENCY_ID = "sfmta-cis"
27
TEST_ROUTE_ID = "F"
38
TEST_STOP_ID = "5184"
49
TEST_DIRECTION_ID = "F_0_var0"
510

6-
MOCK_AGENCY_LIST_RESPONSE = [
11+
MOCK_AGENCY_LIST_RESPONSE: list[AgencyInfo] = [
712
{
813
"id": "sfmta-cis",
914
"name": "San Francisco Muni CIS",
@@ -15,7 +20,7 @@
1520
},
1621
]
1722

18-
MOCK_ROUTE_LIST_RESPONSE = [
23+
MOCK_ROUTE_LIST_RESPONSE: list[RouteInfo] = [
1924
{
2025
"id": "F",
2126
"rev": 1057,
@@ -28,7 +33,7 @@
2833
},
2934
]
3035

31-
MOCK_ROUTE_DETAILS_RESPONSE = {
36+
MOCK_ROUTE_DETAILS_RESPONSE: RouteDetails = {
3237
"id": "F",
3338
"rev": 1057,
3439
"title": "F Market & Wharves",
@@ -105,7 +110,7 @@
105110
"timestamp": "2024-06-23T03:06:58Z",
106111
}
107112

108-
MOCK_PREDICTIONS_RESPONSE_NO_ROUTE = [
113+
MOCK_PREDICTIONS_RESPONSE_NO_ROUTE: list[StopPrediction] = [
109114
{
110115
"serverTimestamp": 1724038210798,
111116
"nxbs2RedirectUrl": "",
@@ -241,7 +246,7 @@
241246
},
242247
]
243248

244-
MOCK_PREDICTIONS_RESPONSE_WITH_ROUTE = [
249+
MOCK_PREDICTIONS_RESPONSE_WITH_ROUTE: list[StopPrediction] = [
245250
{
246251
"serverTimestamp": 1720034290432,
247252
"nxbs2RedirectUrl": "",

0 commit comments

Comments
 (0)