From 85ab5c72091f4fc854e319c2f9696425e23cd5da Mon Sep 17 00:00:00 2001 From: Marc Adolf Date: Mon, 21 Aug 2023 17:19:22 +0200 Subject: [PATCH 01/26] added a retry object to requests --- .../service/sensorthingsservice.py | 40 ++++++++++++++----- 1 file changed, 29 insertions(+), 11 deletions(-) diff --git a/frost_sta_client/service/sensorthingsservice.py b/frost_sta_client/service/sensorthingsservice.py index 3edd183..8cb53ec 100644 --- a/frost_sta_client/service/sensorthingsservice.py +++ b/frost_sta_client/service/sensorthingsservice.py @@ -15,6 +15,8 @@ # along with this program. If not, see . import requests +from requests.adapters import HTTPAdapter, Retry + from furl import furl import logging @@ -24,12 +26,21 @@ class SensorThingsService: - def __init__(self, url, auth_handler=None, proxies=None): self.url = url self.auth_handler = auth_handler self.proxies = proxies + self.request_session = requests.Session() + + retries = Retry( + total=5, backoff_factor=0.1, status_forcelist=[500, 502, 503, 504] + ) + + adapter = HTTPAdapter(max_retries=retries) + self.request_session.mount("http://", adapter) + self.request_session.mount("https://", adapter) + @property def url(self): return self._url @@ -45,7 +56,6 @@ def url(self, value): logging.error("received invalid url") raise e - @property def auth_handler(self): return self._auth_handler @@ -56,10 +66,9 @@ def auth_handler(self, value): self._auth_handler = None return if not isinstance(value, auth_handler.AuthHandler): - raise ValueError('auth should be of type AuthHandler!') + raise ValueError("auth should be of type AuthHandler!") self._auth_handler = value - @property def proxies(self): return self._proxies @@ -70,15 +79,22 @@ def proxies(self, value): self._proxies = None return elif not isinstance(value, dict): - raise ValueError('Proxies must be a Dictionary!') + 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) + response = self.request_session.request( + method, + url, + proxies=self.proxies, + auth=self.auth_handler.add_auth_header(), + **kwargs + ) else: - response = requests.request(method, url, proxies=self.proxies, **kwargs) + response = self.request_session.request( + method, url, proxies=self.proxies, **kwargs + ) try: response.raise_for_status() except requests.exceptions.HTTPError as e: @@ -90,10 +106,12 @@ def get_path(self, parent, relation): if parent is None: return relation this_entity_type = entity_type.get_list_for_class(type(parent)) - return "{entity_type}({id})/{relation}".format(entity_type=this_entity_type, id=parent.id, relation=relation) + return "{entity_type}({id})/{relation}".format( + entity_type=this_entity_type, id=parent.id, relation=relation + ) def get_full_path(self, parent, relation): - slash = "" if self.url.pathstr[-1] == '/' else "/" + slash = "" if self.url.pathstr[-1] == "/" else "/" url = self.url.url + slash + self.get_path(parent, relation) return furl(url) From dbb179936f9e0256eef4b96423e86a556416815f Mon Sep 17 00:00:00 2001 From: Marc Adolf Date: Mon, 21 Aug 2023 17:28:33 +0200 Subject: [PATCH 02/26] added gitlab ci --- .gitlab-ci.yml | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) create mode 100644 .gitlab-ci.yml diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml new file mode 100644 index 0000000..0ac9e2f --- /dev/null +++ b/.gitlab-ci.yml @@ -0,0 +1,23 @@ +variables: + + TWINE_PASSWORD: ${CI_JOB_TOKEN} + TWINE_USERNAME: gitlab-ci-token + TWINE_REPO_URL: https://git.geomar.de/api/v4/projects/${CI_PROJECT_ID}/packages/pypi + +stages: + - release + + +build_wheel: + stage: release + image: python:3.10 + script: + - python -V + - pip install virtualenv + - virtualenv venv + - source venv/bin/activate + - pip install twine + - python setup.py bdist_wheel + - python -m twine upload --repository-url $TWINE_REPO_URL dist/* --verbose + allow_failure: false # uploading same version results in failure. + From 31d9eadb46bb2d6deb3f5a4fdd4b80d68d59f5eb Mon Sep 17 00:00:00 2001 From: Marc Adolf Date: Thu, 5 Oct 2023 09:49:35 +0000 Subject: [PATCH 03/26] Update sensorthingsservice.py --- .../service/sensorthingsservice.py | 328 +++++++++--------- 1 file changed, 164 insertions(+), 164 deletions(-) diff --git a/frost_sta_client/service/sensorthingsservice.py b/frost_sta_client/service/sensorthingsservice.py index 8cb53ec..0d2f77f 100644 --- a/frost_sta_client/service/sensorthingsservice.py +++ b/frost_sta_client/service/sensorthingsservice.py @@ -1,164 +1,164 @@ -# 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 - -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 - - self.request_session = requests.Session() - - retries = Retry( - total=5, backoff_factor=0.1, status_forcelist=[500, 502, 503, 504] - ) - - adapter = HTTPAdapter(max_retries=retries) - self.request_session.mount("http://", adapter) - self.request_session.mount("https://", adapter) - - @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 = self.request_session.request( - method, - url, - proxies=self.proxies, - auth=self.auth_handler.add_auth_header(), - **kwargs - ) - else: - response = self.request_session.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)) - return "{entity_type}({id})/{relation}".format( - entity_type=this_entity_type, id=parent.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 requests.adapters import HTTPAdapter, Retry + +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 + + self.request_session = requests.Session() + + retries = Retry( + total=20,connect=15, backoff_factor=0.3, status_forcelist=[500, 502, 503, 504] + ) + + adapter = HTTPAdapter(max_retries=retries) + self.request_session.mount("http://", adapter) + self.request_session.mount("https://", adapter) + + @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 = self.request_session.request( + method, + url, + proxies=self.proxies, + auth=self.auth_handler.add_auth_header(), + **kwargs + ) + else: + response = self.request_session.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)) + return "{entity_type}({id})/{relation}".format( + entity_type=this_entity_type, id=parent.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) From 331b8082fe9d4c56aeec79becbd624c61e34bd71 Mon Sep 17 00:00:00 2001 From: Marc Adolf Date: Thu, 15 Feb 2024 12:49:36 +0100 Subject: [PATCH 04/26] added config for basic auth variables in the requests --- frost_sta_client/__version__.py | 2 +- frost_sta_client/config.py | 10 ++++++++++ .../service/sensorthingsservice.py | 20 ++++++++++++++++--- 3 files changed, 28 insertions(+), 4 deletions(-) create mode 100644 frost_sta_client/config.py diff --git a/frost_sta_client/__version__.py b/frost_sta_client/__version__.py index 951404e..e76ff74 100644 --- a/frost_sta_client/__version__.py +++ b/frost_sta_client/__version__.py @@ -1,5 +1,5 @@ __title__ = 'frost_sta_client' -__version__ = '1.1.45' +__version__ = '1.1.46' __license__ = 'LGPL3' __author__ = 'Jonathan Vogl' __copyright__ = 'Fraunhofer IOSB' diff --git a/frost_sta_client/config.py b/frost_sta_client/config.py new file mode 100644 index 0000000..3733a59 --- /dev/null +++ b/frost_sta_client/config.py @@ -0,0 +1,10 @@ +import os + + +class Config(object): + ## Enable HTTP_AUTH + HTTP_AUTH = os.environ.get("HTTP_AUTH") or False + if HTTP_AUTH: + HTTP_AUTH_USER = os.environ.get("HTTP_AUTH_USER") + HTTP_AUTH_PASSWORD = os.environ.get("HTTP_AUTH_PASSWORD") + \ No newline at end of file diff --git a/frost_sta_client/service/sensorthingsservice.py b/frost_sta_client/service/sensorthingsservice.py index c18721e..6ff34c5 100644 --- a/frost_sta_client/service/sensorthingsservice.py +++ b/frost_sta_client/service/sensorthingsservice.py @@ -20,6 +20,8 @@ from furl import furl import logging +from frost_sta_client.config import Config + from frost_sta_client.dao import * from frost_sta_client.service import auth_handler from frost_sta_client.model.ext import entity_type @@ -30,17 +32,27 @@ def __init__(self, url, auth_handler=None, proxies=None): self.url = url self.auth_handler = auth_handler self.proxies = proxies + config = Config() self.request_session = requests.Session() retries = Retry( - total=20,connect=15, backoff_factor=0.3, status_forcelist=[500, 502, 503, 504] + total=20, + connect=15, + backoff_factor=0.3, + status_forcelist=[500, 502, 503, 504], ) adapter = HTTPAdapter(max_retries=retries) self.request_session.mount("http://", adapter) self.request_session.mount("https://", adapter) + if config.HTTP_AUTH: + user = config.HTTP_AUTH_USER + password = config.HTTP_AUTH_PASSWORD + if user and password: + self.request_session.auth = (user, password) + @property def url(self): return self._url @@ -89,7 +101,7 @@ def execute(self, method, url, **kwargs): url, proxies=self.proxies, auth=self.auth_handler.add_auth_header(), - **kwargs + **kwargs, ) else: response = self.request_session.request( @@ -107,7 +119,9 @@ def get_path(self, parent, relation): 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) + 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 "/" From 8bd524a0fab5be726af57614479ccdd55ba526a0 Mon Sep 17 00:00:00 2001 From: Marc Adolf Date: Thu, 15 Feb 2024 13:00:14 +0100 Subject: [PATCH 05/26] added retry parameters as config --- frost_sta_client/config.py | 9 +++++++++ frost_sta_client/service/sensorthingsservice.py | 13 ++++++++----- 2 files changed, 17 insertions(+), 5 deletions(-) diff --git a/frost_sta_client/config.py b/frost_sta_client/config.py index 3733a59..2a88b4a 100644 --- a/frost_sta_client/config.py +++ b/frost_sta_client/config.py @@ -2,6 +2,15 @@ class Config(object): + ## configure request retries + # Total number of retries to allow. Takes precedence over other counts. + total_retries = os.environ.get("HTTP_RETRY_TOTAL", 20) + # How many connection-related errors to retry on. + connect = os.environ.get("HTTP_RETRY_CONNECT", 15) + # A backoff factor to apply between attempts after the second try + backoff_factor = os.environ.get("HTTP_RETRY_BACKOFF_FACTOR", 0.3) + # A set of integer HTTP status codes that we should force a retry on + status_forcelist = os.environ.get("HTTP_RETRY_STATUS_FORCELIST", [500, 502, 503, 504]) ## Enable HTTP_AUTH HTTP_AUTH = os.environ.get("HTTP_AUTH") or False if HTTP_AUTH: diff --git a/frost_sta_client/service/sensorthingsservice.py b/frost_sta_client/service/sensorthingsservice.py index 6ff34c5..3ab2674 100644 --- a/frost_sta_client/service/sensorthingsservice.py +++ b/frost_sta_client/service/sensorthingsservice.py @@ -33,14 +33,17 @@ def __init__(self, url, auth_handler=None, proxies=None): self.auth_handler = auth_handler self.proxies = proxies config = Config() - + total_retries = config.total_retries + connect = config.connect + backoff_factor = config.backoff_factor + status_forcelist = config.status_forcelist self.request_session = requests.Session() retries = Retry( - total=20, - connect=15, - backoff_factor=0.3, - status_forcelist=[500, 502, 503, 504], + total=total_retries, + connect=connect, + backoff_factor=backoff_factor, + status_forcelist=status_forcelist, ) adapter = HTTPAdapter(max_retries=retries) From 1a8a80ce3f3cde704a3d9b6afdedb97b25297380 Mon Sep 17 00:00:00 2001 From: Marc Adolf Date: Fri, 24 Jan 2025 15:49:00 +0100 Subject: [PATCH 06/26] exemp the fields properties in things from comparison --- frost_sta_client/__version__.py | 2 +- frost_sta_client/model/thing.py | 10 +++++++++- 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/frost_sta_client/__version__.py b/frost_sta_client/__version__.py index e76ff74..ab91d79 100644 --- a/frost_sta_client/__version__.py +++ b/frost_sta_client/__version__.py @@ -1,5 +1,5 @@ __title__ = 'frost_sta_client' -__version__ = '1.1.46' +__version__ = '1.1.47' __license__ = 'LGPL3' __author__ = 'Jonathan Vogl' __copyright__ = 'Fraunhofer IOSB' diff --git a/frost_sta_client/model/thing.py b/frost_sta_client/model/thing.py index 5de1b32..93bf19e 100644 --- a/frost_sta_client/model/thing.py +++ b/frost_sta_client/model/thing.py @@ -231,10 +231,18 @@ def __eq__(self, other): return False if self.description != other.description: return False - if self.properties != other.properties: + if not self._important_properties_are_equal(self.properties, other.properties): return False return True + def _important_properties_are_equal(self, prop1, prop2): + #fields are variable properties, that should not be compared + if prop1 is None or prop2 is None: + return prop1 == prop2 + filtered_prop1 = {k: v for k, v in prop1.items() if k != "fields"} + filtered_prop2 = {k: v for k, v in prop2.items() if k != "fields"} + return filtered_prop1 == filtered_prop2 + def __ne__(self, other): return not self == other From 7786dac135e610ff22990337385102a50ba06e93 Mon Sep 17 00:00:00 2001 From: Marc Adolf Date: Fri, 10 Oct 2025 11:59:19 +0200 Subject: [PATCH 07/26] https://git.geomar.de/dm/services/frost-sta/frost-python-client-geomar-clone/-/issues/1 --- .gitlab-ci.yml | 36 ++++++++++++++++++++++++++++++------ 1 file changed, 30 insertions(+), 6 deletions(-) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 0ac9e2f..a487648 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -5,19 +5,43 @@ variables: TWINE_REPO_URL: https://git.geomar.de/api/v4/projects/${CI_PROJECT_ID}/packages/pypi stages: + - build + - test - release - build_wheel: - stage: release + stage: build image: python:3.10 script: - python -V - pip install virtualenv - virtualenv venv - source venv/bin/activate - - pip install twine - - python setup.py bdist_wheel - - python -m twine upload --repository-url $TWINE_REPO_URL dist/* --verbose - allow_failure: false # uploading same version results in failure. + - pip install build + - python -m build + artifacts: + paths: + - dist/*.whl + +test_package: + stage: test + image: python:3.10 + script: + - python -V + - pip install -r requirements.txt + - python -m unittest discover -s tests + dependencies: + - build_wheel + artifacts: + paths: + - tests/test_results.xml +upload_package: + stage: release + script: + - pip install twine + - twine upload --verbose --repository-url $TWINE_REPO_URL + only: + - main + - tags + allow_failure: false # uploading same version results in failure. \ No newline at end of file From cf1b25ee4fc7415efc1ea869a242f118b23e9dfe Mon Sep 17 00:00:00 2001 From: Marc Adolf Date: Fri, 10 Oct 2025 12:05:12 +0200 Subject: [PATCH 08/26] added dist as release target --- .gitlab-ci.yml | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index a487648..6beeec3 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -30,6 +30,7 @@ test_package: - python -V - pip install -r requirements.txt - python -m unittest discover -s tests + dependencies: - build_wheel artifacts: @@ -40,7 +41,9 @@ upload_package: stage: release script: - pip install twine - - twine upload --verbose --repository-url $TWINE_REPO_URL + - twine upload --verbose --repository-url $TWINE_REPO_URL dist/* + dependencies: + - build_wheel only: - main - tags From 9d03faa9fe0059e69e51dc288e193bbfd5e43cef Mon Sep 17 00:00:00 2001 From: Marc Adolf Date: Fri, 10 Oct 2025 12:57:49 +0200 Subject: [PATCH 09/26] adapted tests for our changes --- frost_sta_client/service/sensorthingsservice.py | 7 ++++--- tests/test_service_unit.py | 4 ++-- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/frost_sta_client/service/sensorthingsservice.py b/frost_sta_client/service/sensorthingsservice.py index 3ab2674..aa3d21f 100644 --- a/frost_sta_client/service/sensorthingsservice.py +++ b/frost_sta_client/service/sensorthingsservice.py @@ -99,16 +99,17 @@ def proxies(self, value): def execute(self, method, url, **kwargs): if self.auth_handler is not None: + #use normales requests if separate auth_handler is set response = self.request_session.request( - method, - url, + method=method, + url=url, proxies=self.proxies, auth=self.auth_handler.add_auth_header(), **kwargs, ) else: response = self.request_session.request( - method, url, proxies=self.proxies, **kwargs + method=method, url=url, proxies=self.proxies, **kwargs ) try: response.raise_for_status() diff --git a/tests/test_service_unit.py b/tests/test_service_unit.py index 6dc46d8..7eddbfd 100644 --- a/tests/test_service_unit.py +++ b/tests/test_service_unit.py @@ -43,7 +43,7 @@ def test_execute_uses_auth(monkeypatch): svc.auth_handler = AuthHandler('user', 'pass') captured = {} - def fake_request(method, url, proxies=None, auth=None, **kwargs): + def fake_request(self,method, url, proxies=None, auth=None, **kwargs): captured['auth'] = auth class R: status_code = 200 @@ -53,6 +53,6 @@ def json(self): return {} return R() - monkeypatch.setattr(requests, 'request', fake_request) + monkeypatch.setattr(requests.Session, 'request', fake_request) svc.execute('get', 'http://example.org') assert isinstance(captured['auth'], HTTPBasicAuth) From 113ed0d4d9f9667ab86e7e94a5d59a6913f30697 Mon Sep 17 00:00:00 2001 From: Marc Adolf Date: Fri, 10 Oct 2025 13:04:06 +0200 Subject: [PATCH 10/26] now use pytest --- .gitlab-ci.yml | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 6beeec3..6d0726f 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -29,14 +29,16 @@ test_package: script: - python -V - pip install -r requirements.txt - - python -m unittest discover -s tests - + - pip install pytest pytest-cov + - pytest --maxfail=1 --disable-warnings -q --junitxml=tests/test_results.xml dependencies: - build_wheel artifacts: + when: always + reports: + junit: tests/test_results.xml paths: - tests/test_results.xml - upload_package: stage: release script: From 9cd1361a10a8a1623f19a617c9e5c93d7473fac8 Mon Sep 17 00:00:00 2001 From: Marc Adolf Date: Fri, 10 Oct 2025 13:04:32 +0200 Subject: [PATCH 11/26] version ++ --- frost_sta_client/__version__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frost_sta_client/__version__.py b/frost_sta_client/__version__.py index 47d1ff1..d524e99 100644 --- a/frost_sta_client/__version__.py +++ b/frost_sta_client/__version__.py @@ -1,5 +1,5 @@ __title__ = 'frost_sta_client' -__version__ = '1.1.53' +__version__ = '1.1.54' __license__ = 'LGPL3' __author__ = 'Fraunhofer IOSB' __copyright__ = 'Fraunhofer IOSB' From 53850d6c057424f33e9c45436c606a28188f2e58 Mon Sep 17 00:00:00 2001 From: Marc Adolf Date: Fri, 10 Oct 2025 11:06:36 +0000 Subject: [PATCH 12/26] Marc fix pipeline --- .gitlab-ci.yml | 39 +++++++++++++++++++++++++++++++++------ 1 file changed, 33 insertions(+), 6 deletions(-) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 0ac9e2f..6beeec3 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -5,19 +5,46 @@ variables: TWINE_REPO_URL: https://git.geomar.de/api/v4/projects/${CI_PROJECT_ID}/packages/pypi stages: + - build + - test - release - build_wheel: - stage: release + stage: build image: python:3.10 script: - python -V - pip install virtualenv - virtualenv venv - source venv/bin/activate - - pip install twine - - python setup.py bdist_wheel - - python -m twine upload --repository-url $TWINE_REPO_URL dist/* --verbose - allow_failure: false # uploading same version results in failure. + - pip install build + - python -m build + artifacts: + paths: + - dist/*.whl + +test_package: + stage: test + image: python:3.10 + script: + - python -V + - pip install -r requirements.txt + - python -m unittest discover -s tests + + dependencies: + - build_wheel + artifacts: + paths: + - tests/test_results.xml +upload_package: + stage: release + script: + - pip install twine + - twine upload --verbose --repository-url $TWINE_REPO_URL dist/* + dependencies: + - build_wheel + only: + - main + - tags + allow_failure: false # uploading same version results in failure. \ No newline at end of file From baf9a37e88045d68ff086ccb5070288da8ffc6bc Mon Sep 17 00:00:00 2001 From: Marc Adolf Date: Mon, 13 Oct 2025 09:28:39 +0200 Subject: [PATCH 13/26] add python to pytest call --- .gitlab-ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index b3d3e22..d7a90ea 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -32,7 +32,7 @@ test_package: - python -V - pip install -r requirements.txt - pip install pytest pytest-cov - - pytest --maxfail=1 --disable-warnings -q --junitxml=tests/test_results.xml + - python -m pytest --maxfail=1 --disable-warnings -q --junitxml=tests/test_results.xml dependencies: - build_wheel artifacts: From 9bebc415e6a0e070c7c6fc96f1a492a868e48f93 Mon Sep 17 00:00:00 2001 From: Marc Adolf Date: Wed, 15 Oct 2025 09:40:47 +0200 Subject: [PATCH 14/26] added session handler and tests --- frost_sta_client/__init__.py | 1 + frost_sta_client/service/__init__.py | 1 + .../service/sensorthingsservice.py | 62 +++++++++---------- tests/test_service_unit.py | 53 +++++++++++++++- 4 files changed, 84 insertions(+), 33 deletions(-) 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 aa3d21f..2597a0b 100644 --- a/frost_sta_client/service/sensorthingsservice.py +++ b/frost_sta_client/service/sensorthingsservice.py @@ -15,46 +15,22 @@ # along with this program. If not, see . import requests -from requests.adapters import HTTPAdapter, Retry from furl import furl import logging -from frost_sta_client.config import Config - 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, proxies=None): + 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 - config = Config() - total_retries = config.total_retries - connect = config.connect - backoff_factor = config.backoff_factor - status_forcelist = config.status_forcelist - self.request_session = requests.Session() - - retries = Retry( - total=total_retries, - connect=connect, - backoff_factor=backoff_factor, - status_forcelist=status_forcelist, - ) - - adapter = HTTPAdapter(max_retries=retries) - self.request_session.mount("http://", adapter) - self.request_session.mount("https://", adapter) - - if config.HTTP_AUTH: - user = config.HTTP_AUTH_USER - password = config.HTTP_AUTH_PASSWORD - if user and password: - self.request_session.auth = (user, password) @property def url(self): @@ -82,7 +58,26 @@ def auth_handler(self, value): 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): @@ -98,9 +93,14 @@ def proxies(self, value): self._proxies = value def execute(self, method, url, **kwargs): - if self.auth_handler is not None: - #use normales requests if separate auth_handler is set - response = self.request_session.request( + + 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, @@ -108,7 +108,7 @@ def execute(self, method, url, **kwargs): **kwargs, ) else: - response = self.request_session.request( + response = requests.request( method=method, url=url, proxies=self.proxies, **kwargs ) try: diff --git a/tests/test_service_unit.py b/tests/test_service_unit.py index 7eddbfd..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,7 +44,8 @@ def test_execute_uses_auth(monkeypatch): svc.auth_handler = AuthHandler('user', 'pass') captured = {} - def fake_request(self,method, url, proxies=None, auth=None, **kwargs): + + def fake_request(method, url, proxies=None, auth=None, **kwargs): captured['auth'] = auth class R: status_code = 200 @@ -53,6 +55,53 @@ def json(self): return {} return R() - monkeypatch.setattr(requests.Session, 'request', fake_request) + 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) + From 5f2c1ddfde116edf2d296ee9ae2f47a2f51360df Mon Sep 17 00:00:00 2001 From: Marc Adolf Date: Wed, 15 Oct 2025 09:41:04 +0200 Subject: [PATCH 15/26] add session_handler file --- frost_sta_client/service/session_handler.py | 78 +++++++++++++++++++++ 1 file changed, 78 insertions(+) create mode 100644 frost_sta_client/service/session_handler.py 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 From d56d9383dc835be814a95b31bc7701ae45c2f1dd Mon Sep 17 00:00:00 2001 From: Marc Adolf Date: Wed, 15 Oct 2025 10:13:42 +0200 Subject: [PATCH 16/26] changed gitlab ci images --- .gitlab-ci.yml | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index d7a90ea..10f794e 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -4,6 +4,9 @@ variables: TWINE_USERNAME: gitlab-ci-token TWINE_REPO_URL: https://git.geomar.de/api/v4/projects/${CI_PROJECT_ID}/packages/pypi + +image: python:3.14 + stages: - build - test @@ -13,7 +16,6 @@ stages: build_wheel: stage: build - image: python:3.10 script: - python -V - pip install virtualenv @@ -27,7 +29,6 @@ build_wheel: test_package: stage: test - image: python:3.10 script: - python -V - pip install -r requirements.txt @@ -43,6 +44,7 @@ test_package: - tests/test_results.xml upload_package: stage: release + script: - pip install twine - twine upload --verbose --repository-url $TWINE_REPO_URL dist/* From 719788c645c278eaf2eac48d55d6f70034151767 Mon Sep 17 00:00:00 2001 From: Marc Adolf Date: Wed, 15 Oct 2025 08:23:23 +0000 Subject: [PATCH 17/26] Update python-publish.yml --- .github/workflows/python-app.yml | 12 +- .github/workflows/python-publish.yml | 9 +- .gitlab-ci.yml | 22 +- README.md | 4 +- frost_server/docker-compose.yaml | 44 ++ frost_sta_client/__init__.py | 1 + frost_sta_client/__version__.py | 6 +- frost_sta_client/dao/base.py | 393 +++++----- frost_sta_client/dao/observation.py | 108 ++- frost_sta_client/model/actuator.py | 2 +- frost_sta_client/model/datastream.py | 676 ++++++++--------- .../model/ext/data_array_document.py | 135 ++-- frost_sta_client/model/ext/entity_list.py | 328 +++++---- frost_sta_client/model/feature_of_interest.py | 2 +- frost_sta_client/model/historical_location.py | 2 +- frost_sta_client/model/location.py | 5 +- frost_sta_client/model/multi_datastream.py | 691 +++++++++--------- frost_sta_client/model/observedproperty.py | 420 +++++------ frost_sta_client/model/task.py | 6 +- frost_sta_client/model/tasking_capability.py | 4 +- frost_sta_client/query/query.py | 325 ++++---- frost_sta_client/service/__init__.py | 1 + .../service/sensorthingsservice.py | 67 +- frost_sta_client/service/session_handler.py | 78 ++ frost_sta_client/utils.py | 257 ++++--- tests/conftest.py | 47 ++ tests/test_dao_base.py | 71 ++ tests/test_entity_behaviour.py | 21 + tests/test_entity_list.py | 50 ++ tests/test_entity_reader.py | 533 +++++++------- tests/test_error_handling_non_json.py | 66 ++ tests/test_ext_data_array.py | 20 + tests/test_ext_entity_type.py | 7 + tests/test_integration.py | 125 ++++ tests/test_query_unit.py | 42 ++ tests/test_service_unit.py | 107 +++ tests/test_utils_more.py | 33 + 37 files changed, 2722 insertions(+), 1998 deletions(-) create mode 100644 frost_server/docker-compose.yaml create mode 100644 frost_sta_client/service/session_handler.py create mode 100644 tests/conftest.py create mode 100644 tests/test_dao_base.py create mode 100644 tests/test_entity_behaviour.py create mode 100644 tests/test_entity_list.py create mode 100644 tests/test_error_handling_non_json.py create mode 100644 tests/test_ext_data_array.py create mode 100644 tests/test_ext_entity_type.py create mode 100644 tests/test_integration.py create mode 100644 tests/test_query_unit.py create mode 100644 tests/test_service_unit.py create mode 100644 tests/test_utils_more.py diff --git a/.github/workflows/python-app.yml b/.github/workflows/python-app.yml index 1d35427..49878c3 100644 --- a/.github/workflows/python-app.yml +++ b/.github/workflows/python-app.yml @@ -16,13 +16,18 @@ jobs: steps: - uses: actions/checkout@v2 - - name: Set up Python 3.8 + - name: Set up Python 3.12 uses: actions/setup-python@v2 with: - python-version: "3.8" + python-version: "3.12" - name: Install dependencies run: | - python -m pip install --upgrade pip + echo '#!/usr/bin/env bash' | sudo tee /usr/local/bin/podman >/dev/null + echo 'exec docker "$@"' | sudo tee -a /usr/local/bin/podman >/dev/null + sudo chmod +x /usr/local/bin/podman + docker info + docker compose version + python3 -m pip install --upgrade pip pip install flake8 pytest if [ -f requirements.txt ]; then pip install -r requirements.txt; fi - name: Lint with flake8 @@ -33,4 +38,5 @@ jobs: flake8 . --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics - name: Test with pytest run: | + export FROST_STA_CLIENT_RUN_INTEGRATION=1 python -m pytest diff --git a/.github/workflows/python-publish.yml b/.github/workflows/python-publish.yml index cfb9787..8bb285c 100644 --- a/.github/workflows/python-publish.yml +++ b/.github/workflows/python-publish.yml @@ -10,13 +10,12 @@ name: Publish forst_sta_client distribution to PyPI on: push: - branches: [ master ] -# pull_request: -# branches: [ master ] - + tags: + - "v[0-9]+.[0-9]+.[0-9]+" + # Allows you to run this workflow manually from the Actions tab + workflow_dispatch: jobs: deploy: - runs-on: ubuntu-latest steps: diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 6beeec3..10f794e 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -4,14 +4,18 @@ variables: TWINE_USERNAME: gitlab-ci-token TWINE_REPO_URL: https://git.geomar.de/api/v4/projects/${CI_PROJECT_ID}/packages/pypi + +image: python:3.14 + stages: + - build + - test - build - test - release build_wheel: stage: build - image: python:3.10 script: - python -V - pip install virtualenv @@ -25,25 +29,33 @@ build_wheel: test_package: stage: test - image: python:3.10 script: - python -V - pip install -r requirements.txt - - python -m unittest discover -s tests - + - pip install pytest pytest-cov + - python -m pytest --maxfail=1 --disable-warnings -q --junitxml=tests/test_results.xml dependencies: - build_wheel artifacts: + when: always + reports: + junit: tests/test_results.xml paths: - tests/test_results.xml - upload_package: stage: release + script: - pip install twine - twine upload --verbose --repository-url $TWINE_REPO_URL dist/* dependencies: - build_wheel + only: + - main + - tags + - twine upload --verbose --repository-url $TWINE_REPO_URL dist/* + dependencies: + - build_wheel only: - main - tags diff --git a/README.md b/README.md index 58b6b30..8f6a3b1 100644 --- a/README.md +++ b/README.md @@ -19,7 +19,8 @@ The source code below demonstrates the CRUD operations for Thing objects. Operat import frost_sta_client as fsc url = "exampleserver.com/FROST-Server/v1.1" -service = fsc.SensorThingsService(url) +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) ``` #### Creating Entities ```python @@ -75,6 +76,7 @@ observations_list = datastream.get_observations().query().filter("result gt 10") ``` ### Callback function in `EntityList` + The progress of the loading process can be tracked by supplying a callback function along with a step size. The callback function and the step size must both be provided to the `list` function (see example below). diff --git a/frost_server/docker-compose.yaml b/frost_server/docker-compose.yaml new file mode 100644 index 0000000..59b3d36 --- /dev/null +++ b/frost_server/docker-compose.yaml @@ -0,0 +1,44 @@ +services: + web: + image: docker.io/fraunhoferiosb/frost-server:latest + environment: + # For all settings see: https://fraunhoferiosb.github.io/FROST-Server/settings/settings.html + - serviceRootUrl=http://localhost:8080/FROST-Server + - plugins_multiDatastream.enable=false + - http_cors_enable=true + - http_cors_allowed_origins=* + - persistence_db_driver=org.postgresql.Driver + - persistence_db_url=jdbc:postgresql://database:5432/sensorthings + - persistence_db_username=sensorthings + - persistence_db_password=ChangeMe + - persistence_autoUpdateDatabase=true + - auth_provider=de.fraunhofer.iosb.ilt.frostserver.auth.basic.BasicAuthProvider + - auth_db_driver=org.postgresql.Driver + - auth_db_url=jdbc:postgresql://database:5432/sensorthings + - auth_db_username=sensorthings + - auth_db_password=ChangeMe + - auth_autoUpdateDatabase=true + ports: + - 8080:8080 + - 1883:1883 + depends_on: + database: + condition: service_healthy + + database: + image: docker.io/postgis/postgis:16-3.4-alpine + environment: + - POSTGRES_DB=sensorthings + - POSTGRES_USER=sensorthings + - POSTGRES_PASSWORD=ChangeMe + volumes: + - postgis_volume:/var/lib/postgresql/data + healthcheck: + test: ["CMD-SHELL", "pg_isready -d sensorthings -U sensorthings "] + interval: 2s + timeout: 2s + retries: 10 + +volumes: + postgis_volume: + 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/__version__.py b/frost_sta_client/__version__.py index ab91d79..d524e99 100644 --- a/frost_sta_client/__version__.py +++ b/frost_sta_client/__version__.py @@ -1,8 +1,8 @@ __title__ = 'frost_sta_client' -__version__ = '1.1.47' +__version__ = '1.1.54' __license__ = 'LGPL3' -__author__ = 'Jonathan Vogl' +__author__ = 'Fraunhofer IOSB' __copyright__ = 'Fraunhofer IOSB' -__contact__ = 'jonathan.vogl@iosb.fraunhofer.de' +__contact__ = 'frost@iosb.fraunhofer.de' __url__ = 'https://github.com/FraunhoferIOSB/FROST-Python-Client' __description__ = 'a client library to facilitate interaction with a FROST SensorThingsAPI Server' diff --git a/frost_sta_client/dao/base.py b/frost_sta_client/dao/base.py index 4ca7b45..5ec7253 100644 --- a/frost_sta_client/dao/base.py +++ b/frost_sta_client/dao/base.py @@ -1,209 +1,184 @@ -# 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 frost_sta_client.query.query -import frost_sta_client.utils - -import logging -import requests -import jsonpatch -import json -from furl import furl - - -class BaseDao: - """ - The entity independent implementation of a data access object. Specific entity Daos - can be implemented by inheriting from this class. - """ - APPLICATION_JSON_PATCH = {'Content-type': 'application/json-patch+json'} - - def __init__(self, service, entitytype): - """ - Constructor. - params: - service: the service to operate on - entitytype: a dictionary describing the type of the entity - """ - self.service = service - self.entitytype = entitytype["singular"] - self.entitytype_plural = entitytype["plural"] - self.entity_class = entitytype["class"] - self.parent = None - - @property - def service(self): - return self._service - - @service.setter - def service(self, value): - if value is None or isinstance(value, frost_sta_client.service.sensorthingsservice.SensorThingsService): - self._service = value - return - raise ValueError('service should be of type SensorThingsService') - - @property - def entitytype(self): - return self._entitytype - - @entitytype.setter - def entitytype(self, value): - if value is None or isinstance(value, str): - self._entitytype = value - return - raise ValueError('entitytype should be of type String') - - @property - def entitytype_plural(self): - return self._entitytype_plural - - @entitytype_plural.setter - def entitytype_plural(self, value): - if value is None or isinstance(value, str): - self._entitytype_plural = value - return - raise ValueError('entitytype_plural should be of type String') - - @property - def entity_class(self): - return self._entity_class - - @entity_class.setter - def entity_class(self, value): - if value is None or isinstance(value, str): - self._entity_class = value - return - raise ValueError('entity_class should be of type string') - - @property - def parent(self): - return self._parent - - @parent.setter - def parent(self, value): - self._parent = value - - def create(self, entity): - url = furl(self.service.url) - url.path.add(self.entitytype_plural) - logging.debug('Posting to ' + str(url.url)) - json_dict = frost_sta_client.utils.transform_entity_to_json_dict(entity) - try: - response = self.service.execute('post', url, json=json_dict) - except requests.exceptions.HTTPError as e: - error_json = e.response.json() - error_message = error_json['message'] - logging.error("Creating {} failed with status-code {}, {}".format(type(entity).__name__, - e.response.status_code, - error_message)) - raise e - entity.id = frost_sta_client.utils.extract_value(response.headers['location']) - entity.service = self.service - logging.debug('Received response: ' + str(response.status_code)) - - def patch(self, entity, patches): - """ - method to patch STA entities - param entity: entity, that the patches should be applied to - param patches: either a JsonPatch object or list of dictionaries, containing jsonpatch commands - """ - url = furl(self.service.url) - if entity.id is None or entity.id == '': - raise AttributeError('please provide an entity with a valid id') - url.path.add(self.entity_path(entity.id)) - logging.debug(f'Patching to {url.url}') - headers = self.APPLICATION_JSON_PATCH - if patches is None: - raise ValueError('please provide a list of patches, either as a jsonpatch object or a ' - 'list of dictionaries') - if not isinstance(patches, jsonpatch.JsonPatch) and \ - not (isinstance(patches, list) and all(isinstance(x, dict) for x in patches)): - raise ValueError('please provide a list of patches, either as a jsonpatch object or a ' - 'list of dictionaries') - if isinstance(patches, jsonpatch.JsonPatch): - patches = patches.patch - try: - response = self.service.execute('patch', url, json=patches, headers=headers) - except requests.exceptions.HTTPError as e: - error_json = e.response.json() - error_message = error_json['message'] - logging.error("Patching {} failed with status-code {}, {}".format(type(entity).__name__, - e.response.status_code, - error_message)) - raise e - logging.debug(f'Received response: {str(response.status_code)}') - - def update(self, entity): - url = furl(self.service.url) - if entity.id is None or entity.id == '': - raise AttributeError('please provide an entity with a valid id') - url.path.add(self.entity_path(entity.id)) - logging.debug('Updating to {}'.format(url.url)) - json_dict = frost_sta_client.utils.transform_entity_to_json_dict(entity) - try: - response = self.service.execute('put', url, json=json_dict) - except requests.exceptions.HTTPError as e: - error_json = e.response.json() - error_message = error_json['message'] - logging.error("Updating {} failed with status-code {}, {}".format(type(entity).__name__, - e.response.status_code, - error_message)) - raise e - logging.debug('Received response: {}'.format(str(response.status_code))) - - def find(self, id): - url = furl(self.service.url) - url.path.add(self.entity_path(id)) - logging.debug('Fetching: {}'.format(url.url)) - try: - response = self.service.execute('get', url) - except requests.exceptions.HTTPError as e: - error_json = e.response.json() - error_message = error_json['message'] - logging.error("Finding {} failed with status-code {}, {}".format(id, - e.response.status_code, - error_message)) - raise e - logging.debug('Received response: {}'.format(response.status_code)) - json_response = response.json() - json_response['id'] = json_response['@iot.id'] - entity = frost_sta_client.utils.transform_json_to_entity(json_response, self.entity_class) - entity.service = self.service - return entity - - def delete(self, entity): - url = furl(self.service.url) - url.path.add(self.entity_path(entity.id)) - logging.debug('Deleting: {}'.format(url.url)) - try: - response = self.service.execute('delete', url) - except requests.exceptions.HTTPError as e: - error_json = e.response.json() - error_message = error_json['message'] - logging.error("Deleting {} failed with status-code {}, {}".format(type(entity).__name__, - e.response.status_code, - error_message)) - raise e - logging.debug('Received response: {}'.format(response.status_code)) - - def entity_path(self, id): - if isinstance(id, int): - return "{}({})".format(self.entitytype_plural, id) - return "{}('{}')".format(self.entitytype_plural, id) - - def query(self): - return frost_sta_client.query.query.Query(self.service, self.entitytype, self.entitytype_plural, - self.entity_class, self.parent) +# 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 frost_sta_client.query.query +import frost_sta_client.utils + +import logging +import requests +import jsonpatch +import json +from furl import furl + + +class BaseDao: + """ + The entity independent implementation of a data access object. Specific entity Daos + can be implemented by inheriting from this class. + """ + APPLICATION_JSON_PATCH = {'Content-type': 'application/json-patch+json'} + + def __init__(self, service, entitytype): + """ + Constructor. + params: + service: the service to operate on + entitytype: a dictionary describing the type of the entity + """ + self.service = service + self.entitytype = entitytype["singular"] + self.entitytype_plural = entitytype["plural"] + self.entity_class = entitytype["class"] + self.parent = None + + @property + def service(self): + return self._service + + @service.setter + def service(self, value): + if value is None or isinstance(value, frost_sta_client.service.sensorthingsservice.SensorThingsService): + self._service = value + return + raise ValueError('service should be of type SensorThingsService') + + @property + def entitytype(self): + return self._entitytype + + @entitytype.setter + def entitytype(self, value): + if value is None or isinstance(value, str): + self._entitytype = value + return + raise ValueError('entitytype should be of type String') + + @property + def entitytype_plural(self): + return self._entitytype_plural + + @entitytype_plural.setter + def entitytype_plural(self, value): + if value is None or isinstance(value, str): + self._entitytype_plural = value + return + raise ValueError('entitytype_plural should be of type String') + + @property + def entity_class(self): + return self._entity_class + + @entity_class.setter + def entity_class(self, value): + if value is None or isinstance(value, str): + self._entity_class = value + return + raise ValueError('entity_class should be of type string') + + @property + def parent(self): + return self._parent + + @parent.setter + def parent(self, value): + self._parent = value + + def create(self, entity): + url = furl(self.service.url) + url.path.add(self.entitytype_plural) + logging.debug('Posting to ' + str(url.url)) + json_dict = frost_sta_client.utils.transform_entity_to_json_dict(entity) + try: + response = self.service.execute('post', url, json=json_dict) + except requests.exceptions.HTTPError as e: + frost_sta_client.utils.handle_server_error(e, 'Creating {}'.format(type(entity).__name__)) + entity.id = frost_sta_client.utils.extract_value(response.headers['location']) + entity.service = self.service + logging.debug('Received response: ' + str(response.status_code)) + + def patch(self, entity, patches): + """ + method to patch STA entities + param entity: entity, that the patches should be applied to + param patches: either a JsonPatch object or list of dictionaries, containing jsonpatch commands + """ + url = furl(self.service.url) + if entity.id is None or entity.id == '': + raise AttributeError('please provide an entity with a valid id') + url.path.add(self.entity_path(entity.id)) + logging.debug(f'Patching to {url.url}') + headers = self.APPLICATION_JSON_PATCH + if patches is None: + raise ValueError('please provide a list of patches, either as a jsonpatch object or a ' + 'list of dictionaries') + if not isinstance(patches, jsonpatch.JsonPatch) and \ + not (isinstance(patches, list) and all(isinstance(x, dict) for x in patches)): + raise ValueError('please provide a list of patches, either as a jsonpatch object or a ' + 'list of dictionaries') + if isinstance(patches, jsonpatch.JsonPatch): + patches = patches.patch + try: + response = self.service.execute('patch', url, json=patches, headers=headers) + except requests.exceptions.HTTPError as e: + frost_sta_client.utils.handle_server_error(e, 'Patching {}'.format(type(entity).__name__)) + logging.debug(f'Received response: {str(response.status_code)}') + + def update(self, entity): + url = furl(self.service.url) + if entity.id is None or entity.id == '': + raise AttributeError('please provide an entity with a valid id') + url.path.add(self.entity_path(entity.id)) + logging.debug('Updating to {}'.format(url.url)) + json_dict = frost_sta_client.utils.transform_entity_to_json_dict(entity) + try: + response = self.service.execute('put', url, json=json_dict) + except requests.exceptions.HTTPError as e: + frost_sta_client.utils.handle_server_error(e, 'Updating {}'.format(type(entity).__name__)) + logging.debug('Received response: {}'.format(str(response.status_code))) + + def find(self, id): + url = furl(self.service.url) + url.path.add(self.entity_path(id)) + logging.debug('Fetching: {}'.format(url.url)) + try: + response = self.service.execute('get', url) + except requests.exceptions.HTTPError as e: + frost_sta_client.utils.handle_server_error(e, 'Finding {}'.format(id)) + logging.debug('Received response: {}'.format(response.status_code)) + json_response = response.json() + json_response['id'] = json_response['@iot.id'] + entity = frost_sta_client.utils.transform_json_to_entity(json_response, self.entity_class) + entity.service = self.service + return entity + + def delete(self, entity): + url = furl(self.service.url) + url.path.add(self.entity_path(entity.id)) + logging.debug('Deleting: {}'.format(url.url)) + try: + response = self.service.execute('delete', url) + except requests.exceptions.HTTPError as e: + frost_sta_client.utils.handle_server_error(e, 'Deleting {}'.format(type(entity).__name__)) + logging.debug('Received response: {}'.format(response.status_code)) + + def entity_path(self, id): + if isinstance(id, int): + return "{}({})".format(self.entitytype_plural, id) + return "{}('{}')".format(self.entitytype_plural, id) + + def query(self): + return frost_sta_client.query.query.Query(self.service, self.entitytype, self.entitytype_plural, + self.entity_class, self.parent) diff --git a/frost_sta_client/dao/observation.py b/frost_sta_client/dao/observation.py index e7b644e..a15b52d 100644 --- a/frost_sta_client/dao/observation.py +++ b/frost_sta_client/dao/observation.py @@ -1,56 +1,52 @@ -# 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 . - -from frost_sta_client.dao import base -from frost_sta_client.model.ext.entity_type import EntityTypes -from frost_sta_client.utils import transform_entity_to_json_dict -import frost_sta_client - -import logging -import requests -import json - - - -class ObservationDao(base.BaseDao): - CREATE_OBSERVATIONS = "CreateObservations" - - def __init__(self, service): - """ - A data access object for operations with the Observation entity - """ - base.BaseDao.__init__(self, service, EntityTypes["Observation"]) - - def create(self, entity): - if isinstance(entity, frost_sta_client.model.observation.Observation): - super().create(entity) - else: - # entity is probably a data array - url = self.service.url.copy() - url.path.add(self.CREATE_OBSERVATIONS) - logging.debug('Posting to ' + str(url.url)) - json_dict = transform_entity_to_json_dict(entity.value) - try: - response = self.service.execute('post', url, json=json_dict) - except requests.exceptions.HTTPError as e: - error_json = e.response.json() - error_message = error_json['message'] - logging.error("Creating {} failed with status-code {}, {}".format("Data Array", - e.response.status_code, - error_message)) - response_text_as_list = json.loads(response.text) - result = [frost_sta_client.model.observation.Observation(self_link=link) for link in response_text_as_list] - return result +# 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 . + +from frost_sta_client.dao import base +from frost_sta_client.model.ext.entity_type import EntityTypes +from frost_sta_client.utils import transform_entity_to_json_dict +import frost_sta_client + +import logging +import requests +import json + + + +class ObservationDao(base.BaseDao): + CREATE_OBSERVATIONS = "CreateObservations" + + def __init__(self, service): + """ + A data access object for operations with the Observation entity + """ + base.BaseDao.__init__(self, service, EntityTypes["Observation"]) + + def create(self, entity): + if isinstance(entity, frost_sta_client.model.observation.Observation): + super().create(entity) + else: + # entity is probably a data array + url = self.service.url.copy() + url.path.add(self.CREATE_OBSERVATIONS) + logging.debug('Posting to ' + str(url.url)) + json_dict = [transform_entity_to_json_dict(dav) for dav in entity.value] + try: + response = self.service.execute('post', url, json=json_dict) + except requests.exceptions.HTTPError as e: + frost_sta_client.utils.handle_server_error(e, 'Creating Data Array') + response_text_as_list = json.loads(response.text) + result = [frost_sta_client.model.observation.Observation(self_link=link) for link in response_text_as_list] + return result diff --git a/frost_sta_client/model/actuator.py b/frost_sta_client/model/actuator.py index cbfb3e3..9f52db1 100644 --- a/frost_sta_client/model/actuator.py +++ b/frost_sta_client/model/actuator.py @@ -165,7 +165,7 @@ def __getstate__(self): if self.properties is not None and self.properties != {}: data['properties'] = self.properties if self.tasking_capabilities is not None and len(self.tasking_capabilities.entities) > 0: - data['taskingCapabilities'] = self.tasking_capabilities.__getstate__() + data['TaskingCapabilities'] = self.tasking_capabilities.__getstate__() return data def __setstate__(self, state): diff --git a/frost_sta_client/model/datastream.py b/frost_sta_client/model/datastream.py index e487c69..e16d9c6 100644 --- a/frost_sta_client/model/datastream.py +++ b/frost_sta_client/model/datastream.py @@ -1,338 +1,338 @@ -# 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 inspect -import json - -import frost_sta_client.model.ext.unitofmeasurement -from frost_sta_client.dao.datastream import DatastreamDao - -from . import thing -from . import sensor -from . import observedproperty -from . import observation -from . import entity -from .ext import unitofmeasurement -from .ext import entity_list -from .ext import entity_type - -from frost_sta_client import utils - -import geojson.geometry - - -class Datastream(entity.Entity): - - def __init__(self, - name='', - description='', - observation_type='', - unit_of_measurement=None, - observed_area=None, - properties=None, - phenomenon_time=None, - result_time=None, - thing=None, - sensor=None, - observed_property=None, - observations=None, - **kwargs): - """ - This class handles Datastreams assigned to a Thing. Before you create a Datastreams, you firstly have to - create a Thing, a Sensor and an observedProperty to which you have to refer by specifying its ids. - - Parameters - ---------- - name: str - description: str - observation_type: str - unit_of_measurement: dict - Should be a dict of keys 'name', 'symbol', 'definition' with values of str. - - """ - super().__init__(**kwargs) - if properties is None: - properties = {} - self.name = name - self.description = description - self.observation_type = observation_type - self.unit_of_measurement = unit_of_measurement - self.observed_area = observed_area - self.properties = properties - self.phenomenon_time = phenomenon_time - self.result_time = result_time - self.thing = thing - self.sensor = sensor - self.observations = observations - self.observed_property = observed_property - - - def __new__(cls, *args, **kwargs): - new_datastream = super().__new__(cls) - attributes = dict(_id=None, _name='', _description='', _properties={}, _observation_type='', - _unit_of_measurement=None, _observed_area=None, _phenomenon_time=None, _result_time=None, - _thing=None, _sensor=None, _observed_property=None, _observations=None, _self_link='', - _service=None) - for key, value in attributes.items(): - new_datastream.__dict__[key] = value - return new_datastream - - @property - def name(self): - return self._name - - @name.setter - def name(self, value): - if value is None: - self._name = None - return - if not isinstance(value, str): - raise ValueError('name should be of type str!') - self._name = value - - @property - def description(self): - return self._description - - @description.setter - def description(self, value): - if value is None: - self._description = None - return - if not isinstance(value, str): - raise ValueError('description should be of type str!') - self._description = value - - @property - def observation_type(self): - return self._observation_type - - @observation_type.setter - def observation_type(self, value): - if value is None: - self._observation_type = None - return - if not isinstance(value, str): - raise ValueError('observation_type should be of type str!') - self._observation_type = value - - @property - def unit_of_measurement(self): - return self._unit_of_measurement - - @unit_of_measurement.setter - def unit_of_measurement(self, value): - if value is None or isinstance(value, unitofmeasurement.UnitOfMeasurement): - self._unit_of_measurement = value - return - raise ValueError('unitOfMeasurement should be of type UnitOfMeasurement!') - - @property - def observed_area(self): - return self._observed_area - - @observed_area.setter - def observed_area(self, value): - if value is None: - self._location = None - return - geo_classes = [obj for _, obj in inspect.getmembers(geojson) if inspect.isclass(obj) and - obj.__module__ == 'geojson.geometry'] - if type(value) in geo_classes: - self._observed_area = value - return - else: - try: - json.dumps(value) - except TypeError: - raise ValueError('observedArea should be of json_serializable!') - self._observed_area = value - - @property - def properties(self): - return self._properties - - @properties.setter - def properties(self, value): - if value is None: - self._properties = None - return - if not isinstance(value, dict): - raise ValueError('properties should be of type dict') - self._properties = value - - @property - def phenomenon_time(self): - return self._phenomenon_time - - @phenomenon_time.setter - def phenomenon_time(self, value): - self._phenomenon_time = utils.check_datetime(value, 'phenomenon_time') - - @property - def result_time(self): - return self._result_time - - @result_time.setter - def result_time(self, value): - self._result_time = utils.check_datetime(value, 'result_time') - - @property - def thing(self): - return self._thing - - @thing.setter - def thing(self, value): - if value is None or isinstance(value, thing.Thing): - self._thing = value - return - raise ValueError('thing should be of type Thing!') - - @property - def sensor(self): - return self._sensor - - @sensor.setter - def sensor(self, value): - if value is None or isinstance(value, sensor.Sensor): - self._sensor = value - return - raise ValueError('sensor should be of type Sensor!') - - @property - def observed_property(self): - return self._observed_property - - @observed_property.setter - def observed_property(self, value): - if isinstance(value, observedproperty.ObservedProperty) or value is None: - self._observed_property = value - return - raise ValueError('observed property should by of type ObservedProperty!') - - @property - def observations(self): - return self._observations - - @observations.setter - def observations(self, values): - if values is None: - self._observations = None - return - if isinstance(values, list) and all(isinstance(ob, observation.Observation) for ob in values): - entity_class = entity_type.EntityTypes['Observation']['class'] - self._observations = entity_list.EntityList(entity_class=entity_class, entities=values) - return - if isinstance(values, entity_list.EntityList) and \ - all(isinstance(ob, observation.Observation) for ob in values.entities): - self._observations = values - return - raise ValueError('Observations should be a list of Observations') - - def get_observations(self): - result = self.service.observations() - result.parent = self - return result - - def ensure_service_on_children(self, service): - if self.thing is not None: - self.thing.set_service(service) - if self.sensor is not None: - self.sensor.set_service(service) - if self.observed_property is not None: - self.observed_property.set_service(service) - if self.observations is not None: - self.observations.set_service(service) - - def __eq__(self, other): - if not super().__eq__(other): - return False - if self.name != other.name: - return False - if self.description != other.description: - return False - if self.observation_type != other.observation_type: - return False - if self.unit_of_measurement != other.unit_of_measurement: - return False - if self.properties != other.properties: - return False - if self.result_time != other.result_time: - return False - return True - - def __ne__(self, other): - return not self == other - - def __getstate__(self): - data = super().__getstate__() - if self.name is not None and self.name != '': - data['name'] = self.name - if self.description is not None and self.description != '': - data['description'] = self.description - if self.observation_type is not None and self.observation_type != '': - data['observationType'] = self.observation_type - if self.properties is not None and self.properties != {}: - data['properties'] = self.properties - if self.unit_of_measurement is not None: - data['unitOfMeasurement'] = self.unit_of_measurement - if self.observed_area is not None: - data['observedArea'] = self.observed_area - if self.phenomenon_time is not None: - data['phenomenonTime'] = utils.parse_datetime(self.phenomenon_time) - if self.result_time is not None: - data['resultTime'] = utils.parse_datetime(self.result_time) - if self.thing is not None: - data['Thing'] = self.thing - if self.sensor is not None: - data['Sensor'] = self.sensor - if self.observed_property is not None: - data['ObservedProperty'] = self.observed_property - if self.observations is not None and len(self.observations.entities) > 0: - data['Observations'] = self.observations.__getstate__() - return data - - def __setstate__(self, state): - super().__setstate__(state) - self.name = state.get("name", None) - self.description = state.get("description", None) - self.observation_type = state.get("observationType", None) - self.properties = state.get("properties", {}) - if state.get("unitOfMeasurement", None) is not None: - self.unit_of_measurement = frost_sta_client.model.ext.unitofmeasurement.UnitOfMeasurement() - self.unit_of_measurement.__setstate__(state["unitOfMeasurement"]) - if state.get("observedArea", None) is not None: - self.observed_area = frost_sta_client.utils.process_area(state["observedArea"]) - if state.get("phenomenonTime", None) is not None: - self.phenomenon_time = state["phenomenonTime"] - if state.get("resultTime", None) is not None: - self.result_time = state["resultTime"] - if state.get("Thing", None) is not None: - self.thing = frost_sta_client.model.thing.Thing() - self.thing.__setstate__(state["Thing"]) - if state.get("ObservedProperty", None) is not None: - self.observed_property = frost_sta_client.model.observedproperty.ObservedProperty() - self.observed_property.__setstate__(state["ObservedProperty"]) - if state.get("Sensor", None) is not None: - self.sensor = frost_sta_client.model.sensor.Sensor() - self.sensor.__setstate__(state["Sensor"]) - if state.get("Observations", None) is not None and isinstance(state["Observations"], list): - entity_class = entity_type.EntityTypes['Observation']['class'] - self.observations = utils.transform_json_to_entity_list(state['Observations'], entity_class) - self.observations.next_link = state.get("Observations@iot.nextLink", None) - self.observations.count = state.get("Observations@iot.count", None) - - def get_dao(self, service): - return DatastreamDao(service) +# 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 inspect +import json + +import frost_sta_client.model.ext.unitofmeasurement +from frost_sta_client.dao.datastream import DatastreamDao + +from . import thing +from . import sensor +from . import observedproperty +from . import observation +from . import entity +from .ext import unitofmeasurement +from .ext import entity_list +from .ext import entity_type + +from frost_sta_client import utils + +import geojson.geometry + + +class Datastream(entity.Entity): + + def __init__(self, + name='', + description='', + observation_type='', + unit_of_measurement=None, + observed_area=None, + properties=None, + phenomenon_time=None, + result_time=None, + thing=None, + sensor=None, + observed_property=None, + observations=None, + **kwargs): + """ + This class handles Datastreams assigned to a Thing. Before you create a Datastreams, you firstly have to + create a Thing, a Sensor and an observedProperty to which you have to refer by specifying its ids. + + Parameters + ---------- + name: str + description: str + observation_type: str + unit_of_measurement: dict + Should be a dict of keys 'name', 'symbol', 'definition' with values of str. + + """ + super().__init__(**kwargs) + if properties is None: + properties = {} + self.name = name + self.description = description + self.observation_type = observation_type + self.unit_of_measurement = unit_of_measurement + self.observed_area = observed_area + self.properties = properties + self.phenomenon_time = phenomenon_time + self.result_time = result_time + self.thing = thing + self.sensor = sensor + self.observations = observations + self.observed_property = observed_property + + + def __new__(cls, *args, **kwargs): + new_datastream = super().__new__(cls) + attributes = dict(_id=None, _name='', _description='', _properties={}, _observation_type='', + _unit_of_measurement=None, _observed_area=None, _phenomenon_time=None, _result_time=None, + _thing=None, _sensor=None, _observed_property=None, _observations=None, _self_link='', + _service=None) + for key, value in attributes.items(): + new_datastream.__dict__[key] = value + return new_datastream + + @property + def name(self): + return self._name + + @name.setter + def name(self, value): + if value is None: + self._name = None + return + if not isinstance(value, str): + raise ValueError('name should be of type str!') + self._name = value + + @property + def description(self): + return self._description + + @description.setter + def description(self, value): + if value is None: + self._description = None + return + if not isinstance(value, str): + raise ValueError('description should be of type str!') + self._description = value + + @property + def observation_type(self): + return self._observation_type + + @observation_type.setter + def observation_type(self, value): + if value is None: + self._observation_type = None + return + if not isinstance(value, str): + raise ValueError('observation_type should be of type str!') + self._observation_type = value + + @property + def unit_of_measurement(self): + return self._unit_of_measurement + + @unit_of_measurement.setter + def unit_of_measurement(self, value): + if value is None or isinstance(value, unitofmeasurement.UnitOfMeasurement): + self._unit_of_measurement = value + return + raise ValueError('unitOfMeasurement should be of type UnitOfMeasurement!') + + @property + def observed_area(self): + return self._observed_area + + @observed_area.setter + def observed_area(self, value): + if value is None: + self._observed_area = None + return + geo_classes = [obj for _, obj in inspect.getmembers(geojson) if inspect.isclass(obj) and + obj.__module__ == 'geojson.geometry'] + if type(value) in geo_classes: + self._observed_area = value + return + else: + try: + json.dumps(value) + except TypeError: + raise ValueError('observedArea should be of json_serializable!') + self._observed_area = value + + @property + def properties(self): + return self._properties + + @properties.setter + def properties(self, value): + if value is None: + self._properties = None + return + if not isinstance(value, dict): + raise ValueError('properties should be of type dict') + self._properties = value + + @property + def phenomenon_time(self): + return self._phenomenon_time + + @phenomenon_time.setter + def phenomenon_time(self, value): + self._phenomenon_time = utils.check_datetime(value, 'phenomenon_time') + + @property + def result_time(self): + return self._result_time + + @result_time.setter + def result_time(self, value): + self._result_time = utils.check_datetime(value, 'result_time') + + @property + def thing(self): + return self._thing + + @thing.setter + def thing(self, value): + if value is None or isinstance(value, thing.Thing): + self._thing = value + return + raise ValueError('thing should be of type Thing!') + + @property + def sensor(self): + return self._sensor + + @sensor.setter + def sensor(self, value): + if value is None or isinstance(value, sensor.Sensor): + self._sensor = value + return + raise ValueError('sensor should be of type Sensor!') + + @property + def observed_property(self): + return self._observed_property + + @observed_property.setter + def observed_property(self, value): + if isinstance(value, observedproperty.ObservedProperty) or value is None: + self._observed_property = value + return + raise ValueError('observed property should by of type ObservedProperty!') + + @property + def observations(self): + return self._observations + + @observations.setter + def observations(self, values): + if values is None: + self._observations = None + return + if isinstance(values, list) and all(isinstance(ob, observation.Observation) for ob in values): + entity_class = entity_type.EntityTypes['Observation']['class'] + self._observations = entity_list.EntityList(entity_class=entity_class, entities=values) + return + if isinstance(values, entity_list.EntityList) and \ + all(isinstance(ob, observation.Observation) for ob in values.entities): + self._observations = values + return + raise ValueError('Observations should be a list of Observations') + + def get_observations(self): + result = self.service.observations() + result.parent = self + return result + + def ensure_service_on_children(self, service): + if self.thing is not None: + self.thing.set_service(service) + if self.sensor is not None: + self.sensor.set_service(service) + if self.observed_property is not None: + self.observed_property.set_service(service) + if self.observations is not None: + self.observations.set_service(service) + + def __eq__(self, other): + if not super().__eq__(other): + return False + if self.name != other.name: + return False + if self.description != other.description: + return False + if self.observation_type != other.observation_type: + return False + if self.unit_of_measurement != other.unit_of_measurement: + return False + if self.properties != other.properties: + return False + if self.result_time != other.result_time: + return False + return True + + def __ne__(self, other): + return not self == other + + def __getstate__(self): + data = super().__getstate__() + if self.name is not None and self.name != '': + data['name'] = self.name + if self.description is not None and self.description != '': + data['description'] = self.description + if self.observation_type is not None and self.observation_type != '': + data['observationType'] = self.observation_type + if self.properties is not None and self.properties != {}: + data['properties'] = self.properties + if self.unit_of_measurement is not None: + data['unitOfMeasurement'] = self.unit_of_measurement.__getstate__() + if self.observed_area is not None: + data['observedArea'] = self.observed_area + if self.phenomenon_time is not None: + data['phenomenonTime'] = utils.parse_datetime(self.phenomenon_time) + if self.result_time is not None: + data['resultTime'] = utils.parse_datetime(self.result_time) + if self.thing is not None: + data['Thing'] = self.thing.__getstate__() + if self.sensor is not None: + data['Sensor'] = self.sensor.__getstate__() + if self.observed_property is not None: + data['ObservedProperty'] = self.observed_property.__getstate__() + if self.observations is not None and len(self.observations.entities) > 0: + data['Observations'] = self.observations.__getstate__() + return data + + def __setstate__(self, state): + super().__setstate__(state) + self.name = state.get("name", None) + self.description = state.get("description", None) + self.observation_type = state.get("observationType", None) + self.properties = state.get("properties", {}) + if state.get("unitOfMeasurement", None) is not None: + self.unit_of_measurement = frost_sta_client.model.ext.unitofmeasurement.UnitOfMeasurement() + self.unit_of_measurement.__setstate__(state["unitOfMeasurement"]) + if state.get("observedArea", None) is not None: + self.observed_area = frost_sta_client.utils.process_area(state["observedArea"]) + if state.get("phenomenonTime", None) is not None: + self.phenomenon_time = state["phenomenonTime"] + if state.get("resultTime", None) is not None: + self.result_time = state["resultTime"] + if state.get("Thing", None) is not None: + self.thing = frost_sta_client.model.thing.Thing() + self.thing.__setstate__(state["Thing"]) + if state.get("ObservedProperty", None) is not None: + self.observed_property = frost_sta_client.model.observedproperty.ObservedProperty() + self.observed_property.__setstate__(state["ObservedProperty"]) + if state.get("Sensor", None) is not None: + self.sensor = frost_sta_client.model.sensor.Sensor() + self.sensor.__setstate__(state["Sensor"]) + if state.get("Observations", None) is not None and isinstance(state["Observations"], list): + entity_class = entity_type.EntityTypes['Observation']['class'] + self.observations = utils.transform_json_to_entity_list(state['Observations'], entity_class) + self.observations.next_link = state.get("Observations@iot.nextLink", None) + self.observations.count = state.get("Observations@iot.count", None) + + def get_dao(self, service): + return DatastreamDao(service) diff --git a/frost_sta_client/model/ext/data_array_document.py b/frost_sta_client/model/ext/data_array_document.py index c553023..10ce4fc 100644 --- a/frost_sta_client/model/ext/data_array_document.py +++ b/frost_sta_client/model/ext/data_array_document.py @@ -1,68 +1,67 @@ -# 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 . -from .data_array_value import DataArrayValue - - -class DataArrayDocument: - def __init__(self, count=-1, next_link = None, value=None): - if value is None: - value = [] - self._count = count - self._next_link = next_link - self._value = value - - @property - def count(self): - return self._count - - @count.setter - def count(self, value): - if type(value) == int or value is None: - self._count = value - else: - raise TypeError('count should be of type int') - - @property - def next_link(self): - return self._next_link - - @next_link.setter - def next_link(self, value): - if type(value) == str or value is None: - self._next_link = value - else: - raise TypeError('nextLink should be of type str') - - @property - def value(self): - return self._value - - @value.setter - def value(self, value): - if type(value) == list and all(isinstance(x, DataArrayValue) for x in value): - self._value = value - else: - raise TypeError('value should be a list of type DataArrayValue') - - def get_observations(self): - obs_list = [] - for dav in self.value: - obs_list.concat(dav.get_observations()) - return obs_list - - def add_data_array_value(self, dav): - self.value.append(dav) - +# 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 . +from .data_array_value import DataArrayValue + + +class DataArrayDocument: + def __init__(self, count=-1, next_link = None, value=None): + if value is None: + value = [] + self._count = count + self._next_link = next_link + self._value = value + + @property + def count(self): + return self._count + + @count.setter + def count(self, value): + if type(value) == int or value is None: + self._count = value + else: + raise TypeError('count should be of type int') + + @property + def next_link(self): + return self._next_link + + @next_link.setter + def next_link(self, value): + if type(value) == str or value is None: + self._next_link = value + else: + raise TypeError('nextLink should be of type str') + + @property + def value(self): + return self._value + + @value.setter + def value(self, value): + if type(value) == list and all(isinstance(x, DataArrayValue) for x in value): + self._value = value + else: + raise TypeError('value should be a list of type DataArrayValue') + + def get_observations(self): + obs_list = [] + for dav in self.value: + obs_list.extend(dav.observations) + return obs_list + + def add_data_array_value(self, dav): + self.value.append(dav) diff --git a/frost_sta_client/model/ext/entity_list.py b/frost_sta_client/model/ext/entity_list.py index af3797e..568cf0e 100644 --- a/frost_sta_client/model/ext/entity_list.py +++ b/frost_sta_client/model/ext/entity_list.py @@ -1,162 +1,166 @@ -# 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 logging -import requests -import frost_sta_client - - -class EntityList: - def __init__(self, entity_class, entities=None): - if entities is None: - entities = [] - self.entities = entities - self.entity_class = entity_class - self.next_link = None - self.service = None - self.iterable_entities = None - self.count = None - self.callback = None - self.step_size = None - - def __new__(cls, *args, **kwargs): - new_entity_list = super().__new__(cls) - attributes = {'_entities': None, '_entity_class': '', '_next_link': '', '_service': {}, '_count': '', - '_iterable_entities': None, '_callback': None, - '_step_size': None} - for key, value in attributes.items(): - new_entity_list.__dict__[key] = value - return new_entity_list - - def __iter__(self): - self.iterable_entities = iter(enumerate(self.entities)) - return self - - def __next__(self): - idx, next_entity = next(self.iterable_entities, (len(self.entities), None)) - if self.step_size is not None and idx is not None and idx % self.step_size == 0: - self.callback(idx) - if next_entity is not None: - return next_entity - if self.next_link is not None: - try: - response = self.service.execute('get', self.next_link) - except requests.exceptions.HTTPError as e: - error_json = e.response.json() - error_message = error_json['message'] - logging.error("Query failed with status-code {}, {}".format(e.response.status_code, error_message)) - raise e - logging.debug('Received response: {} from {}'.format(response.status_code, self.next_link)) - try: - json_response = response.json() - except ValueError: - raise ValueError('Cannot find json in http response') - - result_list = frost_sta_client.utils.transform_json_to_entity_list(json_response, self.entity_class) - self.entities += result_list.entities - self.set_service(self.service) - self.next_link = json_response.get("@iot.nextLink", None) - self.iterable_entities = iter(enumerate(self.entities[-len(result_list.entities):], start=idx)) - return next(self.iterable_entities)[1] - raise StopIteration - - def get(self, index): - if not isinstance(index, int): - raise IndexError('index must be an integer') - if index >= len(self.entities): - raise IndexError('index exceeds total number of entities') - if index < 0: - raise IndexError('negative indices cannot be accessed') - return self.entities[index] - - @property - def entity_class(self): - return self._entity_class - - @entity_class.setter - def entity_class(self, value): - if isinstance(value, str): - self._entity_class = value - return - raise ValueError('entity_class should be of type str') - - @property - def entities(self): - return self._entities - - @entities.setter - def entities(self, values): - if isinstance(values, list) and all(isinstance(v, frost_sta_client.model.entity.Entity) for v in values): - self._entities = values - return - raise ValueError('entities should be a list of entities') - - @property - def callback(self): - return self._callback - - @callback.setter - def callback(self, callback): - if callable(callback) or callback is None: - self._callback = callback - - @property - def step_size(self): - return self._step_size - - @step_size.setter - def step_size(self, value): - if isinstance(value, int) or value is None: - self._step_size = value - return - raise ValueError('step_size should be of type int') - - @property - def next_link(self): - return self._next_link - - @next_link.setter - def next_link(self, value): - if value is None or isinstance(value, str): - self._next_link = value - return - raise ValueError('next_link should be of type string') - - @property - def service(self): - return self._service - - @service.setter - def service(self, value): - if value is None or isinstance(value, frost_sta_client.service.sensorthingsservice.SensorThingsService): - self._service = value - return - raise ValueError('service should be of type SensorThingsService') - - def set_service(self, service): - self.service = service - for entity in self.entities: - entity.set_service(service) - - def __getstate__(self): - data = [] - for entity in self.entities: - data.append(entity.__getstate__()) - return data - - def __setstate__(self, state): - self._next_link = state.get(self.entities + '@nextLink') - pass +# 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 logging +import requests +import frost_sta_client + + +class EntityList: + def __init__(self, entity_class, entities=None): + if entities is None: + entities = [] + self.entities = entities + self.entity_class = entity_class + self.next_link = None + self.service = None + self.iterable_entities = None + self.count = None + self.callback = None + self.step_size = None + + def __new__(cls, *args, **kwargs): + new_entity_list = super().__new__(cls) + attributes = {'_entities': None, '_entity_class': '', '_next_link': '', '_service': {}, '_count': '', + '_iterable_entities': None, '_callback': None, + '_step_size': None} + for key, value in attributes.items(): + new_entity_list.__dict__[key] = value + return new_entity_list + + def __iter__(self): + self.iterable_entities = iter(enumerate(self.entities)) + return self + + def __next__(self): + idx, next_entity = next(self.iterable_entities, (None, None)) + # Only trigger callback when returning a real entity, not on sentinel indices + if next_entity is None: + # If current page is exhausted, try to load the next page + if self.next_link is None: + raise StopIteration + try: + response = self.service.execute('get', self.next_link) + except requests.exceptions.HTTPError as e: + frost_sta_client.utils.handle_server_error(e, 'Query') + logging.debug('Received response: {} from {}'.format(response.status_code, self.next_link)) + try: + json_response = response.json() + except ValueError: + raise ValueError('Cannot find json in http response') + + result_list = frost_sta_client.utils.transform_json_to_entity_list(json_response, self.entity_class) + # Append new entities and reset iterator to iterate over the newly fetched page + start_index = len(self.entities) + self.entities += result_list.entities + self.set_service(self.service) + self.next_link = json_response.get("@iot.nextLink", None) + self.iterable_entities = iter(enumerate(self.entities[start_index:], start=start_index)) + idx, next_entity = next(self.iterable_entities, (None, None)) + if next_entity is None: + raise StopIteration + if self.step_size is not None and self.callback is not None and idx % self.step_size == 0: + self.callback(idx) + return next_entity + raise StopIteration + + def get(self, index): + if not isinstance(index, int): + raise IndexError('index must be an integer') + if index >= len(self.entities): + raise IndexError('index exceeds total number of entities') + if index < 0: + raise IndexError('negative indices cannot be accessed') + return self.entities[index] + + @property + def entity_class(self): + return self._entity_class + + @entity_class.setter + def entity_class(self, value): + if isinstance(value, str): + self._entity_class = value + return + raise ValueError('entity_class should be of type str') + + @property + def entities(self): + return self._entities + + @entities.setter + def entities(self, values): + if isinstance(values, list) and all(isinstance(v, frost_sta_client.model.entity.Entity) for v in values): + self._entities = values + return + raise ValueError('entities should be a list of entities') + + @property + def callback(self): + return self._callback + + @callback.setter + def callback(self, callback): + if callable(callback) or callback is None: + self._callback = callback + + @property + def step_size(self): + return self._step_size + + @step_size.setter + def step_size(self, value): + if isinstance(value, int) or value is None: + self._step_size = value + return + raise ValueError('step_size should be of type int') + + @property + def next_link(self): + return self._next_link + + @next_link.setter + def next_link(self, value): + if value is None or isinstance(value, str): + self._next_link = value + return + raise ValueError('next_link should be of type string') + + @property + def service(self): + return self._service + + @service.setter + def service(self, value): + if value is None or isinstance(value, frost_sta_client.service.sensorthingsservice.SensorThingsService): + self._service = value + return + raise ValueError('service should be of type SensorThingsService') + + def set_service(self, service): + self.service = service + for entity in self.entities: + entity.set_service(service) + + def __getstate__(self): + data = [] + for entity in self.entities: + data.append(entity.__getstate__()) + return data + + def __setstate__(self, state): + self._next_link = state.get(self.entities + '@nextLink') + pass diff --git a/frost_sta_client/model/feature_of_interest.py b/frost_sta_client/model/feature_of_interest.py index 5b87993..d2fbe8f 100644 --- a/frost_sta_client/model/feature_of_interest.py +++ b/frost_sta_client/model/feature_of_interest.py @@ -179,7 +179,7 @@ def __getstate__(self): if self.feature is not None: data['feature'] = self.feature if self.observations is not None and len(self.observations.entities) > 0: - data['Observations'] = self.observations.__gestate__() + data['Observations'] = self.observations.__getstate__() return data def __setstate__(self, state): diff --git a/frost_sta_client/model/historical_location.py b/frost_sta_client/model/historical_location.py index 198f433..9d2072c 100644 --- a/frost_sta_client/model/historical_location.py +++ b/frost_sta_client/model/historical_location.py @@ -105,7 +105,7 @@ def __getstate__(self): if self.time is not None: data['time'] = utils.parse_datetime(self.time) if self.thing is not None: - data['Thing'] = self.thing + data['Thing'] = self.thing.__getstate__() if self.locations is not None and len(self.locations.entities) > 0: data['Locations'] = self.locations.__getstate__() return data diff --git a/frost_sta_client/model/location.py b/frost_sta_client/model/location.py index 3b04eff..4d34463 100644 --- a/frost_sta_client/model/location.py +++ b/frost_sta_client/model/location.py @@ -168,6 +168,7 @@ def historical_locations(self, values): if isinstance(values, entity_list.EntityList) and \ all(isinstance(hl, historical_location.HistoricalLocation) for hl in values.entities): self._historical_locations = values + return raise ValueError('historical_location should be of type HistoricalLocation!') def get_things(self): @@ -217,9 +218,9 @@ def __getstate__(self): if self.location is not None: data['location'] = self.location if self.things is not None: - data['Things'] = self.things + data['Things'] = self.things.__getstate__() if self.historical_locations is not None and len(self.historical_locations.entities) > 0: - data['HistoricalLocation'] = self.historical_locations.__getstate__() + data['HistoricalLocations'] = self.historical_locations.__getstate__() return data def __setstate__(self, state): diff --git a/frost_sta_client/model/multi_datastream.py b/frost_sta_client/model/multi_datastream.py index 32a8b63..5772744 100644 --- a/frost_sta_client/model/multi_datastream.py +++ b/frost_sta_client/model/multi_datastream.py @@ -1,345 +1,346 @@ -# 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 frost_sta_client.model -from frost_sta_client.dao.multi_datastream import MultiDatastreamDao - -from . import entity -from . import thing -from . import sensor -from . import observation -from . import observedproperty -from .ext import unitofmeasurement - -from frost_sta_client import utils -from .ext import entity_list -from .ext import entity_type - -import geojson.geometry - - -class MultiDatastream(entity.Entity): - def __init__(self, - name='', - description='', - properties=None, - unit_of_measurements=None, - observation_type='', - multi_observation_data_types=None, - observed_area=None, - phenomenon_time=None, - result_time=None, - thing=None, - sensor=None, - observed_properties=None, - observations=None, - **kwargs): - super().__init__(**kwargs) - if properties is None: - properties = {} - if multi_observation_data_types is None: - multi_observation_data_types = [] - self.name = name - self.description = description - self.properties = properties - self.unit_of_measurements = unit_of_measurements - self.observation_type = observation_type - self.multi_observation_data_types = multi_observation_data_types - self.observed_area = observed_area - self.phenomenon_time = phenomenon_time - self.result_time = result_time - self.thing = thing - self.sensor = sensor - self.observed_properties = observed_properties - self.observations = observations - - def __new__(cls, *args, **kwargs): - new_mds = super().__new__(cls) - attributes = dict(_id=None, _name='', _description='', _properties={}, _observation_type='', _multi_observation_data_types=[], - _unit_of_measurements=[], _observed_area=None, _phenomenon_time=None, _result_time=None, - _thing=None, _sensor=None, _observed_properties=None, _observations=None, _self_link='', - _service=None) - for key, value in attributes.items(): - new_mds.__dict__[key] = value - return new_mds - - @property - def name(self): - return self._name - - @name.setter - def name(self, value): - if value is None: - self._name = None - return - if not isinstance(value, str): - raise ValueError('name should be of type str!') - self._name = value - - @property - def description(self): - return self._description - - @description.setter - def description(self, value): - if value is None: - self._description = None - return - if not isinstance(value, str): - raise ValueError('description should be of type str!') - self._description = value - - @property - def properties(self): - return self._properties - - @properties.setter - def properties(self, values): - if values is None: - self._properties = {} - return - if not isinstance(values, dict): - raise ValueError('properties should be of type dict!') - self._properties = values - - @property - def unit_of_measurements(self): - return self._unit_of_measurements - - @unit_of_measurements.setter - def unit_of_measurements(self, values): - if values is not None and (not isinstance(values, list) or \ - any((not isinstance(uom, unitofmeasurement.UnitOfMeasurement)) for uom in values)): - raise ValueError('unit_of_measurements should be a list of type UnitOfMeasurement') - self._unit_of_measurements = values - - @property - def observation_type(self): - return self._observation_type - - @observation_type.setter - def observation_type(self, value): - if not isinstance(value, str): - raise ValueError('observation_type should be of type str!') - self._observation_type = value - - @property - def multi_observation_data_types(self): - return self._multi_observation_data_types - - @multi_observation_data_types.setter - def multi_observation_data_types(self, values): - if values is not None and (not isinstance(values, list) or any((not isinstance(dtype, str)) for dtype in values)): - raise ValueError('multi_observations_data_types should be list of type str!') - self._multi_observation_data_types = values - - @property - def observed_area(self): - return self._observed_area - - @observed_area.setter - def observed_area(self, value): - if value is None: - self._observed_area = None - return - if not isinstance(value, geojson.geometry.Polygon): - raise ValueError('observedArea should be geojson object') - self._observed_area = value - - @property - def phenomenon_time(self): - return self._phenomenon_time - - @phenomenon_time.setter - def phenomenon_time(self, value): - self._phenomenon_time = utils.check_datetime(value, 'phenomenon_time') - - @property - def result_time(self): - return self._result_time - - @result_time.setter - def result_time(self, value): - self._result_time = utils.check_datetime(value, 'result_time') - - @property - def thing(self): - return self._thing - - @thing.setter - def thing(self, value): - if value is not None and not isinstance(value, thing.Thing): - raise ValueError('thing should be of type Thing!') - self._thing = value - - @property - def sensor(self): - return self._sensor - - @sensor.setter - def sensor(self, value): - if value is not None and not isinstance(value, sensor.Sensor): - raise ValueError('sensor should be of type Sensor!') - self._sensor = value - - @property - def observed_properties(self): - return self._observed_properties - - @observed_properties.setter - def observed_properties(self, values): - if values is None: - self._observed_properties = None - return - if isinstance(values, list) and all(isinstance(op, observedproperty.ObservedProperty) for op in values): - entity_class = entity_type.EntityTypes['ObservedProperty']['class'] - self._observed_properties = entity_list.EntityList(entity_class=entity_class, entities=values) - return - if not isinstance(values, entity_list.EntityList) or \ - any(not isinstance(op, observedproperty.ObservedProperty) for op in values.entities): - raise ValueError('observed_properties should be an entity list of ObservedProperty!') - self._observed_properties = values - - @property - def observations(self): - return self._observations - - @observations.setter - def observations(self, values): - if values is None: - self._observations = None - return - if isinstance(values, list) and all(isinstance(ob, observation.Observation) for ob in values): - entity_class = entity_type.EntityTypes['Observation']['class'] - self._observations = entity_list.EntityList(entity_class=entity_class, entities=values) - return - if not isinstance(values, entity_list.EntityList) or \ - any(not isinstance(ob, observation.Observation) for ob in values.entities): - raise ValueError('Observations should be an entity list of Observations') - self._observations = values - - def get_observations(self): - result = self.service.observations() - result.parent = self - return result - - def get_observed_properties(self): - result = self.service.observed_properties() - result.parent = self - return result - - def ensure_service_on_children(self, service): - if self.thing is not None: - self.thing.set_service(service) - if self.sensor is not None: - self.sensor.set_service(service) - if self.observations is not None: - self.observations.set_service(service) - if self.observed_properties is not None: - self.observed_properties.set_service(service) - - def __eq__(self, other): - if not super().__eq__(other): - return False - if self.name != other.name: - return False - if self.description != other.description: - return False - if self.observation_type != other.observation_type: - return False - if self.observed_area != other.observation_area: - return False - if self.properties != other.properties: - return False - if self.result_time != other.result_time: - return False - if self.unit_of_measurements != other.unit_of_measurements: - return False - if self.multi_observation_data_types != other.multi_observation_data_types: - return False - return True - - def __ne__(self, other): - return not self == other - - def __getstate__(self): - data = super().__getstate__() - if self.name is not None and self.name != '': - data['name'] = self.name - if self.description is not None and self.description != '': - data['description'] = self.description - if self.observation_type is not None and self.observation_type != '': - data['observationType'] = self.observation_type - if self.observed_area is not None: - data['observedArea'] = self.observed_area - if self.phenomenon_time is not None: - data['phenomenonTime'] = utils.parse_datetime(self.phenomenon_time) - if self.result_time is not None: - data['resultTime'] = utils.parse_datetime(self.result_time) - if self.thing is not None: - data['Thing'] = self.thing - if self.sensor is not None: - data['Sensor'] = self.sensor - if self.properties is not None and self.properties != {}: - data['properties'] = self.properties - if self.unit_of_measurements is not None and len(self.unit_of_measurements) > 0: - data['unitOfMeasurements'] = self.unit_of_measurements - if self.multi_observation_data_types is not None and len(self.multi_observation_data_types) > 0: - data['multiObservationDataTypes'] = self.multi_observation_data_types - if self.observed_properties is not None and len(self.observed_properties.entities) > 0: - data['ObservedProperties'] = self.observed_properties.__getstate__() - if self.observations is not None and len(self.observations.entities) > 0: - data['Observations'] = self.observations.__getstate__() - return data - - def __setstate__(self, state): - super().__setstate__(state) - self.name = state.get('name', None) - self.description = state.get('description', None) - self.observation_type = state.get('observationType', None) - self.observation_area = state.get('observedArea', None) - self.phenomenon_time = state.get('phenomenonTime', None) - self.result_time = state.get('resultTime', None) - self.properties = state.get('properties', None) - if state.get('Thing', None) is not None: - self.thing = frost_sta_client.model.thing.Thing() - self.thing.__setstate__(state['Thing']) - if state.get('Sensor', None) is not None: - self.sensor = frost_sta_client.model.sensor.Sensor() - self.sensor.__setstate__(state['Sensor']) - if state.get('unitOfMeasurements', None) is not None \ - and isinstance(state['unitOfMeasurements'], list): - self.unit_of_measurements = [] - for value in state['unitOfMeasurements']: - self.unit_of_measurements.append(value) - if state.get('multiObservationDataTypes', None) is not None \ - and isinstance(state['multiObservationDataTypes'], list): - self.multi_observation_data_types = [] - for value in state['multiObservationDataTypes']: - self.multi_observation_data_types.append(value) - if state.get('ObservedProperties', None) is not None and isinstance(state['ObservedProperties'], list): - entity_class = entity_type.EntityTypes['ObservedProperty']['class'] - self.observed_properties = utils.transform_json_to_entity_list(state['ObservedProperties'], entity_class) - self.observed_properties.next_link = state.get('ObservedProperties@iot.nextLink') - self.observed_properties.count = state.get('ObservedProperties@iot.count') - if state.get('Observations', None) is not None and isinstance(state['Observations'], list): - entity_class = entity_type.EntityTypes['Observation']['class'] - self.observations = utils.transform_json_to_entity_list(state['Observations'], entity_class) - self.observed_properties.next_link = state.get('Observations@iot.nextLink') - self.observed_properties.count = state.get('Observations@iot.count') - - def get_dao(self, service): - return MultiDatastreamDao(service) +# 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 frost_sta_client.model +from frost_sta_client.dao.multi_datastream import MultiDatastreamDao + +from . import entity +from . import thing +from . import sensor +from . import observation +from . import observedproperty +from .ext import unitofmeasurement + +from frost_sta_client import utils +from .ext import entity_list +from .ext import entity_type + +import geojson.geometry + + +class MultiDatastream(entity.Entity): + def __init__(self, + name='', + description='', + properties=None, + unit_of_measurements=None, + observation_type='', + multi_observation_data_types=None, + observed_area=None, + phenomenon_time=None, + result_time=None, + thing=None, + sensor=None, + observed_properties=None, + observations=None, + **kwargs): + super().__init__(**kwargs) + if properties is None: + properties = {} + if multi_observation_data_types is None: + multi_observation_data_types = [] + self.name = name + self.description = description + self.properties = properties + self.unit_of_measurements = unit_of_measurements + self.observation_type = observation_type + self.multi_observation_data_types = multi_observation_data_types + self.observed_area = observed_area + self.phenomenon_time = phenomenon_time + self.result_time = result_time + self.thing = thing + self.sensor = sensor + self.observed_properties = observed_properties + self.observations = observations + + def __new__(cls, *args, **kwargs): + new_mds = super().__new__(cls) + attributes = dict(_id=None, _name='', _description='', _properties={}, _observation_type='', _multi_observation_data_types=[], + _unit_of_measurements=[], _observed_area=None, _phenomenon_time=None, _result_time=None, + _thing=None, _sensor=None, _observed_properties=None, _observations=None, _self_link='', + _service=None) + for key, value in attributes.items(): + new_mds.__dict__[key] = value + return new_mds + + @property + def name(self): + return self._name + + @name.setter + def name(self, value): + if value is None: + self._name = None + return + if not isinstance(value, str): + raise ValueError('name should be of type str!') + self._name = value + + @property + def description(self): + return self._description + + @description.setter + def description(self, value): + if value is None: + self._description = None + return + if not isinstance(value, str): + raise ValueError('description should be of type str!') + self._description = value + + @property + def properties(self): + return self._properties + + @properties.setter + def properties(self, values): + if values is None: + self._properties = {} + return + if not isinstance(values, dict): + raise ValueError('properties should be of type dict!') + self._properties = values + + @property + def unit_of_measurements(self): + return self._unit_of_measurements + + @unit_of_measurements.setter + def unit_of_measurements(self, values): + if values is not None and (not isinstance(values, list) or \ + any((not isinstance(uom, unitofmeasurement.UnitOfMeasurement)) for uom in values)): + raise ValueError('unit_of_measurements should be a list of type UnitOfMeasurement') + self._unit_of_measurements = values + + @property + def observation_type(self): + return self._observation_type + + @observation_type.setter + def observation_type(self, value): + if not isinstance(value, str): + raise ValueError('observation_type should be of type str!') + self._observation_type = value + + @property + def multi_observation_data_types(self): + return self._multi_observation_data_types + + @multi_observation_data_types.setter + def multi_observation_data_types(self, values): + if values is not None and (not isinstance(values, list) or any((not isinstance(dtype, str)) for dtype in values)): + raise ValueError('multi_observations_data_types should be list of type str!') + self._multi_observation_data_types = values + + @property + def observed_area(self): + return self._observed_area + + @observed_area.setter + def observed_area(self, value): + if value is None: + self._observed_area = None + return + if not isinstance(value, geojson.geometry.Polygon): + raise ValueError('observedArea should be geojson object') + self._observed_area = value + + @property + def phenomenon_time(self): + return self._phenomenon_time + + @phenomenon_time.setter + def phenomenon_time(self, value): + self._phenomenon_time = utils.check_datetime(value, 'phenomenon_time') + + @property + def result_time(self): + return self._result_time + + @result_time.setter + def result_time(self, value): + self._result_time = utils.check_datetime(value, 'result_time') + + @property + def thing(self): + return self._thing + + @thing.setter + def thing(self, value): + if value is not None and not isinstance(value, thing.Thing): + raise ValueError('thing should be of type Thing!') + self._thing = value + + @property + def sensor(self): + return self._sensor + + @sensor.setter + def sensor(self, value): + if value is not None and not isinstance(value, sensor.Sensor): + raise ValueError('sensor should be of type Sensor!') + self._sensor = value + + @property + def observed_properties(self): + return self._observed_properties + + @observed_properties.setter + def observed_properties(self, values): + if values is None: + self._observed_properties = None + return + if isinstance(values, list) and all(isinstance(op, observedproperty.ObservedProperty) for op in values): + entity_class = entity_type.EntityTypes['ObservedProperty']['class'] + self._observed_properties = entity_list.EntityList(entity_class=entity_class, entities=values) + return + if not isinstance(values, entity_list.EntityList) or \ + any(not isinstance(op, observedproperty.ObservedProperty) for op in values.entities): + raise ValueError('observed_properties should be an entity list of ObservedProperty!') + self._observed_properties = values + + @property + def observations(self): + return self._observations + + @observations.setter + def observations(self, values): + if values is None: + self._observations = None + return + if isinstance(values, list) and all(isinstance(ob, observation.Observation) for ob in values): + entity_class = entity_type.EntityTypes['Observation']['class'] + self._observations = entity_list.EntityList(entity_class=entity_class, entities=values) + return + if not isinstance(values, entity_list.EntityList) or \ + any(not isinstance(ob, observation.Observation) for ob in values.entities): + raise ValueError('Observations should be an entity list of Observations') + self._observations = values + + def get_observations(self): + result = self.service.observations() + result.parent = self + return result + + def get_observed_properties(self): + result = self.service.observed_properties() + result.parent = self + return result + + def ensure_service_on_children(self, service): + if self.thing is not None: + self.thing.set_service(service) + if self.sensor is not None: + self.sensor.set_service(service) + if self.observations is not None: + self.observations.set_service(service) + if self.observed_properties is not None: + self.observed_properties.set_service(service) + + def __eq__(self, other): + if not super().__eq__(other): + return False + if self.name != other.name: + return False + if self.description != other.description: + return False + if self.observation_type != other.observation_type: + return False + if self.observed_area != other.observed_area: + return False + if self.properties != other.properties: + return False + if self.result_time != other.result_time: + return False + if self.unit_of_measurements != other.unit_of_measurements: + return False + if self.multi_observation_data_types != other.multi_observation_data_types: + return False + return True + + def __ne__(self, other): + return not self == other + + def __getstate__(self): + data = super().__getstate__() + if self.name is not None and self.name != '': + data['name'] = self.name + if self.description is not None and self.description != '': + data['description'] = self.description + if self.observation_type is not None and self.observation_type != '': + data['observationType'] = self.observation_type + if self.observed_area is not None: + data['observedArea'] = self.observed_area + if self.phenomenon_time is not None: + data['phenomenonTime'] = utils.parse_datetime(self.phenomenon_time) + if self.result_time is not None: + data['resultTime'] = utils.parse_datetime(self.result_time) + if self.thing is not None: + data['Thing'] = self.thing.__getstate__() + if self.sensor is not None: + data['Sensor'] = self.sensor.__getstate__() + if self.properties is not None and self.properties != {}: + data['properties'] = self.properties + if self.unit_of_measurements is not None and len(self.unit_of_measurements) > 0: + data['unitOfMeasurements'] = self.unit_of_measurements.__getstate__() + if self.multi_observation_data_types is not None and len(self.multi_observation_data_types) > 0: + data['multiObservationDataTypes'] = self.multi_observation_data_types + if self.observed_properties is not None and len(self.observed_properties.entities) > 0: + data['ObservedProperties'] = self.observed_properties.__getstate__() + if self.observations is not None and len(self.observations.entities) > 0: + data['Observations'] = self.observations.__getstate__() + return data + + def __setstate__(self, state): + super().__setstate__(state) + self.name = state.get('name', None) + self.description = state.get('description', None) + self.observation_type = state.get('observationType', None) + if state.get('observedArea', None) is not None: + self.observed_area = frost_sta_client.utils.process_area(state['observedArea']) + self.phenomenon_time = state.get('phenomenonTime', None) + self.result_time = state.get('resultTime', None) + self.properties = state.get('properties', None) + if state.get('Thing', None) is not None: + self.thing = frost_sta_client.model.thing.Thing() + self.thing.__setstate__(state['Thing']) + if state.get('Sensor', None) is not None: + self.sensor = frost_sta_client.model.sensor.Sensor() + self.sensor.__setstate__(state['Sensor']) + if state.get('unitOfMeasurements', None) is not None \ + and isinstance(state['unitOfMeasurements'], list): + self.unit_of_measurements = [] + for value in state['unitOfMeasurements']: + self.unit_of_measurements.append(value) + if state.get('multiObservationDataTypes', None) is not None \ + and isinstance(state['multiObservationDataTypes'], list): + self.multi_observation_data_types = [] + for value in state['multiObservationDataTypes']: + self.multi_observation_data_types.append(value) + if state.get('ObservedProperties', None) is not None and isinstance(state['ObservedProperties'], list): + entity_class = entity_type.EntityTypes['ObservedProperty']['class'] + self.observed_properties = utils.transform_json_to_entity_list(state['ObservedProperties'], entity_class) + self.observed_properties.next_link = state.get('ObservedProperties@iot.nextLink') + self.observed_properties.count = state.get('ObservedProperties@iot.count') + if state.get('Observations', None) is not None and isinstance(state['Observations'], list): + entity_class = entity_type.EntityTypes['Observation']['class'] + self.observations = utils.transform_json_to_entity_list(state['Observations'], entity_class) + self.observations.next_link = state.get('Observations@iot.nextLink') + self.observations.count = state.get('Observations@iot.count') + + def get_dao(self, service): + return MultiDatastreamDao(service) diff --git a/frost_sta_client/model/observedproperty.py b/frost_sta_client/model/observedproperty.py index 59882d5..6586f45 100644 --- a/frost_sta_client/model/observedproperty.py +++ b/frost_sta_client/model/observedproperty.py @@ -1,210 +1,210 @@ -# 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 . - -from frost_sta_client.dao.observedproperty import ObservedPropertyDao - -from . import entity -from . import datastream -from . import multi_datastream - -from frost_sta_client import utils -from .ext import entity_type -from .ext import entity_list - - -class ObservedProperty(entity.Entity): - - def __init__(self, - name='', - definition='', - description='', - datastreams=None, - properties=None, - multi_datastreams=None, - **kwargs): - super().__init__(**kwargs) - if properties is None: - properties = {} - self.properties = properties - self.name = name - self.definition = definition - self.description = description - self.datastreams = datastreams - self.multi_datastreams = multi_datastreams - - def __new__(cls, *args, **kwargs): - new_observed_property = super().__new__(cls) - attributes = {'_id': None, '_name': '', '_definition': '', '_description': '', - '_datastreams': None, '_multi_datastreams': None, '_self_link': None, '_service': None} - for key, value in attributes.items(): - new_observed_property.__dict__[key] = value - return new_observed_property - - @property - def name(self): - return self._name - - @name.setter - def name(self, value): - if value is None: - self._name = None - return - if not isinstance(value, str): - raise ValueError('name should be of type str!') - self._name = value - - @property - def description(self): - return self._description - - @description.setter - def description(self, value): - if value is None: - self._description = None - return - if not isinstance(value, str): - raise ValueError('description should be of type str!') - self._description = value - - @property - def definition(self): - return self._definition - - @definition.setter - def definition(self, value): - if value is None: - self._definition = None - return - if not isinstance(value, str): - raise ValueError('description should be of type str!') - self._definition = value - - @property - def properties(self): - return self._properties - - @properties.setter - def properties(self, value): - if value is None: - self._properties = {} - return - if not isinstance(value, dict): - raise ValueError('properties should be of type dict!') - self._properties = value - - @property - def datastreams(self): - return self._datastreams - - @datastreams.setter - def datastreams(self, value): - if value is None: - self._datastreams = None - return - if isinstance(value, list) and all(isinstance(ds, datastream.Datastream) for ds in value): - entity_class = entity_type.EntityTypes['Datastream']['class'] - self._datastreams = entity_list.EntityList(entity_class=entity_class, entities=value) - return - if not isinstance(value, entity_list.EntityList) \ - or any((not isinstance(ds, datastream.Datastream)) for ds in value.entities): - raise ValueError('datastreams should be of list of type Datastream!') - self._datastreams = value - - @property - def multi_datastreams(self): - return self._multi_datastreams - - @multi_datastreams.setter - def multi_datastreams(self, values): - if values is None: - self._multi_datastreams = None - return - if isinstance(values, list) and all(isinstance(mds, multi_datastream.MultiDatastream) for mds in values): - entity_class = entity_type.EntityTypes['MultiDatastream']['class'] - self._multi_datastreams = entity_list.EntityList(entity_class=entity_class, entities=values) - return - if not isinstance(values, entity_list.EntityList) or\ - any((not isinstance(mds, multi_datastream.MultiDatastream)) for mds in values.entities): - raise ValueError('multi_datastreams should be a list of multi_datastreams!') - self._multi_datastreams = values - - def get_datastreams(self): - result = self.service.datastreams() - result.parent = self - return result - - def get_multi_datastreams(self): - result = self.service.multi_datastreams() - result.parent = self - return result - - def ensure_service_on_children(self, service): - if self.datastreams is not None: - self.datastreams.set_service(service) - if self.multi_datastreams is not None: - self.multi_datastreams.set_service(service) - - def __eq__(self, other): - if not super().__eq__(other): - return False - if self.name != other.name: - return False - if self.description != other.description: - return False - if self.definition != other.definition: - return False - if self.properties != other.properties: - return False - return True - - def __ne__(self, other): - return not self == other - - def __getstate__(self): - data = super().__getstate__() - if self.name is not None and self.name != '': - data['name'] = self.name - if self.description is not None and self.description != '': - data['description'] = self.description - if self.definition is not None and self.definition != '': - data['definition'] = self.definition - if self.properties is not None and self.properties != {}: - data['properties'] = self.properties - if self.datastreams is not None and len(self.datastreams.entities) > 0: - data['Datastream'] = self.datastreams.__getstate__() - if self.multi_datastreams is not None and len(self.multi_datastreams.entities) > 0: - data['MultiDatastreams'] = self.multi_datastreams.__getstate__() - return data - - def __setstate__(self, state): - super().__setstate__(state) - self.name = state.get("name", None) - self.description = state.get("description", None) - self.definition = state.get("definition", None) - self.properties = state.get("properties", {}) - if state.get("Datastreams", None) is not None and isinstance(state["Datastreams"], list): - entity_class = entity_type.EntityTypes['Datastream']['class'] - self.datastreams = utils.transform_json_to_entity_list(state['Datastreams'], entity_class) - self.datastreams.next_link = state.get('Datastreams@iot.nextLink', None) - self.datastreams.count = state.get('Datastreams@iot.count', None) - if state.get("MultiDatastreams", None) is not None and isinstance(state["MultiDatastreams"], list): - entity_class = entity_type.EntityTypes['MultiDatastream']['class'] - self.multi_datastreams = utils.transform_json_to_entity_list(state['MultiDatatstreams'], entity_class) - self.multi_datastreams.next_link = state.get('MultiDatastreams@iot.nextLink', None) - self.multi_datastreams.count = state.get('MultiDatastreams@iot.count', None) - - def get_dao(self, service): - return ObservedPropertyDao(service) +# 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 . + +from frost_sta_client.dao.observedproperty import ObservedPropertyDao + +from . import entity +from . import datastream +from . import multi_datastream + +from frost_sta_client import utils +from .ext import entity_type +from .ext import entity_list + + +class ObservedProperty(entity.Entity): + + def __init__(self, + name='', + definition='', + description='', + datastreams=None, + properties=None, + multi_datastreams=None, + **kwargs): + super().__init__(**kwargs) + if properties is None: + properties = {} + self.properties = properties + self.name = name + self.definition = definition + self.description = description + self.datastreams = datastreams + self.multi_datastreams = multi_datastreams + + def __new__(cls, *args, **kwargs): + new_observed_property = super().__new__(cls) + attributes = {'_id': None, '_name': '', '_definition': '', '_description': '', + '_datastreams': None, '_multi_datastreams': None, '_self_link': None, '_service': None} + for key, value in attributes.items(): + new_observed_property.__dict__[key] = value + return new_observed_property + + @property + def name(self): + return self._name + + @name.setter + def name(self, value): + if value is None: + self._name = None + return + if not isinstance(value, str): + raise ValueError('name should be of type str!') + self._name = value + + @property + def description(self): + return self._description + + @description.setter + def description(self, value): + if value is None: + self._description = None + return + if not isinstance(value, str): + raise ValueError('description should be of type str!') + self._description = value + + @property + def definition(self): + return self._definition + + @definition.setter + def definition(self, value): + if value is None: + self._definition = None + return + if not isinstance(value, str): + raise ValueError('description should be of type str!') + self._definition = value + + @property + def properties(self): + return self._properties + + @properties.setter + def properties(self, value): + if value is None: + self._properties = {} + return + if not isinstance(value, dict): + raise ValueError('properties should be of type dict!') + self._properties = value + + @property + def datastreams(self): + return self._datastreams + + @datastreams.setter + def datastreams(self, value): + if value is None: + self._datastreams = None + return + if isinstance(value, list) and all(isinstance(ds, datastream.Datastream) for ds in value): + entity_class = entity_type.EntityTypes['Datastream']['class'] + self._datastreams = entity_list.EntityList(entity_class=entity_class, entities=value) + return + if not isinstance(value, entity_list.EntityList) \ + or any((not isinstance(ds, datastream.Datastream)) for ds in value.entities): + raise ValueError('datastreams should be of list of type Datastream!') + self._datastreams = value + + @property + def multi_datastreams(self): + return self._multi_datastreams + + @multi_datastreams.setter + def multi_datastreams(self, values): + if values is None: + self._multi_datastreams = None + return + if isinstance(values, list) and all(isinstance(mds, multi_datastream.MultiDatastream) for mds in values): + entity_class = entity_type.EntityTypes['MultiDatastream']['class'] + self._multi_datastreams = entity_list.EntityList(entity_class=entity_class, entities=values) + return + if not isinstance(values, entity_list.EntityList) or\ + any((not isinstance(mds, multi_datastream.MultiDatastream)) for mds in values.entities): + raise ValueError('multi_datastreams should be a list of multi_datastreams!') + self._multi_datastreams = values + + def get_datastreams(self): + result = self.service.datastreams() + result.parent = self + return result + + def get_multi_datastreams(self): + result = self.service.multi_datastreams() + result.parent = self + return result + + def ensure_service_on_children(self, service): + if self.datastreams is not None: + self.datastreams.set_service(service) + if self.multi_datastreams is not None: + self.multi_datastreams.set_service(service) + + def __eq__(self, other): + if not super().__eq__(other): + return False + if self.name != other.name: + return False + if self.description != other.description: + return False + if self.definition != other.definition: + return False + if self.properties != other.properties: + return False + return True + + def __ne__(self, other): + return not self == other + + def __getstate__(self): + data = super().__getstate__() + if self.name is not None and self.name != '': + data['name'] = self.name + if self.description is not None and self.description != '': + data['description'] = self.description + if self.definition is not None and self.definition != '': + data['definition'] = self.definition + if self.properties is not None and self.properties != {}: + data['properties'] = self.properties + if self.datastreams is not None and len(self.datastreams.entities) > 0: + data['Datastreams'] = self.datastreams.__getstate__() + if self.multi_datastreams is not None and len(self.multi_datastreams.entities) > 0: + data['MultiDatastreams'] = self.multi_datastreams.__getstate__() + return data + + def __setstate__(self, state): + super().__setstate__(state) + self.name = state.get("name", None) + self.description = state.get("description", None) + self.definition = state.get("definition", None) + self.properties = state.get("properties", {}) + if state.get("Datastreams", None) is not None and isinstance(state["Datastreams"], list): + entity_class = entity_type.EntityTypes['Datastream']['class'] + self.datastreams = utils.transform_json_to_entity_list(state['Datastreams'], entity_class) + self.datastreams.next_link = state.get('Datastreams@iot.nextLink', None) + self.datastreams.count = state.get('Datastreams@iot.count', None) + if state.get("MultiDatastreams", None) is not None and isinstance(state["MultiDatastreams"], list): + entity_class = entity_type.EntityTypes['MultiDatastream']['class'] + self.multi_datastreams = utils.transform_json_to_entity_list(state['MultiDatastreams'], entity_class) + self.multi_datastreams.next_link = state.get('MultiDatastreams@iot.nextLink', None) + self.multi_datastreams.count = state.get('MultiDatastreams@iot.count', None) + + def get_dao(self, service): + return ObservedPropertyDao(service) diff --git a/frost_sta_client/model/task.py b/frost_sta_client/model/task.py index e015cbc..60ebcdc 100644 --- a/frost_sta_client/model/task.py +++ b/frost_sta_client/model/task.py @@ -98,16 +98,16 @@ def __getstate__(self): if self.creation_time is not None: data['creationTime'] = utils.parse_datetime(self.creation_time) if self.tasking_capability is not None: - data['TaskingCapability'] = self.tasking_capability + data['TaskingCapability'] = self.tasking_capability.__getstate__() return data def __setstate__(self, state): super().__setstate__(state) self.tasking_parameters = state.get('taskingParameters', {}) self.creation_time = state.get('creationTime', None) - if state.get('taskingCapability', None) is not None: + if state.get('TaskingCapability', None) is not None: self.tasking_capability = frost_sta_client.model.tasking_capability.TaskingCapability() - self.tasking_capability.__setstate__(state['taskingCapability']) + self.tasking_capability.__setstate__(state['TaskingCapability']) def get_dao(self, service): return TaskDao(service) diff --git a/frost_sta_client/model/tasking_capability.py b/frost_sta_client/model/tasking_capability.py index f6c4de9..13b81fc 100644 --- a/frost_sta_client/model/tasking_capability.py +++ b/frost_sta_client/model/tasking_capability.py @@ -186,11 +186,11 @@ def __getstate__(self): if self.properties is not None and self.properties != {}: data['properties'] = self.properties if self.thing is not None: - data['Thing'] = self.thing + data['Thing'] = self.thing.__getstate__() if self.tasks is not None and len(self.tasks.entities) > 0: data['Tasks'] = self.tasks.__getstate__() if self.actuator is not None: - data['Actuator'] = self.actuator + data['Actuator'] = self.actuator.__getstate__() return data def __setstate__(self, state): diff --git a/frost_sta_client/query/query.py b/frost_sta_client/query/query.py index e0ffa7f..3e7a332 100644 --- a/frost_sta_client/query/query.py +++ b/frost_sta_client/query/query.py @@ -1,164 +1,161 @@ -# 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 frost_sta_client.utils -import frost_sta_client.model.ext.entity_list - - -import logging -import requests -from requests.exceptions import JSONDecodeError - - -class Query: - def __init__(self, service, entity, entitytype_plural, entity_class, parent): - self.service = service - self.entity = entity - self.entitytype_plural = entitytype_plural - self.entity_class = entity_class - self.params = {} - self.parent = parent - - @property - def service(self): - return self._service - - @service.setter - def service(self, service): - if service is None: - self._service = service - return - if not isinstance(service, frost_sta_client.service.sensorthingsservice.SensorThingsService): - raise ValueError('service should be of type SensorThingsService') - self._service = service - - @property - def entity(self): - return self._entity - - @entity.setter - def entity(self, value): - if value is None or isinstance(value, str): - self._entity = value - return - raise ValueError('entity should be of type String') - - @property - def entitytype_plural(self): - return self._entitytype_plural - - @entitytype_plural.setter - def entitytype_plural(self, value): - if value is None or isinstance(value, str): - self._entitytype_plural = value - return - raise ValueError('entitytype_plural should be of type String') - - @property - def entity_class(self): - return self._entity_class - - @entity_class.setter - def entity_class(self, value): - if value is None or isinstance(value, str): - self._entity_class = value - return - raise ValueError('entity_class should be of type string') - - @property - def parent(self): - return self._parent - - @parent.setter - def parent(self, value): - self._parent = value - - def remove_all_params(self, key): - self.params.pop(key, None) - - def count(self): - self.remove_all_params('$count') - self.params['$count'] = 'true' - return self - - def top(self, num): - self.remove_all_params('$top') - self.params['$top'] = num - return self - - def skip(self, num): - self.remove_all_params('$skip') - self.params['$skip'] = num - return self - - def select(self, *args): - self.remove_all_params('$select') - if args is None: - return self - values = '' - for item in args: - if not isinstance(item, str): - return self - values = values + item + ',' - values = values[:-1] - self.params['$select'] = values - return self - - def filter(self, statement=None): - self.remove_all_params('$filter') - if statement is None: - return self - self.params['$filter'] = statement - return self - - def orderby(self, criteria, order='DESC'): - self.remove_all_params('$orderby') - self.params['$orderby'] = criteria + ' ' + order - return self - - def expand(self, expansion): - self.remove_all_params('$expand') - self.params['$expand'] = expansion - return self - - # exception: similar functions in basedao - def list(self, callback=None, step_size=None): - """ - Get an entity collection as a dictionary - callbacks so far only work in combination with step_size. If step_size is set, then the callback function - is called at every iteration of the step_size - """ - url = self.service.get_full_path(self.parent, self.entitytype_plural) - url.args = self.params - try: - response = self.service.execute('get', url) - except requests.exceptions.HTTPError as e: - error_json = e.response.json() - error_message = error_json['message'] - logging.error("Query failed with status-code {}, {}".format(e.response.status_code, error_message)) - raise e - logging.debug('Received response: {} from {}'.format(response.status_code, url)) - try: - json_response = response.json() - except JSONDecodeError: - raise ValueError('Cannot find json in http response') - entity_list = frost_sta_client.utils.transform_json_to_entity_list(json_response, self.entity_class) - entity_list.set_service(self.service) - - entity_list.callback = callback - entity_list.step_size = step_size - - return entity_list +# 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 frost_sta_client.utils +import frost_sta_client.model.ext.entity_list + + +import logging +import requests +from requests.exceptions import JSONDecodeError + + +class Query: + def __init__(self, service, entity, entitytype_plural, entity_class, parent): + self.service = service + self.entity = entity + self.entitytype_plural = entitytype_plural + self.entity_class = entity_class + self.params = {} + self.parent = parent + + @property + def service(self): + return self._service + + @service.setter + def service(self, service): + if service is None: + self._service = service + return + if not isinstance(service, frost_sta_client.service.sensorthingsservice.SensorThingsService): + raise ValueError('service should be of type SensorThingsService') + self._service = service + + @property + def entity(self): + return self._entity + + @entity.setter + def entity(self, value): + if value is None or isinstance(value, str): + self._entity = value + return + raise ValueError('entity should be of type String') + + @property + def entitytype_plural(self): + return self._entitytype_plural + + @entitytype_plural.setter + def entitytype_plural(self, value): + if value is None or isinstance(value, str): + self._entitytype_plural = value + return + raise ValueError('entitytype_plural should be of type String') + + @property + def entity_class(self): + return self._entity_class + + @entity_class.setter + def entity_class(self, value): + if value is None or isinstance(value, str): + self._entity_class = value + return + raise ValueError('entity_class should be of type string') + + @property + def parent(self): + return self._parent + + @parent.setter + def parent(self, value): + self._parent = value + + def remove_all_params(self, key): + self.params.pop(key, None) + + def count(self): + self.remove_all_params('$count') + self.params['$count'] = 'true' + return self + + def top(self, num): + self.remove_all_params('$top') + self.params['$top'] = num + return self + + def skip(self, num): + self.remove_all_params('$skip') + self.params['$skip'] = num + return self + + def select(self, *args): + self.remove_all_params('$select') + if args is None: + return self + values = '' + for item in args: + if not isinstance(item, str): + return self + values = values + item + ',' + values = values[:-1] + self.params['$select'] = values + return self + + def filter(self, statement=None): + self.remove_all_params('$filter') + if statement is None: + return self + self.params['$filter'] = statement + return self + + def orderby(self, criteria, order='DESC'): + self.remove_all_params('$orderby') + self.params['$orderby'] = criteria + ' ' + order + return self + + def expand(self, expansion): + self.remove_all_params('$expand') + self.params['$expand'] = expansion + return self + + # exception: similar functions in basedao + def list(self, callback=None, step_size=None): + """ + Get an entity collection as a dictionary + callbacks so far only work in combination with step_size. If step_size is set, then the callback function + is called at every iteration of the step_size + """ + url = self.service.get_full_path(self.parent, self.entitytype_plural) + url.args = self.params + try: + response = self.service.execute('get', url) + except requests.exceptions.HTTPError as e: + frost_sta_client.utils.handle_server_error(e, 'Query') + logging.debug('Received response: {} from {}'.format(response.status_code, url)) + try: + json_response = response.json() + except JSONDecodeError: + raise ValueError('Cannot find json in http response') + entity_list = frost_sta_client.utils.transform_json_to_entity_list(json_response, self.entity_class) + entity_list.set_service(self.service) + + entity_list.callback = callback + entity_list.step_size = step_size + + return entity_list 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 3ab2674..2597a0b 100644 --- a/frost_sta_client/service/sensorthingsservice.py +++ b/frost_sta_client/service/sensorthingsservice.py @@ -15,46 +15,22 @@ # along with this program. If not, see . import requests -from requests.adapters import HTTPAdapter, Retry from furl import furl import logging -from frost_sta_client.config import Config - 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, proxies=None): + 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 - config = Config() - total_retries = config.total_retries - connect = config.connect - backoff_factor = config.backoff_factor - status_forcelist = config.status_forcelist - self.request_session = requests.Session() - - retries = Retry( - total=total_retries, - connect=connect, - backoff_factor=backoff_factor, - status_forcelist=status_forcelist, - ) - - adapter = HTTPAdapter(max_retries=retries) - self.request_session.mount("http://", adapter) - self.request_session.mount("https://", adapter) - - if config.HTTP_AUTH: - user = config.HTTP_AUTH_USER - password = config.HTTP_AUTH_PASSWORD - if user and password: - self.request_session.auth = (user, password) @property def url(self): @@ -82,7 +58,26 @@ def auth_handler(self, value): 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): @@ -98,17 +93,23 @@ def proxies(self, value): self._proxies = value def execute(self, method, url, **kwargs): - if self.auth_handler is not None: - response = self.request_session.request( - method, - url, + + 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 = self.request_session.request( - method, url, proxies=self.proxies, **kwargs + response = requests.request( + method=method, url=url, proxies=self.proxies, **kwargs ) try: response.raise_for_status() 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/frost_sta_client/utils.py b/frost_sta_client/utils.py index 9ae5f4f..bed566f 100644 --- a/frost_sta_client/utils.py +++ b/frost_sta_client/utils.py @@ -1,119 +1,138 @@ -# 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 jsonpickle -import datetime -from dateutil.parser import isoparse -import geojson -import logging -import sys -import frost_sta_client.model.ext.entity_list - - -def extract_value(location): - try: - value = int(location[location.find('(')+1: location.find(')')]) - except ValueError: - value = str(location[location.find('(')+2: location.find(')')-1]) - return value - -def transform_entity_to_json_dict(entity): - json_str = jsonpickle.encode(entity, unpicklable=False) - return jsonpickle.decode(json_str) - -def class_from_string(string): - module_name, class_name = string.rsplit(".", 1) - return getattr(sys.modules[module_name], class_name) - -def transform_json_to_entity(json_response, entity_class): - cl = class_from_string(entity_class) - obj = cl() - obj.__setstate__(json_response) - return obj - -def transform_json_to_entity_list(json_response, entity_class): - entity_list = frost_sta_client.model.ext.entity_list.EntityList(entity_class) - result_list = [] - if isinstance(json_response, dict): - try: - response_list = json_response['value'] - entity_list.next_link = json_response.get("@iot.nextLink", None) - entity_list.count = json_response.get("@iot.count", None) - except AttributeError as e: - raise e - elif isinstance(json_response, list): - response_list = json_response - else: - raise ValueError("expected json as a dict or list to transform into entity list") - entity_list.entities = [transform_json_to_entity(item, entity_list.entity_class) for item in response_list] - return entity_list - - -def check_datetime(value, time_entity): - try: - parse_datetime(value) - except ValueError as e: - logging.error(f"error during {time_entity} check") - raise e - return value - - -def parse_datetime(value) -> str: - if value is None: - return value - if isinstance(value, str): - if '/' in value: - try: - times = value.split('/') - if len(times) != 2: - raise ValueError("If the time interval is provided as a string," - " it should be in isoformat") - result = [isoparse(times[0]), - isoparse(times[1])] - except ValueError: - raise ValueError("If the time entity interval is provided as a string," - " it should be in isoformat") - result = result[0].isoformat() + '/' + result[1].isoformat() - return result - else: - try: - result = isoparse(value) - except ValueError: - raise ValueError("If the phenomenon time is provided as string, it should be in isoformat") - result = result.isoformat() - return result - if isinstance(value, datetime.datetime): - return value.isoformat() - if isinstance(value, list) and all(isinstance(v, datetime.datetime) for v in value): - return value[0].isoformat() + value[1].isoformat() - else: - raise ValueError('time entities should consist of one or two datetimes') - - -def process_area(value): - if not isinstance(value, dict): - raise ValueError("geojsons can only be handled as dictionaries!") - if value.get("type", None) is None or value.get("coordinates", None) is None: - raise ValueError("Both type and coordinates need to be specified in the dictionary") - if value["type"] == "Point": - return geojson.geometry.Point(value["coordinates"]) - if value["type"] == "Polygon": - return geojson.geometry.Polygon(value["coordinates"]) - if value["type"] == "Geometry": - return geojson.geometry.Geometry(value["coordinates"]) - if value["type"] == "LineString": - return geojson.geometry.LineString(value["coordinates"]) - raise ValueError("can only handle geojson of type Point, Polygon, Geometry or LineString") +# 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 jsonpickle +import datetime +from dateutil.parser import isoparse +import geojson +import logging +import sys +import frost_sta_client.model.ext.entity_list + + +def extract_value(location): + try: + value = int(location[location.find('(')+1: location.find(')')]) + except ValueError: + value = str(location[location.find('(')+2: location.find(')')-1]) + return value + +def transform_entity_to_json_dict(entity): + try: + data = entity.__getstate__() + except AttributeError: + data = entity.__dict__ + return data + +def class_from_string(string): + module_name, class_name = string.rsplit(".", 1) + return getattr(sys.modules[module_name], class_name) + +def transform_json_to_entity(json_response, entity_class): + cl = class_from_string(entity_class) + obj = cl() + obj.__setstate__(json_response) + return obj + +def transform_json_to_entity_list(json_response, entity_class): + entity_list = frost_sta_client.model.ext.entity_list.EntityList(entity_class) + result_list = [] + if isinstance(json_response, dict): + try: + response_list = json_response['value'] + entity_list.next_link = json_response.get("@iot.nextLink", None) + entity_list.count = json_response.get("@iot.count", None) + except AttributeError as e: + raise e + elif isinstance(json_response, list): + response_list = json_response + else: + raise ValueError("expected json as a dict or list to transform into entity list") + entity_list.entities = [transform_json_to_entity(item, entity_list.entity_class) for item in response_list] + return entity_list + + +def check_datetime(value, time_entity): + try: + parse_datetime(value) + except ValueError as e: + logging.error(f"error during {time_entity} check") + raise e + return value + + +def parse_datetime(value) -> str: + if value is None: + return value + if isinstance(value, str): + if '/' in value: + try: + times = value.split('/') + if len(times) != 2: + raise ValueError("If the time interval is provided as a string," + " it should be in isoformat") + result = [isoparse(times[0]), + isoparse(times[1])] + except ValueError: + raise ValueError("If the time entity interval is provided as a string," + " it should be in isoformat") + result = result[0].isoformat() + '/' + result[1].isoformat() + return result + else: + try: + result = isoparse(value) + except ValueError: + raise ValueError("If the phenomenon time is provided as string, it should be in isoformat") + result = result.isoformat() + return result + if isinstance(value, datetime.datetime): + return value.isoformat() + if isinstance(value, list) and all(isinstance(v, datetime.datetime) for v in value): + return value[0].isoformat() + value[1].isoformat() + else: + raise ValueError('time entities should consist of one or two datetimes') + + +def process_area(value): + if not isinstance(value, dict): + raise ValueError("geojsons can only be handled as dictionaries!") + if value.get("type", None) is None or value.get("coordinates", None) is None: + raise ValueError("Both type and coordinates need to be specified in the dictionary") + if value["type"] == "Point": + return geojson.geometry.Point(value["coordinates"]) + if value["type"] == "Polygon": + return geojson.geometry.Polygon(value["coordinates"]) + if value["type"] == "Geometry": + return geojson.geometry.Geometry(value["coordinates"]) + if value["type"] == "LineString": + return geojson.geometry.LineString(value["coordinates"]) + raise ValueError("can only handle geojson of type Point, Polygon, Geometry or LineString") + +def handle_server_error(error, failed_action): + # Try to extract a meaningful error message even if the response is not JSON + try: + err = error.response.json() + if isinstance(err, dict): + error_message = err.get('message', err.get('error', str(err))) + else: + error_message = str(err) + except Exception: + try: + error_message = getattr(error.response, 'text', str(error)) + except Exception: + error_message = str(error) + logging.error("{} failed with status-code {}, {}".format(failed_action, getattr(error.response, 'status_code', 'unknown'), error_message)) + raise error \ No newline at end of file diff --git a/tests/conftest.py b/tests/conftest.py new file mode 100644 index 0000000..55423cd --- /dev/null +++ b/tests/conftest.py @@ -0,0 +1,47 @@ +import pytest +import subprocess +import time +import requests +import os + +from frost_sta_client.service.sensorthingsservice import SensorThingsService +from frost_sta_client.service.auth_handler import AuthHandler + +@pytest.fixture(scope='session') +def frost_server(): + if os.environ.get('FROST_STA_CLIENT_RUN_INTEGRATION') != '1': + # Skip starting server if not requested + yield + return + # Start FROST-Server using Podman + subprocess.run(['podman', 'compose', '-f', 'frost_server/docker-compose.yaml', 'up', '-d']) + # Wait for server to start + url = 'http://localhost:8080/FROST-Server' + for _ in range(30): + try: + response = requests.get(url) + if response.status_code == 200: + break + except requests.ConnectionError: + time.sleep(1) + else: + raise RuntimeError('FROST-Server failed to start') + vrl = 'http://localhost:8080/FROST-Server/v1.1' + auth_handler = AuthHandler( + username="read", + password="read" + ) + response = requests.get(vrl, auth=auth_handler.add_auth_header()) + if response.status_code == 401: + raise RuntimeError('Failed to authorize at FROST-Server') + yield + subprocess.run(['podman', 'compose', '-f', 'frost_server/docker-compose.yaml', 'down']) + +@pytest.fixture +def sensorthings_service(frost_server): + url = 'http://localhost:8080/FROST-Server/v1.1' + auth_handler = AuthHandler( + username="admin", + password="admin" + ) + return SensorThingsService(url, auth_handler=auth_handler) diff --git a/tests/test_dao_base.py b/tests/test_dao_base.py new file mode 100644 index 0000000..b8426bd --- /dev/null +++ b/tests/test_dao_base.py @@ -0,0 +1,71 @@ +import pytest +import requests +from frost_sta_client.service.sensorthingsservice import SensorThingsService +from frost_sta_client.model.thing import Thing + + +class MockResponse: + def __init__(self, status_code=200, json_data=None, headers=None): + self.status_code = status_code + self._json = json_data if json_data is not None else {} + self.headers = headers or {} + + def json(self): + return self._json + + def raise_for_status(self): + if self.status_code >= 400: + raise requests.exceptions.HTTPError(response=self) + + +class DummyService(SensorThingsService): + def __init__(self): + super().__init__('http://example.org/FROST-Server/v1.1') + self.calls = [] + + def execute(self, method, url, **kwargs): + self.calls.append((method, str(url), kwargs)) + if method == 'post': + return MockResponse(201, {}, headers={'location': 'Things(42)'}) + if method == 'get': + return MockResponse(200, {"@iot.id": 5, "name": "MyThing"}) + return MockResponse(200, {}) + + +def test_base_dao_create_sets_id_and_service(): + svc = DummyService() + t = Thing(name='X') + svc.create(t) + assert t.id == 42 + assert t.service is svc + + +def test_base_dao_find_returns_entity(): + svc = DummyService() + found = svc.things().find(5) + assert found.id == 5 + assert found.name == 'MyThing' + assert found.service is svc + + +def test_base_dao_update_without_id_raises(): + svc = DummyService() + t = Thing(name='noid') + with pytest.raises(AttributeError): + svc.update(t) + + +def test_base_dao_patch_validates_and_sends_headers(): + svc = DummyService() + t = Thing(id=7) + patches = [{"op": "replace", "path": "/name", "value": "new"}] + svc.patch(t, patches) + method, url, kwargs = svc.calls[-1] + assert method == 'patch' + assert kwargs['headers']['Content-type'] == 'application/json-patch+json' + + +def test_entity_path_formats_string_and_int(): + svc = DummyService() + assert svc.things().entity_path(1) == 'Things(1)' + assert svc.things().entity_path('abc') == "Things('abc')" diff --git a/tests/test_entity_behaviour.py b/tests/test_entity_behaviour.py new file mode 100644 index 0000000..a30dfe0 --- /dev/null +++ b/tests/test_entity_behaviour.py @@ -0,0 +1,21 @@ +from frost_sta_client.service.sensorthingsservice import SensorThingsService +from frost_sta_client.model.thing import Thing +from frost_sta_client.model.location import Location +from frost_sta_client.model.ext.entity_list import EntityList +from frost_sta_client.model.ext.entity_type import EntityTypes + + +def test_entity_equality_by_id(): + a = Thing(id=1, name='A') + b = Thing(id=1, name='B') + assert a != b + + +def test_set_service_propagates_to_children(): + t = Thing(name='T') + loc = Location(name='L') + t.locations = EntityList(entity_class=EntityTypes['Location']['class'], entities=[loc]) + svc = SensorThingsService('http://example.org/FROST-Server/v1.1') + t.set_service(svc) + assert t.service is svc + assert t.locations.entities[0].service is svc diff --git a/tests/test_entity_list.py b/tests/test_entity_list.py new file mode 100644 index 0000000..3d31e66 --- /dev/null +++ b/tests/test_entity_list.py @@ -0,0 +1,50 @@ +from frost_sta_client.utils import transform_json_to_entity_list +from frost_sta_client.service.sensorthingsservice import SensorThingsService + + +class MockResponse: + def __init__(self, json_data): + self._json = json_data + self.status_code = 200 + + def json(self): + return self._json + + def raise_for_status(self): + pass + + +class DummyService(SensorThingsService): + def __init__(self, url, page2): + super().__init__(url) + self.page2 = page2 + self.calls = 0 + + def execute(self, method, url, **kwargs): + self.calls += 1 + return MockResponse(self.page2) + + +def test_entity_list_iterates_across_pages(): + page1 = { + "value": [ + {"@iot.id": 1, "name": "A"}, + {"@iot.id": 2, "name": "B"}, + ], + "@iot.nextLink": "http://example.org/FROST-Server/v1.1/Things?$skip=2" + } + page2 = { + "value": [ + {"@iot.id": 3, "name": "C"}, + {"@iot.id": 4, "name": "D"}, + ] + } + elist = transform_json_to_entity_list(page1, 'frost_sta_client.model.thing.Thing') + svc = DummyService('http://example.org/FROST-Server/v1.1', page2) + elist.set_service(svc) + called = [] + elist.step_size = 2 + elist.callback = lambda idx: called.append(idx) + names = [e.name for e in elist] + assert names == ['A', 'B', 'C', 'D'] + assert called == [0, 2] diff --git a/tests/test_entity_reader.py b/tests/test_entity_reader.py index d855b62..03de714 100644 --- a/tests/test_entity_reader.py +++ b/tests/test_entity_reader.py @@ -1,267 +1,266 @@ -import unittest - -import frost_sta_client.model.observation -import frost_sta_client.utils -import frost_sta_client.model.ext.entity_type -import datetime - - -class TestEntityReader(unittest.TestCase): - - def test_write_thing_basic_success(self): - json_dict = { - 'phenomenonTime': '2016-01-07T02:00:00.000+00:00', - 'resultTime': None, - 'result': '0.15', - 'Datastream@iot.navigationLink': 'https://server.de/SensorThingsService/v1.0/' - 'Observations(7179373)/Datastream', - 'FeatureOfInterest@iot.navigationLink': 'https://server.de/SensorThingsService/v1.0/' - 'Observations(7179373)/FeatureOfInterest', - '@iot.id': '719373', - '@iot.selfLink': 'https://server.de/SensorThingsService/v1.0/Observations(7179373)' - } - result = frost_sta_client.utils.transform_json_to_entity(json_dict, "frost_sta_client.model.observation.Observation") - - exp_observation = frost_sta_client.model.observation.Observation( - phenomenon_time='2016-01-07T02:00:00.000+00:00', - result='0.15', - id='719373', - self_link='https://server.de/SensorThingsService/v1.0/Observations(7179373)' - ) - self.assertEqual(result, exp_observation) - - def test_read_entity_list(self): - json_dict = { - "@iot.nextLink": "https://server.de/SensorThingsService/v1.0/Things?$top=2&" - "$skip=14&$expand=Datastreams%28%24top%3D2%3B%24count%3Dtrue%29", - "value": [ - { - "name": "Recoaro 1000", - "description": "Weather station Recoaro 1000", - "Datastreams@iot.navigationLink": "https://server.de/SensorThingsService/v1.0/Things(19)/" - "Datastreams", - "Datastreams@iot.count": 6, - "Datastreams": [ - { - "name": "Air Temperature Recoaro 1000", - "description": "The Air Temperature at Recoaro 1000", - "observationType": "http://www.opengis.net/def/observationType/OGC-OM/2.0/OM_Measurement", - "phenomenonTime": "2010-01-01T00:00:00.000Z/2019-01-13T06:00:00.000Z", - "unitOfMeasurement": { - "name": "degree celcius", - "symbol": "°C", - "definition": "ucum:Cel", - }, - "Observations": [ - { - "result": 0.0 - } - ], - "@iot.id": 66, - "@iot.selfLink": "https://server.de/SensorThingsService/v1.0/Datastreams(66)", - }, - { - "name": "Precipitation Recoaro 1000", - "description": "The Precipitation at Recoaro 1000", - "observationType": "http://www.opengis.net/def/observationType/OGC-OM/2.0/OM_Measurement", - "phenomenonTime": "2010-01-01T00:00:00.000Z/2019-01-13T06:00:00.000Z", - "unitOfMeasurement": { - "name": "mm/h", - "symbol": "mm/h", - "definition": "ucum:mm/h" - }, - "@iot.id": 130, - "@iot.selfLink": "https://server.de/SensorThingsService/v1.0/Datastreams(130)" - } - ], - "Datastreams@iot.nextLink": "https://server.de/SensorThingsService/v1.0/Things(19)/" - "Datastreams?$top=2&$skip=2&$count=true", - "MultiDatastreams@iot.navigationLink": "https://server.de/SensorThingsService/v1.0/" - "Things(19)/MultiDatastreams", - "Locations@iot.navigationLink": "https://server.de/SensorThingsService/v1.0/Things(19)/Locations", - "HistoricalLocations@iot.navigationLink": "https://server.de/SensorThingsService/v1.0/" - "Things(19)/HistoricalLocations", - "@iot.id": 19, - "@iot.selfLink": "https://server.de/SensorThingsService/v1.0/Things(19)" - }, - { - "name": "Valdagno", - "description": "Weather station Valdagno", - "Datastreams@iot.navigationLink": "https://server.de/SensorThingsService/v1.0/" - "Things(20)/Datastreams", - "Datastreams": [], - "Datastreams@iot.count": 6, - "Datastreams@iot.nextLink": "https://server.de/SensorThingsService/v1.0/Things(20)/" - "Datastreams?$top=2&$skip=2&$count=true", - "MultiDatastreams@iot.navigationLink": "https://server.de/SensorThingsService/v1.0/" - "Things(20)/MultiDatastreams", - "Locations@iot.navigationLink": "https://server.de/SensorThingsService/v1.0/Things(20)/Locations", - "HistoricalLocations@iot.navigationLink": "https://server.de/SensorThingsService/v1.0/" - "Things(20)/HistoricalLocations", - "@iot.id": 20, - "@iot.selfLink": "https://server.de/SensorThingsService/v1.0/Things(20)" - } - ] - } - - things = frost_sta_client.utils.transform_json_to_entity_list(json_dict, 'frost_sta_client.model.thing.Thing') - - self.assertEqual('https://server.de/SensorThingsService/v1.0/Things?$top=2&$' - 'skip=14&$expand=Datastreams%28%24top%3D2%3B%24count%3Dtrue%29', things.next_link) - - thing = things.entities[0] - self.assertEqual(19, thing.id) - self.assertEqual('Recoaro 1000', thing.name) - self.assertEqual('Weather station Recoaro 1000', thing.description) - - ds_list = thing.datastreams - self.assertEqual('https://server.de/SensorThingsService/v1.0/Things(19)/Datastreams?$top=2&$skip=2&$count=true', - ds_list.next_link) - # self.assertEqual(6, ds_list.count) - - ds = ds_list.entities[0] - self.assertEqual('Air Temperature Recoaro 1000', ds.name) - self.assertEqual('The Air Temperature at Recoaro 1000', ds.description) - - result = ds.observations.entities[0].result - self.assertEqual(0, result) - - ds = ds_list.entities[1] - self.assertEqual(130, ds.id) - self.assertEqual('Precipitation Recoaro 1000', ds.name) - self.assertEqual('The Precipitation at Recoaro 1000', ds.description) - - thing = things.entities[1] - self.assertEqual(20, thing.id) - self.assertEqual('Valdagno', thing.name) - self.assertEqual('Weather station Valdagno', thing.description) - - ds_list = thing.datastreams - self.assertEqual('https://server.de/SensorThingsService/v1.0/Things(20)/Datastreams?$top=2&$skip=2&$count=true', - ds_list.next_link) - # self.assertEqual(6, ds_list.count) - - def test_read_empty_entity_list(self): - json_dict = {'value': []} - things = frost_sta_client.utils.transform_json_to_entity_list(json_dict, 'frost_sta_client.model.thing.Thing') - - self.assertEqual(None, things.next_link) - self.assertTrue(len(things.entities) == 0) - - def test_read_tasking_capabilities(self): - json_dict = { - 'name': 'createNewVA', - 'description': 'Virtual Actuator Server, starts new Virtual Actuators', - 'taskingParameters': { - 'type': 'DataRecord', - 'field': [{ - 'type': 'Text', - 'label': 'Aktor-Name', - 'description': 'Name des neuen virtuellen Aktors', - 'name': 'vaName' - }, { - 'type': 'Text', - 'label': 'Aktor-Beschreibung', - 'description': 'Beschreibung des neuen virtuellen Aktors', - 'name': 'vaDescription' - }] - }, - 'Actuator@iot.navigationLink': 'https://server.de/SensorThingsService/v1.0/Actuator', - 'Thing@iot.navigationLink': 'https://server.de/SensorThingsService/v1.0/TaskingCapabilities(1)/Thing', - 'Tasks@iot.navigationLink': 'https://server.de/SensorThingsService/v1.0/TaskingCapabilities(1)/Tasks', - '@iot.id': 1, - '@iot.selfLink': 'https://server.de/SensorThingsService/v1.0/TaskingCapabilities(1)'} - - capability = frost_sta_client.utils. \ - transform_json_to_entity(json_dict, - 'frost_sta_client.model.tasking_capability.TaskingCapability') - - expected = frost_sta_client.model.tasking_capability.TaskingCapability() - expected.name = 'createNewVA' - expected.description = 'Virtual Actuator Server, starts new Virtual Actuators' - expected.tasking_parameters = { - 'type': 'DataRecord', - 'field': [{ - 'type': 'Text', - 'label': 'Aktor-Name', - 'description': 'Name des neuen virtuellen Aktors', - 'name': 'vaName' - }, { - 'type': 'Text', - 'label': 'Aktor-Beschreibung', - 'description': 'Beschreibung des neuen virtuellen Aktors', - 'name': 'vaDescription' - }] - } - expected.id = 1 - expected.self_link = 'https://server.de/SensorThingsService/v1.0/TaskingCapabilities(1)' - - self.assertEqual(expected, capability) - - def test_read_tasking_capabilities_with_constraint(self): - json_dict = { - 'name': 'DatastreamCopierCapability', - 'description': 'Copies Observations from one Datastream to another', - 'taskingParameters': - { - 'type': 'DataRecord', - 'field': [{ - 'type': 'Count', - 'name': 'sourceDatastream', - 'label': 'Source Datastream', - 'description': 'ID of the datastream from which the observations should be taken.', - 'constraint': { - 'type': 'AllowedValues', - 'interval': [[0, 10000]] - } - }, - { - 'type': 'Count', - 'name': 'destinationDatastream', - 'label': 'Destination Datastream', - 'description': 'ID of the datastream to which the observations should be copied.', - 'constraint': { - 'type': 'AllowedValues', - 'interval': [[0, 10000]] - } - }] - }, - 'Actuator@iot.navigationLink': 'https://server.de/SensorThingsService/v1.0/Actuator', - 'Thing@iot.navigationLink': 'https://server.de/SensorThingsService/v1.0/TaskingCapabilities(1)/Thing', - 'Tasks@iot.navigationLink': 'https://server.de/SensorThingsService/v1.0/TaskingCapabilities(1)/Tasks', - '@iot.id': 1, - '@iot.selfLink': 'https://server.de/SensorThingsService/v1.0/TaskingCapabilities(1)' - } - - entity_class = 'frost_sta_client.model.tasking_capability.TaskingCapability' - capability = frost_sta_client.utils.transform_json_to_entity(json_dict, entity_class) - - expected = frost_sta_client.model.tasking_capability.TaskingCapability() - expected.name = 'DatastreamCopierCapability' - expected.description = 'Copies Observations from one Datastream to another' - expected.id = 1 - expected.tasking_parameters = { - 'type': 'DataRecord', - 'field': [{ - 'type': 'Count', - 'name': 'sourceDatastream', - 'label': 'Source Datastream', - 'description': 'ID of the datastream from which the observations should be taken.', - 'constraint': { - 'type': 'AllowedValues', - 'interval': [[0, 10000]] - } - }, - { - 'type': 'Count', - 'name': 'destinationDatastream', - 'label': 'Destination Datastream', - 'description': 'ID of the datastream to which the observations should be copied.', - 'constraint': { - 'type': 'AllowedValues', - 'interval': [[0, 10000]] - } - }] - } - - self.assertEqual(expected, capability) +import unittest + +import frost_sta_client.model.observation +import frost_sta_client.utils +import frost_sta_client.model.ext.entity_type +import datetime + + +class TestEntityReader(unittest.TestCase): + + def test_write_thing_basic_success(self): + json_dict = { + 'phenomenonTime': '2016-01-07T02:00:00.000+00:00', + 'resultTime': None, + 'result': '0.15', + 'Datastream@iot.navigationLink': 'https://server.de/SensorThingsService/v1.0/' + 'Observations(7179373)/Datastream', + 'FeatureOfInterest@iot.navigationLink': 'https://server.de/SensorThingsService/v1.0/' + 'Observations(7179373)/FeatureOfInterest', + '@iot.id': '719373', + '@iot.selfLink': 'https://server.de/SensorThingsService/v1.0/Observations(7179373)' + } + result = frost_sta_client.utils.transform_json_to_entity(json_dict, "frost_sta_client.model.observation.Observation") + + exp_observation = frost_sta_client.model.observation.Observation( + phenomenon_time='2016-01-07T02:00:00.000+00:00', + result='0.15', + id='719373', + self_link='https://server.de/SensorThingsService/v1.0/Observations(7179373)' + ) + self.assertEqual(result, exp_observation) + + def test_read_entity_list(self): + json_dict = { + "@iot.nextLink": "https://server.de/SensorThingsService/v1.0/Things?$top=2&" + "$skip=14&$expand=Datastreams%28%24top%3D2%3B%24count%3Dtrue%29", + "value": [ + { + "name": "Recoaro 1000", + "description": "Weather station Recoaro 1000", + "Datastreams@iot.navigationLink": "https://server.de/SensorThingsService/v1.0/Things(19)/" + "Datastreams", + "Datastreams@iot.count": 6, + "Datastreams": [ + { + "name": "Air Temperature Recoaro 1000", + "description": "The Air Temperature at Recoaro 1000", + "observationType": "http://www.opengis.net/def/observationType/OGC-OM/2.0/OM_Measurement", + "phenomenonTime": "2010-01-01T00:00:00.000Z/2019-01-13T06:00:00.000Z", + "unitOfMeasurement": { + "name": "degree celcius", + "symbol": "°C", + "definition": "ucum:Cel", + }, + "Observations": [ + { + "result": 0.0 + } + ], + "@iot.id": 66, + "@iot.selfLink": "https://server.de/SensorThingsService/v1.0/Datastreams(66)", + }, + { + "name": "Precipitation Recoaro 1000", + "description": "The Precipitation at Recoaro 1000", + "observationType": "http://www.opengis.net/def/observationType/OGC-OM/2.0/OM_Measurement", + "phenomenonTime": "2010-01-01T00:00:00.000Z/2019-01-13T06:00:00.000Z", + "unitOfMeasurement": { + "name": "mm/h", + "symbol": "mm/h", + "definition": "ucum:mm/h" + }, + "@iot.id": 130, + "@iot.selfLink": "https://server.de/SensorThingsService/v1.0/Datastreams(130)" + } + ], + "Datastreams@iot.nextLink": "https://server.de/SensorThingsService/v1.0/Things(19)/" + "Datastreams?$top=2&$skip=2&$count=true", + "MultiDatastreams@iot.navigationLink": "https://server.de/SensorThingsService/v1.0/" + "Things(19)/MultiDatastreams", + "Locations@iot.navigationLink": "https://server.de/SensorThingsService/v1.0/Things(19)/Locations", + "HistoricalLocations@iot.navigationLink": "https://server.de/SensorThingsService/v1.0/" + "Things(19)/HistoricalLocations", + "@iot.id": 19, + "@iot.selfLink": "https://server.de/SensorThingsService/v1.0/Things(19)" + }, + { + "name": "Valdagno", + "description": "Weather station Valdagno", + "Datastreams@iot.navigationLink": "https://server.de/SensorThingsService/v1.0/" + "Things(20)/Datastreams", + "Datastreams": [], + "Datastreams@iot.count": 6, + "Datastreams@iot.nextLink": "https://server.de/SensorThingsService/v1.0/Things(20)/" + "Datastreams?$top=2&$skip=2&$count=true", + "MultiDatastreams@iot.navigationLink": "https://server.de/SensorThingsService/v1.0/" + "Things(20)/MultiDatastreams", + "Locations@iot.navigationLink": "https://server.de/SensorThingsService/v1.0/Things(20)/Locations", + "HistoricalLocations@iot.navigationLink": "https://server.de/SensorThingsService/v1.0/" + "Things(20)/HistoricalLocations", + "@iot.id": 20, + "@iot.selfLink": "https://server.de/SensorThingsService/v1.0/Things(20)" + } + ] + } + + things = frost_sta_client.utils.transform_json_to_entity_list(json_dict, 'frost_sta_client.model.thing.Thing') + + self.assertEqual('https://server.de/SensorThingsService/v1.0/Things?$top=2&$skip=14&$expand=Datastreams%28%24top%3D2%3B%24count%3Dtrue%29', things.next_link) + + thing = things.entities[0] + self.assertEqual(19, thing.id) + self.assertEqual('Recoaro 1000', thing.name) + self.assertEqual('Weather station Recoaro 1000', thing.description) + + ds_list = thing.datastreams + self.assertEqual('https://server.de/SensorThingsService/v1.0/Things(19)/Datastreams?$top=2&$skip=2&$count=true', + ds_list.next_link) + # self.assertEqual(6, ds_list.count) + + ds = ds_list.entities[0] + self.assertEqual('Air Temperature Recoaro 1000', ds.name) + self.assertEqual('The Air Temperature at Recoaro 1000', ds.description) + + result = ds.observations.entities[0].result + self.assertEqual(0, result) + + ds = ds_list.entities[1] + self.assertEqual(130, ds.id) + self.assertEqual('Precipitation Recoaro 1000', ds.name) + self.assertEqual('The Precipitation at Recoaro 1000', ds.description) + + thing = things.entities[1] + self.assertEqual(20, thing.id) + self.assertEqual('Valdagno', thing.name) + self.assertEqual('Weather station Valdagno', thing.description) + + ds_list = thing.datastreams + self.assertEqual('https://server.de/SensorThingsService/v1.0/Things(20)/Datastreams?$top=2&$skip=2&$count=true', + ds_list.next_link) + # self.assertEqual(6, ds_list.count) + + def test_read_empty_entity_list(self): + json_dict = {'value': []} + things = frost_sta_client.utils.transform_json_to_entity_list(json_dict, 'frost_sta_client.model.thing.Thing') + + self.assertEqual(None, things.next_link) + self.assertTrue(len(things.entities) == 0) + + def test_read_tasking_capabilities(self): + json_dict = { + 'name': 'createNewVA', + 'description': 'Virtual Actuator Server, starts new Virtual Actuators', + 'taskingParameters': { + 'type': 'DataRecord', + 'field': [{ + 'type': 'Text', + 'label': 'Aktor-Name', + 'description': 'Name des neuen virtuellen Aktors', + 'name': 'vaName' + }, { + 'type': 'Text', + 'label': 'Aktor-Beschreibung', + 'description': 'Beschreibung des neuen virtuellen Aktors', + 'name': 'vaDescription' + }] + }, + 'Actuator@iot.navigationLink': 'https://server.de/SensorThingsService/v1.0/Actuator', + 'Thing@iot.navigationLink': 'https://server.de/SensorThingsService/v1.0/TaskingCapabilities(1)/Thing', + 'Tasks@iot.navigationLink': 'https://server.de/SensorThingsService/v1.0/TaskingCapabilities(1)/Tasks', + '@iot.id': 1, + '@iot.selfLink': 'https://server.de/SensorThingsService/v1.0/TaskingCapabilities(1)'} + + capability = frost_sta_client.utils. \ + transform_json_to_entity(json_dict, + 'frost_sta_client.model.tasking_capability.TaskingCapability') + + expected = frost_sta_client.model.tasking_capability.TaskingCapability() + expected.name = 'createNewVA' + expected.description = 'Virtual Actuator Server, starts new Virtual Actuators' + expected.tasking_parameters = { + 'type': 'DataRecord', + 'field': [{ + 'type': 'Text', + 'label': 'Aktor-Name', + 'description': 'Name des neuen virtuellen Aktors', + 'name': 'vaName' + }, { + 'type': 'Text', + 'label': 'Aktor-Beschreibung', + 'description': 'Beschreibung des neuen virtuellen Aktors', + 'name': 'vaDescription' + }] + } + expected.id = 1 + expected.self_link = 'https://server.de/SensorThingsService/v1.0/TaskingCapabilities(1)' + + self.assertEqual(expected, capability) + + def test_read_tasking_capabilities_with_constraint(self): + json_dict = { + 'name': 'DatastreamCopierCapability', + 'description': 'Copies Observations from one Datastream to another', + 'taskingParameters': + { + 'type': 'DataRecord', + 'field': [{ + 'type': 'Count', + 'name': 'sourceDatastream', + 'label': 'Source Datastream', + 'description': 'ID of the datastream from which the observations should be taken.', + 'constraint': { + 'type': 'AllowedValues', + 'interval': [[0, 10000]] + } + }, + { + 'type': 'Count', + 'name': 'destinationDatastream', + 'label': 'Destination Datastream', + 'description': 'ID of the datastream to which the observations should be copied.', + 'constraint': { + 'type': 'AllowedValues', + 'interval': [[0, 10000]] + } + }] + }, + 'Actuator@iot.navigationLink': 'https://server.de/SensorThingsService/v1.0/Actuator', + 'Thing@iot.navigationLink': 'https://server.de/SensorThingsService/v1.0/TaskingCapabilities(1)/Thing', + 'Tasks@iot.navigationLink': 'https://server.de/SensorThingsService/v1.0/TaskingCapabilities(1)/Tasks', + '@iot.id': 1, + '@iot.selfLink': 'https://server.de/SensorThingsService/v1.0/TaskingCapabilities(1)' + } + + entity_class = 'frost_sta_client.model.tasking_capability.TaskingCapability' + capability = frost_sta_client.utils.transform_json_to_entity(json_dict, entity_class) + + expected = frost_sta_client.model.tasking_capability.TaskingCapability() + expected.name = 'DatastreamCopierCapability' + expected.description = 'Copies Observations from one Datastream to another' + expected.id = 1 + expected.tasking_parameters = { + 'type': 'DataRecord', + 'field': [{ + 'type': 'Count', + 'name': 'sourceDatastream', + 'label': 'Source Datastream', + 'description': 'ID of the datastream from which the observations should be taken.', + 'constraint': { + 'type': 'AllowedValues', + 'interval': [[0, 10000]] + } + }, + { + 'type': 'Count', + 'name': 'destinationDatastream', + 'label': 'Destination Datastream', + 'description': 'ID of the datastream to which the observations should be copied.', + 'constraint': { + 'type': 'AllowedValues', + 'interval': [[0, 10000]] + } + }] + } + + self.assertEqual(expected, capability) diff --git a/tests/test_error_handling_non_json.py b/tests/test_error_handling_non_json.py new file mode 100644 index 0000000..7cfbf3b --- /dev/null +++ b/tests/test_error_handling_non_json.py @@ -0,0 +1,66 @@ +import pytest +import requests +import json +from frost_sta_client.service.sensorthingsservice import SensorThingsService +from frost_sta_client.utils import transform_json_to_entity_list +from frost_sta_client.model.thing import Thing + + +class DummyService(SensorThingsService): + def __init__(self, url): + super().__init__(url) + + def execute(self, method, url, **kwargs): + class Resp: + status_code = 500 + text = "Server Error" + + def json(self): + raise ValueError("No JSON body") + + raise requests.exceptions.HTTPError(response=Resp()) + + +def test_query_handles_non_json_error(): + svc = DummyService('http://example.org/FROST-Server/v1.1') + with pytest.raises(requests.exceptions.HTTPError): + svc.things().query().list() + + +def test_basedao_handles_non_json_error(): + svc = DummyService('http://example.org/FROST-Server/v1.1') + with pytest.raises(requests.exceptions.HTTPError): + svc.things().find(1) + + +def test_entitylist_iter_handles_non_json_error(): + svc = DummyService('http://example.org/FROST-Server/v1.1') + page = {"value": [], "@iot.nextLink": "http://example.org/next"} + elist = transform_json_to_entity_list(page, 'frost_sta_client.model.thing.Thing') + elist.set_service(svc) + it = iter(elist) + with pytest.raises(requests.exceptions.HTTPError): + next(it) + + +class WorkingService(SensorThingsService): + def __init__(self, url): + super().__init__(url) + + def execute(self, method, url, **kwargs): + class Resp: + status_code = 400 + text = '{"code":400,"type":"error","message":"Not a valid path for DELETE."}' + + def json(self): + return json.loads(self.text) + + raise requests.exceptions.HTTPError(response=Resp()) + + +def test_basedao_handles_json_error(caplog): + svc = WorkingService('http://example.org/FROST-Server/v1.1/Things') + with pytest.raises(requests.exceptions.HTTPError): + thing = Thing(id=1) + svc.delete(thing) + assert caplog.messages[-1] == "Deleting Thing failed with status-code 400, Not a valid path for DELETE." diff --git a/tests/test_ext_data_array.py b/tests/test_ext_data_array.py new file mode 100644 index 0000000..3bf9150 --- /dev/null +++ b/tests/test_ext_data_array.py @@ -0,0 +1,20 @@ +import pytest +from frost_sta_client.model.ext.data_array_value import DataArrayValue as DAV +from frost_sta_client.model.observation import Observation +from frost_sta_client.model.datastream import Datastream +from frost_sta_client.model.feature_of_interest import FeatureOfInterest + + +def test_data_array_value_components_and_add_observation(): + dav = DAV() + ds = Datastream() + ds.id = 99 + dav.datastream = ds + components = {DAV.Property.PHENOMENON_TIME, DAV.Property.RESULT, DAV.Property.FEATURE_OF_INTEREST} + dav.components = components + o = Observation(result=3, phenomenon_time='2023-01-01T00:00:00Z', feature_of_interest=FeatureOfInterest(id=1), datastream=ds) + dav.add_observation(o) + state = dav.__getstate__() + assert 'components' in state and 'dataArray' in state and state['Datastream']['@iot.id'] == 99 + with pytest.raises(ValueError): + dav.components = components diff --git a/tests/test_ext_entity_type.py b/tests/test_ext_entity_type.py new file mode 100644 index 0000000..c8e8673 --- /dev/null +++ b/tests/test_ext_entity_type.py @@ -0,0 +1,7 @@ +from frost_sta_client.model.ext.entity_type import get_list_for_class +from frost_sta_client.model.thing import Thing + + +def test_get_list_for_class(): + t = Thing() + assert get_list_for_class(type(t)) == 'Things' diff --git a/tests/test_integration.py b/tests/test_integration.py new file mode 100644 index 0000000..733ed60 --- /dev/null +++ b/tests/test_integration.py @@ -0,0 +1,125 @@ +import pytest +import os +pytestmark = pytest.mark.skipif(os.environ.get('FROST_STA_CLIENT_RUN_INTEGRATION') != '1', reason='Integration tests require FROST server. Set FROST_STA_CLIENT_RUN_INTEGRATION=1 to run.') +from geojson import Point +from frost_sta_client.model import thing, sensor, observedproperty, datastream, observation, feature_of_interest +from frost_sta_client.model.ext import unitofmeasurement + + +def test_create_thing(sensorthings_service): + t = thing.Thing( + name='Test Thing', + description='A test thing') + sensorthings_service.create(t) + assert t.id is not None + + retrieved = sensorthings_service.things().find(t.id) + assert retrieved.name == 'Test Thing' + + +def test_crud_datastream(sensorthings_service): + # Create dependencies + t = thing.Thing( + name='Test Thing DS', + description='Thing for DS') + sensorthings_service.create(t) + s = sensor.Sensor( + name='Test Sensor', + description='Sensor', + encoding_type='application/pdf', + metadata='http://example.org/sensor.pdf') + sensorthings_service.create(s) + op = observedproperty.ObservedProperty( + name='Test OP', + definition='http://www.example.org/op', + description='OP') + sensorthings_service.create(op) + um = unitofmeasurement.UnitOfMeasurement( + name="degree Celsius", + symbol="°C", + definition="physical definition...") + + # Create Datastream + ds = datastream.Datastream( + name='Test DS', + description='DS', + observation_type='OM_Measurement', + unit_of_measurement=um, + thing=t, + sensor=s, + observed_property=op) + sensorthings_service.create(ds) + assert ds.id is not None + + # Read + retrieved_ds = sensorthings_service.datastreams().find(ds.id) + assert retrieved_ds.name == 'Test DS' + + # Update + retrieved_ds.description = 'Updated DS' + sensorthings_service.update(retrieved_ds) + updated_ds = sensorthings_service.datastreams().find(ds.id) + assert updated_ds.description == 'Updated DS' + + # Delete + sensorthings_service.delete(updated_ds) + with pytest.raises(Exception): + sensorthings_service.datastreams().find(ds.id) + + +def test_crud_observation(sensorthings_service): + # Create dependencies + t = thing.Thing( + name='Test Thing Obs', + description='Thing for Obs') + sensorthings_service.create(t) + s = sensor.Sensor( + name='Test Sensor Obs', + description='Sensor Obs', + encoding_type='application/pdf', + metadata='http://example.org/sensor_obs.pdf') + sensorthings_service.create(s) + op = observedproperty.ObservedProperty( + name='Test OP Obs', + definition='http://www.example.org/op_obs', + description='OP Obs') + sensorthings_service.create(op) + um = unitofmeasurement.UnitOfMeasurement( + name="degree Celsius", + symbol="°C", + definition="physical definition...") + ds = datastream.Datastream( + name='Test DS Obs', + description='DS Obs', + observation_type='OM_Measurement', + unit_of_measurement=um, + thing=t, + sensor=s, + observed_property=op) + sensorthings_service.create(ds) + point = Point((-115.81, 37.24)) + foi = feature_of_interest.FeatureOfInterest(name="here", description="and there", feature=point, encoding_type='application/geo+json') + + # Create Observation + obs = observation.Observation( + result=25.0, + phenomenon_time='2023-01-01T00:00:00Z', + datastream=ds, + feature_of_interest=foi) + sensorthings_service.create(obs) + assert obs.id is not None + + # Read + retrieved_obs = sensorthings_service.observations().find(obs.id) + assert retrieved_obs.result == 25.0 + + # Update + retrieved_obs.result = 30.0 + sensorthings_service.update(retrieved_obs) + updated_obs = sensorthings_service.observations().find(obs.id) + assert updated_obs.result == 30.0 + + # Delete + sensorthings_service.delete(updated_obs) + with pytest.raises(Exception): + sensorthings_service.observations().find(obs.id) diff --git a/tests/test_query_unit.py b/tests/test_query_unit.py new file mode 100644 index 0000000..ccd0695 --- /dev/null +++ b/tests/test_query_unit.py @@ -0,0 +1,42 @@ +import requests +import frost_sta_client.model.ext.entity_list +from frost_sta_client.service.sensorthingsservice import SensorThingsService + + +class MockResponse: + def __init__(self, status_code=200, json_data=None): + self.status_code = status_code + self._json = json_data if json_data is not None else {} + + def json(self): + return self._json + + def raise_for_status(self): + if self.status_code >= 400: + raise requests.exceptions.HTTPError(response=self) + + +class DummyService(SensorThingsService): + def __init__(self, url, responses): + super().__init__(url) + self.responses = list(responses) + self.calls = [] + + def execute(self, method, url, **kwargs): + self.calls.append((method, str(url))) + if self.responses: + return self.responses.pop(0) + return MockResponse(200, {"value": [], "@iot.nextLink": None}) + + +def test_query_builds_and_lists(): + first = MockResponse(200, {"value": [{"@iot.id": 1, "name": "A"}], "@iot.nextLink": None}) + svc = DummyService('http://example.org/FROST-Server/v1.1', [first]) + lst = svc.things().query().filter("name eq 'A'").select('name').orderby('name', 'ASC').top(1).skip(0).expand('Datastreams').list() + assert len(svc.calls) == 1 + assert 'get' == svc.calls[0][0] + assert 'Things' in svc.calls[0][1] + assert '%24filter' in svc.calls[0][1] + assert len(lst.entities) == 1 + assert isinstance(lst, frost_sta_client.model.ext.entity_list.EntityList) + assert isinstance(lst.entities[0], frost_sta_client.model.thing.Thing) diff --git a/tests/test_service_unit.py b/tests/test_service_unit.py new file mode 100644 index 0000000..1889283 --- /dev/null +++ b/tests/test_service_unit.py @@ -0,0 +1,107 @@ +import pytest +import requests +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 + + +def test_get_path_numeric_id(): + svc = SensorThingsService('http://example.org/FROST-Server/v1.1') + t = Thing(id=1) + assert svc.get_path(t, 'Datastreams') == 'Things(1)/Datastreams' + + +def test_get_path_string_id(): + svc = SensorThingsService('http://example.org/FROST-Server/v1.1/') + t = Thing(id='abc') + assert svc.get_path(t, 'Locations') == "Things('abc')/Locations" + + +def test_get_full_path_handles_trailing_slash(): + svc = SensorThingsService('http://example.org/FROST-Server/v1.1/') + t = Thing(id=2) + full = svc.get_full_path(t, 'Datastreams') + assert str(full) == 'http://example.org/FROST-Server/v1.1/Things(2)/Datastreams' + + +def test_auth_handler_type_check(): + svc = SensorThingsService('http://example.org/FROST-Server/v1.1') + with pytest.raises(ValueError): + svc.auth_handler = 'not-auth' + + +def test_proxies_type_check(): + svc = SensorThingsService('http://example.org/FROST-Server/v1.1') + with pytest.raises(ValueError): + svc.proxies = 'not-a-dict' + svc.proxies = {'http': 'http://proxy'} + + +def test_execute_uses_auth(monkeypatch): + svc = SensorThingsService('http://example.org/FROST-Server/v1.1') + svc.auth_handler = AuthHandler('user', 'pass') + captured = {} + + + def fake_request(method, url, proxies=None, auth=None, **kwargs): + captured['auth'] = auth + class R: + status_code = 200 + def raise_for_status(self): + pass + def json(self): + return {} + return R() + + 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) + diff --git a/tests/test_utils_more.py b/tests/test_utils_more.py new file mode 100644 index 0000000..8f8765e --- /dev/null +++ b/tests/test_utils_more.py @@ -0,0 +1,33 @@ +import pytest +from frost_sta_client import utils + + +def test_extract_value_numeric_and_string(): + assert utils.extract_value('Things(22)') == 22 + assert utils.extract_value("Things('abc')") == 'abc' + + +def test_transform_json_to_entity_list_with_dict(): + data = {"value": [{"@iot.id": 1, "name": "A"}]} + elist = utils.transform_json_to_entity_list(data, 'frost_sta_client.model.thing.Thing') + assert len(elist.entities) == 1 + + +def test_transform_json_to_entity_list_with_list(): + data = [{"@iot.id": 2, "name": "B"}] + elist = utils.transform_json_to_entity_list(data, 'frost_sta_client.model.thing.Thing') + assert len(elist.entities) == 1 + + +def test_parse_datetime_invalid(): + with pytest.raises(ValueError): + utils.parse_datetime('invalid') + + +def test_process_area_point_and_polygon(): + p = utils.process_area({"type": "Point", "coordinates": [1, 2]}) + assert getattr(p, 'type', 'Point') == 'Point' + poly = utils.process_area({"type": "Polygon", "coordinates": [[[0, 0], [1, 0], [1, 1], [0, 1], [0, 0]]]}) + assert getattr(poly, 'type', 'Polygon') == 'Polygon' + with pytest.raises(ValueError): + utils.process_area({"type": "Unknown", "coordinates": []}) From e077904b24019493bfa23b9ad4a359406e17038c Mon Sep 17 00:00:00 2001 From: Marc Adolf Date: Wed, 15 Oct 2025 10:33:04 +0200 Subject: [PATCH 18/26] removed unnessecary lines --- .gitlab-ci.yml | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 10f794e..89dcec2 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -42,6 +42,7 @@ test_package: junit: tests/test_results.xml paths: - tests/test_results.xml + upload_package: stage: release @@ -50,10 +51,6 @@ upload_package: - twine upload --verbose --repository-url $TWINE_REPO_URL dist/* dependencies: - build_wheel - only: - - main - - tags - - twine upload --verbose --repository-url $TWINE_REPO_URL dist/* dependencies: - build_wheel only: From 5bb6bbc552db1c5ddbc4f3128cd100397638be73 Mon Sep 17 00:00:00 2001 From: Marc Adolf Date: Wed, 15 Oct 2025 08:38:21 +0000 Subject: [PATCH 19/26] Update python-publish.yml --- .gitlab-ci.yml | 10 +--------- 1 file changed, 1 insertion(+), 9 deletions(-) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 10f794e..3fe6151 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -8,8 +8,6 @@ variables: image: python:3.14 stages: - - build - - test - build - test - release @@ -42,20 +40,14 @@ test_package: junit: tests/test_results.xml paths: - tests/test_results.xml + upload_package: stage: release - script: - pip install twine - twine upload --verbose --repository-url $TWINE_REPO_URL dist/* dependencies: - build_wheel - only: - - main - - tags - - twine upload --verbose --repository-url $TWINE_REPO_URL dist/* - dependencies: - - build_wheel only: - main - tags From 696d723f18d3a0e039f352133f72e54e53dc899c Mon Sep 17 00:00:00 2001 From: Marc Adolf Date: Wed, 15 Oct 2025 10:44:10 +0200 Subject: [PATCH 20/26] just tags should trigger a new package push --- .gitlab-ci.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 3fe6151..6582fd3 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -49,6 +49,5 @@ upload_package: dependencies: - build_wheel only: - - main - tags allow_failure: false # uploading same version results in failure. \ No newline at end of file From 75e4a2093cbd5f4f244a6a258189705c45a197aa Mon Sep 17 00:00:00 2001 From: Marc Adolf Date: Wed, 15 Oct 2025 10:52:28 +0200 Subject: [PATCH 21/26] updated readme with session --- README.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) 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 From 499e3e70283ed57af1a75014f384c0533ef86db1 Mon Sep 17 00:00:00 2001 From: Marc Adolf Date: Wed, 15 Oct 2025 10:55:37 +0200 Subject: [PATCH 22/26] wrongfully readded main branch at release job --- .gitlab-ci.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 3fe6151..6582fd3 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -49,6 +49,5 @@ upload_package: dependencies: - build_wheel only: - - main - tags allow_failure: false # uploading same version results in failure. \ No newline at end of file From 03b096593c60de75032cd11e7626e9e77c13a0c6 Mon Sep 17 00:00:00 2001 From: Marc Adolf Date: Wed, 15 Oct 2025 08:57:36 +0000 Subject: [PATCH 23/26] Update python-publish.yml --- .gitlab-ci.yml | 1 - README.md | 3 ++- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 3fe6151..6582fd3 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -49,6 +49,5 @@ upload_package: dependencies: - build_wheel only: - - main - tags allow_failure: false # uploading same version results in failure. \ No newline at end of file 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 From 5766c0f5db7fab8f6ba3edd5b18ee84e98a27f10 Mon Sep 17 00:00:00 2001 From: Marc Adolf Date: Wed, 15 Oct 2025 11:06:10 +0200 Subject: [PATCH 24/26] removed gitlab ci and set version to match github --- .gitlab-ci.yml | 53 --------------------------------- frost_sta_client/__version__.py | 2 +- 2 files changed, 1 insertion(+), 54 deletions(-) delete mode 100644 .gitlab-ci.yml diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml deleted file mode 100644 index 6582fd3..0000000 --- a/.gitlab-ci.yml +++ /dev/null @@ -1,53 +0,0 @@ -variables: - - TWINE_PASSWORD: ${CI_JOB_TOKEN} - TWINE_USERNAME: gitlab-ci-token - TWINE_REPO_URL: https://git.geomar.de/api/v4/projects/${CI_PROJECT_ID}/packages/pypi - - -image: python:3.14 - -stages: - - build - - test - - release - -build_wheel: - stage: build - script: - - python -V - - pip install virtualenv - - virtualenv venv - - source venv/bin/activate - - pip install build - - python -m build - artifacts: - paths: - - dist/*.whl - -test_package: - stage: test - script: - - python -V - - pip install -r requirements.txt - - pip install pytest pytest-cov - - python -m pytest --maxfail=1 --disable-warnings -q --junitxml=tests/test_results.xml - dependencies: - - build_wheel - artifacts: - when: always - reports: - junit: tests/test_results.xml - paths: - - tests/test_results.xml - -upload_package: - stage: release - script: - - pip install twine - - twine upload --verbose --repository-url $TWINE_REPO_URL dist/* - dependencies: - - build_wheel - only: - - tags - allow_failure: false # uploading same version results in failure. \ No newline at end of file diff --git a/frost_sta_client/__version__.py b/frost_sta_client/__version__.py index d524e99..47d1ff1 100644 --- a/frost_sta_client/__version__.py +++ b/frost_sta_client/__version__.py @@ -1,5 +1,5 @@ __title__ = 'frost_sta_client' -__version__ = '1.1.54' +__version__ = '1.1.53' __license__ = 'LGPL3' __author__ = 'Fraunhofer IOSB' __copyright__ = 'Fraunhofer IOSB' From 0d9148a23f4aaca0e4ab8887489dce389538e8ad Mon Sep 17 00:00:00 2001 From: Marc Adolf Date: Wed, 15 Oct 2025 11:13:11 +0200 Subject: [PATCH 25/26] removed config object --- frost_sta_client/config.py | 19 ------------------- 1 file changed, 19 deletions(-) delete mode 100644 frost_sta_client/config.py diff --git a/frost_sta_client/config.py b/frost_sta_client/config.py deleted file mode 100644 index 2a88b4a..0000000 --- a/frost_sta_client/config.py +++ /dev/null @@ -1,19 +0,0 @@ -import os - - -class Config(object): - ## configure request retries - # Total number of retries to allow. Takes precedence over other counts. - total_retries = os.environ.get("HTTP_RETRY_TOTAL", 20) - # How many connection-related errors to retry on. - connect = os.environ.get("HTTP_RETRY_CONNECT", 15) - # A backoff factor to apply between attempts after the second try - backoff_factor = os.environ.get("HTTP_RETRY_BACKOFF_FACTOR", 0.3) - # A set of integer HTTP status codes that we should force a retry on - status_forcelist = os.environ.get("HTTP_RETRY_STATUS_FORCELIST", [500, 502, 503, 504]) - ## Enable HTTP_AUTH - HTTP_AUTH = os.environ.get("HTTP_AUTH") or False - if HTTP_AUTH: - HTTP_AUTH_USER = os.environ.get("HTTP_AUTH_USER") - HTTP_AUTH_PASSWORD = os.environ.get("HTTP_AUTH_PASSWORD") - \ No newline at end of file From a1e0cc6041762fd768433dad74272b58b64a053d Mon Sep 17 00:00:00 2001 From: Marc Adolf Date: Wed, 15 Oct 2025 11:19:02 +0200 Subject: [PATCH 26/26] removed changed to thing --- frost_sta_client/model/thing.py | 10 +--------- 1 file changed, 1 insertion(+), 9 deletions(-) diff --git a/frost_sta_client/model/thing.py b/frost_sta_client/model/thing.py index 93bf19e..5de1b32 100644 --- a/frost_sta_client/model/thing.py +++ b/frost_sta_client/model/thing.py @@ -231,18 +231,10 @@ def __eq__(self, other): return False if self.description != other.description: return False - if not self._important_properties_are_equal(self.properties, other.properties): + if self.properties != other.properties: return False return True - def _important_properties_are_equal(self, prop1, prop2): - #fields are variable properties, that should not be compared - if prop1 is None or prop2 is None: - return prop1 == prop2 - filtered_prop1 = {k: v for k, v in prop1.items() if k != "fields"} - filtered_prop2 = {k: v for k, v in prop2.items() if k != "fields"} - return filtered_prop1 == filtered_prop2 - def __ne__(self, other): return not self == other