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"
+ )