Skip to content

Commit 227c5d3

Browse files
committed
Add departure and arrival datetime to Direction
1 parent 53dfbeb commit 227c5d3

File tree

12 files changed

+415
-81
lines changed

12 files changed

+415
-81
lines changed

poetry.lock

Lines changed: 1 addition & 3 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

pyproject.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ matplotlib = {version = "^3.4.1", optional = true}
2424
contextily = {version = "^1.1.0", optional = true}
2525
geopandas = {version = "^0.8.2", optional = true}
2626
descartes = {version = "^1.0.0", optional = true}
27+
pytz = "^2023.3"
2728

2829
[tool.poetry.extras]
2930
notebooks = ["shapely", "ipykernel", "geopandas", "contextily", "matplotlib", "descartes"]

requirements.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
certifi==2023.7.22 ; python_full_version >= "3.8.0" and python_full_version < "4.0.0"
22
charset-normalizer==3.2.0 ; python_full_version >= "3.8.0" and python_full_version < "4.0.0"
33
idna==3.4 ; python_full_version >= "3.8.0" and python_full_version < "4.0.0"
4+
pytz==2023.3 ; python_full_version >= "3.8.0" and python_full_version < "4.0.0"
45
requests==2.31.0 ; python_full_version >= "3.8.0" and python_full_version < "4.0.0"
56
urllib3==2.0.4 ; python_full_version >= "3.8.0" and python_full_version < "4.0.0"

requirements_dev.txt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ pluggy==1.2.0 ; python_full_version >= "3.8.0" and python_full_version < "4.0.0"
2525
pre-commit==2.21.0 ; python_full_version >= "3.8.0" and python_full_version < "4.0.0"
2626
pygments==2.15.1 ; python_full_version >= "3.8.0" and python_full_version < "4.0.0"
2727
pytest==7.4.0 ; python_full_version >= "3.8.0" and python_full_version < "4.0.0"
28-
pytz==2023.3 ; python_full_version >= "3.8.0" and python_version < "3.9"
28+
pytz==2023.3 ; python_full_version >= "3.8.0" and python_full_version < "4.0.0"
2929
pyyaml==6.0.1 ; python_full_version >= "3.8.0" and python_full_version < "4.0.0"
3030
requests==2.31.0 ; python_full_version >= "3.8.0" and python_full_version < "4.0.0"
3131
responses==0.10.16 ; python_full_version >= "3.8.0" and python_full_version < "4.0.0"

routingpy/direction.py

Lines changed: 36 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
"""
1818
:class:`.Direction` returns directions results.
1919
"""
20+
import datetime
2021
from typing import List, Optional
2122

2223

@@ -65,7 +66,15 @@ class Direction(object):
6566
Contains a parsed directions' response. Access via properties ``geometry``, ``duration`` and ``distance``.
6667
"""
6768

68-
def __init__(self, geometry=None, duration=None, distance=None, raw=None):
69+
def __init__(
70+
self,
71+
geometry: List[List[float]] = None,
72+
duration: int = None,
73+
distance: int = None,
74+
departure_datetime: datetime.datetime = None,
75+
arrival_datetime: datetime.datetime = None,
76+
raw: dict = None,
77+
):
6978
"""
7079
Initialize a :class:`Direction` object to hold the properties of a directions request.
7180
@@ -78,13 +87,21 @@ def __init__(self, geometry=None, duration=None, distance=None, raw=None):
7887
:param distance: The distance of the direction in meters.
7988
:type distance: int
8089
90+
:param departure_datetime: The departure timezone aware date and time of the direction.
91+
:type departure_datetime: datetime.datetime
92+
93+
:param arrival_datetime: The arrival timezone aware date and time of the direction.
94+
:type arrival_datetime: datetime.datetime
95+
8196
:param raw: The raw response of an individual direction (for multiple alternative routes) or the whole direction
8297
response.
8398
:type raw: dict
8499
"""
85100
self._geometry = geometry
86101
self._duration = duration
87102
self._distance = distance
103+
self._departure_datetime = departure_datetime
104+
self._arrival_datetime = arrival_datetime
88105
self._raw = raw
89106

90107
@property
@@ -114,6 +131,24 @@ def distance(self) -> int:
114131
"""
115132
return self._distance
116133

