Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[jormungandr] Init connector Forseti for bss stations #4101

Merged
merged 11 commits into from
Sep 19, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 22 additions & 0 deletions source/jormungandr/jormungandr/instance.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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 []
Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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
Expand Down
1 change: 1 addition & 0 deletions source/jormungandr/jormungandr/instance_manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
4 changes: 4 additions & 0 deletions source/jormungandr/jormungandr/interfaces/v1/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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'] = []
kadhikari marked this conversation as resolved.
Show resolved Hide resolved
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'
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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))
Expand Down Expand Up @@ -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())
Original file line number Diff line number Diff line change
@@ -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 <http://www.gnu.org/licenses/>.
#
# 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))
kadhikari marked this conversation as resolved.
Show resolved Hide resolved
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)
Comment on lines +138 to +143
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
if not data:
return Stands(0, 0, StandsStatus.unavailable)
obj_stations = data.get('stations', [])
if not obj_stations:
return Stands(0, 0, StandsStatus.unavailable)
if not data or not data.get('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)
Original file line number Diff line number Diff line change
@@ -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 <http://www.gnu.org/licenses/>.
#
# 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)
Original file line number Diff line number Diff line change
Expand Up @@ -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,
kadhikari marked this conversation as resolved.
Show resolved Hide resolved
self.attribute,
self.logger,
'Error while handling individual BSS realtime availability',
)

if show_car_park and instance and instance.car_park_provider:
Expand All @@ -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
Expand Down
8 changes: 7 additions & 1 deletion source/navitiacommon/navitiacommon/constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -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',
)
6 changes: 6 additions & 0 deletions source/navitiacommon/navitiacommon/models/external_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
kadhikari marked this conversation as resolved.
Show resolved Hide resolved
"""
generate args form jormungandr implementation of a bss providers from configuration in external service
"""
return self.args
Loading