diff --git a/poetry.lock b/poetry.lock
index bf0014f..b521a2e 100644
--- a/poetry.lock
+++ b/poetry.lock
@@ -1507,7 +1507,6 @@ files = [
{file = "Pillow-10.0.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:3b08d4cc24f471b2c8ca24ec060abf4bebc6b144cb89cba638c720546b1cf538"},
{file = "Pillow-10.0.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:d737a602fbd82afd892ca746392401b634e278cb65d55c4b7a8f48e9ef8d008d"},
{file = "Pillow-10.0.0-cp311-cp311-win_amd64.whl", hash = "sha256:3a82c40d706d9aa9734289740ce26460a11aeec2d9c79b7af87bb35f0073c12f"},
- {file = "Pillow-10.0.0-cp311-cp311-win_arm64.whl", hash = "sha256:bc2ec7c7b5d66b8ec9ce9f720dbb5fa4bace0f545acd34870eff4a369b44bf37"},
{file = "Pillow-10.0.0-cp312-cp312-macosx_10_10_x86_64.whl", hash = "sha256:d80cf684b541685fccdd84c485b31ce73fc5c9b5d7523bf1394ce134a60c6883"},
{file = "Pillow-10.0.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:76de421f9c326da8f43d690110f0e79fe3ad1e54be811545d7d91898b4c8493e"},
{file = "Pillow-10.0.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:81ff539a12457809666fef6624684c008e00ff6bf455b4b89fd00a140eecd640"},
@@ -1517,7 +1516,6 @@ files = [
{file = "Pillow-10.0.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:d50b6aec14bc737742ca96e85d6d0a5f9bfbded018264b3b70ff9d8c33485551"},
{file = "Pillow-10.0.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:00e65f5e822decd501e374b0650146063fbb30a7264b4d2744bdd7b913e0cab5"},
{file = "Pillow-10.0.0-cp312-cp312-win_amd64.whl", hash = "sha256:f31f9fdbfecb042d046f9d91270a0ba28368a723302786c0009ee9b9f1f60199"},
- {file = "Pillow-10.0.0-cp312-cp312-win_arm64.whl", hash = "sha256:1ce91b6ec08d866b14413d3f0bbdea7e24dfdc8e59f562bb77bc3fe60b6144ca"},
{file = "Pillow-10.0.0-cp38-cp38-macosx_10_10_x86_64.whl", hash = "sha256:349930d6e9c685c089284b013478d6f76e3a534e36ddfa912cde493f235372f3"},
{file = "Pillow-10.0.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:3a684105f7c32488f7153905a4e3015a3b6c7182e106fe3c37fbb5ef3e6994c3"},
{file = "Pillow-10.0.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b4f69b3700201b80bb82c3a97d5e9254084f6dd5fb5b16fc1a7b974260f89f43"},
@@ -2512,4 +2510,4 @@ notebooks = ["contextily", "descartes", "geopandas", "ipykernel", "matplotlib",
[metadata]
lock-version = "2.0"
python-versions = "^3.8.0"
-content-hash = "6f31eff8fbc853c1a1d7ab6c1e7aff443e78f5a2ee359c805aa194a8c44be470"
+content-hash = "2bc4ea129445ced049d7df944426f9683ecb992b67f10fefad7169d90eb0fdf9"
diff --git a/pyproject.toml b/pyproject.toml
index 9c68166..55883ec 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -24,6 +24,7 @@ matplotlib = {version = "^3.4.1", optional = true}
contextily = {version = "^1.1.0", optional = true}
geopandas = {version = "^0.8.2", optional = true}
descartes = {version = "^1.0.0", optional = true}
+pytz = "^2023.3"
[tool.poetry.extras]
notebooks = ["shapely", "ipykernel", "geopandas", "contextily", "matplotlib", "descartes"]
diff --git a/requirements.txt b/requirements.txt
index 992003e..c985289 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -1,5 +1,6 @@
certifi==2023.7.22 ; python_full_version >= "3.8.0" and python_full_version < "4.0.0"
charset-normalizer==3.2.0 ; python_full_version >= "3.8.0" and python_full_version < "4.0.0"
idna==3.4 ; python_full_version >= "3.8.0" and python_full_version < "4.0.0"
+pytz==2023.3 ; python_full_version >= "3.8.0" and python_full_version < "4.0.0"
requests==2.31.0 ; python_full_version >= "3.8.0" and python_full_version < "4.0.0"
urllib3==2.0.4 ; python_full_version >= "3.8.0" and python_full_version < "4.0.0"
diff --git a/requirements_dev.txt b/requirements_dev.txt
index 5838221..35d3d1b 100644
--- a/requirements_dev.txt
+++ b/requirements_dev.txt
@@ -25,7 +25,7 @@ pluggy==1.2.0 ; python_full_version >= "3.8.0" and python_full_version < "4.0.0"
pre-commit==2.21.0 ; python_full_version >= "3.8.0" and python_full_version < "4.0.0"
pygments==2.15.1 ; python_full_version >= "3.8.0" and python_full_version < "4.0.0"
pytest==7.4.0 ; python_full_version >= "3.8.0" and python_full_version < "4.0.0"
-pytz==2023.3 ; python_full_version >= "3.8.0" and python_version < "3.9"
+pytz==2023.3 ; python_full_version >= "3.8.0" and python_full_version < "4.0.0"
pyyaml==6.0.1 ; python_full_version >= "3.8.0" and python_full_version < "4.0.0"
requests==2.31.0 ; python_full_version >= "3.8.0" and python_full_version < "4.0.0"
responses==0.10.16 ; python_full_version >= "3.8.0" and python_full_version < "4.0.0"
diff --git a/routingpy/direction.py b/routingpy/direction.py
index cb5554d..d6a5732 100644
--- a/routingpy/direction.py
+++ b/routingpy/direction.py
@@ -17,6 +17,7 @@
"""
:class:`.Direction` returns directions results.
"""
+import datetime
from typing import List, Optional
@@ -65,7 +66,15 @@ class Direction(object):
Contains a parsed directions' response. Access via properties ``geometry``, ``duration`` and ``distance``.
"""
- def __init__(self, geometry=None, duration=None, distance=None, raw=None):
+ def __init__(
+ self,
+ geometry: List[List[float]] = None,
+ duration: int = None,
+ distance: int = None,
+ departure_datetime: datetime.datetime = None,
+ arrival_datetime: datetime.datetime = None,
+ raw: dict = None,
+ ):
"""
Initialize a :class:`Direction` object to hold the properties of a directions request.
@@ -78,6 +87,12 @@ def __init__(self, geometry=None, duration=None, distance=None, raw=None):
:param distance: The distance of the direction in meters.
:type distance: int
+ :param departure_datetime: The departure date and time (timezone aware) of the direction.
+ :type departure_datetime: datetime.datetime
+
+ :param arrival_datetime: The arrival date and time (timezone aware) of the direction.
+ :type arrival_datetime: datetime.datetime
+
:param raw: The raw response of an individual direction (for multiple alternative routes) or the whole direction
response.
:type raw: dict
@@ -85,6 +100,8 @@ def __init__(self, geometry=None, duration=None, distance=None, raw=None):
self._geometry = geometry
self._duration = duration
self._distance = distance
+ self._departure_datetime = departure_datetime
+ self._arrival_datetime = arrival_datetime
self._raw = raw
@property
@@ -114,6 +131,24 @@ def distance(self) -> int:
"""
return self._distance
+ @property
+ def departure_datetime(self) -> Optional[datetime.datetime]:
+ """
+ The departure date and time (timezone aware) of the direction.
+
+ :rtype: datetime.datetime or None
+ """
+ return self._departure_datetime
+
+ @property
+ def arrival_datetime(self) -> Optional[datetime.datetime]:
+ """
+ The arrival date and time (timezone aware) of the direction.
+
+ :rtype: datetime.datetime or None
+ """
+ return self._arrival_datetime
+
@property
def raw(self) -> Optional[dict]:
"""
diff --git a/routingpy/routers/google.py b/routingpy/routers/google.py
index d832179..c1e571b 100644
--- a/routingpy/routers/google.py
+++ b/routingpy/routers/google.py
@@ -15,9 +15,12 @@
# the License.
#
+import datetime
from operator import itemgetter
from typing import List, Optional, Tuple, Union
+import pytz
+
from .. import convert, utils
from ..client_base import DEFAULT
from ..client_default import Client
@@ -319,12 +322,40 @@ def directions( # noqa: C901
if transit_routing_preference:
params["transit_routing_preference"] = transit_routing_preference
- return self.parse_direction_json(
+ return self._parse_direction_json(
self.client._request("/directions/json", get_params=params, dry_run=dry_run), alternatives
)
- @staticmethod
- def parse_direction_json(response, alternatives):
+ def _time_object_to_aware_datetime(self, time_object):
+ timestamp = time_object["value"]
+ dt = datetime.datetime.fromtimestamp(timestamp)
+ timezone = pytz.timezone(time_object["time_zone"])
+ return dt.astimezone(timezone)
+
+ def _parse_legs(self, legs):
+ duration = 0
+ distance = 0
+ geometry = []
+ departure_datetime = None
+ arrival_datetime = None
+
+ for leg in legs:
+ duration += leg["duration"]["value"]
+ distance += leg["distance"]["value"]
+ for step in leg["steps"]:
+ geometry.extend(utils.decode_polyline5(step["polyline"]["points"]))
+
+ departure_time = legs[0].get("departure_time")
+ if departure_time:
+ departure_datetime = self._time_object_to_aware_datetime(departure_time)
+
+ arrival_time = legs[-1].get("arrival_time")
+ if arrival_time:
+ arrival_datetime = self._time_object_to_aware_datetime(arrival_time)
+
+ return duration, distance, geometry, departure_datetime, arrival_datetime
+
+ def _parse_direction_json(self, response, alternatives):
if response is None: # pragma: no cover
if alternatives:
return Directions()
@@ -345,33 +376,27 @@ def parse_direction_json(response, alternatives):
raise error(STATUS_CODES[status]["code"], STATUS_CODES[status]["message"])
- if alternatives:
- routes = []
- for route in response["routes"]:
- geometry = []
- duration, distance = 0, 0
- for leg in route["legs"]:
- duration += leg["duration"]["value"]
- distance += leg["distance"]["value"]
- for step in leg["steps"]:
- geometry.extend(utils.decode_polyline5(step["polyline"]["points"]))
-
- routes.append(
- Direction(
- geometry=geometry, duration=int(duration), distance=int(distance), raw=route
- )
+ directions = []
+ for route in response["routes"]:
+ duration, distance, geometry, departure_datetime, arrival_datetime = self._parse_legs(
+ route["legs"]
+ )
+ directions.append(
+ Direction(
+ geometry=geometry,
+ duration=int(duration),
+ distance=int(distance),
+ departure_datetime=departure_datetime,
+ arrival_datetime=arrival_datetime,
+ raw=route,
)
- return Directions(routes, response)
- else:
- geometry = []
- duration, distance = 0, 0
- for leg in response["routes"][0]["legs"]:
- duration += leg["duration"]["value"]
- distance += leg["distance"]["value"]
- for step in leg["steps"]:
- geometry.extend(utils.decode_polyline5(step["polyline"]["points"]))
-
- return Direction(geometry=geometry, duration=duration, distance=distance, raw=response)
+ )
+
+ if alternatives:
+ return Directions(directions, raw=response)
+
+ elif directions:
+ return directions[0]
def isochrones(self): # pragma: no cover
raise NotImplementedError
@@ -506,12 +531,11 @@ def matrix( # noqa: C901
if transit_routing_preference:
params["transit_routing_preference"] = transit_routing_preference
- return self.parse_matrix_json(
+ return self._parse_matrix_json(
self.client._request("/distancematrix/json", get_params=params, dry_run=dry_run)
)
- @staticmethod
- def parse_matrix_json(response):
+ def _parse_matrix_json(self, response):
if response is None: # pragma: no cover
return Matrix()
diff --git a/routingpy/routers/opentripplanner_v2.py b/routingpy/routers/opentripplanner_v2.py
index eb0a5bd..f9a8eba 100644
--- a/routingpy/routers/opentripplanner_v2.py
+++ b/routingpy/routers/opentripplanner_v2.py
@@ -15,7 +15,7 @@
# the License.
#
import datetime
-from typing import List, Optional # noqa: F401
+from typing import List, Optional
from .. import convert, utils
from ..client_base import DEFAULT
@@ -167,30 +167,35 @@ def directions(
)
return self._parse_directions_response(response, num_itineraries)
+ def _timestamp_to_utc_datetime(self, timestamp):
+ dt = datetime.datetime.fromtimestamp(timestamp / 1000)
+ return dt.astimezone(datetime.timezone.utc)
+
def _parse_directions_response(self, response, num_itineraries):
if response is None: # pragma: no cover
return Directions() if num_itineraries > 1 else Direction()
- routes = []
+ directions = []
for itinerary in response["data"]["plan"]["itineraries"]:
- geometry, distance = self._parse_legs(itinerary["legs"])
- routes.append(
+ distance, geometry = self._parse_legs(itinerary["legs"])
+ departure_datetime = self._timestamp_to_utc_datetime(itinerary["startTime"])
+ arrival_datetime = self._timestamp_to_utc_datetime(itinerary["endTime"])
+ directions.append(
Direction(
geometry=geometry,
duration=int(itinerary["duration"]),
distance=distance,
+ departure_datetime=departure_datetime,
+ arrival_datetime=arrival_datetime,
raw=itinerary,
)
)
if num_itineraries > 1:
- return Directions(routes, raw=response)
-
- elif routes:
- return routes[0]
+ return Directions(directions, raw=response)
- else:
- return Direction()
+ elif directions:
+ return directions[0]
def _parse_legs(self, legs):
distance = 0
@@ -200,7 +205,7 @@ def _parse_legs(self, legs):
geometry.extend(list(reversed(points)))
distance += int(leg["distance"])
- return geometry, distance
+ return distance, geometry
def isochrones(
self,
diff --git a/tests/test_base.py b/tests/test_base.py
index 6b64080..668d95d 100644
--- a/tests/test_base.py
+++ b/tests/test_base.py
@@ -84,7 +84,6 @@ def test_skip_api_error(self):
)
client = ClientMock(base_url="https://httpbin.org", skip_api_error=False)
- print(client.skip_api_error)
with self.assertRaises(routingpy.exceptions.RouterApiError):
client.directions(url="/post", post_params=self.params)
diff --git a/tests/test_google.py b/tests/test_google.py
index eb40bc0..3a1af98 100644
--- a/tests/test_google.py
+++ b/tests/test_google.py
@@ -47,7 +47,7 @@ def test_full_directions(self):
content_type="application/json",
)
- routes = self.client.directions(**query)
+ directions = self.client.directions(**query)
self.assertEqual(1, len(responses.calls))
self.assertURLEqual(
"https://maps.googleapis.com/maps/api/directions/json?alternatives=true&arrival_time=1567512000&"
@@ -57,12 +57,44 @@ def test_full_directions(self):
responses.calls[0].request.url,
)
- self.assertIsInstance(routes, Directions)
- self.assertIsInstance(routes[0], Direction)
- self.assertIsInstance(routes[0].geometry, list)
- self.assertIsInstance(routes[0].distance, int)
- self.assertIsInstance(routes[0].duration, int)
- self.assertIsInstance(routes[0].raw, dict)
+ self.assertIsInstance(directions, Directions)
+ self.assertIsInstance(directions[0], Direction)
+ self.assertIsInstance(directions[0].geometry, list)
+ self.assertIsInstance(directions[0].distance, int)
+ self.assertIsInstance(directions[0].duration, int)
+ self.assertIsInstance(directions[0].raw, dict)
+
+ @responses.activate
+ def test_directions_transit(self):
+ query = ENDPOINTS_QUERIES[self.name]["directions_transit"]
+
+ responses.add(
+ responses.GET,
+ "https://maps.googleapis.com/maps/api/directions/json",
+ status=200,
+ json=ENDPOINTS_RESPONSES[self.name]["directions_transit"],
+ content_type="application/json",
+ )
+
+ direction = self.client.directions(**query)
+ self.assertEqual(1, len(responses.calls))
+ self.assertURLEqual(
+ "https://maps.googleapis.com/maps/api/directions/json?arrival_time=1691064000&"
+ "destination=49.415776%2C8.680916&key=sample_key&mode=transit&"
+ "origin=49.420577%2C8.688641",
+ responses.calls[0].request.url,
+ )
+
+ self.assertIsInstance(direction, Direction)
+ self.assertIsInstance(direction, Direction)
+ self.assertIsInstance(direction.geometry, list)
+ self.assertIsInstance(direction.distance, int)
+ self.assertIsInstance(direction.duration, int)
+ self.assertIsInstance(direction.departure_datetime, datetime.datetime)
+ self.assertEqual(direction.departure_datetime.tzinfo.zone, "Europe/Berlin")
+ self.assertIsInstance(direction.arrival_datetime, datetime.datetime)
+ self.assertEqual(direction.arrival_datetime.tzinfo.zone, "Europe/Berlin")
+ self.assertIsInstance(direction.raw, dict)
@responses.activate
def test_full_directions_no_alternatives(self):
@@ -77,7 +109,7 @@ def test_full_directions_no_alternatives(self):
content_type="application/json",
)
- routes = self.client.directions(**query)
+ direction = self.client.directions(**query)
self.assertEqual(1, len(responses.calls))
self.assertURLEqual(
"https://maps.googleapis.com/maps/api/directions/json?alternatives=false&arrival_time=1567512000&"
@@ -87,11 +119,11 @@ def test_full_directions_no_alternatives(self):
responses.calls[0].request.url,
)
- self.assertIsInstance(routes, Direction)
- self.assertIsInstance(routes.geometry, list)
- self.assertIsInstance(routes.duration, int)
- self.assertIsInstance(routes.distance, int)
- self.assertIsInstance(routes.raw, dict)
+ self.assertIsInstance(direction, Direction)
+ self.assertIsInstance(direction.geometry, list)
+ self.assertIsInstance(direction.duration, int)
+ self.assertIsInstance(direction.distance, int)
+ self.assertIsInstance(direction.raw, dict)
@responses.activate
def test_waypoint_generator_directions(self):
@@ -240,11 +272,11 @@ def test_status_codes(self):
for alternatives in [True, False]:
with self.assertRaises(RouterApiError):
- self.client.parse_direction_json(
+ self.client._parse_direction_json(
error_responses["ZERO_RESULTS"], alternatives=alternatives
)
with self.assertRaises(RouterServerError):
- self.client.parse_direction_json(
+ self.client._parse_direction_json(
error_responses["UNKNOWN_ERROR"], alternatives=alternatives
)
diff --git a/tests/test_helper.py b/tests/test_helper.py
index b7ce4b1..66f3d04 100644
--- a/tests/test_helper.py
+++ b/tests/test_helper.py
@@ -416,6 +416,228 @@
],
"status": "OK",
},
+ "directions_transit": {
+ "geocoded_waypoints": [
+ {
+ "geocoder_status": "OK",
+ "place_id": "ChIJi2as5j3Bl0cRQNkSY5pCTDQ",
+ "types": ["premise"],
+ },
+ {
+ "geocoder_status": "OK",
+ "place_id": "ChIJITuAsCTBl0cRH5oKE9qv8Ys",
+ "types": ["street_address"],
+ },
+ ],
+ "routes": [
+ {
+ "bounds": {
+ "northeast": {"lat": 49.4204993, "lng": 8.690909999999999},
+ "southwest": {"lat": 49.4157593, "lng": 8.6809166},
+ },
+ "copyrights": "Map data ©2023 GeoBasis-DE/BKG (©2009)",
+ "legs": [
+ {
+ "arrival_time": {
+ "text": "1:57\u202fPM",
+ "time_zone": "Europe/Berlin",
+ "value": 1691063878,
+ },
+ "departure_time": {
+ "text": "1:46\u202fPM",
+ "time_zone": "Europe/Berlin",
+ "value": 1691063212,
+ },
+ "distance": {"text": "1.3 km", "value": 1345},
+ "duration": {"text": "11 mins", "value": 666},
+ "end_address": "Schröderstraße 78, 69120 Heidelberg, Germany",
+ "end_location": {"lat": 49.4157593, "lng": 8.6809166},
+ "start_address": "Roonstraße 6, 69120 Heidelberg, Germany",
+ "start_location": {"lat": 49.4204039, "lng": 8.6886808},
+ "steps": [
+ {
+ "distance": {"text": "0.5 km", "value": 463},
+ "duration": {"text": "6 mins", "value": 352},
+ "end_location": {"lat": 49.4173109, "lng": 8.6909041},
+ "html_instructions": "Walk to Neuenheim, Lutherstraße",
+ "polyline": {
+ "points": "olslHg_`t@S{D?Gr@a@pAs@n@a@z@a@HD`@A\\@VDZATCr@FdDC\\gB"
+ },
+ "start_location": {"lat": 49.4204039, "lng": 8.6886808},
+ "steps": [
+ {
+ "distance": {"text": "71 m", "value": 71},
+ "duration": {"text": "1 min", "value": 51},
+ "end_location": {
+ "lat": 49.4204993,
+ "lng": 8.689657799999999,
+ },
+ "html_instructions": "Head east on Roonstraße toward Handschuhsheimer Landstraße/B3",
+ "polyline": {"points": "olslHg_`t@S{D?G"},
+ "start_location": {"lat": 49.4204039, "lng": 8.6886808},
+ "travel_mode": "WALKING",
+ },
+ {
+ "distance": {"text": "0.1 km", "value": 146},
+ "duration": {"text": "2 mins", "value": 109},
+ "end_location": {"lat": 49.41928799999999, "lng": 8.6904262},
+ "html_instructions": "Turn right onto Handschuhsheimer Landstraße/B3",
+ "maneuver": "turn-right",
+ "polyline": {"points": "cmslHke`t@r@a@pAs@n@a@z@a@"},
+ "start_location": {
+ "lat": 49.4204993,
+ "lng": 8.689657799999999,
+ },
+ "travel_mode": "WALKING",
+ },
+ {
+ "distance": {"text": "25 m", "value": 25},
+ "duration": {"text": "1 min", "value": 24},
+ "end_location": {"lat": 49.4190674, "lng": 8.690413},
+ "html_instructions": "Slight right to stay on Handschuhsheimer Landstraße/B3",
+ "maneuver": "turn-slight-right",
+ "polyline": {"points": "qeslHej`t@HD`@A"},
+ "start_location": {
+ "lat": 49.41928799999999,
+ "lng": 8.6904262,
+ },
+ "travel_mode": "WALKING",
+ },
+ {
+ "distance": {"text": "0.2 km", "value": 179},
+ "duration": {"text": "2 mins", "value": 136},
+ "end_location": {"lat": 49.4174571, "lng": 8.6903761},
+ "html_instructions": "Continue onto Lutherstraße",
+ "polyline": {"points": "edslHaj`t@\\@VDZATCr@FdDC"},
+ "start_location": {"lat": 49.4190674, "lng": 8.690413},
+ "travel_mode": "WALKING",
+ },
+ {
+ "distance": {"text": "42 m", "value": 42},
+ "duration": {"text": "1 min", "value": 32},
+ "end_location": {"lat": 49.4173109, "lng": 8.6909041},
+ "html_instructions": "Turn left onto Mönchhofstraße",
+ "maneuver": "turn-left",
+ "polyline": {"points": "czrlH{i`t@\\gB"},
+ "start_location": {"lat": 49.4174571, "lng": 8.6903761},
+ "travel_mode": "WALKING",
+ },
+ ],
+ "travel_mode": "WALKING",
+ },
+ {
+ "distance": {"text": "0.7 km", "value": 652},
+ "duration": {"text": "2 mins", "value": 120},
+ "end_location": {"lat": 49.417445, "lng": 8.682053999999999},
+ "html_instructions": "Bus towards Neuenheim, Kopfklinik",
+ "polyline": {
+ "points": "gyrlHem`t@@@]fB@^Ax@H`FDdA?~D?jC@nA@dC?jAAdCAzE?jB@~@AbCK?"
+ },
+ "start_location": {"lat": 49.41732, "lng": 8.690909999999999},
+ "transit_details": {
+ "arrival_stop": {
+ "location": {"lat": 49.417445, "lng": 8.682053999999999},
+ "name": "Neuenheim, Wielandtstraße",
+ },
+ "arrival_time": {
+ "text": "1:55\u202fPM",
+ "time_zone": "Europe/Berlin",
+ "value": 1691063700,
+ },
+ "departure_stop": {
+ "location": {"lat": 49.41732, "lng": 8.690909999999999},
+ "name": "Neuenheim, Lutherstraße",
+ },
+ "departure_time": {
+ "text": "1:53\u202fPM",
+ "time_zone": "Europe/Berlin",
+ "value": 1691063580,
+ },
+ "headsign": "Neuenheim, Kopfklinik",
+ "line": {
+ "agencies": [
+ {
+ "name": "RNV Rhein-Neckar-Verkehr GmbH",
+ "url": "https://vrn.de/",
+ }
+ ],
+ "color": "#0000f2",
+ "name": "HD Universitätsplatz - Bismarckplatz - Neuenheim - Uniklinikum",
+ "short_name": "RNV 31",
+ "text_color": "#ffffff",
+ "vehicle": {
+ "icon": "//maps.gstatic.com/mapfiles/transit/iw2/6/bus2.png",
+ "name": "Bus",
+ "type": "BUS",
+ },
+ },
+ "num_stops": 2,
+ },
+ "travel_mode": "TRANSIT",
+ },
+ {
+ "distance": {"text": "0.2 km", "value": 230},
+ "duration": {"text": "3 mins", "value": 166},
+ "end_location": {"lat": 49.4157593, "lng": 8.6809166},
+ "html_instructions": "Walk to Schröderstraße 78, 69120 Heidelberg, Germany",
+ "polyline": {"points": "syrlHyu~s@?X|Cr@bDl@@dB"},
+ "start_location": {"lat": 49.4173827, "lng": 8.6820519},
+ "steps": [
+ {
+ "distance": {"text": "10 m", "value": 10},
+ "duration": {"text": "1 min", "value": 7},
+ "end_location": {
+ "lat": 49.4173846,
+ "lng": 8.681917799999999,
+ },
+ "html_instructions": "Head west on Mönchhofstraße toward Wielandtstraße",
+ "polyline": {"points": "syrlHyu~s@?X"},
+ "start_location": {"lat": 49.4173827, "lng": 8.6820519},
+ "travel_mode": "WALKING",
+ },
+ {
+ "distance": {"text": "0.2 km", "value": 183},
+ "duration": {"text": "2 mins", "value": 132},
+ "end_location": {"lat": 49.4157671, "lng": 8.6814298},
+ "html_instructions": "Turn left onto Wielandtstraße",
+ "maneuver": "turn-left",
+ "polyline": {"points": "syrlH_u~s@|Cr@bDl@"},
+ "start_location": {
+ "lat": 49.4173846,
+ "lng": 8.681917799999999,
+ },
+ "travel_mode": "WALKING",
+ },
+ {
+ "distance": {"text": "37 m", "value": 37},
+ "duration": {"text": "1 min", "value": 27},
+ "end_location": {"lat": 49.4157593, "lng": 8.6809166},
+ "html_instructions": "Turn right onto Schröderstraße",
+ "maneuver": "turn-right",
+ "polyline": {"points": "qorlH}q~s@@dB"},
+ "start_location": {"lat": 49.4157671, "lng": 8.6814298},
+ "travel_mode": "WALKING",
+ },
+ ],
+ "travel_mode": "WALKING",
+ },
+ ],
+ "traffic_speed_entry": [],
+ "via_waypoint": [],
+ }
+ ],
+ "overview_polyline": {
+ "points": "olslHg_`t@ScEdCuAn@a@z@a@HD`@At@Fp@Er@FdDCZiB[hB?xANfH?jI@fLAjOK?J??X|Cr@bDl@@dB"
+ },
+ "summary": "",
+ "warnings": [
+ "Walking directions are in beta. Use caution – This route may be missing sidewalks or pedestrian paths."
+ ],
+ "waypoint_order": [],
+ }
+ ],
+ "status": "OK",
+ },
"matrix": {
"rows": [
{
@@ -436,10 +658,15 @@
"itineraries": [
{
"duration": 178,
+ "startTime": 1691053917000,
+ "endTime": 1691054095000,
"legs": [
{
+ "startTime": 1691053917000,
+ "endTime": 1691054095000,
"duration": 178.0,
"distance": 1073.17,
+ "mode": "CAR",
"legGeometry": {
"points": "olslHg_`t@@N`@bI\\E~AMJAJAF?tAKjBSFAFApBY~@EPAHA?P?j@AV?l@?P?|@AhC@t@?P?JAt@?bA@bEATAj@?jC?h@AfA?H?`@T@@?d@H`B`@dB\\v@NJ??X?jA"
},
@@ -833,6 +1060,11 @@
"transit_mode": ["bus", "rail"],
"transit_routing_preference": "less_walking",
},
+ "directions_transit": {
+ "profile": "transit",
+ "locations": PARAM_LINE,
+ "arrival_time": 1691064000,
+ },
"matrix": {
"profile": "driving",
"locations": PARAM_LINE_MULTI,
diff --git a/tests/test_opentripplanner_v2.py b/tests/test_opentripplanner_v2.py
index 57ff785..faa1b55 100644
--- a/tests/test_opentripplanner_v2.py
+++ b/tests/test_opentripplanner_v2.py
@@ -45,17 +45,20 @@ def test_directions(self):
json=ENDPOINTS_RESPONSES["otp_v2"]["directions"],
content_type="application/json",
)
- routes = self.client.directions(**query)
+ direction = self.client.directions(**query)
self.assertEqual(1, len(responses.calls))
self.assertURLEqual(
"http://localhost:8080/otp/routers/default/index/graphql",
responses.calls[0].request.url,
)
- self.assertIsInstance(routes, Direction)
- self.assertIsInstance(routes.distance, int)
- self.assertIsInstance(routes.duration, int)
- self.assertIsInstance(routes.geometry, list)
- self.assertIsInstance(routes.raw, dict)
+ self.assertIsInstance(direction, Direction)
+ self.assertIsInstance(direction.distance, int)
+ self.assertIsInstance(direction.duration, int)
+ self.assertIsInstance(direction.geometry, list)
+ self.assertIsInstance(direction.departure_datetime, datetime.datetime)
+ self.assertEqual(direction.departure_datetime.tzinfo, datetime.timezone.utc)
+ self.assertIsInstance(direction.arrival_datetime, datetime.datetime)
+ self.assertEqual(direction.arrival_datetime.tzinfo, datetime.timezone.utc)
@responses.activate
def test_directions_alternative(self):
@@ -67,20 +70,24 @@ def test_directions_alternative(self):
json=ENDPOINTS_RESPONSES["otp_v2"]["directions"],
content_type="application/json",
)
- routes = self.client.directions(**query)
+ directions = self.client.directions(**query)
self.assertEqual(1, len(responses.calls))
self.assertURLEqual(
"http://localhost:8080/otp/routers/default/index/graphql",
responses.calls[0].request.url,
)
- self.assertIsInstance(routes, Directions)
- self.assertEqual(1, len(routes))
- for route in routes:
- self.assertIsInstance(route, Direction)
- self.assertIsInstance(route.duration, int)
- self.assertIsInstance(route.distance, int)
- self.assertIsInstance(route.geometry, list)
- self.assertIsInstance(route.raw, dict)
+ self.assertIsInstance(directions, Directions)
+ self.assertEqual(1, len(directions))
+ for direction in directions:
+ self.assertIsInstance(direction, Direction)
+ self.assertIsInstance(direction.duration, int)
+ self.assertIsInstance(direction.distance, int)
+ self.assertIsInstance(direction.geometry, list)
+ self.assertIsInstance(direction.raw, dict)
+ self.assertIsInstance(direction.departure_datetime, datetime.datetime)
+ self.assertEqual(direction.departure_datetime.tzinfo, datetime.timezone.utc)
+ self.assertIsInstance(direction.arrival_datetime, datetime.datetime)
+ self.assertEqual(direction.arrival_datetime.tzinfo, datetime.timezone.utc)
@responses.activate
def test_isochrones(self):
diff --git a/tests/test_osrm.py b/tests/test_osrm.py
index e5fc39c..2bbd00e 100644
--- a/tests/test_osrm.py
+++ b/tests/test_osrm.py
@@ -238,7 +238,6 @@ def test_full_matrix(self):
matrix = self.client.matrix(**query)
- print(responses.calls[0].request.url)
self.assertEqual(1, len(responses.calls))
self.assertURLEqual(
f"https://routing.openstreetmap.de/routed-bike/table/v1/{query['profile']}/8.688641,49.420577;8.680916,49.415776;8.780916,49.445776?annotations=distance%2Cduration&bearings=50%2C50%3B50%2C50%3B50%2C50&fallback_speed=42&radiuses=500%3B500%3B500",
@@ -268,7 +267,6 @@ def test_few_sources_destinations_matrix(self):
self.client.matrix(**query)
self.assertEqual(1, len(responses.calls))
- print(responses.calls[0].request.url)
self.assertURLEqual(
f"https://routing.openstreetmap.de/routed-bike/table/v1/{query['profile']}/8.688641,49.420577;8.680916,49.415776;8.780916,49.445776?annotations=distance%2Cduration&bearings=50%2C50%3B50%2C50%3B50%2C50&destinations=0%3B2&radiuses=500%3B500%3B500&sources=1%3B2",
responses.calls[0].request.url,