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