diff --git a/README.md b/README.md index 6aaff01..41743e8 100644 --- a/README.md +++ b/README.md @@ -40,7 +40,7 @@ The library supports the following [APIs](https://doc.navitia.io/#api-catalog): | Autocomplete on geographical objects | ✅ | | | Places nearby | ✅ | | | Journeys | ✅ | | -| Isochrones | ❌ | | +| Isochrones | ✅ | Beta endpoint according to API response | | Route Schedules | ✅ | | | Stop Schedules | ✅ | | | Terminus Schedules | ✅ | | diff --git a/navitia_client/client/apis/isochrone_apis.py b/navitia_client/client/apis/isochrone_apis.py new file mode 100644 index 0000000..c8eae64 --- /dev/null +++ b/navitia_client/client/apis/isochrone_apis.py @@ -0,0 +1,91 @@ +from datetime import datetime +from typing import Any, Optional, Sequence +from navitia_client.client.apis.api_base_client import ApiBaseClient +from navitia_client.entities.isochrones import Isochrone + + +class IsochronesApiClient(ApiBaseClient): + def _get_traffic_reports(self, url: str, filters: dict) -> Sequence[Isochrone]: + results = self.get_navitia_api(url + self._generate_filter_query(filters)) + isochrones = [ + Isochrone.from_payload(data) for data in results.json()["isochrones"] + ] + return isochrones + + def list_isochrones_with_region_id( + self, + from_: str, + region_id: str, + start_datetime: datetime = datetime.now(), + boundary_duration: Sequence[int] = [], + to: Optional[str] = None, + first_section_mode: Optional[Sequence[str]] = None, + last_section_mode: Optional[Sequence[str]] = None, + min_duration: Optional[int] = None, + max_duration: Optional[int] = None, + ) -> Sequence[Isochrone]: + request_url = f"{self.base_navitia_url}/coverage/{region_id}/isochrones" + + filters: dict[str, Any] = { + "datetime": start_datetime.isoformat(), + "boundary_duration[]": boundary_duration, + "from": from_, + } + + if to: + filters["to"] = to + + if min_duration: + filters["min_duration"] = min_duration + + if first_section_mode: + filters["first_section_mode[]"] = first_section_mode + + if last_section_mode: + filters["last_section_mode[]"] = last_section_mode + + if max_duration: + filters["max_duration"] = max_duration + if len(boundary_duration) == 0: + # From API: you should provide a 'boundary_duration[]' or a 'max_duration' + filters.pop("min_duration") + + return self._get_traffic_reports(request_url, filters) + + def list_isochrones( + self, + from_: str, + start_datetime: datetime = datetime.now(), + boundary_duration: Sequence[int] = [], + to: Optional[str] = None, + first_section_mode: Optional[Sequence[str]] = None, + last_section_mode: Optional[Sequence[str]] = None, + min_duration: Optional[int] = None, + max_duration: Optional[int] = None, + ) -> Sequence[Isochrone]: + request_url = f"{self.base_navitia_url}/isochrones" + + filters: dict[str, Any] = { + "datetime": start_datetime.isoformat(), + "boundary_duration[]": boundary_duration, + "from": from_, + } + if to: + filters["to"] = to + + if first_section_mode: + filters["first_section_mode[]"] = first_section_mode + + if last_section_mode: + filters["last_section_mode[]"] = last_section_mode + + if min_duration: + filters["min_duration"] = min_duration + + if max_duration: + filters["max_duration"] = max_duration + if len(boundary_duration) == 0: + # From API: you should provide a 'boundary_duration[]' or a 'max_duration' + filters.pop("min_duration") + + return self._get_traffic_reports(request_url, filters) diff --git a/navitia_client/client/navitia_client.py b/navitia_client/client/navitia_client.py index 06469c0..2a04204 100644 --- a/navitia_client/client/navitia_client.py +++ b/navitia_client/client/navitia_client.py @@ -8,6 +8,7 @@ from navitia_client.client.apis.inverted_geocoding_apis import ( InvertedGeocodingApiClient, ) +from navitia_client.client.apis.isochrone_apis import IsochronesApiClient from navitia_client.client.apis.journeys_apis import JourneyApiClient from navitia_client.client.apis.line_report_apis import LineReportsApiClient from navitia_client.client.apis.place_apis import PlacesApiClient @@ -193,3 +194,9 @@ def journeys(self) -> JourneyApiClient: return JourneyApiClient( auth_token=self.auth_token, base_navitia_url=self.base_navitia_url ) + + @property + def isochrones(self) -> IsochronesApiClient: + return IsochronesApiClient( + auth_token=self.auth_token, base_navitia_url=self.base_navitia_url + ) diff --git a/navitia_client/entities/isochrones.py b/navitia_client/entities/isochrones.py new file mode 100644 index 0000000..61ea4da --- /dev/null +++ b/navitia_client/entities/isochrones.py @@ -0,0 +1,30 @@ +from dataclasses import dataclass +from datetime import datetime +from typing import Any + +from navitia_client.entities.place import Place + + +@dataclass +class Isochrone: + from_: Place + geojson: Any + max_date_time: datetime + max_duration: int + min_date_time: datetime + min_duration: int + requested_date_time: datetime + + @classmethod + def from_payload(cls, payload: dict[str, Any]) -> "Isochrone": + return cls( + from_=Place.from_payload(payload["from"]), + geojson=payload["geojson"], + max_date_time=datetime.strptime(payload["max_date_time"], "%Y%m%dT%H%M%S"), + max_duration=payload["max_duration"], + min_date_time=datetime.strptime(payload["min_date_time"], "%Y%m%dT%H%M%S"), + min_duration=payload["min_duration"], + requested_date_time=datetime.strptime( + payload["requested_date_time"], "%Y%m%dT%H%M%S" + ), + ) diff --git a/tests/client/apis/test_isochrones_apis.py b/tests/client/apis/test_isochrones_apis.py new file mode 100644 index 0000000..01a5eaa --- /dev/null +++ b/tests/client/apis/test_isochrones_apis.py @@ -0,0 +1,54 @@ +import json +from unittest.mock import MagicMock, patch + +import pytest + +from navitia_client.client.apis.isochrone_apis import IsochronesApiClient +from navitia_client.entities.isochrones import Isochrone + + +@pytest.fixture +def isochrones_apis(): + return IsochronesApiClient( + auth_token="foobar", base_navitia_url="https://api.navitia.io/v1/" + ) + + +@patch.object(IsochronesApiClient, "get_navitia_api") +def test_list_covered_areas_with_region_id( + mock_get_navitia_api: MagicMock, isochrones_apis: IsochronesApiClient +) -> None: + # Given + mock_response = MagicMock() + with open("tests/test_data/isochrones.json", encoding="utf-8") as file: + mock_response.json.return_value = json.load(file) + + mock_get_navitia_api.return_value = mock_response + + # When + isocrhones = isochrones_apis.list_isochrones_with_region_id( + region_id="bar", from_="foo" + ) + + # Then + assert len(isocrhones) == 1 + assert isinstance(isocrhones[0], Isochrone) + + +@patch.object(IsochronesApiClient, "get_navitia_api") +def test_list_covered_areas( + mock_get_navitia_api: MagicMock, isochrones_apis: IsochronesApiClient +) -> None: + # Given + mock_response = MagicMock() + with open("tests/test_data/isochrones.json", encoding="utf-8") as file: + mock_response.json.return_value = json.load(file) + + mock_get_navitia_api.return_value = mock_response + + # When + isocrhones = isochrones_apis.list_isochrones(from_="foo") + + # Then + assert len(isocrhones) == 1 + assert isinstance(isocrhones[0], Isochrone) diff --git a/tests/test_data/isochrones.json b/tests/test_data/isochrones.json new file mode 100644 index 0000000..e63b0cf --- /dev/null +++ b/tests/test_data/isochrones.json @@ -0,0 +1,797 @@ +{ + "context": { + "current_datetime": "20240523T094155", + "timezone": "Europe/Paris" + }, + "isochrones": [ + { + "from": { + "embedded_type": "stop_area", + "id": "stop_area:SNCF:87758896", + "name": "Saint-Rémy-lès-Chevreuse ", + "quality": 0, + "stop_area": { + "administrative_regions": [ + { + "coord": { + "lat": "48.7054888", + "lon": "2.071109" + }, + "id": "admin:fr:78575", + "insee": "78575", + "label": "Saint-Rémy-lès-Chevreuse", + "level": 8, + "name": "Saint-Rémy-lès-Chevreuse", + "zip_code": "78470" + } + ], + "codes": [ + { + "type": "source", + "value": "87758896" + }, + { + "type": "uic", + "value": "87758896" + } + ], + "coord": { + "lat": "48.702722", + "lon": "2.070924" + }, + "id": "stop_area:SNCF:87758896", + "label": "Saint-Rémy-lès-Chevreuse", + "links": [], + "name": "Saint-Rémy-lès-Chevreuse", + "timezone": "Europe/Paris" + } + }, + "geojson": { + "coordinates": [ + [ + [ + [ + 2.0709878854, + 48.7039292206 + ], + [ + 2.0710517046, + 48.7039270132 + ], + [ + 2.0711153697, + 48.7039233373 + ], + [ + 2.0711788016, + 48.7039181972 + ], + [ + 2.0712419232, + 48.7039115993 + ], + [ + 2.0713046602, + 48.7039035513 + ], + [ + 2.071366929, + 48.7038940635 + ], + [ + 2.0714286611, + 48.7038831474 + ], + [ + 2.0714897758, + 48.703870816 + ], + [ + 2.0715502035, + 48.7038570844 + ], + [ + 2.0716098666, + 48.7038419696 + ], + [ + 2.0716686948, + 48.7038254896 + ], + [ + 2.071726616, + 48.7038076649 + ], + [ + 2.0717835592, + 48.7037885167 + ], + [ + 2.0718394553, + 48.7037680688 + ], + [ + 2.0718942347, + 48.7037463462 + ], + [ + 2.0719478324, + 48.7037233748 + ], + [ + 2.072000184, + 48.7036991829 + ], + [ + 2.072051223, + 48.7036738002 + ], + [ + 2.0721008892, + 48.7036472572 + ], + [ + 2.0721491212, + 48.7036195865 + ], + [ + 2.0721958609, + 48.7035908219 + ], + [ + 2.0722410509, + 48.7035609979 + ], + [ + 2.0722846358, + 48.7035301515 + ], + [ + 2.072326563, + 48.7034983201 + ], + [ + 2.0723667818, + 48.7034655422 + ], + [ + 2.072405242, + 48.7034318583 + ], + [ + 2.0724418986, + 48.7033973086 + ], + [ + 2.072476704, + 48.7033619362 + ], + [ + 2.0725096192, + 48.7033257832 + ], + [ + 2.0725406019, + 48.7032888943 + ], + [ + 2.0725696157, + 48.7032513139 + ], + [ + 2.0725966232, + 48.7032130887 + ], + [ + 2.0726215937, + 48.7031742641 + ], + [ + 2.0726444955, + 48.7031348888 + ], + [ + 2.0726653018, + 48.7030950093 + ], + [ + 2.0726839857, + 48.7030546753 + ], + [ + 2.0727005259, + 48.7030139356 + ], + [ + 2.0727149012, + 48.7029728395 + ], + [ + 2.0727270953, + 48.7029314363 + ], + [ + 2.0727370917, + 48.702889779 + ], + [ + 2.0727448794, + 48.702847916 + ], + [ + 2.0727504483, + 48.7028058998 + ], + [ + 2.0727537918, + 48.7027637827 + ], + [ + 2.0727549071, + 48.7027216094 + ], + [ + 2.0727537918, + 48.7026802173 + ], + [ + 2.0727504483, + 48.7026381002 + ], + [ + 2.0727448794, + 48.702596084 + ], + [ + 2.0727370917, + 48.702554221 + ], + [ + 2.0727270953, + 48.7025125637 + ], + [ + 2.0727149012, + 48.7024711605 + ], + [ + 2.0727005259, + 48.7024300644 + ], + [ + 2.0726839857, + 48.7023893247 + ], + [ + 2.0726653018, + 48.7023489907 + ], + [ + 2.0726444955, + 48.7023091112 + ], + [ + 2.0726215937, + 48.7022697359 + ], + [ + 2.0725966232, + 48.7022309113 + ], + [ + 2.0725696157, + 48.7021926861 + ], + [ + 2.0725406019, + 48.7021551057 + ], + [ + 2.0725096192, + 48.7021182168 + ], + [ + 2.072476704, + 48.7020820638 + ], + [ + 2.0724418986, + 48.7020466914 + ], + [ + 2.072405242, + 48.7020121417 + ], + [ + 2.0723667818, + 48.7019784578 + ], + [ + 2.072326563, + 48.7019456799 + ], + [ + 2.0722846358, + 48.7019138485 + ], + [ + 2.0722410509, + 48.7018830021 + ], + [ + 2.0721958609, + 48.7018531781 + ], + [ + 2.0721491212, + 48.7018244135 + ], + [ + 2.0721008892, + 48.7017967428 + ], + [ + 2.072051223, + 48.7017701998 + ], + [ + 2.072000184, + 48.7017448171 + ], + [ + 2.0719478324, + 48.7017206252 + ], + [ + 2.0718942347, + 48.7016976538 + ], + [ + 2.0718394553, + 48.7016759312 + ], + [ + 2.0717835592, + 48.7016554833 + ], + [ + 2.071726616, + 48.7016363351 + ], + [ + 2.0716686948, + 48.7016185104 + ], + [ + 2.0716098666, + 48.7016020304 + ], + [ + 2.0715502035, + 48.7015869156 + ], + [ + 2.0714897758, + 48.701573184 + ], + [ + 2.0714286611, + 48.7015608526 + ], + [ + 2.071366929, + 48.7015499365 + ], + [ + 2.0713046602, + 48.7015404487 + ], + [ + 2.0712419232, + 48.7015324007 + ], + [ + 2.0711788016, + 48.7015258028 + ], + [ + 2.0711153697, + 48.7015206627 + ], + [ + 2.0710517046, + 48.7015169868 + ], + [ + 2.0709878854, + 48.7015147794 + ], + [ + 2.0709239834, + 48.7015140433 + ], + [ + 2.0708601146, + 48.7015147794 + ], + [ + 2.0707962954, + 48.7015169868 + ], + [ + 2.0707326303, + 48.7015206627 + ], + [ + 2.0706691984, + 48.7015258028 + ], + [ + 2.0706060768, + 48.7015324007 + ], + [ + 2.0705433398, + 48.7015404487 + ], + [ + 2.070481071, + 48.7015499365 + ], + [ + 2.0704193389, + 48.7015608526 + ], + [ + 2.0703582242, + 48.701573184 + ], + [ + 2.0702977965, + 48.7015869156 + ], + [ + 2.0702381334, + 48.7016020304 + ], + [ + 2.0701793052, + 48.7016185104 + ], + [ + 2.070121384, + 48.7016363351 + ], + [ + 2.0700644408, + 48.7016554833 + ], + [ + 2.0700085447, + 48.7016759312 + ], + [ + 2.0699537653, + 48.7016976538 + ], + [ + 2.0699001676, + 48.7017206252 + ], + [ + 2.069847816, + 48.7017448171 + ], + [ + 2.069796777, + 48.7017701998 + ], + [ + 2.0697471108, + 48.7017967428 + ], + [ + 2.0696988788, + 48.7018244135 + ], + [ + 2.0696521391, + 48.7018531781 + ], + [ + 2.0696069491, + 48.7018830021 + ], + [ + 2.0695633642, + 48.7019138485 + ], + [ + 2.069521437, + 48.7019456799 + ], + [ + 2.0694812182, + 48.7019784578 + ], + [ + 2.069442758, + 48.7020121417 + ], + [ + 2.0694061014, + 48.7020466914 + ], + [ + 2.069371296, + 48.7020820638 + ], + [ + 2.0693383808, + 48.7021182168 + ], + [ + 2.0693073981, + 48.7021551057 + ], + [ + 2.0692783843, + 48.7021926861 + ], + [ + 2.0692513768, + 48.7022309113 + ], + [ + 2.0692264063, + 48.7022697359 + ], + [ + 2.0692035045, + 48.7023091112 + ], + [ + 2.0691826982, + 48.7023489907 + ], + [ + 2.0691640143, + 48.7023893247 + ], + [ + 2.0691474741, + 48.7024300644 + ], + [ + 2.0691330988, + 48.7024711605 + ], + [ + 2.0691209047, + 48.7025125637 + ], + [ + 2.0691109083, + 48.702554221 + ], + [ + 2.0691031206, + 48.702596084 + ], + [ + 2.0690975517, + 48.7026381002 + ], + [ + 2.0690942082, + 48.7026802173 + ], + [ + 2.0690930929, + 48.7027216094 + ], + [ + 2.0690942082, + 48.7027637827 + ], + [ + 2.0690975517, + 48.7028058998 + ], + [ + 2.0691031206, + 48.702847916 + ], + [ + 2.0691109083, + 48.702889779 + ], + [ + 2.0691209047, + 48.7029314363 + ], + [ + 2.0691330988, + 48.7029728395 + ], + [ + 2.0691474741, + 48.7030139356 + ], + [ + 2.0691640143, + 48.7030546753 + ], + [ + 2.0691826982, + 48.7030950093 + ], + [ + 2.0692035045, + 48.7031348888 + ], + [ + 2.0692264063, + 48.7031742641 + ], + [ + 2.0692513768, + 48.7032130887 + ], + [ + 2.0692783843, + 48.7032513139 + ], + [ + 2.0693073981, + 48.7032888943 + ], + [ + 2.0693383808, + 48.7033257832 + ], + [ + 2.069371296, + 48.7033619362 + ], + [ + 2.0694061014, + 48.7033973086 + ], + [ + 2.069442758, + 48.7034318583 + ], + [ + 2.0694812182, + 48.7034655422 + ], + [ + 2.069521437, + 48.7034983201 + ], + [ + 2.0695633642, + 48.7035301515 + ], + [ + 2.0696069491, + 48.7035609979 + ], + [ + 2.0696521391, + 48.7035908219 + ], + [ + 2.0696988788, + 48.7036195865 + ], + [ + 2.0697471108, + 48.7036472572 + ], + [ + 2.069796777, + 48.7036738002 + ], + [ + 2.069847816, + 48.7036991829 + ], + [ + 2.0699001676, + 48.7037233748 + ], + [ + 2.0699537653, + 48.7037463462 + ], + [ + 2.0700085447, + 48.7037680688 + ], + [ + 2.0700644408, + 48.7037885167 + ], + [ + 2.070121384, + 48.7038076649 + ], + [ + 2.0701793052, + 48.7038254896 + ], + [ + 2.0702381334, + 48.7038419696 + ], + [ + 2.0702977965, + 48.7038570844 + ], + [ + 2.0703582242, + 48.703870816 + ], + [ + 2.0704193389, + 48.7038831474 + ], + [ + 2.070481071, + 48.7038940635 + ], + [ + 2.0705433398, + 48.7039035513 + ], + [ + 2.0706060768, + 48.7039115993 + ], + [ + 2.0706691984, + 48.7039181972 + ], + [ + 2.0707326303, + 48.7039233373 + ], + [ + 2.0707962954, + 48.7039270132 + ], + [ + 2.0708601146, + 48.7039292206 + ], + [ + 2.0709239834, + 48.7039299567 + ], + [ + 2.0709878854, + 48.7039292206 + ] + ] + ] + ], + "type": "MultiPolygon" + }, + "max_date_time": "20240523T094355", + "max_duration": 120, + "min_date_time": "20240523T094155", + "min_duration": 0, + "requested_date_time": "20240523T094155" + } + ], + "links": [ + { + "href": "https://api.navitia.io/v1/coverage/sncf/stop_areas/{stop_area.id}", + "rel": "stop_areas", + "templated": true, + "type": "stop_area" + } + ] +}