134+
@property
135+
def departure_datetime(self) -> Optional[datetime.datetime]:
136+
"""
137+
The departure timezone aware date and time of the direction.
138+
139+
:rtype: datetime.datetime or None
140+
"""
141+
return self._departure_datetime
142+
143+
@property
144+
def arrival_datetime(self) -> Optional[datetime.datetime]:
145+
"""
146+
The arrival timezone aware date and time of the direction.
147+
148+
:rtype: datetime.datetime or None
149+
"""
150+
return self._arrival_datetime
151+
117152
@property
118153
def raw(self) -> Optional[dict]:
119154
"""

routingpy/routers/google.py

Lines changed: 58 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -15,9 +15,12 @@
1515
# the License.
1616
#
1717

18+
import datetime
1819
from operator import itemgetter
1920
from typing import List, Optional, Tuple, Union
2021

22+
import pytz
23+
2124
from .. import convert, utils
2225
from ..client_base import DEFAULT
2326
from ..client_default import Client
@@ -319,12 +322,42 @@ def directions( # noqa: C901
319322
if transit_routing_preference:
320323
params["transit_routing_preference"] = transit_routing_preference
321324

322-
return self.parse_direction_json(
325+
return self._parse_direction_json(
323326
self.client._request("/directions/json", get_params=params, dry_run=dry_run), alternatives
324327
)
325328

326-
@staticmethod
327-
def parse_direction_json(response, alternatives):
329+
def _time_object_to_aware_datetime(self, time_object):
330+
timestamp = time_object["value"]
331+
dt = datetime.datetime.fromtimestamp(timestamp)
332+
timezone = pytz.timezone(time_object["time_zone"])
333+
return dt.astimezone(timezone)
334+
335+
def _parse_legs(self, legs):
336+
duration = 0
337+
distance = 0
338+
geometry = []
339+
departure_datetime = None
340+
arrival_datetime = None
341+
342+
for leg in legs:
343+
departure_time = leg.get("departure_time")
344+
if departure_time:
345+
assert len(legs) == 1, "departure_time is only supported for single leg routes"
346+
departure_datetime = self._time_object_to_aware_datetime(departure_time)
347+
348+
arrival_time = leg.get("arrival_time")
349+
if arrival_time:
350+
assert len(legs) == 1, "arrival_time is only supported for single leg routes"
351+
arrival_datetime = self._time_object_to_aware_datetime(arrival_time)
352+
353+
duration += leg["duration"]["value"]
354+
distance += leg["distance"]["value"]
355+
for step in leg["steps"]:
356+
geometry.extend(utils.decode_polyline5(step["polyline"]["points"]))
357+
358+
return duration, distance, geometry, departure_datetime, arrival_datetime
359+
360+
def _parse_direction_json(self, response, alternatives):
328361
if response is None: # pragma: no cover
329362
if alternatives:
330363
return Directions()
@@ -345,33 +378,27 @@ def parse_direction_json(response, alternatives):
345378

346379
raise error(STATUS_CODES[status]["code"], STATUS_CODES[status]["message"])
347380

348-
if alternatives:
349-
routes = []
350-
for route in response["routes"]:
351-
geometry = []
352-
duration, distance = 0, 0
353-
for leg in route["legs"]:
354-
duration += leg["duration"]["value"]
355-
distance += leg["distance"]["value"]
356-
for step in leg["steps"]:
357-
geometry.extend(utils.decode_polyline5(step["polyline"]["points"]))
358-
359-
routes.append(
360-
Direction(
361-
geometry=geometry, duration=int(duration), distance=int(distance), raw=route
362-
)
381+
directions = []
382+
for route in response["routes"]:
383+
duration, distance, geometry, departure_datetime, arrival_datetime = self._parse_legs(
384+
route["legs"]
385+
)
386+
directions.append(
387+
Direction(
388+
geometry=geometry,
389+
duration=int(duration),
390+
distance=int(distance),
391+
departure_datetime=departure_datetime,
392+
arrival_datetime=arrival_datetime,
393+
raw=route,
363394
)
364-
return Directions(routes, response)
365-
else:
366-
geometry = []
367-
duration, distance = 0, 0
368-
for leg in response["routes"][0]["legs"]:
369-
duration += leg["duration"]["value"]
370-
distance += leg["distance"]["value"]
371-
for step in leg["steps"]:
372-
geometry.extend(utils.decode_polyline5(step["polyline"]["points"]))
373-
374-
return Direction(geometry=geometry, duration=duration, distance=distance, raw=response)
395+
)
396+
397+
if alternatives:
398+
return Directions(directions, raw=response)
399+
400+
elif directions:
401+
return directions[0]
375402

376403
def isochrones(self): # pragma: no cover
377404
raise NotImplementedError
@@ -506,12 +533,11 @@ def matrix( # noqa: C901
506533
if transit_routing_preference:
507534
params["transit_routing_preference"] = transit_routing_preference
508535

509-
return self.parse_matrix_json(
536+
return self._parse_matrix_json(
510537
self.client._request("/distancematrix/json", get_params=params, dry_run=dry_run)
511538
)
512539

513-
@staticmethod
514-
def parse_matrix_json(response):
540+
def _parse_matrix_json(self, response):
515541
if response is None: # pragma: no cover
516542
return Matrix()
517543

routingpy/routers/opentripplanner_v2.py

Lines changed: 16 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515
# the License.
1616
#
1717
import datetime
18-
from typing import List, Optional # noqa: F401
18+
from typing import List, Optional
1919

2020
from .. import convert, utils
2121
from ..client_base import DEFAULT
@@ -167,30 +167,35 @@ def directions(
167167
)
168168
return self._parse_directions_response(response, num_itineraries)
169169

170+
def _timestamp_to_utc_datetime(self, timestamp):
171+
dt = datetime.datetime.fromtimestamp(timestamp / 1000)
172+
return dt.astimezone(datetime.timezone.utc)
173+
170174
def _parse_directions_response(self, response, num_itineraries):
171175
if response is None: # pragma: no cover
172176
return Directions() if num_itineraries > 1 else Direction()
173177

174-
routes = []
178+
directions = []
175179
for itinerary in response["data"]["plan"]["itineraries"]:
176-
geometry, distance = self._parse_legs(itinerary["legs"])
177-
routes.append(
180+
distance, geometry = self._parse_legs(itinerary["legs"])
181+
departure_datetime = self._timestamp_to_utc_datetime(itinerary["startTime"])
182+
arrival_datetime = self._timestamp_to_utc_datetime(itinerary["endTime"])
183+
directions.append(
178184
Direction(
179185
geometry=geometry,
180186
duration=int(itinerary["duration"]),
181187
distance=distance,
188+
departure_datetime=departure_datetime,
189+
arrival_datetime=arrival_datetime,
182190
raw=itinerary,
183191
)
184192
)
185193

186194
if num_itineraries > 1:
187-
return Directions(routes, raw=response)
188-
189-
elif routes:
190-
return routes[0]
195+
return Directions(directions, raw=response)
191196

192-
else:
193-
return Direction()
197+
elif directions:
198+
return directions[0]
194199

195200
def _parse_legs(self, legs):
196201
distance = 0
@@ -200,7 +205,7 @@ def _parse_legs(self, legs):
200205
geometry.extend(list(reversed(points)))
201206
distance += int(leg["distance"])
202207

203-
return geometry, distance
208+
return distance, geometry
204209

205210
def isochrones(
206211
self,

tests/test_base.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -84,7 +84,6 @@ def test_skip_api_error(self):
8484
)
8585

8686
client = ClientMock(base_url="https://httpbin.org", skip_api_error=False)
87-
print(client.skip_api_error)
8887
with self.assertRaises(routingpy.exceptions.RouterApiError):
8988
client.directions(url="/post", post_params=self.params)
9089

0 commit comments

Comments
 (0)