Skip to content

Commit 5af90eb

Browse files
authored
feat(apis): add support for isochrone apis (#48)
1 parent f4a0ab7 commit 5af90eb

File tree

6 files changed

+980
-1
lines changed

6 files changed

+980
-1
lines changed

README.md

+1-1
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ The library supports the following [APIs](https://doc.navitia.io/#api-catalog):
4040
| Autocomplete on geographical objects || |
4141
| Places nearby || |
4242
| Journeys || |
43-
| Isochrones | | |
43+
| Isochrones | | Beta endpoint according to API response |
4444
| Route Schedules || |
4545
| Stop Schedules || |
4646
| Terminus Schedules || |
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
from datetime import datetime
2+
from typing import Any, Optional, Sequence
3+
from navitia_client.client.apis.api_base_client import ApiBaseClient
4+
from navitia_client.entities.isochrones import Isochrone
5+
6+
7+
class IsochronesApiClient(ApiBaseClient):
8+
def _get_traffic_reports(self, url: str, filters: dict) -> Sequence[Isochrone]:
9+
results = self.get_navitia_api(url + self._generate_filter_query(filters))
10+
isochrones = [
11+
Isochrone.from_payload(data) for data in results.json()["isochrones"]
12+
]
13+
return isochrones
14+
15+
def list_isochrones_with_region_id(
16+
self,
17+
from_: str,
18+
region_id: str,
19+
start_datetime: datetime = datetime.now(),
20+
boundary_duration: Sequence[int] = [],
21+
to: Optional[str] = None,
22+
first_section_mode: Optional[Sequence[str]] = None,
23+
last_section_mode: Optional[Sequence[str]] = None,
24+
min_duration: Optional[int] = None,
25+
max_duration: Optional[int] = None,
26+
) -> Sequence[Isochrone]:
27+
request_url = f"{self.base_navitia_url}/coverage/{region_id}/isochrones"
28+
29+
filters: dict[str, Any] = {
30+
"datetime": start_datetime.isoformat(),
31+
"boundary_duration[]": boundary_duration,
32+
"from": from_,
33+
}
34+
35+
if to:
36+
filters["to"] = to
37+
38+
if min_duration:
39+
filters["min_duration"] = min_duration
40+
41+
if first_section_mode:
42+
filters["first_section_mode[]"] = first_section_mode
43+
44+
if last_section_mode:
45+
filters["last_section_mode[]"] = last_section_mode
46+
47+
if max_duration:
48+
filters["max_duration"] = max_duration
49+
if len(boundary_duration) == 0:
50+
# From API: you should provide a 'boundary_duration[]' or a 'max_duration'
51+
filters.pop("min_duration")
52+
53+
return self._get_traffic_reports(request_url, filters)
54+
55+
def list_isochrones(
56+
self,
57+
from_: str,
58+
start_datetime: datetime = datetime.now(),
59+
boundary_duration: Sequence[int] = [],
60+
to: Optional[str] = None,
61+
first_section_mode: Optional[Sequence[str]] = None,
62+
last_section_mode: Optional[Sequence[str]] = None,
63+
min_duration: Optional[int] = None,
64+
max_duration: Optional[int] = None,
65+
) -> Sequence[Isochrone]:
66+
request_url = f"{self.base_navitia_url}/isochrones"
67+
68+
filters: dict[str, Any] = {
69+
"datetime": start_datetime.isoformat(),
70+
"boundary_duration[]": boundary_duration,
71+
"from": from_,
72+
}
73+
if to:
74+
filters["to"] = to
75+
76+
if first_section_mode:
77+
filters["first_section_mode[]"] = first_section_mode
78+
79+
if last_section_mode:
80+
filters["last_section_mode[]"] = last_section_mode
81+
82+
if min_duration:
83+
filters["min_duration"] = min_duration
84+
85+
if max_duration:
86+
filters["max_duration"] = max_duration
87+
if len(boundary_duration) == 0:
88+
# From API: you should provide a 'boundary_duration[]' or a 'max_duration'
89+
filters.pop("min_duration")
90+
91+
return self._get_traffic_reports(request_url, filters)

navitia_client/client/navitia_client.py

+7
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
from navitia_client.client.apis.inverted_geocoding_apis import (
99
InvertedGeocodingApiClient,
1010
)
11+
from navitia_client.client.apis.isochrone_apis import IsochronesApiClient
1112
from navitia_client.client.apis.journeys_apis import JourneyApiClient
1213
from navitia_client.client.apis.line_report_apis import LineReportsApiClient
1314
from navitia_client.client.apis.place_apis import PlacesApiClient
@@ -193,3 +194,9 @@ def journeys(self) -> JourneyApiClient:
193194
return JourneyApiClient(
194195
auth_token=self.auth_token, base_navitia_url=self.base_navitia_url
195196
)
197+
198+
@property
199+
def isochrones(self) -> IsochronesApiClient:
200+
return IsochronesApiClient(
201+
auth_token=self.auth_token, base_navitia_url=self.base_navitia_url
202+
)

navitia_client/entities/isochrones.py

+30
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
from dataclasses import dataclass
2+
from datetime import datetime
3+
from typing import Any
4+
5+
from navitia_client.entities.place import Place
6+
7+
8+
@dataclass
9+
class Isochrone:
10+
from_: Place
11+
geojson: Any
12+
max_date_time: datetime
13+
max_duration: int
14+
min_date_time: datetime
15+
min_duration: int
16+
requested_date_time: datetime
17+
18+
@classmethod
19+
def from_payload(cls, payload: dict[str, Any]) -> "Isochrone":
20+
return cls(
21+
from_=Place.from_payload(payload["from"]),
22+
geojson=payload["geojson"],
23+
max_date_time=datetime.strptime(payload["max_date_time"], "%Y%m%dT%H%M%S"),
24+
max_duration=payload["max_duration"],
25+
min_date_time=datetime.strptime(payload["min_date_time"], "%Y%m%dT%H%M%S"),
26+
min_duration=payload["min_duration"],
27+
requested_date_time=datetime.strptime(
28+
payload["requested_date_time"], "%Y%m%dT%H%M%S"
29+
),
30+
)
+54
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
import json
2+
from unittest.mock import MagicMock, patch
3+
4+
import pytest
5+
6+
from navitia_client.client.apis.isochrone_apis import IsochronesApiClient
7+
from navitia_client.entities.isochrones import Isochrone
8+
9+
10+
@pytest.fixture
11+
def isochrones_apis():
12+
return IsochronesApiClient(
13+
auth_token="foobar", base_navitia_url="https://api.navitia.io/v1/"
14+
)
15+
16+
17+
@patch.object(IsochronesApiClient, "get_navitia_api")
18+
def test_list_covered_areas_with_region_id(
19+
mock_get_navitia_api: MagicMock, isochrones_apis: IsochronesApiClient
20+
) -> None:
21+
# Given
22+
mock_response = MagicMock()
23+
with open("tests/test_data/isochrones.json", encoding="utf-8") as file:
24+
mock_response.json.return_value = json.load(file)
25+
26+
mock_get_navitia_api.return_value = mock_response
27+
28+
# When
29+
isocrhones = isochrones_apis.list_isochrones_with_region_id(
30+
region_id="bar", from_="foo"
31+
)
32+
33+
# Then
34+
assert len(isocrhones) == 1
35+
assert isinstance(isocrhones[0], Isochrone)
36+
37+
38+
@patch.object(IsochronesApiClient, "get_navitia_api")
39+
def test_list_covered_areas(
40+
mock_get_navitia_api: MagicMock, isochrones_apis: IsochronesApiClient
41+
) -> None:
42+
# Given
43+
mock_response = MagicMock()
44+
with open("tests/test_data/isochrones.json", encoding="utf-8") as file:
45+
mock_response.json.return_value = json.load(file)
46+
47+
mock_get_navitia_api.return_value = mock_response
48+
49+
# When
50+
isocrhones = isochrones_apis.list_isochrones(from_="foo")
51+
52+
# Then
53+
assert len(isocrhones) == 1
54+
assert isinstance(isocrhones[0], Isochrone)

0 commit comments

Comments
 (0)