Skip to content

Commit

Permalink
Merge pull request #4101 from hove-io/add_forseti_in_bss_parking_spac…
Browse files Browse the repository at this point in the history
…e_availability

Init connector Forseti for bss stations
  • Loading branch information
kadhikari authored Sep 19, 2023
2 parents 980392e + c15d3e9 commit 1ff5054
Show file tree
Hide file tree
Showing 11 changed files with 322 additions and 5 deletions.
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'] = []
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))
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)
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,
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):
"""
generate args form jormungandr implementation of a bss providers from configuration in external service
"""
return self.args
Loading

0 comments on commit 1ff5054

Please sign in to comment.