Skip to content

Commit

Permalink
Init connector Forseti for bss stations
Browse files Browse the repository at this point in the history
  • Loading branch information
kadhikari committed Sep 6, 2023
1 parent 0e03f54 commit bea9623
Show file tree
Hide file tree
Showing 10 changed files with 309 additions and 1 deletion.
19 changes: 19 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 @@ -257,6 +258,13 @@ 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(app.config['BSS_PROVIDER'])
else:
self.bss_provider_manager = BssProviderManager(app.config['BSS_PROVIDER'], self.get_bss_stations_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 +341,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_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 None
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 +1003,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
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 BSSStationsSerializer(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 = BSSStationsSerializer(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 form 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 @@ -119,3 +120,7 @@ def _get_providers(self):

def get_providers(self):
return self._get_providers()

def exist_provider(self):
self.update_config()
return any((self._bss_providers.values(), self._bss_providers_legacy))
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
# 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.forseti.fr'}


class ForsetiProvider(CommonBssProvider):
"""
class managing calls to Forseti external service providing real-time BSS stands availability
"""

def __init__(self, service_url, feed_publisher=DEFAULT_FORSETI_FEED_PUBLISHER, timeout=2, **kwargs):
self.logger = logging.getLogger(__name__)
self.service_url = service_url
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

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 latitude is None:
return Stands(0, 0, StandsStatus.unavailable)

arguments = 'coord={}%3B{}&distance=50'.format(longitude, latitude)
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 = 0
for v in obj_stations[0].get('vehicles'):
vehicle_count = vehicle_count + v.get('count', 0)

stand = Stands(obj_stations[0].get('docks', {}).get('available', 0), vehicle_count, StandsStatus.open)
return stand
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
# 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'}
}

BSS_PROVIDER = [
{
"id": "forseti_stations",
"class": "jormungandr.parking_space_availability.bss.forseti.ForsetiProvider",
"args": {
"service_url": "https://gbfs-station.forseti.sbx.aws.private/stations",
"timeout": 20
}
}
]


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 @@ -83,6 +83,15 @@ def wrapper(*args, **kwargs):
'Error while handling BSS realtime availability',
)

if show_bss_stands and instance and instance.bss_provider_manager.exist_provider():
_handle(
response,
instance.bss_provider_manager,
self.attribute,
self.logger,
'Error while handling BSS realtime availability',
)

if show_car_park and instance and instance.car_park_provider:
_handle(
response,
Expand Down
2 changes: 1 addition & 1 deletion source/navitiacommon/navitiacommon/constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,4 +34,4 @@
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
Original file line number Diff line number Diff line change
@@ -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"
)

0 comments on commit bea9623

Please sign in to comment.