diff --git a/source/jormungandr/jormungandr/instance.py b/source/jormungandr/jormungandr/instance.py index dd862e8b55..b19724b1f5 100644 --- a/source/jormungandr/jormungandr/instance.py +++ b/source/jormungandr/jormungandr/instance.py @@ -63,6 +63,7 @@ from navitiacommon import default_values from jormungandr.equipments import EquipmentProviderManager from jormungandr.external_services import ExternalServiceManager +from jormungandr.parking_space_availability.bss.bss_provider_manager import BssProviderManager from jormungandr.utils import ( can_connect_to_database, make_origin_destination_key, @@ -169,6 +170,7 @@ def __init__( use_multi_reverse=False, resp_content_limit_bytes=None, resp_content_limit_endpoints_whitelist=None, + individual_bss_provider=[], ): super(Instance, self).__init__( name=name, @@ -257,6 +259,15 @@ def __init__( self.external_service_provider_manager = ExternalServiceManager( self, external_service_provider_configurations, self.get_external_service_providers_from_db ) + + # Init BSS provider manager from config from external services in bdd + if disable_database: + self.bss_provider_manager = BssProviderManager(individual_bss_provider) + else: + self.bss_provider_manager = BssProviderManager( + individual_bss_provider, self.get_bss_stations_services_from_db + ) + self.external_service_provider_manager.init_external_services() self.instance_db = instance_db self._ghost_words = ghost_words or [] @@ -333,6 +344,14 @@ def get_realtime_proxies_from_db(self): result = models.external_services if models else None return [res for res in result if res.navitia_service == 'realtime_proxies'] + def get_bss_stations_services_from_db(self): + """ + :return: a callable query of external services associated to the current instance in db + """ + models = self._get_models() + result = models.external_services if models else [] + return [res for res in result if res.navitia_service == 'bss_stations'] + @property def autocomplete(self): if self._autocomplete_type: @@ -987,6 +1006,9 @@ def get_all_street_networks(self): def get_all_ridesharing_services(self): return self.ridesharing_services_manager.get_all_ridesharing_services() + def get_all_bss_providers(self): + return self.bss_provider_manager.get_providers() + def get_autocomplete(self, requested_autocomplete): if not requested_autocomplete: return self.autocomplete diff --git a/source/jormungandr/jormungandr/instance_manager.py b/source/jormungandr/jormungandr/instance_manager.py index 0907a18425..da3a9003b1 100644 --- a/source/jormungandr/jormungandr/instance_manager.py +++ b/source/jormungandr/jormungandr/instance_manager.py @@ -128,6 +128,7 @@ def register_instance(self, config): use_multi_reverse=config.get('use_multi_reverse', False), resp_content_limit_bytes=config.get('resp_content_limit_bytes', None), resp_content_limit_endpoints_whitelist=config.get('resp_content_limit_endpoints_whitelist', None), + individual_bss_provider=config.get('individual_bss_provider', []), ) self.instances[instance.name] = instance diff --git a/source/jormungandr/jormungandr/interfaces/v1/__init__.py b/source/jormungandr/jormungandr/interfaces/v1/__init__.py index 0265c65681..b6968c8ded 100644 --- a/source/jormungandr/jormungandr/interfaces/v1/__init__.py +++ b/source/jormungandr/jormungandr/interfaces/v1/__init__.py @@ -56,6 +56,10 @@ def add_common_status(response, instance): for rss in instance.get_all_ridesharing_services(): response['status']['ridesharing_services'].append(rss.status()) + response['status']['bss_providers'] = [] + for bp in instance.get_all_bss_providers(): + response['status']['bss_providers'].append(bp.status()) + response['status']['equipment_providers_services'] = {} response['status']['equipment_providers_services'][ 'equipment_providers_keys' diff --git a/source/jormungandr/jormungandr/interfaces/v1/serializer/status.py b/source/jormungandr/jormungandr/interfaces/v1/serializer/status.py index eabba34b25..ca9745e0b3 100644 --- a/source/jormungandr/jormungandr/interfaces/v1/serializer/status.py +++ b/source/jormungandr/jormungandr/interfaces/v1/serializer/status.py @@ -204,6 +204,12 @@ def get_arrival_radius(self, obj): return obj.get('arrival_radius') +class BSSStationsServiceSerializer(OutsideServiceCommon): + id = Field(display_none=True) + url = Field(display_none=True) + class_ = Field(schema_type=str, label='class', attr='class') + + class EquipmentProvidersSerializer(NullableDictSerializer): key = Field(schema_type=str, display_none=False) codes_types = Field(schema_type=str, many=True, display_none=True) @@ -255,6 +261,7 @@ class CommonStatusSerializer(NullableDictSerializer): publication_date = Field(schema_type=str, display_none=False) street_networks = StreetNetworkSerializer(many=True, display_none=False) ridesharing_services = RidesharingServicesSerializer(many=True, display_none=False) + bss_providers = BSSStationsServiceSerializer(many=True, display_none=False) equipment_providers_services = EquipmentProvidersServicesSerializer(display_none=False) external_providers_services = ExternalServiceProvidersServicesSerializer(display_none=False) start_production_date = Field(schema_type=str, display_none=False) diff --git a/source/jormungandr/jormungandr/parking_space_availability/bss/bss_provider_manager.py b/source/jormungandr/jormungandr/parking_space_availability/bss/bss_provider_manager.py index 021fc11992..014292ba90 100644 --- a/source/jormungandr/jormungandr/parking_space_availability/bss/bss_provider_manager.py +++ b/source/jormungandr/jormungandr/parking_space_availability/bss/bss_provider_manager.py @@ -67,6 +67,7 @@ def update_config(self): self._last_update = datetime.datetime.utcnow() try: + # BSS provider list from the database (table bss_provider) providers = self._providers_getter() except Exception as e: logger.exception('No access to table bss_provider (error: {})'.format(e)) @@ -111,11 +112,13 @@ def _handle_poi(self, item): return provider return None - # TODO use public version everywhere def _get_providers(self): self.update_config() - # providers from the database have priority on legacies providers return list(self._bss_providers.values()) + self._bss_providers_legacy def get_providers(self): return self._get_providers() + + def exist_provider(self): + self.update_config() + return any(self.get_providers()) diff --git a/source/jormungandr/jormungandr/parking_space_availability/bss/forseti.py b/source/jormungandr/jormungandr/parking_space_availability/bss/forseti.py new file mode 100644 index 0000000000..52dff87235 --- /dev/null +++ b/source/jormungandr/jormungandr/parking_space_availability/bss/forseti.py @@ -0,0 +1,146 @@ +# coding: utf-8 + +# Copyright (c) 2001-2022, Hove and/or its affiliates. All rights reserved. +# +# This file is part of Navitia, +# the software to build cool stuff with public transport. +# +# Hope you'll enjoy and contribute to this project, +# powered by Hove (www.hove.com). +# Help us simplify mobility and open public transport: +# a non ending quest to the responsive locomotion way of traveling! +# +# LICENCE: This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# +# Stay tuned using +# twitter @navitia +# channel `#navitia` on riot https://riot.im/app/#/room/#navitia:matrix.org +# https://groups.google.com/d/forum/navitia +# www.navitia.io +from __future__ import absolute_import, print_function, unicode_literals, division +from jormungandr import cache, app +import pybreaker +import logging +import requests as requests +from jormungandr.ptref import FeedPublisher +from jormungandr.parking_space_availability.bss.stands import Stands, StandsStatus +from jormungandr.parking_space_availability.bss.common_bss_provider import CommonBssProvider, BssProxyError +import six + +DEFAULT_FORSETI_FEED_PUBLISHER = { + 'id': 'forseti', + 'name': 'forseti', + 'license': 'Private', + 'url': 'www.navitia.io', +} + + +class ForsetiProvider(CommonBssProvider): + """ + class managing calls to Forseti external service providing real-time BSS stands availability + + """ + + def __init__( + self, + service_url, + distance=50, + organizations=[], + feed_publisher=DEFAULT_FORSETI_FEED_PUBLISHER, + timeout=2, + **kwargs + ): + self.logger = logging.getLogger(__name__) + self.service_url = service_url + self.distance = distance + self.timeout = timeout + self.network = "Forseti" + self.breaker = pybreaker.CircuitBreaker( + fail_max=kwargs.get('circuit_breaker_max_fail', app.config['CIRCUIT_BREAKER_MAX_FORSETI_FAIL']), + reset_timeout=kwargs.get( + 'circuit_breaker_reset_timeout', app.config['CIRCUIT_BREAKER_FORSETI_TIMEOUT_S'] + ), + ) + + self._feed_publisher = FeedPublisher(**feed_publisher) if feed_publisher else None + if not isinstance(organizations, list): + import json + self.organizations = json.loads(str(organizations)) + else: + self.organizations = organizations + + def service_caller(self, method, url): + try: + response = self.breaker.call(method, url, timeout=self.timeout, verify=False) + if not response or response.status_code != 200: + logging.getLogger(__name__).error( + 'Forseti, Invalid response, status_code: {}'.format(response.status_code) + ) + raise BssProxyError('non 200 response') + return response + except pybreaker.CircuitBreakerError as e: + logging.getLogger(__name__).error('forseti service dead (error: {})'.format(e)) + raise BssProxyError('circuit breaker open') + except requests.Timeout as t: + logging.getLogger(__name__).error('forseti service timeout (error: {})'.format(t)) + raise BssProxyError('timeout') + except Exception as e: + logging.getLogger(__name__).exception('forseti error : {}'.format(str(e))) + raise BssProxyError(str(e)) + + @cache.memoize(app.config.get(str('CACHE_CONFIGURATION'), {}).get(str('TIMEOUT_FORSETI'), 30)) + def _call_webservice(self, arguments): + url = "{}?{}".format(self.service_url, arguments) + data = self.service_caller(method=requests.get, url=url) + return data.json() + + def support_poi(self, poi): + return True + + def status(self): + # return {'network': self.network, 'operators': self.operators} + return { + 'id': six.text_type(self.network), + 'url': self.service_url, + 'class': self.__class__.__name__, + } + + def feed_publisher(self): + return self._feed_publisher + + def _get_informations(self, poi): + longitude = poi.get('coord', {}).get('lon', None) + latitude = poi.get('coord', {}).get('lat', None) + if latitude is None or longitude is None: + return Stands(0, 0, StandsStatus.unavailable) + + params_organizations = '' + for param in self.organizations: + params_organizations += '&organization[]={}'.format(param) + + # /stations?coord=lon%3Blat&distance=self.distance&organization[]=org1&organization[]=org2 ... + arguments = 'coord={}%3B{}&distance={}{}'.format( + longitude, latitude, self.distance, params_organizations + ) + data = self._call_webservice(arguments) + + if not data: + return Stands(0, 0, StandsStatus.unavailable) + obj_stations = data.get('stations', []) + + if not obj_stations: + return Stands(0, 0, StandsStatus.unavailable) + + vehicle_count = sum((v.get('count', 0) for v in obj_stations[0].get('vehicles', {}))) + return Stands(obj_stations[0].get('docks', {}).get('available', 0), vehicle_count, StandsStatus.open) diff --git a/source/jormungandr/jormungandr/parking_space_availability/bss/tests/forseti_test.py b/source/jormungandr/jormungandr/parking_space_availability/bss/tests/forseti_test.py new file mode 100644 index 0000000000..21b65efab9 --- /dev/null +++ b/source/jormungandr/jormungandr/parking_space_availability/bss/tests/forseti_test.py @@ -0,0 +1,75 @@ +# encoding: utf-8 +# Copyright (c) 2001-2022, Hove and/or its affiliates. All rights reserved. +# +# This file is part of Navitia, +# the software to build cool stuff with public transport. +# +# Hope you'll enjoy and contribute to this project, +# powered by Hove (www.hove.com). +# Help us simplify mobility and open public transport: +# a non ending quest to the responsive locomotion way of traveling! +# +# LICENCE: This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# +# Stay tuned using +# twitter @navitia +# channel `#navitia` on riot https://riot.im/app/#/room/#navitia:matrix.org +# https://groups.google.com/d/forum/navitia +# www.navitia.io + +from __future__ import absolute_import, print_function, unicode_literals, division +from copy import deepcopy +from jormungandr.parking_space_availability.bss.forseti import ForsetiProvider +from jormungandr.parking_space_availability.bss.stands import Stands, StandsStatus +from mock import MagicMock + +poi = { + 'poi_type': {'name': 'station vls', 'id': 'poi_type:amenity:bicycle_rental'}, + 'coord': {'lat': '48.0981147', 'lon': '-1.6552921'}, +} + + +def parking_space_availability_forseti_support_poi_test(): + """ + ForsetiProvider bss provider support + Since we search bss station in forseti with coordinate, it is always True + """ + provider = ForsetiProvider('http://forseti') + poi_copy = deepcopy(poi) + assert provider.support_poi(poi_copy) + + +def parking_space_availability_forseti_get_informations_test(): + webservice_response = { + "stations": [ + { + "id": "TAN:Station:18", + "name": "018-VIARME", + "coord": {"lat": 48.0981147, "lon": -1.6552921}, + "vehicles": [{"type": "bicycle", "count": 9}], + "docks": {"available": 4, "total": 13}, + "status": "OPEN", + } + ], + "pagination": {"start_page": 0, "items_on_page": 2, "items_per_page": 25, "total_result": 2}, + } + + provider = ForsetiProvider('http://forseti') + provider._call_webservice = MagicMock(return_value=webservice_response) + assert provider.get_informations(poi) == Stands(4, 9, StandsStatus.open) + + provider._call_webservice = MagicMock(return_value=None) + assert provider.get_informations(poi) == Stands(0, 0, StandsStatus.unavailable) + invalid_poi = {} + assert provider.get_informations(invalid_poi) == Stands(0, 0, StandsStatus.unavailable) diff --git a/source/jormungandr/jormungandr/parking_space_availability/parking_places_manager.py b/source/jormungandr/jormungandr/parking_space_availability/parking_places_manager.py index 31ef2858db..578ac6fe13 100644 --- a/source/jormungandr/jormungandr/parking_space_availability/parking_places_manager.py +++ b/source/jormungandr/jormungandr/parking_space_availability/parking_places_manager.py @@ -80,7 +80,21 @@ def wrapper(*args, **kwargs): bss_provider_manager, self.attribute, self.logger, - 'Error while handling BSS realtime availability', + 'Error while handling global BSS realtime availability', + ) + + if ( + show_bss_stands + and instance + and instance.bss_provider_manager + and instance.bss_provider_manager.exist_provider() + ): + _handle( + response, + instance.bss_provider_manager, + self.attribute, + self.logger, + 'Error while handling individual BSS realtime availability', ) if show_car_park and instance and instance.car_park_provider: @@ -89,7 +103,7 @@ def wrapper(*args, **kwargs): car_park_provider_manager, self.attribute, self.logger, - 'Error while handling car park realtime availability', + f'Error while handling global car park realtime availability with configuration for instance: {instance}', ) return response, status, h diff --git a/source/navitiacommon/navitiacommon/constants.py b/source/navitiacommon/navitiacommon/constants.py index 0630c25ec2..b4cd6c20be 100644 --- a/source/navitiacommon/navitiacommon/constants.py +++ b/source/navitiacommon/navitiacommon/constants.py @@ -34,4 +34,10 @@ ENUM_SHAPE_SCOPE = ('admin', 'street', 'addr', 'poi', 'stop') DEFAULT_SHAPE_SCOPE = ('admin', 'street', 'addr', 'poi') -ENUM_EXTERNAL_SERVICE = ('free_floatings', 'vehicle_occupancies', 'realtime_proxies', 'vehicle_positions') +ENUM_EXTERNAL_SERVICE = ( + 'free_floatings', + 'vehicle_occupancies', + 'realtime_proxies', + 'vehicle_positions', + 'bss_stations', +) diff --git a/source/navitiacommon/navitiacommon/models/external_service.py b/source/navitiacommon/navitiacommon/models/external_service.py index a2d6055658..5fb9c2eab4 100644 --- a/source/navitiacommon/navitiacommon/models/external_service.py +++ b/source/navitiacommon/navitiacommon/models/external_service.py @@ -77,3 +77,9 @@ def get_default(cls, navitia_service=None): def last_update(self): return self.updated_at if self.updated_at else self.created_at + + def full_args(self): + """ + generate args form jormungandr implementation of a bss providers from configuration in external service + """ + return self.args diff --git a/source/tyr/migrations/versions/cab8323f71bd_add_bss_stations_in_navitia_service_type.py b/source/tyr/migrations/versions/cab8323f71bd_add_bss_stations_in_navitia_service_type.py new file mode 100644 index 0000000000..85599b4175 --- /dev/null +++ b/source/tyr/migrations/versions/cab8323f71bd_add_bss_stations_in_navitia_service_type.py @@ -0,0 +1,33 @@ +"""Add bss_stations in navitia_service_type + +Revision ID: cab8323f71bd +Revises: 20d8caa23b26 +Create Date: 2023-09-06 14:19:03.579738 + +""" + +# revision identifiers, used by Alembic. +revision = 'cab8323f71bd' +down_revision = '20d8caa23b26' + +from alembic import op + + +def upgrade(): + op.execute("COMMIT") # See https://bitbucket.org/zzzeek/alembic/issue/123 + op.execute("ALTER TYPE navitia_service_type ADD VALUE 'bss_stations'") + + +def downgrade(): + op.execute( + "DELETE FROM associate_instance_external_service WHERE external_service_id in (SELECT ID FROM external_service WHERE navitia_service = 'bss_stations')" + ) + op.execute("DELETE FROM external_service WHERE navitia_service = 'bss_stations'") + op.execute("ALTER TABLE external_service ALTER COLUMN navitia_service TYPE text") + op.execute("DROP TYPE navitia_service_type CASCADE") + op.execute( + "CREATE TYPE navitia_service_type AS ENUM ('free_floatings', 'vehicle_occupancies', 'realtime_proxies', 'vehicle_positions')" + ) + op.execute( + "ALTER TABLE external_service ALTER COLUMN navitia_service TYPE navitia_service_type USING navitia_service::navitia_service_type" + )