diff --git a/README.md b/README.md index 8f6a3b1..2dda238 100644 --- a/README.md +++ b/README.md @@ -20,7 +20,8 @@ import frost_sta_client as fsc url = "exampleserver.com/FROST-Server/v1.1" auth_handler = fsc.AuthHandler(username="admin", password="admin") # if server is configured for basic auth, else None -service = fsc.SensorThingsService(url, auth_handler=auth_handler) +session_handler = fsc.SessionHandler() #if continuous requests from requests.Sessions should be used +service = fsc.SensorThingsService(url, auth_handler=auth_handler, session_handler=session_handler) ``` #### Creating Entities ```python diff --git a/frost_sta_client/__init__.py b/frost_sta_client/__init__.py index ccc8204..53dd8ff 100644 --- a/frost_sta_client/__init__.py +++ b/frost_sta_client/__init__.py @@ -19,6 +19,7 @@ from frost_sta_client.model.ext.unitofmeasurement import UnitOfMeasurement from frost_sta_client.service.sensorthingsservice import SensorThingsService from frost_sta_client.service.auth_handler import AuthHandler +from frost_sta_client.service.session_handler import SessionHandler from frost_sta_client.model.ext.entity_type import EntityTypes from frost_sta_client.model.ext.entity_list import EntityList from frost_sta_client.model.ext.data_array_value import DataArrayValue diff --git a/frost_sta_client/service/__init__.py b/frost_sta_client/service/__init__.py index 322b58e..c0000e5 100644 --- a/frost_sta_client/service/__init__.py +++ b/frost_sta_client/service/__init__.py @@ -1,2 +1,3 @@ from frost_sta_client.service import sensorthingsservice from frost_sta_client.service import auth_handler +from frost_sta_client.service import session_handler \ No newline at end of file diff --git a/frost_sta_client/service/sensorthingsservice.py b/frost_sta_client/service/sensorthingsservice.py index cb68e32..2597a0b 100644 --- a/frost_sta_client/service/sensorthingsservice.py +++ b/frost_sta_client/service/sensorthingsservice.py @@ -1,147 +1,181 @@ -# Copyright (C) 2021 Fraunhofer Institut IOSB, Fraunhoferstr. 1, D 76131 -# Karlsruhe, Germany. -# -# This program is free software: you can redistribute it and/or modify -# it under the terms of the GNU Lesser 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 Lesser General Public License for more details. -# -# You should have received a copy of the GNU Lesser General Public License -# along with this program. If not, see . - -import requests -from furl import furl -import logging - -from frost_sta_client.dao import * -from frost_sta_client.service import auth_handler -from frost_sta_client.model.ext import entity_type - - -class SensorThingsService: - - def __init__(self, url, auth_handler=None, proxies=None): - self.url = url - self.auth_handler = auth_handler - self.proxies = proxies - - @property - def url(self): - return self._url - - @url.setter - def url(self, value): - if value is None: - self._url = value - return - try: - self._url = furl(value) - except ValueError as e: - logging.error("received invalid url") - raise e - - - @property - def auth_handler(self): - return self._auth_handler - - @auth_handler.setter - def auth_handler(self, value): - if value is None: - self._auth_handler = None - return - if not isinstance(value, auth_handler.AuthHandler): - raise ValueError('auth should be of type AuthHandler!') - self._auth_handler = value - - - @property - def proxies(self): - return self._proxies - - @proxies.setter - def proxies(self, value): - if value is None: - self._proxies = None - return - elif not isinstance(value, dict): - raise ValueError('Proxies must be a Dictionary!') - self._proxies = value - - - def execute(self, method, url, **kwargs): - if self.auth_handler is not None: - response = requests.request(method, url, proxies=self.proxies, auth=self.auth_handler.add_auth_header(), **kwargs) - else: - response = requests.request(method, url, proxies=self.proxies, **kwargs) - try: - response.raise_for_status() - except requests.exceptions.HTTPError as e: - raise e - - return response - - def get_path(self, parent, relation): - if parent is None: - return relation - this_entity_type = entity_type.get_list_for_class(type(parent)) - _id = f"'{parent.id}'" if isinstance(parent.id, str) else parent.id - return "{entity_type}({id})/{relation}".format(entity_type=this_entity_type, id=_id, relation=relation) - - def get_full_path(self, parent, relation): - slash = "" if self.url.pathstr[-1] == '/' else "/" - url = self.url.url + slash + self.get_path(parent, relation) - return furl(url) - - def create(self, entity): - entity.get_dao(self).create(entity) - - def update(self, entity): - entity.get_dao(self).update(entity) - - def patch(self, entity, patches): - entity.get_dao(self).patch(entity, patches) - - def delete(self, entity): - entity.get_dao(self).delete(entity) - - def actuators(self): - return actuator.ActuatorDao(self) - - def datastreams(self): - return datastream.DatastreamDao(self) - - def features_of_interest(self): - return features_of_interest.FeaturesOfInterestDao(self) - - def historical_locations(self): - return historical_location.HistoricalLocationDao(self) - - def locations(self): - return location.LocationDao(self) - - def multi_datastreams(self): - return multi_datastream.MultiDatastreamDao(self) - - def observations(self): - return observation.ObservationDao(self) - - def observed_properties(self): - return observedproperty.ObservedPropertyDao(self) - - def sensors(self): - return sensor.SensorDao(self) - - def tasks(self): - return task.TaskDao(self) - - def tasking_capabilities(self): - return tasking_capability.TaskingCapabilityDao(self) - - def things(self): - return thing.ThingDao(self) +# Copyright (C) 2021 Fraunhofer Institut IOSB, Fraunhoferstr. 1, D 76131 +# Karlsruhe, Germany. +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Lesser 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 Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with this program. If not, see . + +import requests + +from furl import furl +import logging + +from frost_sta_client.dao import * +from frost_sta_client.service import auth_handler +from frost_sta_client.service import session_handler +from frost_sta_client.model.ext import entity_type + + +class SensorThingsService: + def __init__(self, url, auth_handler=None, session_handler=None, proxies=None): + self.url = url + self.session_handler = session_handler + self.auth_handler = auth_handler + self.proxies = proxies + + @property + def url(self): + return self._url + + @url.setter + def url(self, value): + if value is None: + self._url = value + return + try: + self._url = furl(value) + except ValueError as e: + logging.error("received invalid url") + raise e + + @property + def auth_handler(self): + return self._auth_handler + + @auth_handler.setter + def auth_handler(self, value): + if value is None: + self._auth_handler = None + return + if not isinstance(value, auth_handler.AuthHandler): + raise ValueError("auth should be of type AuthHandler!") + + self._auth_handler = value + if self.session_handler is not None: + self.session_handler.set_auth(value.add_auth_header()) + + @property + def session_handler(self): + return self._session_handler + + @session_handler.setter + def session_handler(self, value): + if value is None: + self._session_handler = None + return + if not isinstance(value, session_handler.SessionHandler): + raise ValueError("session should be of type SessionHandler!") + self._session_handler = value + + if self.auth_handler is not None: + self.session_handler.set_auth(self.auth_handler.add_auth_header()) + + @property + def proxies(self): + return self._proxies + + @proxies.setter + def proxies(self, value): + if value is None: + self._proxies = None + return + elif not isinstance(value, dict): + raise ValueError("Proxies must be a Dictionary!") + self._proxies = value + + def execute(self, method, url, **kwargs): + + if self.session_handler is not None: + request_session = self.session_handler.get_session() + response = request_session.request( + method=method, url=url, proxies=self.proxies, **kwargs + ) + elif self.auth_handler is not None: + response = requests.request( + method=method, + url=url, + proxies=self.proxies, + auth=self.auth_handler.add_auth_header(), + **kwargs, + ) + else: + response = requests.request( + method=method, url=url, proxies=self.proxies, **kwargs + ) + try: + response.raise_for_status() + except requests.exceptions.HTTPError as e: + raise e + + return response + + def get_path(self, parent, relation): + if parent is None: + return relation + this_entity_type = entity_type.get_list_for_class(type(parent)) + _id = f"'{parent.id}'" if isinstance(parent.id, str) else parent.id + return "{entity_type}({id})/{relation}".format( + entity_type=this_entity_type, id=_id, relation=relation + ) + + def get_full_path(self, parent, relation): + slash = "" if self.url.pathstr[-1] == "/" else "/" + url = self.url.url + slash + self.get_path(parent, relation) + return furl(url) + + def create(self, entity): + entity.get_dao(self).create(entity) + + def update(self, entity): + entity.get_dao(self).update(entity) + + def patch(self, entity, patches): + entity.get_dao(self).patch(entity, patches) + + def delete(self, entity): + entity.get_dao(self).delete(entity) + + def actuators(self): + return actuator.ActuatorDao(self) + + def datastreams(self): + return datastream.DatastreamDao(self) + + def features_of_interest(self): + return features_of_interest.FeaturesOfInterestDao(self) + + def historical_locations(self): + return historical_location.HistoricalLocationDao(self) + + def locations(self): + return location.LocationDao(self) + + def multi_datastreams(self): + return multi_datastream.MultiDatastreamDao(self) + + def observations(self): + return observation.ObservationDao(self) + + def observed_properties(self): + return observedproperty.ObservedPropertyDao(self) + + def sensors(self): + return sensor.SensorDao(self) + + def tasks(self): + return task.TaskDao(self) + + def tasking_capabilities(self): + return tasking_capability.TaskingCapabilityDao(self) + + def things(self): + return thing.ThingDao(self) diff --git a/frost_sta_client/service/session_handler.py b/frost_sta_client/service/session_handler.py new file mode 100644 index 0000000..8318753 --- /dev/null +++ b/frost_sta_client/service/session_handler.py @@ -0,0 +1,78 @@ +# Copyright (C) 2021 Fraunhofer Institut IOSB, Fraunhoferstr. 1, D 76131 +# Karlsruhe, Germany. +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Lesser 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 Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with this program. If not, see . + + +import requests +from requests.adapters import HTTPAdapter, Retry + + +class SessionHandler: + """ + Handles the request session management and retries. + + Attributes: + total_retries: Total number of retries to allow. Takes precedence over other counts. + connect: How many connection-related errors to retry on. + backoff_factor: A backoff factor to apply between attempts after the second try + status_forcelist: A set of integer HTTP status codes that we should force a retry on + """ + def __init__(self, total_retries = 20, connect = 15, backoff_factor = 0.3,status_forcelist = [500, 502, 503, 504]): + + self.current_session = None + self.total_retries =total_retries + self.connect=connect + self.backoff_factor = backoff_factor + self.status_forcelist= status_forcelist + self.auth = None + + def get_session(self): + if self.current_session is None: + self.create_new_session() + + return self.current_session + + def create_new_session(self): + self.current_session = requests.Session() + + retries = Retry( + total=self.total_retries, + connect=self.connect, + backoff_factor=self.backoff_factor, + status_forcelist=self.status_forcelist, + ) + + adapter = HTTPAdapter(max_retries=retries) + self.current_session.mount("http://", adapter) + self.current_session.mount("https://", adapter) + + if self.auth is not None: + self.current_session.auth = self.auth + + + def close_session(self): + if self.current_session is not None: + self.current_session.close() + self.current_session = None + + def restart_session(self): + self.close_session() + self.create_new_session + + def set_auth(self, auth): + self.auth=auth + if self.current_session is not None: + self.current_session.auth=auth + \ No newline at end of file diff --git a/tests/test_service_unit.py b/tests/test_service_unit.py index 6dc46d8..1889283 100644 --- a/tests/test_service_unit.py +++ b/tests/test_service_unit.py @@ -3,6 +3,7 @@ from requests.auth import HTTPBasicAuth from frost_sta_client.service.sensorthingsservice import SensorThingsService from frost_sta_client.service.auth_handler import AuthHandler +from frost_sta_client.service.session_handler import SessionHandler from frost_sta_client.model.thing import Thing @@ -43,6 +44,7 @@ def test_execute_uses_auth(monkeypatch): svc.auth_handler = AuthHandler('user', 'pass') captured = {} + def fake_request(method, url, proxies=None, auth=None, **kwargs): captured['auth'] = auth class R: @@ -56,3 +58,50 @@ def json(self): monkeypatch.setattr(requests, 'request', fake_request) svc.execute('get', 'http://example.org') assert isinstance(captured['auth'], HTTPBasicAuth) + +def test_execute_uses_session(monkeypatch): + svc = SensorThingsService('http://example.org/FROST-Server/v1.1') + svc.session_handler = SessionHandler() + captured = {} + + + def fake_request_session(self,method, url, proxies=None, auth=None, **kwargs): + captured['session'] = self + class R: + status_code = 200 + def raise_for_status(self): + pass + def json(self): + return {} + return R() + + monkeypatch.setattr(requests.Session, 'request', fake_request_session) + svc.execute('get', 'http://example.org') + assert isinstance(captured['session'], requests.Session) + + + +def test_execute_uses_auth_and_session(monkeypatch): + svc = SensorThingsService('http://example.org/FROST-Server/v1.1') + svc.auth_handler = AuthHandler('user', 'pass') + svc.session_handler = SessionHandler() + captured = {} + + + def fake_request_session(self,method, url, proxies=None, auth=None, **kwargs): + captured['auth'] = self.auth + captured['session'] = self + + class R: + status_code = 200 + def raise_for_status(self): + pass + def json(self): + return {} + return R() + + monkeypatch.setattr(requests.Session, 'request', fake_request_session) + svc.execute('get', 'http://example.org') + assert isinstance(captured['auth'], HTTPBasicAuth) + assert isinstance(captured['session'], requests.Session) +