diff --git a/navitia_client/client/apis/freefloatings_nearby_apis.py b/navitia_client/client/apis/freefloatings_nearby_apis.py new file mode 100644 index 0000000..f7cad3e --- /dev/null +++ b/navitia_client/client/apis/freefloatings_nearby_apis.py @@ -0,0 +1,247 @@ +from typing import Any, Dict, Optional, Sequence, Tuple + +from navitia_client.client.apis.api_base_client import ApiBaseClient +from navitia_client.entities.free_floating import FreeFloating +from navitia_client.entities.pagination import Pagination + + +class FreefloatingsNearbyApiClient(ApiBaseClient): + """ + A client class to interact with the Navitia API for fetching nearby free-floating vehicles. + + See https://doc.navitia.io/#freefloatings-nearby-api + + Methods + ------- + _get_freefloatings_nearby( + url: str, filters: dict + ) -> Tuple[Sequence[FreeFloating], Pagination]: + Retrieves free-floating vehicles from the Navitia API based on provided URL and filters. + + list_freefloatings_nearby( + region_id: str, + lon: float, + lat: float, + distance: int = 500, + type: Optional[Sequence[str]] = None, + count: int = 10, + ) -> Tuple[Sequence[FreeFloating], Pagination]: + Retrieves free-floating vehicles near coordinates in a specific region from the Navitia API. + + list_freefloatings_nearby_with_resource_path( + region_id: str, + resource_path: str, + distance: int = 500, + type: Optional[Sequence[str]] = None, + count: int = 10, + ) -> Tuple[Sequence[FreeFloating], Pagination]: + Retrieves free-floating vehicles near a specific resource path in a region from the Navitia API. + + list_freefloatings_nearby_by_coordinates( + region_lon: float, + region_lat: float, + lon: float, + lat: float, + distance: int = 500, + type: Optional[Sequence[str]] = None, + count: int = 10, + ) -> Tuple[Sequence[FreeFloating], Pagination]: + Retrieves free-floating vehicles near coordinates, navitia guesses the region from coordinates. + + list_freefloatings_nearby_by_coordinates_only( + lon: float, + lat: float, + distance: int = 500, + type: Optional[Sequence[str]] = None, + count: int = 10, + ) -> Tuple[Sequence[FreeFloating], Pagination]: + Retrieves free-floating vehicles near coordinates without any region id. + """ + + def _get_freefloatings_nearby( + self, url: str, filters: dict + ) -> Tuple[Sequence[FreeFloating], Pagination]: + """ + Retrieves free-floating vehicles from the Navitia API based on provided URL and filters. + + Parameters: + url (str): The URL for the API request. + filters (dict): Filters to apply to the API request. + + Returns: + Tuple[Sequence[FreeFloating], Pagination]: A tuple containing sequences of FreeFloating objects and Pagination object. + """ + results = self.get_navitia_api(url + self._generate_filter_query(filters)) + free_floatings = [ + FreeFloating.from_payload(data) for data in results.json()["free_floatings"] + ] + pagination = Pagination.from_payload(results.json()["pagination"]) + return free_floatings, pagination + + def list_freefloatings_nearby( + self, + region_id: str, + lon: float, + lat: float, + distance: int = 500, + type: Optional[Sequence[str]] = None, + count: int = 10, + ) -> Tuple[Sequence[FreeFloating], Pagination]: + """ + Retrieves free-floating vehicles near coordinates in a specific region from the Navitia API. + + This service provides access to nearby shared mobility options (such as bikes, + scooters, or cars) based on user-provided coordinates. + + Parameters: + region_id (str): The region ID (coverage identifier). + lon (float): The longitude coordinate. + lat (float): The latitude coordinate. + distance (int): Search radius in meters. Defaults to 500. + type (Optional[Sequence[str]]): The type of shared mobility vehicles to return (e.g., bike, scooter, car). + count (int): Maximum number of results to return. Defaults to 10. + + Returns: + Tuple[Sequence[FreeFloating], Pagination]: A tuple containing sequences of FreeFloating objects and Pagination object. + + Note: + This feature requires a specific configuration from a freefloating data service provider. + Therefore, this service is not available by default. + """ + request_url = f"{self.base_navitia_url}/coverage/{region_id}/coords/{lon};{lat}/freefloatings_nearby" + + filters: Dict[str, Any] = { + "distance": distance, + "count": count, + } + + if type: + filters["type[]"] = type + + return self._get_freefloatings_nearby(request_url, filters) + + def list_freefloatings_nearby_with_resource_path( + self, + region_id: str, + resource_path: str, + distance: int = 500, + type: Optional[Sequence[str]] = None, + count: int = 10, + ) -> Tuple[Sequence[FreeFloating], Pagination]: + """ + Retrieves free-floating vehicles near a specific resource path in a region from the Navitia API. + + This service provides access to nearby shared mobility options (such as bikes, + scooters, or cars) near a specific resource (stop area, address, etc.). + + Parameters: + region_id (str): The region ID (coverage identifier). + resource_path (str): The resource path (e.g., 'stop_areas/stop_area:XXX'). + distance (int): Search radius in meters. Defaults to 500. + type (Optional[Sequence[str]]): The type of shared mobility vehicles to return (e.g., bike, scooter, car). + count (int): Maximum number of results to return. Defaults to 10. + + Returns: + Tuple[Sequence[FreeFloating], Pagination]: A tuple containing sequences of FreeFloating objects and Pagination object. + + Note: + This feature requires a specific configuration from a freefloating data service provider. + Therefore, this service is not available by default. + """ + request_url = f"{self.base_navitia_url}/coverage/{region_id}/{resource_path}/freefloatings_nearby" + + filters: Dict[str, Any] = { + "distance": distance, + "count": count, + } + + if type: + filters["type[]"] = type + + return self._get_freefloatings_nearby(request_url, filters) + + def list_freefloatings_nearby_by_coordinates( + self, + region_lon: float, + region_lat: float, + lon: float, + lat: float, + distance: int = 500, + type: Optional[Sequence[str]] = None, + count: int = 10, + ) -> Tuple[Sequence[FreeFloating], Pagination]: + """ + Retrieves free-floating vehicles near coordinates, navitia guesses the region from coordinates. + + This service provides access to nearby shared mobility options (such as bikes, + scooters, or cars) based on user-provided coordinates. Navitia will automatically + determine the region based on the provided region coordinates. + + Parameters: + region_lon (float): The longitude coordinate for region identification. + region_lat (float): The latitude coordinate for region identification. + lon (float): The longitude coordinate for the search center. + lat (float): The latitude coordinate for the search center. + distance (int): Search radius in meters. Defaults to 500. + type (Optional[Sequence[str]]): The type of shared mobility vehicles to return (e.g., bike, scooter, car). + count (int): Maximum number of results to return. Defaults to 10. + + Returns: + Tuple[Sequence[FreeFloating], Pagination]: A tuple containing sequences of FreeFloating objects and Pagination object. + + Note: + This feature requires a specific configuration from a freefloating data service provider. + Therefore, this service is not available by default. + """ + request_url = f"{self.base_navitia_url}/coverage/{region_lon};{region_lat}/coords/{lon};{lat}/freefloatings_nearby" + + filters: Dict[str, Any] = { + "distance": distance, + "count": count, + } + + if type: + filters["type[]"] = type + + return self._get_freefloatings_nearby(request_url, filters) + + def list_freefloatings_nearby_by_coordinates_only( + self, + lon: float, + lat: float, + distance: int = 500, + type: Optional[Sequence[str]] = None, + count: int = 10, + ) -> Tuple[Sequence[FreeFloating], Pagination]: + """ + Retrieves free-floating vehicles near coordinates without any region id. + + This service provides access to nearby shared mobility options (such as bikes, + scooters, or cars) based on user-provided coordinates. This method does not require + a region ID; Navitia will automatically determine the appropriate region. + + Parameters: + lon (float): The longitude coordinate. + lat (float): The latitude coordinate. + distance (int): Search radius in meters. Defaults to 500. + type (Optional[Sequence[str]]): The type of shared mobility vehicles to return (e.g., bike, scooter, car). + count (int): Maximum number of results to return. Defaults to 10. + + Returns: + Tuple[Sequence[FreeFloating], Pagination]: A tuple containing sequences of FreeFloating objects and Pagination object. + + Note: + This feature requires a specific configuration from a freefloating data service provider. + Therefore, this service is not available by default. + """ + request_url = f"{self.base_navitia_url}/coord/{lon};{lat}/freefloatings_nearby" + + filters: Dict[str, Any] = { + "distance": distance, + "count": count, + } + + if type: + filters["type[]"] = type + + return self._get_freefloatings_nearby(request_url, filters) diff --git a/navitia_client/client/navitia_client.py b/navitia_client/client/navitia_client.py index 26eb0c6..cabc572 100644 --- a/navitia_client/client/navitia_client.py +++ b/navitia_client/client/navitia_client.py @@ -6,6 +6,9 @@ from navitia_client.client.apis.datasets_apis import DatasetsApiClient from navitia_client.client.apis.departure_apis import DepartureApiClient from navitia_client.client.apis.equipment_report_apis import EquipmentReportsApiClient +from navitia_client.client.apis.freefloatings_nearby_apis import ( + FreefloatingsNearbyApiClient, +) from navitia_client.client.apis.inverted_geocoding_apis import ( InvertedGeocodingApiClient, ) @@ -106,6 +109,8 @@ class NavitiaClient: Get an instance of TrafficReportsApiClient for accessing traffic reports-related endpoints. equipment_reports -> EquipmentReportsApiClient: Get an instance of EquipmentReportsApiClient for accessing equipment reports-related endpoints. + freefloatings_nearby -> FreefloatingsNearbyApiClient: + Get an instance of FreefloatingsNearbyApiClient for accessing freefloatings nearby-related endpoints. journeys -> JourneyApiClient: Get an instance of JourneyApiClient for accessing journey-related endpoints. isochrones -> IsochronesApiClient: @@ -290,6 +295,13 @@ def equipment_reports(self) -> EquipmentReportsApiClient: auth_token=self.auth_token, base_navitia_url=self.base_navitia_url ) + @property + def freefloatings_nearby(self) -> FreefloatingsNearbyApiClient: + """Get an instance of FreefloatingsNearbyApiClient for accessing nearby free-floating vehicle endpoints.""" + return FreefloatingsNearbyApiClient( + auth_token=self.auth_token, base_navitia_url=self.base_navitia_url + ) + @property def journeys(self) -> JourneyApiClient: """Get an instance of JourneyApiClient for accessing journey-related endpoints.""" diff --git a/navitia_client/entities/free_floating.py b/navitia_client/entities/free_floating.py new file mode 100644 index 0000000..be7cf04 --- /dev/null +++ b/navitia_client/entities/free_floating.py @@ -0,0 +1,59 @@ +from dataclasses import dataclass +from typing import Dict, Any, Optional + +from .coord import Coord + + +@dataclass +class FreeFloating: + """ + Represents a free-floating shared mobility vehicle (bike, scooter, car, etc.). + + Attributes: + public_id: Public identifier of the vehicle + provider_name: Name of the service provider + id: Identifier of the vehicle + type: Type of vehicle (bike, scooter, car, etc.) + propulsion: Type of propulsion (electric, human, etc.) + battery: Battery level in percentage (0-100) + distance: Distance from the search point in meters + deeplink: Deep link URL to the provider's app + coord: Coordinates of the vehicle + """ + + public_id: str + provider_name: str + id: str + type: str + propulsion: Optional[str] = None + battery: Optional[int] = None + distance: Optional[int] = None + deeplink: Optional[str] = None + coord: Optional[Coord] = None + + @classmethod + def from_payload(cls, data: Dict[str, Any]) -> "FreeFloating": + """ + Create a FreeFloating instance from API payload data. + + Parameters: + data: Dictionary containing free floating data from the API + + Returns: + FreeFloating: An instance of FreeFloating + """ + coord = None + if "coord" in data: + coord = Coord.from_payload(data["coord"]) + + return cls( + public_id=data.get("public_id", ""), + provider_name=data.get("provider_name", ""), + id=data.get("id", ""), + type=data.get("type", ""), + propulsion=data.get("propulsion"), + battery=data.get("battery"), + distance=data.get("distance"), + deeplink=data.get("deeplink"), + coord=coord, + ) diff --git a/tests/client/apis/test_freefloatings_nearby_apis.py b/tests/client/apis/test_freefloatings_nearby_apis.py new file mode 100644 index 0000000..6595604 --- /dev/null +++ b/tests/client/apis/test_freefloatings_nearby_apis.py @@ -0,0 +1,150 @@ +import json +from unittest.mock import MagicMock, patch + +import pytest + +from navitia_client.client.apis.freefloatings_nearby_apis import ( + FreefloatingsNearbyApiClient, +) + + +@pytest.fixture +def freefloatings_nearby_apis(): + return FreefloatingsNearbyApiClient( + auth_token="foobar", base_navitia_url="https://api.navitia.io/v1/" + ) + + +@patch.object(FreefloatingsNearbyApiClient, "get_navitia_api") +def test_list_freefloatings_nearby( + mock_get_navitia_api: MagicMock, + freefloatings_nearby_apis: FreefloatingsNearbyApiClient, +) -> None: + """ + Test that list_freefloatings_nearby returns free floatings and pagination. + """ + # Given + mock_response = MagicMock() + with open("tests/test_data/freefloatings_nearby.json", encoding="utf-8") as file: + mock_response.json.return_value = json.load(file) + + mock_get_navitia_api.return_value = mock_response + + # When + free_floatings, pagination = freefloatings_nearby_apis.list_freefloatings_nearby( + region_id="fr-idf", lon=2.3522, lat=48.8566 + ) + + # Then + assert len(free_floatings) == 2 + assert free_floatings[0].public_id == "scooter_12345" + assert free_floatings[0].provider_name == "Lime" + assert free_floatings[0].type == "scooter" + assert free_floatings[0].propulsion == "electric" + assert free_floatings[0].battery == 85 + assert free_floatings[0].distance == 120 + assert free_floatings[0].coord is not None + assert free_floatings[0].coord.lat == "48.8560" + assert free_floatings[0].coord.lon == "2.3500" + assert pagination.total_result == 2 + assert pagination.items_on_page == 2 + + +@patch.object(FreefloatingsNearbyApiClient, "get_navitia_api") +def test_list_freefloatings_nearby_with_resource_path( + mock_get_navitia_api: MagicMock, + freefloatings_nearby_apis: FreefloatingsNearbyApiClient, +) -> None: + """ + Test that list_freefloatings_nearby_with_resource_path returns free floatings for a specific resource path. + """ + # Given + mock_response = MagicMock() + with open("tests/test_data/freefloatings_nearby.json", encoding="utf-8") as file: + mock_response.json.return_value = json.load(file) + + mock_get_navitia_api.return_value = mock_response + + # When + ( + free_floatings, + pagination, + ) = freefloatings_nearby_apis.list_freefloatings_nearby_with_resource_path( + region_id="fr-idf", resource_path="stop_areas/stop_area:IDFM:71591" + ) + + # Then + called_url = mock_get_navitia_api.call_args[0][0] + assert "stop_areas/stop_area:IDFM:71591/freefloatings_nearby" in called_url + assert len(free_floatings) == 2 + assert free_floatings[0].public_id == "scooter_12345" + assert free_floatings[0].type == "scooter" + assert free_floatings[1].type == "bike" + assert pagination.total_result == 2 + assert pagination.items_on_page == 2 + + +@patch.object(FreefloatingsNearbyApiClient, "get_navitia_api") +def test_list_freefloatings_nearby_by_coordinates( + mock_get_navitia_api: MagicMock, + freefloatings_nearby_apis: FreefloatingsNearbyApiClient, +) -> None: + """ + Test that list_freefloatings_nearby_by_coordinates returns free floatings with region coordinates. + """ + # Given + mock_response = MagicMock() + with open("tests/test_data/freefloatings_nearby.json", encoding="utf-8") as file: + mock_response.json.return_value = json.load(file) + + mock_get_navitia_api.return_value = mock_response + + # When + ( + free_floatings, + pagination, + ) = freefloatings_nearby_apis.list_freefloatings_nearby_by_coordinates( + region_lon=2.3522, region_lat=48.8566, lon=2.3522, lat=48.8566 + ) + + # Then + called_url = mock_get_navitia_api.call_args[0][0] + assert ( + "coverage/2.3522;48.8566/coords/2.3522;48.8566/freefloatings_nearby" + in called_url + ) + assert len(free_floatings) == 2 + assert free_floatings[0].provider_name == "Lime" + assert pagination.total_result == 2 + + +@patch.object(FreefloatingsNearbyApiClient, "get_navitia_api") +def test_list_freefloatings_nearby_by_coordinates_only( + mock_get_navitia_api: MagicMock, + freefloatings_nearby_apis: FreefloatingsNearbyApiClient, +) -> None: + """ + Test that list_freefloatings_nearby_by_coordinates_only returns free floatings without region id. + """ + # Given + mock_response = MagicMock() + with open("tests/test_data/freefloatings_nearby.json", encoding="utf-8") as file: + mock_response.json.return_value = json.load(file) + + mock_get_navitia_api.return_value = mock_response + + # When + ( + free_floatings, + pagination, + ) = freefloatings_nearby_apis.list_freefloatings_nearby_by_coordinates_only( + lon=2.3522, lat=48.8566 + ) + + # Then + called_url = mock_get_navitia_api.call_args[0][0] + assert "coord/2.3522;48.8566/freefloatings_nearby" in called_url + assert len(free_floatings) == 2 + assert free_floatings[0].type == "scooter" + assert free_floatings[1].type == "bike" + assert pagination.total_result == 2 diff --git a/tests/test_data/freefloatings_nearby.json b/tests/test_data/freefloatings_nearby.json new file mode 100644 index 0000000..1652e1d --- /dev/null +++ b/tests/test_data/freefloatings_nearby.json @@ -0,0 +1,38 @@ +{ + "free_floatings": [ + { + "public_id": "scooter_12345", + "provider_name": "Lime", + "id": "scooter_12345", + "type": "scooter", + "propulsion": "electric", + "battery": 85, + "distance": 120, + "deeplink": "https://lime.com/scooter_12345", + "coord": { + "lat": "48.8560", + "lon": "2.3500" + } + }, + { + "public_id": "bike_67890", + "provider_name": "Lime", + "id": "bike_67890", + "type": "bike", + "propulsion": "electric", + "battery": 92, + "distance": 250, + "deeplink": "https://lime.com/bike_67890", + "coord": { + "lat": "48.8570", + "lon": "2.3510" + } + } + ], + "pagination": { + "start_page": 0, + "items_on_page": 2, + "items_per_page": 10, + "total_result": 2 + } +} diff --git a/tests/test_navitia_client.py b/tests/test_navitia_client.py index 4520aa7..480c6f0 100644 --- a/tests/test_navitia_client.py +++ b/tests/test_navitia_client.py @@ -5,6 +5,9 @@ from navitia_client.client.apis.datasets_apis import DatasetsApiClient from navitia_client.client.apis.departure_apis import DepartureApiClient from navitia_client.client.apis.equipment_report_apis import EquipmentReportsApiClient +from navitia_client.client.apis.freefloatings_nearby_apis import ( + FreefloatingsNearbyApiClient, +) from navitia_client.client.apis.inverted_geocoding_apis import ( InvertedGeocodingApiClient, ) @@ -147,6 +150,10 @@ def test_equipment_reports_client(navitia_client): assert isinstance(navitia_client.equipment_reports, EquipmentReportsApiClient) +def test_freefloatings_nearby_client(navitia_client): + assert isinstance(navitia_client.freefloatings_nearby, FreefloatingsNearbyApiClient) + + def test_journeys_client(navitia_client): assert isinstance(navitia_client.journeys, JourneyApiClient)