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,