├── tests ├── context.py ├── test_ext_entity_type.py ├── test_entity_behaviour.py ├── test_ext_data_array.py ├── test_utils.py ├── test_utils_more.py ├── test_entity_list.py ├── test_query_unit.py ├── conftest.py ├── test_service_unit.py ├── test_dao_base.py ├── test_error_handling_non_json.py ├── test_integration.py └── test_entity_formatter.py ├── frost_sta_client ├── model │ ├── ext │ │ ├── __init__.py │ │ ├── data_array_document.py │ │ ├── unitofmeasurement.py │ │ ├── entity_type.py │ │ ├── entity_list.py │ │ └── data_array_value.py │ ├── __init__.py │ ├── entity.py │ ├── task.py │ ├── historical_location.py │ ├── actuator.py │ ├── feature_of_interest.py │ ├── tasking_capability.py │ ├── observedproperty.py │ ├── sensor.py │ ├── observation.py │ ├── location.py │ ├── datastream.py │ └── thing.py ├── query │ ├── __init__.py │ └── query.py ├── service │ ├── __init__.py │ ├── auth_handler.py │ └── sensorthingsservice.py ├── dao │ ├── __init__.py │ ├── task.py │ ├── actuator.py │ ├── thing.py │ ├── sensor.py │ ├── datastream.py │ ├── location.py │ ├── features_of_interest.py │ ├── multi_datastream.py │ ├── observedproperty.py │ ├── tasking_capability.py │ ├── historical_location.py │ ├── observation.py │ └── base.py ├── __version__.py ├── __init__.py └── utils.py ├── setup.cfg ├── .gitignore ├── requirements.txt ├── setup.py ├── .github └── workflows │ ├── python-publish.yml │ └── python-app.yml ├── frost_server └── docker-compose.yaml ├── README.md └── LICENSE /tests/context.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /frost_sta_client/model/ext/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /frost_sta_client/query/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [bdist_wheel] 2 | universal = 1 3 | 4 | [metadata] 5 | license_files = LICENSE -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | dist 3 | **/__pycache__ 4 | **.egg-info/ 5 | 6 | pyproject.toml 7 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | jsonpickle<=2.2.0 2 | demjson3 3 | requests 4 | furl 5 | geojson 6 | jsonpatch 7 | python-dateutil 8 | -------------------------------------------------------------------------------- /frost_sta_client/service/__init__.py: -------------------------------------------------------------------------------- 1 | from frost_sta_client.service import sensorthingsservice 2 | from frost_sta_client.service import auth_handler 3 | -------------------------------------------------------------------------------- /frost_sta_client/dao/__init__.py: -------------------------------------------------------------------------------- 1 | __all__ = ['actuator', 'base', 'datastream', 'features_of_interest', 'historical_location', 'location', 2 | 'multi_datastream', 'observation', 'observedproperty', 'sensor', 'task', 'tasking_capability', 'thing'] 3 | -------------------------------------------------------------------------------- /tests/test_ext_entity_type.py: -------------------------------------------------------------------------------- 1 | from frost_sta_client.model.ext.entity_type import get_list_for_class 2 | from frost_sta_client.model.thing import Thing 3 | 4 | 5 | def test_get_list_for_class(): 6 | t = Thing() 7 | assert get_list_for_class(type(t)) == 'Things' 8 | -------------------------------------------------------------------------------- /frost_sta_client/__version__.py: -------------------------------------------------------------------------------- 1 | __title__ = 'frost_sta_client' 2 | __version__ = '1.1.53' 3 | __license__ = 'LGPL3' 4 | __author__ = 'Fraunhofer IOSB' 5 | __copyright__ = 'Fraunhofer IOSB' 6 | __contact__ = 'frost@iosb.fraunhofer.de' 7 | __url__ = 'https://github.com/FraunhoferIOSB/FROST-Python-Client' 8 | __description__ = 'a client library to facilitate interaction with a FROST SensorThingsAPI Server' 9 | -------------------------------------------------------------------------------- /frost_sta_client/model/__init__.py: -------------------------------------------------------------------------------- 1 | from frost_sta_client.model import actuator 2 | from frost_sta_client.model import datastream 3 | from frost_sta_client.model import entity 4 | from frost_sta_client.model import feature_of_interest 5 | from frost_sta_client.model import historical_location 6 | from frost_sta_client.model import location 7 | from frost_sta_client.model import multi_datastream 8 | from frost_sta_client.model import observation 9 | from frost_sta_client.model import observedproperty 10 | from frost_sta_client.model import sensor 11 | from frost_sta_client.model import task 12 | from frost_sta_client.model import tasking_capability 13 | from frost_sta_client.model import thing 14 | -------------------------------------------------------------------------------- /tests/test_entity_behaviour.py: -------------------------------------------------------------------------------- 1 | from frost_sta_client.service.sensorthingsservice import SensorThingsService 2 | from frost_sta_client.model.thing import Thing 3 | from frost_sta_client.model.location import Location 4 | from frost_sta_client.model.ext.entity_list import EntityList 5 | from frost_sta_client.model.ext.entity_type import EntityTypes 6 | 7 | 8 | def test_entity_equality_by_id(): 9 | a = Thing(id=1, name='A') 10 | b = Thing(id=1, name='B') 11 | assert a != b 12 | 13 | 14 | def test_set_service_propagates_to_children(): 15 | t = Thing(name='T') 16 | loc = Location(name='L') 17 | t.locations = EntityList(entity_class=EntityTypes['Location']['class'], entities=[loc]) 18 | svc = SensorThingsService('http://example.org/FROST-Server/v1.1') 19 | t.set_service(svc) 20 | assert t.service is svc 21 | assert t.locations.entities[0].service is svc 22 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | import os 2 | from setuptools import setup, find_packages 3 | 4 | here = os.path.abspath(os.path.dirname(__file__)) 5 | info = {} 6 | with open(os.path.join(here, 'frost_sta_client', '__version__.py')) as f: 7 | exec(f.read(), info) 8 | 9 | with open("README.md", "r", encoding="utf-8") as fh: 10 | long_description = fh.read() 11 | 12 | setup( 13 | name=info['__title__'], 14 | version=info['__version__'], 15 | description=info['__description__'], 16 | long_description=long_description, 17 | long_description_content_type='text/markdown', 18 | author=info['__author__'], 19 | author_email=info['__contact__'], 20 | license=info['__license__'], 21 | url=info['__url__'], 22 | packages=find_packages(), 23 | install_requires=['demjson3>=3.0.5', 'furl>=2.1.3', 'geojson>=2.5.0', 'jsonpickle>=2.0.0', 'requests>=2.26.0', 24 | 'jsonpatch', 'python-dateutil'], 25 | keywords=['sta', 'ogc', 'frost', 'sensorthingsapi', 'IoT'] 26 | ) 27 | -------------------------------------------------------------------------------- /tests/test_ext_data_array.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from frost_sta_client.model.ext.data_array_value import DataArrayValue as DAV 3 | from frost_sta_client.model.observation import Observation 4 | from frost_sta_client.model.datastream import Datastream 5 | from frost_sta_client.model.feature_of_interest import FeatureOfInterest 6 | 7 | 8 | def test_data_array_value_components_and_add_observation(): 9 | dav = DAV() 10 | ds = Datastream() 11 | ds.id = 99 12 | dav.datastream = ds 13 | components = {DAV.Property.PHENOMENON_TIME, DAV.Property.RESULT, DAV.Property.FEATURE_OF_INTEREST} 14 | dav.components = components 15 | o = Observation(result=3, phenomenon_time='2023-01-01T00:00:00Z', feature_of_interest=FeatureOfInterest(id=1), datastream=ds) 16 | dav.add_observation(o) 17 | state = dav.__getstate__() 18 | assert 'components' in state and 'dataArray' in state and state['Datastream']['@iot.id'] == 99 19 | with pytest.raises(ValueError): 20 | dav.components = components 21 | -------------------------------------------------------------------------------- /tests/test_utils.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | from frost_sta_client.utils import parse_datetime 4 | import datetime 5 | 6 | 7 | class TestUtils(unittest.TestCase): 8 | 9 | def test_parse_iso_time_with_timezone(self): 10 | parsedtime = parse_datetime('2022-04-07T14:00:00+02:00') 11 | self.assertEqual('2022-04-07T14:00:00+02:00', parsedtime) 12 | 13 | def test_parse_iso_time(self): 14 | parsedtime = parse_datetime('2022-04-07T14:00:00Z') 15 | self.assertEqual('2022-04-07T14:00:00+00:00', parsedtime) 16 | 17 | def test_parse_interval(self): 18 | parsedtime = parse_datetime('2022-04-07T14:00:00Z/2022-04-07T15:00:00Z') 19 | self.assertEqual('2022-04-07T14:00:00+00:00/2022-04-07T15:00:00+00:00', parsedtime) 20 | 21 | def test_parse_interval_with_timezone_offset(self): 22 | parsedtime = parse_datetime('2022-04-07T14:00:00+02:00/2022-04-07T15:00:00+02:00') 23 | self.assertEqual('2022-04-07T14:00:00+02:00/2022-04-07T15:00:00+02:00', parsedtime) 24 | 25 | -------------------------------------------------------------------------------- /frost_sta_client/dao/task.py: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2021 Fraunhofer Institut IOSB, Fraunhoferstr. 1, D 76131 2 | # Karlsruhe, Germany. 3 | # 4 | # This program is free software: you can redistribute it and/or modify 5 | # it under the terms of the GNU Lesser General Public License as published by 6 | # the Free Software Foundation, either version 3 of the License, or 7 | # (at your option) any later version. 8 | # 9 | # This program is distributed in the hope that it will be useful, 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | # GNU Lesser General Public License for more details. 13 | # 14 | # You should have received a copy of the GNU Lesser General Public License 15 | # along with this program. If not, see . 16 | 17 | from frost_sta_client.dao import base 18 | from frost_sta_client.model.ext.entity_type import EntityTypes 19 | 20 | 21 | class TaskDao(base.BaseDao): 22 | def __init__(self, service): 23 | """ 24 | A data access object for operations with the Task entity 25 | """ 26 | base.BaseDao.__init__(self, service, EntityTypes['Task']) 27 | -------------------------------------------------------------------------------- /frost_sta_client/dao/actuator.py: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2021 Fraunhofer Institut IOSB, Fraunhoferstr. 1, D 76131 2 | # Karlsruhe, Germany. 3 | # 4 | # This program is free software: you can redistribute it and/or modify 5 | # it under the terms of the GNU Lesser General Public License as published by 6 | # the Free Software Foundation, either version 3 of the License, or 7 | # (at your option) any later version. 8 | # 9 | # This program is distributed in the hope that it will be useful, 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | # GNU Lesser General Public License for more details. 13 | # 14 | # You should have received a copy of the GNU Lesser General Public License 15 | # along with this program. If not, see . 16 | 17 | from frost_sta_client.dao import base 18 | from frost_sta_client.model.ext.entity_type import EntityTypes 19 | 20 | 21 | class ActuatorDao(base.BaseDao): 22 | """ 23 | A data access object for operations with the Actuator entity 24 | """ 25 | def __init__(self, service): 26 | base.BaseDao.__init__(self, service, EntityTypes["Actuator"]) 27 | -------------------------------------------------------------------------------- /frost_sta_client/dao/thing.py: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2021 Fraunhofer Institut IOSB, Fraunhoferstr. 1, D 76131 2 | # Karlsruhe, Germany. 3 | # 4 | # This program is free software: you can redistribute it and/or modify 5 | # it under the terms of the GNU Lesser General Public License as published by 6 | # the Free Software Foundation, either version 3 of the License, or 7 | # (at your option) any later version. 8 | # 9 | # This program is distributed in the hope that it will be useful, 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | # GNU Lesser General Public License for more details. 13 | # 14 | # You should have received a copy of the GNU Lesser General Public License 15 | # along with this program. If not, see . 16 | 17 | from frost_sta_client.dao import base 18 | from frost_sta_client.model.ext.entity_type import EntityTypes 19 | 20 | 21 | class ThingDao(base.BaseDao): 22 | def __init__(self, service): 23 | """ 24 | A data access object for operations with the Thing entity 25 | """ 26 | base.BaseDao.__init__(self, service, EntityTypes["Thing"]) 27 | -------------------------------------------------------------------------------- /frost_sta_client/dao/sensor.py: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2021 Fraunhofer Institut IOSB, Fraunhoferstr. 1, D 76131 2 | # Karlsruhe, Germany. 3 | # 4 | # This program is free software: you can redistribute it and/or modify 5 | # it under the terms of the GNU Lesser General Public License as published by 6 | # the Free Software Foundation, either version 3 of the License, or 7 | # (at your option) any later version. 8 | # 9 | # This program is distributed in the hope that it will be useful, 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | # GNU Lesser General Public License for more details. 13 | # 14 | # You should have received a copy of the GNU Lesser General Public License 15 | # along with this program. If not, see . 16 | 17 | from frost_sta_client.dao import base 18 | from frost_sta_client.model.ext.entity_type import EntityTypes 19 | 20 | 21 | class SensorDao(base.BaseDao): 22 | def __init__(self, service): 23 | """ 24 | A data access object for operations with the Sensor entity 25 | """ 26 | base.BaseDao.__init__(self, service, EntityTypes['Sensor']) 27 | -------------------------------------------------------------------------------- /frost_sta_client/dao/datastream.py: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2021 Fraunhofer Institut IOSB, Fraunhoferstr. 1, D 76131 2 | # Karlsruhe, Germany. 3 | # 4 | # This program is free software: you can redistribute it and/or modify 5 | # it under the terms of the GNU Lesser General Public License as published by 6 | # the Free Software Foundation, either version 3 of the License, or 7 | # (at your option) any later version. 8 | # 9 | # This program is distributed in the hope that it will be useful, 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | # GNU Lesser General Public License for more details. 13 | # 14 | # You should have received a copy of the GNU Lesser General Public License 15 | # along with this program. If not, see . 16 | 17 | from frost_sta_client.dao import base 18 | from frost_sta_client.model.ext.entity_type import EntityTypes 19 | 20 | 21 | class DatastreamDao(base.BaseDao): 22 | """ 23 | A data access object for operations with the Datastream entity 24 | """ 25 | def __init__(self, service): 26 | base.BaseDao.__init__(self, service, EntityTypes["Datastream"]) 27 | -------------------------------------------------------------------------------- /frost_sta_client/dao/location.py: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2021 Fraunhofer Institut IOSB, Fraunhoferstr. 1, D 76131 2 | # Karlsruhe, Germany. 3 | # 4 | # This program is free software: you can redistribute it and/or modify 5 | # it under the terms of the GNU Lesser General Public License as published by 6 | # the Free Software Foundation, either version 3 of the License, or 7 | # (at your option) any later version. 8 | # 9 | # This program is distributed in the hope that it will be useful, 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | # GNU Lesser General Public License for more details. 13 | # 14 | # You should have received a copy of the GNU Lesser General Public License 15 | # along with this program. If not, see . 16 | 17 | from frost_sta_client.dao import base 18 | from frost_sta_client.model.ext.entity_type import EntityTypes 19 | 20 | 21 | class LocationDao(base.BaseDao): 22 | def __init__(self, service): 23 | """ 24 | A data access object for operations with the Location entity 25 | """ 26 | base.BaseDao.__init__(self, service, EntityTypes["Location"]) 27 | -------------------------------------------------------------------------------- /frost_sta_client/service/auth_handler.py: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2021 Fraunhofer Institut IOSB, Fraunhoferstr. 1, D 76131 2 | # Karlsruhe, Germany. 3 | # 4 | # This program is free software: you can redistribute it and/or modify 5 | # it under the terms of the GNU Lesser General Public License as published by 6 | # the Free Software Foundation, either version 3 of the License, or 7 | # (at your option) any later version. 8 | # 9 | # This program is distributed in the hope that it will be useful, 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | # GNU Lesser General Public License for more details. 13 | # 14 | # You should have received a copy of the GNU Lesser General Public License 15 | # along with this program. If not, see . 16 | 17 | from requests.auth import HTTPBasicAuth 18 | 19 | 20 | class AuthHandler: 21 | def __init__(self, username="", password=""): 22 | self.username = username 23 | self.password = password 24 | 25 | def add_auth_header(self): 26 | if not (self.username is None or self.password is None): 27 | return HTTPBasicAuth(self.username, self.password) 28 | -------------------------------------------------------------------------------- /frost_sta_client/dao/features_of_interest.py: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2021 Fraunhofer Institut IOSB, Fraunhoferstr. 1, D 76131 2 | # Karlsruhe, Germany. 3 | # 4 | # This program is free software: you can redistribute it and/or modify 5 | # it under the terms of the GNU Lesser General Public License as published by 6 | # the Free Software Foundation, either version 3 of the License, or 7 | # (at your option) any later version. 8 | # 9 | # This program is distributed in the hope that it will be useful, 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | # GNU Lesser General Public License for more details. 13 | # 14 | # You should have received a copy of the GNU Lesser General Public License 15 | # along with this program. If not, see . 16 | 17 | from frost_sta_client.dao import base 18 | from frost_sta_client.model.ext.entity_type import EntityTypes 19 | 20 | 21 | class FeaturesOfInterestDao(base.BaseDao): 22 | """ 23 | A data access object for operations with the FeatureOfInterest entity 24 | """ 25 | def __init__(self, service): 26 | base.BaseDao.__init__(self, service, EntityTypes["FeatureOfInterest"]) 27 | -------------------------------------------------------------------------------- /frost_sta_client/dao/multi_datastream.py: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2021 Fraunhofer Institut IOSB, Fraunhoferstr. 1, D 76131 2 | # Karlsruhe, Germany. 3 | # 4 | # This program is free software: you can redistribute it and/or modify 5 | # it under the terms of the GNU Lesser General Public License as published by 6 | # the Free Software Foundation, either version 3 of the License, or 7 | # (at your option) any later version. 8 | # 9 | # This program is distributed in the hope that it will be useful, 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | # GNU Lesser General Public License for more details. 13 | # 14 | # You should have received a copy of the GNU Lesser General Public License 15 | # along with this program. If not, see . 16 | 17 | from frost_sta_client.dao import base 18 | from frost_sta_client.model.ext.entity_type import EntityTypes 19 | 20 | 21 | class MultiDatastreamDao(base.BaseDao): 22 | def __init__(self, service): 23 | """ 24 | A data access object for operations with the MultiDatastream entity 25 | """ 26 | base.BaseDao.__init__(self, service, EntityTypes["MultiDatastream"]) 27 | -------------------------------------------------------------------------------- /frost_sta_client/dao/observedproperty.py: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2021 Fraunhofer Institut IOSB, Fraunhoferstr. 1, D 76131 2 | # Karlsruhe, Germany. 3 | # 4 | # This program is free software: you can redistribute it and/or modify 5 | # it under the terms of the GNU Lesser General Public License as published by 6 | # the Free Software Foundation, either version 3 of the License, or 7 | # (at your option) any later version. 8 | # 9 | # This program is distributed in the hope that it will be useful, 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | # GNU Lesser General Public License for more details. 13 | # 14 | # You should have received a copy of the GNU Lesser General Public License 15 | # along with this program. If not, see . 16 | 17 | from frost_sta_client.dao import base 18 | from frost_sta_client.model.ext.entity_type import EntityTypes 19 | 20 | 21 | class ObservedPropertyDao(base.BaseDao): 22 | def __init__(self, service): 23 | """ 24 | A data access object for operations with the ObservedProperty entity 25 | """ 26 | base.BaseDao.__init__(self, service, EntityTypes["ObservedProperty"]) 27 | -------------------------------------------------------------------------------- /frost_sta_client/dao/tasking_capability.py: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2021 Fraunhofer Institut IOSB, Fraunhoferstr. 1, D 76131 2 | # Karlsruhe, Germany. 3 | # 4 | # This program is free software: you can redistribute it and/or modify 5 | # it under the terms of the GNU Lesser General Public License as published by 6 | # the Free Software Foundation, either version 3 of the License, or 7 | # (at your option) any later version. 8 | # 9 | # This program is distributed in the hope that it will be useful, 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | # GNU Lesser General Public License for more details. 13 | # 14 | # You should have received a copy of the GNU Lesser General Public License 15 | # along with this program. If not, see . 16 | 17 | from frost_sta_client.dao import base 18 | from frost_sta_client.model.ext.entity_type import EntityTypes 19 | 20 | 21 | class TaskingCapabilityDao(base.BaseDao): 22 | def __init__(self, service): 23 | """ 24 | A data access object for operations with the TaskingCapability entity 25 | """ 26 | base.BaseDao.__init__(self, service, EntityTypes['TaskingCapability']) 27 | -------------------------------------------------------------------------------- /frost_sta_client/dao/historical_location.py: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2021 Fraunhofer Institut IOSB, Fraunhoferstr. 1, D 76131 2 | # Karlsruhe, Germany. 3 | # 4 | # This program is free software: you can redistribute it and/or modify 5 | # it under the terms of the GNU Lesser General Public License as published by 6 | # the Free Software Foundation, either version 3 of the License, or 7 | # (at your option) any later version. 8 | # 9 | # This program is distributed in the hope that it will be useful, 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | # GNU Lesser General Public License for more details. 13 | # 14 | # You should have received a copy of the GNU Lesser General Public License 15 | # along with this program. If not, see . 16 | 17 | from frost_sta_client.dao import base 18 | from frost_sta_client.model.ext.entity_type import EntityTypes 19 | 20 | 21 | class HistoricalLocationDao(base.BaseDao): 22 | def __init__(self, service): 23 | """ 24 | A data access object for operations with the HistoricalLocation entity 25 | """ 26 | base.BaseDao.__init__(self, service, EntityTypes["HistoricalLocation"]) 27 | -------------------------------------------------------------------------------- /tests/test_utils_more.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from frost_sta_client import utils 3 | 4 | 5 | def test_extract_value_numeric_and_string(): 6 | assert utils.extract_value('Things(22)') == 22 7 | assert utils.extract_value("Things('abc')") == 'abc' 8 | 9 | 10 | def test_transform_json_to_entity_list_with_dict(): 11 | data = {"value": [{"@iot.id": 1, "name": "A"}]} 12 | elist = utils.transform_json_to_entity_list(data, 'frost_sta_client.model.thing.Thing') 13 | assert len(elist.entities) == 1 14 | 15 | 16 | def test_transform_json_to_entity_list_with_list(): 17 | data = [{"@iot.id": 2, "name": "B"}] 18 | elist = utils.transform_json_to_entity_list(data, 'frost_sta_client.model.thing.Thing') 19 | assert len(elist.entities) == 1 20 | 21 | 22 | def test_parse_datetime_invalid(): 23 | with pytest.raises(ValueError): 24 | utils.parse_datetime('invalid') 25 | 26 | 27 | def test_process_area_point_and_polygon(): 28 | p = utils.process_area({"type": "Point", "coordinates": [1, 2]}) 29 | assert getattr(p, 'type', 'Point') == 'Point' 30 | poly = utils.process_area({"type": "Polygon", "coordinates": [[[0, 0], [1, 0], [1, 1], [0, 1], [0, 0]]]}) 31 | assert getattr(poly, 'type', 'Polygon') == 'Polygon' 32 | with pytest.raises(ValueError): 33 | utils.process_area({"type": "Unknown", "coordinates": []}) 34 | -------------------------------------------------------------------------------- /.github/workflows/python-publish.yml: -------------------------------------------------------------------------------- 1 | # This workflow will upload a Python Package using Twine when a release is created 2 | # For more information see: https://help.github.com/en/actions/language-and-framework-guides/using-python-with-github-actions#publishing-to-package-registries 3 | 4 | # This workflow uses actions that are not certified by GitHub. 5 | # They are provided by a third-party and are governed by 6 | # separate terms of service, privacy policy, and support 7 | # documentation. 8 | 9 | name: Publish forst_sta_client distribution to PyPI 10 | 11 | on: 12 | push: 13 | tags: 14 | - "v[0-9]+.[0-9]+.[0-9]+" 15 | # Allows you to run this workflow manually from the Actions tab 16 | workflow_dispatch: 17 | jobs: 18 | deploy: 19 | runs-on: ubuntu-latest 20 | 21 | steps: 22 | - uses: actions/checkout@v2 23 | - name: Set up Python 24 | uses: actions/setup-python@v2 25 | with: 26 | python-version: '3.x' 27 | - name: Install dependencies 28 | run: | 29 | python -m pip install --upgrade pip 30 | pip install build 31 | echo "github.ref:" 32 | echo %github.ref% 33 | - name: Build package 34 | run: python -m build 35 | - name: Publish package 36 | uses: pypa/gh-action-pypi-publish@27b31702a0e7fc50959f5ad993c78deac1bdfc29 37 | with: 38 | user: __token__ 39 | password: ${{ secrets.PYPI_API_TOKEN }} 40 | -------------------------------------------------------------------------------- /tests/test_entity_list.py: -------------------------------------------------------------------------------- 1 | from frost_sta_client.utils import transform_json_to_entity_list 2 | from frost_sta_client.service.sensorthingsservice import SensorThingsService 3 | 4 | 5 | class MockResponse: 6 | def __init__(self, json_data): 7 | self._json = json_data 8 | self.status_code = 200 9 | 10 | def json(self): 11 | return self._json 12 | 13 | def raise_for_status(self): 14 | pass 15 | 16 | 17 | class DummyService(SensorThingsService): 18 | def __init__(self, url, page2): 19 | super().__init__(url) 20 | self.page2 = page2 21 | self.calls = 0 22 | 23 | def execute(self, method, url, **kwargs): 24 | self.calls += 1 25 | return MockResponse(self.page2) 26 | 27 | 28 | def test_entity_list_iterates_across_pages(): 29 | page1 = { 30 | "value": [ 31 | {"@iot.id": 1, "name": "A"}, 32 | {"@iot.id": 2, "name": "B"}, 33 | ], 34 | "@iot.nextLink": "http://example.org/FROST-Server/v1.1/Things?$skip=2" 35 | } 36 | page2 = { 37 | "value": [ 38 | {"@iot.id": 3, "name": "C"}, 39 | {"@iot.id": 4, "name": "D"}, 40 | ] 41 | } 42 | elist = transform_json_to_entity_list(page1, 'frost_sta_client.model.thing.Thing') 43 | svc = DummyService('http://example.org/FROST-Server/v1.1', page2) 44 | elist.set_service(svc) 45 | called = [] 46 | elist.step_size = 2 47 | elist.callback = lambda idx: called.append(idx) 48 | names = [e.name for e in elist] 49 | assert names == ['A', 'B', 'C', 'D'] 50 | assert called == [0, 2] 51 | -------------------------------------------------------------------------------- /frost_server/docker-compose.yaml: -------------------------------------------------------------------------------- 1 | services: 2 | web: 3 | image: docker.io/fraunhoferiosb/frost-server:latest 4 | environment: 5 | # For all settings see: https://fraunhoferiosb.github.io/FROST-Server/settings/settings.html 6 | - serviceRootUrl=http://localhost:8080/FROST-Server 7 | - plugins_multiDatastream.enable=false 8 | - http_cors_enable=true 9 | - http_cors_allowed_origins=* 10 | - persistence_db_driver=org.postgresql.Driver 11 | - persistence_db_url=jdbc:postgresql://database:5432/sensorthings 12 | - persistence_db_username=sensorthings 13 | - persistence_db_password=ChangeMe 14 | - persistence_autoUpdateDatabase=true 15 | - auth_provider=de.fraunhofer.iosb.ilt.frostserver.auth.basic.BasicAuthProvider 16 | - auth_db_driver=org.postgresql.Driver 17 | - auth_db_url=jdbc:postgresql://database:5432/sensorthings 18 | - auth_db_username=sensorthings 19 | - auth_db_password=ChangeMe 20 | - auth_autoUpdateDatabase=true 21 | ports: 22 | - 8080:8080 23 | - 1883:1883 24 | depends_on: 25 | database: 26 | condition: service_healthy 27 | 28 | database: 29 | image: docker.io/postgis/postgis:16-3.4-alpine 30 | environment: 31 | - POSTGRES_DB=sensorthings 32 | - POSTGRES_USER=sensorthings 33 | - POSTGRES_PASSWORD=ChangeMe 34 | volumes: 35 | - postgis_volume:/var/lib/postgresql/data 36 | healthcheck: 37 | test: ["CMD-SHELL", "pg_isready -d sensorthings -U sensorthings "] 38 | interval: 2s 39 | timeout: 2s 40 | retries: 10 41 | 42 | volumes: 43 | postgis_volume: 44 | 45 | -------------------------------------------------------------------------------- /.github/workflows/python-app.yml: -------------------------------------------------------------------------------- 1 | # This workflow will install Python dependencies, run tests and lint with a single version of Python 2 | # For more information see: https://help.github.com/actions/language-and-framework-guides/using-python-with-github-actions 3 | 4 | name: pytesting 5 | 6 | on: 7 | push: 8 | branches: [ master ] 9 | # pull_request: 10 | # branches: [ master ] 11 | 12 | jobs: 13 | build: 14 | 15 | runs-on: ubuntu-latest 16 | 17 | steps: 18 | - uses: actions/checkout@v2 19 | - name: Set up Python 3.12 20 | uses: actions/setup-python@v2 21 | with: 22 | python-version: "3.12" 23 | - name: Install dependencies 24 | run: | 25 | echo '#!/usr/bin/env bash' | sudo tee /usr/local/bin/podman >/dev/null 26 | echo 'exec docker "$@"' | sudo tee -a /usr/local/bin/podman >/dev/null 27 | sudo chmod +x /usr/local/bin/podman 28 | docker info 29 | docker compose version 30 | python3 -m pip install --upgrade pip 31 | pip install flake8 pytest 32 | if [ -f requirements.txt ]; then pip install -r requirements.txt; fi 33 | - name: Lint with flake8 34 | run: | 35 | # stop the build if there are Python syntax errors or undefined names 36 | flake8 . --count --select=E9,F63,F7,F82 --show-source --statistics 37 | # exit-zero treats all errors as warnings. The GitHub editor is 127 chars wide 38 | flake8 . --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics 39 | - name: Test with pytest 40 | run: | 41 | export FROST_STA_CLIENT_RUN_INTEGRATION=1 42 | python -m pytest 43 | -------------------------------------------------------------------------------- /tests/test_query_unit.py: -------------------------------------------------------------------------------- 1 | import requests 2 | import frost_sta_client.model.ext.entity_list 3 | from frost_sta_client.service.sensorthingsservice import SensorThingsService 4 | 5 | 6 | class MockResponse: 7 | def __init__(self, status_code=200, json_data=None): 8 | self.status_code = status_code 9 | self._json = json_data if json_data is not None else {} 10 | 11 | def json(self): 12 | return self._json 13 | 14 | def raise_for_status(self): 15 | if self.status_code >= 400: 16 | raise requests.exceptions.HTTPError(response=self) 17 | 18 | 19 | class DummyService(SensorThingsService): 20 | def __init__(self, url, responses): 21 | super().__init__(url) 22 | self.responses = list(responses) 23 | self.calls = [] 24 | 25 | def execute(self, method, url, **kwargs): 26 | self.calls.append((method, str(url))) 27 | if self.responses: 28 | return self.responses.pop(0) 29 | return MockResponse(200, {"value": [], "@iot.nextLink": None}) 30 | 31 | 32 | def test_query_builds_and_lists(): 33 | first = MockResponse(200, {"value": [{"@iot.id": 1, "name": "A"}], "@iot.nextLink": None}) 34 | svc = DummyService('http://example.org/FROST-Server/v1.1', [first]) 35 | lst = svc.things().query().filter("name eq 'A'").select('name').orderby('name', 'ASC').top(1).skip(0).expand('Datastreams').list() 36 | assert len(svc.calls) == 1 37 | assert 'get' == svc.calls[0][0] 38 | assert 'Things' in svc.calls[0][1] 39 | assert '%24filter' in svc.calls[0][1] 40 | assert len(lst.entities) == 1 41 | assert isinstance(lst, frost_sta_client.model.ext.entity_list.EntityList) 42 | assert isinstance(lst.entities[0], frost_sta_client.model.thing.Thing) 43 | -------------------------------------------------------------------------------- /tests/conftest.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | import subprocess 3 | import time 4 | import requests 5 | import os 6 | 7 | from frost_sta_client.service.sensorthingsservice import SensorThingsService 8 | from frost_sta_client.service.auth_handler import AuthHandler 9 | 10 | @pytest.fixture(scope='session') 11 | def frost_server(): 12 | if os.environ.get('FROST_STA_CLIENT_RUN_INTEGRATION') != '1': 13 | # Skip starting server if not requested 14 | yield 15 | return 16 | # Start FROST-Server using Podman 17 | subprocess.run(['podman', 'compose', '-f', 'frost_server/docker-compose.yaml', 'up', '-d']) 18 | # Wait for server to start 19 | url = 'http://localhost:8080/FROST-Server' 20 | for _ in range(30): 21 | try: 22 | response = requests.get(url) 23 | if response.status_code == 200: 24 | break 25 | except requests.ConnectionError: 26 | time.sleep(1) 27 | else: 28 | raise RuntimeError('FROST-Server failed to start') 29 | vrl = 'http://localhost:8080/FROST-Server/v1.1' 30 | auth_handler = AuthHandler( 31 | username="read", 32 | password="read" 33 | ) 34 | response = requests.get(vrl, auth=auth_handler.add_auth_header()) 35 | if response.status_code == 401: 36 | raise RuntimeError('Failed to authorize at FROST-Server') 37 | yield 38 | subprocess.run(['podman', 'compose', '-f', 'frost_server/docker-compose.yaml', 'down']) 39 | 40 | @pytest.fixture 41 | def sensorthings_service(frost_server): 42 | url = 'http://localhost:8080/FROST-Server/v1.1' 43 | auth_handler = AuthHandler( 44 | username="admin", 45 | password="admin" 46 | ) 47 | return SensorThingsService(url, auth_handler=auth_handler) 48 | -------------------------------------------------------------------------------- /frost_sta_client/__init__.py: -------------------------------------------------------------------------------- 1 | from frost_sta_client import model 2 | from frost_sta_client import dao 3 | from frost_sta_client import query 4 | from frost_sta_client import service 5 | 6 | from frost_sta_client.model.actuator import Actuator 7 | from frost_sta_client.model.datastream import Datastream 8 | from frost_sta_client.model.entity import Entity 9 | from frost_sta_client.model.feature_of_interest import FeatureOfInterest 10 | from frost_sta_client.model.historical_location import HistoricalLocation 11 | from frost_sta_client.model.location import Location 12 | from frost_sta_client.model.multi_datastream import MultiDatastream 13 | from frost_sta_client.model.observation import Observation 14 | from frost_sta_client.model.observedproperty import ObservedProperty 15 | from frost_sta_client.model.sensor import Sensor 16 | from frost_sta_client.model.task import Task 17 | from frost_sta_client.model.tasking_capability import TaskingCapability 18 | from frost_sta_client.model.thing import Thing 19 | from frost_sta_client.model.ext.unitofmeasurement import UnitOfMeasurement 20 | from frost_sta_client.service.sensorthingsservice import SensorThingsService 21 | from frost_sta_client.service.auth_handler import AuthHandler 22 | from frost_sta_client.model.ext.entity_type import EntityTypes 23 | from frost_sta_client.model.ext.entity_list import EntityList 24 | from frost_sta_client.model.ext.data_array_value import DataArrayValue 25 | from frost_sta_client.model.ext.data_array_document import DataArrayDocument 26 | 27 | import jsonpickle 28 | 29 | jsonpickle.load_backend('demjson3', 'encode', 'decode', 'JSONDecodeError') 30 | jsonpickle.set_preferred_backend('demjson3') 31 | jsonpickle.set_decoder_options("demjson3", decode_float=float) 32 | 33 | from .__version__ import (__title__, __version__, __license__, __author__, __contact__, __url__, 34 | __description__, __copyright__) 35 | -------------------------------------------------------------------------------- /tests/test_service_unit.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | import requests 3 | from requests.auth import HTTPBasicAuth 4 | from frost_sta_client.service.sensorthingsservice import SensorThingsService 5 | from frost_sta_client.service.auth_handler import AuthHandler 6 | from frost_sta_client.model.thing import Thing 7 | 8 | 9 | def test_get_path_numeric_id(): 10 | svc = SensorThingsService('http://example.org/FROST-Server/v1.1') 11 | t = Thing(id=1) 12 | assert svc.get_path(t, 'Datastreams') == 'Things(1)/Datastreams' 13 | 14 | 15 | def test_get_path_string_id(): 16 | svc = SensorThingsService('http://example.org/FROST-Server/v1.1/') 17 | t = Thing(id='abc') 18 | assert svc.get_path(t, 'Locations') == "Things('abc')/Locations" 19 | 20 | 21 | def test_get_full_path_handles_trailing_slash(): 22 | svc = SensorThingsService('http://example.org/FROST-Server/v1.1/') 23 | t = Thing(id=2) 24 | full = svc.get_full_path(t, 'Datastreams') 25 | assert str(full) == 'http://example.org/FROST-Server/v1.1/Things(2)/Datastreams' 26 | 27 | 28 | def test_auth_handler_type_check(): 29 | svc = SensorThingsService('http://example.org/FROST-Server/v1.1') 30 | with pytest.raises(ValueError): 31 | svc.auth_handler = 'not-auth' 32 | 33 | 34 | def test_proxies_type_check(): 35 | svc = SensorThingsService('http://example.org/FROST-Server/v1.1') 36 | with pytest.raises(ValueError): 37 | svc.proxies = 'not-a-dict' 38 | svc.proxies = {'http': 'http://proxy'} 39 | 40 | 41 | def test_execute_uses_auth(monkeypatch): 42 | svc = SensorThingsService('http://example.org/FROST-Server/v1.1') 43 | svc.auth_handler = AuthHandler('user', 'pass') 44 | captured = {} 45 | 46 | def fake_request(method, url, proxies=None, auth=None, **kwargs): 47 | captured['auth'] = auth 48 | class R: 49 | status_code = 200 50 | def raise_for_status(self): 51 | pass 52 | def json(self): 53 | return {} 54 | return R() 55 | 56 | monkeypatch.setattr(requests, 'request', fake_request) 57 | svc.execute('get', 'http://example.org') 58 | assert isinstance(captured['auth'], HTTPBasicAuth) 59 | -------------------------------------------------------------------------------- /frost_sta_client/dao/observation.py: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2021 Fraunhofer Institut IOSB, Fraunhoferstr. 1, D 76131 2 | # Karlsruhe, Germany. 3 | # 4 | # This program is free software: you can redistribute it and/or modify 5 | # it under the terms of the GNU Lesser General Public License as published by 6 | # the Free Software Foundation, either version 3 of the License, or 7 | # (at your option) any later version. 8 | # 9 | # This program is distributed in the hope that it will be useful, 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | # GNU Lesser General Public License for more details. 13 | # 14 | # You should have received a copy of the GNU Lesser General Public License 15 | # along with this program. If not, see . 16 | 17 | from frost_sta_client.dao import base 18 | from frost_sta_client.model.ext.entity_type import EntityTypes 19 | from frost_sta_client.utils import transform_entity_to_json_dict 20 | import frost_sta_client 21 | 22 | import logging 23 | import requests 24 | import json 25 | 26 | 27 | 28 | class ObservationDao(base.BaseDao): 29 | CREATE_OBSERVATIONS = "CreateObservations" 30 | 31 | def __init__(self, service): 32 | """ 33 | A data access object for operations with the Observation entity 34 | """ 35 | base.BaseDao.__init__(self, service, EntityTypes["Observation"]) 36 | 37 | def create(self, entity): 38 | if isinstance(entity, frost_sta_client.model.observation.Observation): 39 | super().create(entity) 40 | else: 41 | # entity is probably a data array 42 | url = self.service.url.copy() 43 | url.path.add(self.CREATE_OBSERVATIONS) 44 | logging.debug('Posting to ' + str(url.url)) 45 | json_dict = [transform_entity_to_json_dict(dav) for dav in entity.value] 46 | try: 47 | response = self.service.execute('post', url, json=json_dict) 48 | except requests.exceptions.HTTPError as e: 49 | frost_sta_client.utils.handle_server_error(e, 'Creating Data Array') 50 | response_text_as_list = json.loads(response.text) 51 | result = [frost_sta_client.model.observation.Observation(self_link=link) for link in response_text_as_list] 52 | return result 53 | -------------------------------------------------------------------------------- /tests/test_dao_base.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | import requests 3 | from frost_sta_client.service.sensorthingsservice import SensorThingsService 4 | from frost_sta_client.model.thing import Thing 5 | 6 | 7 | class MockResponse: 8 | def __init__(self, status_code=200, json_data=None, headers=None): 9 | self.status_code = status_code 10 | self._json = json_data if json_data is not None else {} 11 | self.headers = headers or {} 12 | 13 | def json(self): 14 | return self._json 15 | 16 | def raise_for_status(self): 17 | if self.status_code >= 400: 18 | raise requests.exceptions.HTTPError(response=self) 19 | 20 | 21 | class DummyService(SensorThingsService): 22 | def __init__(self): 23 | super().__init__('http://example.org/FROST-Server/v1.1') 24 | self.calls = [] 25 | 26 | def execute(self, method, url, **kwargs): 27 | self.calls.append((method, str(url), kwargs)) 28 | if method == 'post': 29 | return MockResponse(201, {}, headers={'location': 'Things(42)'}) 30 | if method == 'get': 31 | return MockResponse(200, {"@iot.id": 5, "name": "MyThing"}) 32 | return MockResponse(200, {}) 33 | 34 | 35 | def test_base_dao_create_sets_id_and_service(): 36 | svc = DummyService() 37 | t = Thing(name='X') 38 | svc.create(t) 39 | assert t.id == 42 40 | assert t.service is svc 41 | 42 | 43 | def test_base_dao_find_returns_entity(): 44 | svc = DummyService() 45 | found = svc.things().find(5) 46 | assert found.id == 5 47 | assert found.name == 'MyThing' 48 | assert found.service is svc 49 | 50 | 51 | def test_base_dao_update_without_id_raises(): 52 | svc = DummyService() 53 | t = Thing(name='noid') 54 | with pytest.raises(AttributeError): 55 | svc.update(t) 56 | 57 | 58 | def test_base_dao_patch_validates_and_sends_headers(): 59 | svc = DummyService() 60 | t = Thing(id=7) 61 | patches = [{"op": "replace", "path": "/name", "value": "new"}] 62 | svc.patch(t, patches) 63 | method, url, kwargs = svc.calls[-1] 64 | assert method == 'patch' 65 | assert kwargs['headers']['Content-type'] == 'application/json-patch+json' 66 | 67 | 68 | def test_entity_path_formats_string_and_int(): 69 | svc = DummyService() 70 | assert svc.things().entity_path(1) == 'Things(1)' 71 | assert svc.things().entity_path('abc') == "Things('abc')" 72 | -------------------------------------------------------------------------------- /frost_sta_client/model/ext/data_array_document.py: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2021 Fraunhofer Institut IOSB, Fraunhoferstr. 1, D 76131 2 | # Karlsruhe, Germany. 3 | # 4 | # This program is free software: you can redistribute it and/or modify 5 | # it under the terms of the GNU Lesser General Public License as published by 6 | # the Free Software Foundation, either version 3 of the License, or 7 | # (at your option) any later version. 8 | # 9 | # This program is distributed in the hope that it will be useful, 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | # GNU Lesser General Public License for more details. 13 | # 14 | # You should have received a copy of the GNU Lesser General Public License 15 | # along with this program. If not, see . 16 | from .data_array_value import DataArrayValue 17 | 18 | 19 | class DataArrayDocument: 20 | def __init__(self, count=-1, next_link = None, value=None): 21 | if value is None: 22 | value = [] 23 | self._count = count 24 | self._next_link = next_link 25 | self._value = value 26 | 27 | @property 28 | def count(self): 29 | return self._count 30 | 31 | @count.setter 32 | def count(self, value): 33 | if type(value) == int or value is None: 34 | self._count = value 35 | else: 36 | raise TypeError('count should be of type int') 37 | 38 | @property 39 | def next_link(self): 40 | return self._next_link 41 | 42 | @next_link.setter 43 | def next_link(self, value): 44 | if type(value) == str or value is None: 45 | self._next_link = value 46 | else: 47 | raise TypeError('nextLink should be of type str') 48 | 49 | @property 50 | def value(self): 51 | return self._value 52 | 53 | @value.setter 54 | def value(self, value): 55 | if type(value) == list and all(isinstance(x, DataArrayValue) for x in value): 56 | self._value = value 57 | else: 58 | raise TypeError('value should be a list of type DataArrayValue') 59 | 60 | def get_observations(self): 61 | obs_list = [] 62 | for dav in self.value: 63 | obs_list.extend(dav.observations) 64 | return obs_list 65 | 66 | def add_data_array_value(self, dav): 67 | self.value.append(dav) 68 | -------------------------------------------------------------------------------- /tests/test_error_handling_non_json.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | import requests 3 | import json 4 | from frost_sta_client.service.sensorthingsservice import SensorThingsService 5 | from frost_sta_client.utils import transform_json_to_entity_list 6 | from frost_sta_client.model.thing import Thing 7 | 8 | 9 | class DummyService(SensorThingsService): 10 | def __init__(self, url): 11 | super().__init__(url) 12 | 13 | def execute(self, method, url, **kwargs): 14 | class Resp: 15 | status_code = 500 16 | text = "Server Error" 17 | 18 | def json(self): 19 | raise ValueError("No JSON body") 20 | 21 | raise requests.exceptions.HTTPError(response=Resp()) 22 | 23 | 24 | def test_query_handles_non_json_error(): 25 | svc = DummyService('http://example.org/FROST-Server/v1.1') 26 | with pytest.raises(requests.exceptions.HTTPError): 27 | svc.things().query().list() 28 | 29 | 30 | def test_basedao_handles_non_json_error(): 31 | svc = DummyService('http://example.org/FROST-Server/v1.1') 32 | with pytest.raises(requests.exceptions.HTTPError): 33 | svc.things().find(1) 34 | 35 | 36 | def test_entitylist_iter_handles_non_json_error(): 37 | svc = DummyService('http://example.org/FROST-Server/v1.1') 38 | page = {"value": [], "@iot.nextLink": "http://example.org/next"} 39 | elist = transform_json_to_entity_list(page, 'frost_sta_client.model.thing.Thing') 40 | elist.set_service(svc) 41 | it = iter(elist) 42 | with pytest.raises(requests.exceptions.HTTPError): 43 | next(it) 44 | 45 | 46 | class WorkingService(SensorThingsService): 47 | def __init__(self, url): 48 | super().__init__(url) 49 | 50 | def execute(self, method, url, **kwargs): 51 | class Resp: 52 | status_code = 400 53 | text = '{"code":400,"type":"error","message":"Not a valid path for DELETE."}' 54 | 55 | def json(self): 56 | return json.loads(self.text) 57 | 58 | raise requests.exceptions.HTTPError(response=Resp()) 59 | 60 | 61 | def test_basedao_handles_json_error(caplog): 62 | svc = WorkingService('http://example.org/FROST-Server/v1.1/Things') 63 | with pytest.raises(requests.exceptions.HTTPError): 64 | thing = Thing(id=1) 65 | svc.delete(thing) 66 | assert caplog.messages[-1] == "Deleting Thing failed with status-code 400, Not a valid path for DELETE." 67 | -------------------------------------------------------------------------------- /frost_sta_client/model/ext/unitofmeasurement.py: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2021 Fraunhofer Institut IOSB, Fraunhoferstr. 1, D 76131 2 | # Karlsruhe, Germany. 3 | # 4 | # This program is free software: you can redistribute it and/or modify 5 | # it under the terms of the GNU Lesser General Public License as published by 6 | # the Free Software Foundation, either version 3 of the License, or 7 | # (at your option) any later version. 8 | # 9 | # This program is distributed in the hope that it will be useful, 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | # GNU Lesser General Public License for more details. 13 | # 14 | # You should have received a copy of the GNU Lesser General Public License 15 | # along with this program. If not, see . 16 | 17 | class UnitOfMeasurement: 18 | def __init__(self, 19 | name="", 20 | symbol="", 21 | definition=""): 22 | self.name = name 23 | self.symbol = symbol 24 | self.definition = definition 25 | 26 | @property 27 | def name(self): 28 | return self._name 29 | 30 | @name.setter 31 | def name(self, value): 32 | if isinstance(value, str) or value is None: 33 | self._name = value 34 | return 35 | raise ValueError('name should be of type str!') 36 | 37 | @property 38 | def symbol(self): 39 | return self._symbol 40 | 41 | @symbol.setter 42 | def symbol(self, value): 43 | if isinstance(value, str) or value is None: 44 | self._symbol = value 45 | return 46 | raise ValueError('symbol should be of type str!') 47 | 48 | @property 49 | def definition(self): 50 | return self._definition 51 | 52 | @definition.setter 53 | def definition(self, value): 54 | if isinstance(value, str) or value is None: 55 | self._definition = value 56 | return 57 | raise ValueError('definition should be of type str!') 58 | 59 | def __getstate__(self): 60 | data = { 61 | 'symbol': self._symbol, 62 | 'definition': self._definition, 63 | 'name': self._name 64 | } 65 | return data 66 | 67 | def __setstate__(self, state): 68 | self.symbol = state.get("symbol", None) 69 | self.definition = state.get("definition", None) 70 | self.name = state.get("name", None) 71 | 72 | def __eq__(self, other): 73 | if other is None: 74 | return False 75 | if not isinstance(other, type(self)): 76 | return False 77 | if id(self) == id(other): 78 | return True 79 | if self.name != other.name: 80 | return False 81 | if self.definition != other.definition: 82 | return False 83 | if self.symbol != other.symbol: 84 | return False 85 | return True -------------------------------------------------------------------------------- /frost_sta_client/model/entity.py: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2021 Fraunhofer Institut IOSB, Fraunhoferstr. 1, D 76131 2 | # Karlsruhe, Germany. 3 | # 4 | # This program is free software: you can redistribute it and/or modify 5 | # it under the terms of the GNU Lesser General Public License as published by 6 | # the Free Software Foundation, either version 3 of the License, or 7 | # (at your option) any later version. 8 | # 9 | # This program is distributed in the hope that it will be useful, 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | # GNU Lesser General Public License for more details. 13 | # 14 | # You should have received a copy of the GNU Lesser General Public License 15 | # along with this program. If not, see . 16 | 17 | from abc import ABC 18 | from frost_sta_client.service.sensorthingsservice import SensorThingsService 19 | 20 | 21 | class Entity(ABC): 22 | """ 23 | An abstract representation of an entity. 24 | """ 25 | def __init__(self, 26 | id=None, 27 | self_link='', 28 | service=None): 29 | self.id = id 30 | self.self_link = self_link 31 | self.service = service 32 | 33 | @property 34 | def id(self): 35 | return self._id 36 | 37 | @id.setter 38 | def id(self, value): 39 | if value is None: 40 | self._id = None 41 | return 42 | if isinstance(value, int) or isinstance(value, str): 43 | self._id = value 44 | return 45 | raise ValueError('id of entity should be of type int or str!') 46 | 47 | @property 48 | def self_link(self): 49 | return self._self_link 50 | 51 | @self_link.setter 52 | def self_link(self, value): 53 | if not isinstance(value, str): 54 | raise ValueError('self_link should be of type str!') 55 | self._self_link = value 56 | 57 | @property 58 | def service(self): 59 | return self._service 60 | 61 | @service.setter 62 | def service(self, value): 63 | if value is None or isinstance(value, SensorThingsService): 64 | self._service = value 65 | return 66 | raise ValueError('service should be of type SensorThingsService') 67 | 68 | def set_service(self, service): 69 | if self.service != service: 70 | self.service = service 71 | self.ensure_service_on_children(service) 72 | 73 | @property 74 | def IOT_COUNT(self): 75 | return 'iot.count' 76 | 77 | @property 78 | def AT_IOT_COUNT(self): 79 | return '@iot.count' 80 | 81 | @property 82 | def IOT_NAVIGATION_LINK(self): 83 | return 'iot.navigationLink' 84 | 85 | @property 86 | def AT_IOT_NAVIGATION_LINK(self): 87 | return '@iot.navigationLink' 88 | 89 | @property 90 | def IOT_NEXT_LINK(self): 91 | return 'iot.nextLink' 92 | 93 | @property 94 | def AT_IOT_NEXT_LINK(self): 95 | return '@iot.nextLink' 96 | 97 | @property 98 | def IOT_SELF_LINK(self): 99 | return 'iot.selfLink' 100 | 101 | @property 102 | def AT_IOT_SELF_LINK(self): 103 | return '@iot.selfLink' 104 | 105 | def __eq__(self, other): 106 | if other is None: 107 | return False 108 | if not isinstance(other, type(self)): 109 | return False 110 | if id(self) == id(other): 111 | return True 112 | if self.id is not None and other.id is not None: 113 | if self.id != other.id: 114 | return False 115 | return True 116 | 117 | def __ne__(self, other): 118 | return not self == other 119 | 120 | def __getstate__(self): 121 | data = {} 122 | if self.id is not None and self.id != '': 123 | data['@iot.id'] = self.id 124 | return data 125 | 126 | def __setstate__(self, state): 127 | self.id = state.get('@iot.id', None) 128 | self.self_link = state.get('@iot.selfLink', '') 129 | -------------------------------------------------------------------------------- /tests/test_integration.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | import os 3 | 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.') 4 | from geojson import Point 5 | from frost_sta_client.model import thing, sensor, observedproperty, datastream, observation, feature_of_interest 6 | from frost_sta_client.model.ext import unitofmeasurement 7 | 8 | 9 | def test_create_thing(sensorthings_service): 10 | t = thing.Thing( 11 | name='Test Thing', 12 | description='A test thing') 13 | sensorthings_service.create(t) 14 | assert t.id is not None 15 | 16 | retrieved = sensorthings_service.things().find(t.id) 17 | assert retrieved.name == 'Test Thing' 18 | 19 | 20 | def test_crud_datastream(sensorthings_service): 21 | # Create dependencies 22 | t = thing.Thing( 23 | name='Test Thing DS', 24 | description='Thing for DS') 25 | sensorthings_service.create(t) 26 | s = sensor.Sensor( 27 | name='Test Sensor', 28 | description='Sensor', 29 | encoding_type='application/pdf', 30 | metadata='http://example.org/sensor.pdf') 31 | sensorthings_service.create(s) 32 | op = observedproperty.ObservedProperty( 33 | name='Test OP', 34 | definition='http://www.example.org/op', 35 | description='OP') 36 | sensorthings_service.create(op) 37 | um = unitofmeasurement.UnitOfMeasurement( 38 | name="degree Celsius", 39 | symbol="°C", 40 | definition="physical definition...") 41 | 42 | # Create Datastream 43 | ds = datastream.Datastream( 44 | name='Test DS', 45 | description='DS', 46 | observation_type='OM_Measurement', 47 | unit_of_measurement=um, 48 | thing=t, 49 | sensor=s, 50 | observed_property=op) 51 | sensorthings_service.create(ds) 52 | assert ds.id is not None 53 | 54 | # Read 55 | retrieved_ds = sensorthings_service.datastreams().find(ds.id) 56 | assert retrieved_ds.name == 'Test DS' 57 | 58 | # Update 59 | retrieved_ds.description = 'Updated DS' 60 | sensorthings_service.update(retrieved_ds) 61 | updated_ds = sensorthings_service.datastreams().find(ds.id) 62 | assert updated_ds.description == 'Updated DS' 63 | 64 | # Delete 65 | sensorthings_service.delete(updated_ds) 66 | with pytest.raises(Exception): 67 | sensorthings_service.datastreams().find(ds.id) 68 | 69 | 70 | def test_crud_observation(sensorthings_service): 71 | # Create dependencies 72 | t = thing.Thing( 73 | name='Test Thing Obs', 74 | description='Thing for Obs') 75 | sensorthings_service.create(t) 76 | s = sensor.Sensor( 77 | name='Test Sensor Obs', 78 | description='Sensor Obs', 79 | encoding_type='application/pdf', 80 | metadata='http://example.org/sensor_obs.pdf') 81 | sensorthings_service.create(s) 82 | op = observedproperty.ObservedProperty( 83 | name='Test OP Obs', 84 | definition='http://www.example.org/op_obs', 85 | description='OP Obs') 86 | sensorthings_service.create(op) 87 | um = unitofmeasurement.UnitOfMeasurement( 88 | name="degree Celsius", 89 | symbol="°C", 90 | definition="physical definition...") 91 | ds = datastream.Datastream( 92 | name='Test DS Obs', 93 | description='DS Obs', 94 | observation_type='OM_Measurement', 95 | unit_of_measurement=um, 96 | thing=t, 97 | sensor=s, 98 | observed_property=op) 99 | sensorthings_service.create(ds) 100 | point = Point((-115.81, 37.24)) 101 | foi = feature_of_interest.FeatureOfInterest(name="here", description="and there", feature=point, encoding_type='application/geo+json') 102 | 103 | # Create Observation 104 | obs = observation.Observation( 105 | result=25.0, 106 | phenomenon_time='2023-01-01T00:00:00Z', 107 | datastream=ds, 108 | feature_of_interest=foi) 109 | sensorthings_service.create(obs) 110 | assert obs.id is not None 111 | 112 | # Read 113 | retrieved_obs = sensorthings_service.observations().find(obs.id) 114 | assert retrieved_obs.result == 25.0 115 | 116 | # Update 117 | retrieved_obs.result = 30.0 118 | sensorthings_service.update(retrieved_obs) 119 | updated_obs = sensorthings_service.observations().find(obs.id) 120 | assert updated_obs.result == 30.0 121 | 122 | # Delete 123 | sensorthings_service.delete(updated_obs) 124 | with pytest.raises(Exception): 125 | sensorthings_service.observations().find(obs.id) 126 | -------------------------------------------------------------------------------- /frost_sta_client/model/ext/entity_type.py: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2021 Fraunhofer Institut IOSB, Fraunhoferstr. 1, D 76131 2 | # Karlsruhe, Germany. 3 | # 4 | # This program is free software: you can redistribute it and/or modify 5 | # it under the terms of the GNU Lesser General Public License as published by 6 | # the Free Software Foundation, either version 3 of the License, or 7 | # (at your option) any later version. 8 | # 9 | # This program is distributed in the hope that it will be useful, 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | # GNU Lesser General Public License for more details. 13 | # 14 | # You should have received a copy of the GNU Lesser General Public License 15 | # along with this program. If not, see . 16 | 17 | EntityTypes = { 18 | 'Datastream': { 19 | 'singular': 'Datastream', 20 | 'plural': 'Datastreams', 21 | 'class': 'frost_sta_client.model.datastream.Datastream', 22 | 'relations_list': ['Sensor', 'Thing', 'ObservedProperty', 'Observations'] 23 | }, 24 | 'MultiDatastream': { 25 | 'singular': 'MultiDatastream', 26 | 'plural': 'MultiDatastreams', 27 | 'class': 'frost_sta_client.model.multi_datastream.MultiDatastream', 28 | 'relations_list': ['Sensor', 'Thing', 'ObservedProperties', 'Observations'] 29 | }, 30 | 'FeatureOfInterest': { 31 | 'singular': 'FeatureOfInterest', 32 | 'plural': 'FeaturesOfInterest', 33 | 'class': 'frost_sta_client.model.feature_of_interest.FeatureOfInterest', 34 | 'relations_list': ['Observations'] 35 | }, 36 | 'HistoricalLocation': { 37 | 'singular': 'HistoricalLocation', 38 | 'plural': 'HistoricalLocations', 39 | 'class': 'frost_sta_client.model.historical_location.HistoricalLocation', 40 | 'relations_list': ['Thing', 'Locations'] 41 | }, 42 | 'Actuator': { 43 | 'singular': 'Actuator', 44 | 'plural': 'Actuators', 45 | 'class': 'frost_sta_client.model.actuator.Actuator', 46 | 'relations_list': ['TaskingCapabilities'] 47 | }, 48 | 'Location': { 49 | 'singular': 'Location', 50 | 'plural': 'Locations', 51 | 'class': 'frost_sta_client.model.location.Location', 52 | 'relations_list': ['Things', 'HistoricalLocations'] 53 | }, 54 | 'Observation': { 55 | 'singular': 'Observation', 56 | 'plural': 'Observations', 57 | 'class': 'frost_sta_client.model.observation.Observation', 58 | 'relations_list': ['FeatureOfInterest', 'Datastream', 'MultiDatastream'] 59 | }, 60 | 'Thing': { 61 | 'singular': 'Thing', 62 | 'plural': 'Things', 63 | 'class': 'frost_sta_client.model.thing.Thing', 64 | 'relations_list': ['Datastreams', 'MultiDatastreams', 'Locations', 'HistoricalLocations', 'TaskingCapabilities'] 65 | }, 66 | 'ObservedProperty': { 67 | 'singular': 'ObservedProperty', 68 | 'plural': 'ObservedProperties', 69 | 'class': 'frost_sta_client.model.observedproperty.ObservedProperty', 70 | 'relations_list': ['Datastreams', 'MultiDatastreams'] 71 | }, 72 | 'Sensor': { 73 | 'singular': 'Sensor', 74 | 'plural': 'Sensors', 75 | 'class': 'frost_sta_client.model.sensor.Sensor', 76 | 'relations_list': ['Datastreams', 'MultiDatastreams'] 77 | }, 78 | 'Task': { 79 | 'singular': 'Task', 80 | 'plural': 'Tasks', 81 | 'class': 'frost_sta_client.model.task.Task', 82 | 'relations_list': ['TaskingCapability'] 83 | }, 84 | 'TaskingCapability': { 85 | 'singular': 'TaskingCapability', 86 | 'plural': 'TaskingCapabilities', 87 | 'class': 'frost_sta_client.model.tasking_capability.TaskingCapability', 88 | 'relations_list': ['Tasks', 'Actuator', 'Thing'] 89 | }, 90 | 'UnitOfMeasurement': { 91 | 'singular': 'UnitOfMeasurement', 92 | 'plural': 'UnitOfMeasurements', 93 | 'class': 'frost_sta_client.model.ext.unitofmeasurement.UnitOfMeasurement' 94 | }, 95 | 'EntityList': { 96 | 'singular': 'EntityList', 97 | 'plural': 'EntityLists', 98 | 'class': 'frost_sta_client.model.ext.entity_list.EntityList' 99 | } 100 | } 101 | 102 | list_for_class = {} 103 | for key, entity_type in EntityTypes.items(): 104 | list_for_class[entity_type["class"]] = entity_type["plural"] 105 | 106 | def get_list_for_class(clazz): 107 | clazz_name = clazz.__module__ + "." + clazz.__name__ 108 | return list_for_class[clazz_name] 109 | -------------------------------------------------------------------------------- /frost_sta_client/model/task.py: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2021 Fraunhofer Institut IOSB, Fraunhoferstr. 1, D 76131 2 | # Karlsruhe, Germany. 3 | # 4 | # This program is free software: you can redistribute it and/or modify 5 | # it under the terms of the GNU Lesser General Public License as published by 6 | # the Free Software Foundation, either version 3 of the License, or 7 | # (at your option) any later version. 8 | # 9 | # This program is distributed in the hope that it will be useful, 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | # GNU Lesser General Public License for more details. 13 | # 14 | # You should have received a copy of the GNU Lesser General Public License 15 | # along with this program. If not, see . 16 | 17 | import frost_sta_client.model.tasking_capability 18 | from . import entity 19 | from . import tasking_capability 20 | 21 | from frost_sta_client.dao.task import TaskDao 22 | 23 | from frost_sta_client import utils 24 | 25 | 26 | class Task(entity.Entity): 27 | def __init__(self, 28 | tasking_parameters=None, 29 | creation_time=None, 30 | tasking_capability=None, 31 | **kwargs): 32 | super().__init__(**kwargs) 33 | if tasking_parameters is None: 34 | tasking_parameters = {} 35 | self.tasking_parameters = tasking_parameters 36 | self.creation_time = creation_time 37 | self.tasking_capability = tasking_capability 38 | 39 | def __new__(cls, *args, **kwargs): 40 | new_task = super().__new__(cls) 41 | attributes = {'_id': None, '_tasking_parameters': {}, '_creation_time': None, '_tasking_capability': None, 42 | '_self_link': '', '_service': None} 43 | for key, value in attributes.items(): 44 | new_task.__dict__[key] = value 45 | return new_task 46 | 47 | @property 48 | def tasking_parameters(self): 49 | return self._tasking_parameters 50 | 51 | @tasking_parameters.setter 52 | def tasking_parameters(self, value): 53 | if value is None or not isinstance(value, dict): 54 | raise ValueError('tasking parameter should be of type dict!') 55 | self._tasking_parameters = value 56 | 57 | @property 58 | def creation_time(self): 59 | return self._creation_time 60 | 61 | @creation_time.setter 62 | def creation_time(self, value): 63 | self._creation_time = utils.check_datetime(value, 'creation_time') 64 | 65 | @property 66 | def tasking_capability(self): 67 | return self._tasking_capability 68 | 69 | @tasking_capability.setter 70 | def tasking_capability(self, value): 71 | if value is None: 72 | self._tasking_capability = None 73 | return 74 | if not isinstance(value, tasking_capability.TaskingCapability): 75 | raise ValueError('tasking capability should be of type TaskingCapability!') 76 | self._tasking_capability = value 77 | 78 | def ensure_service_on_children(self, service): 79 | if self.tasking_capability is not None: 80 | self.tasking_capability.set_service(service) 81 | 82 | def __eq__(self, other): 83 | if not super().__eq__(other): 84 | return False 85 | if self.tasking_parameters != other.tasking_parameters: 86 | return False 87 | if self.creation_time != other.creation_time: 88 | return False 89 | return True 90 | 91 | def __ne__(self, other): 92 | return not self == other 93 | 94 | def __getstate__(self): 95 | data = super().__getstate__() 96 | if self.tasking_parameters is not None and self.tasking_parameters != {}: 97 | data['taskingParameters'] = self.tasking_parameters 98 | if self.creation_time is not None: 99 | data['creationTime'] = utils.parse_datetime(self.creation_time) 100 | if self.tasking_capability is not None: 101 | data['TaskingCapability'] = self.tasking_capability.__getstate__() 102 | return data 103 | 104 | def __setstate__(self, state): 105 | super().__setstate__(state) 106 | self.tasking_parameters = state.get('taskingParameters', {}) 107 | self.creation_time = state.get('creationTime', None) 108 | if state.get('TaskingCapability', None) is not None: 109 | self.tasking_capability = frost_sta_client.model.tasking_capability.TaskingCapability() 110 | self.tasking_capability.__setstate__(state['TaskingCapability']) 111 | 112 | def get_dao(self, service): 113 | return TaskDao(service) 114 | -------------------------------------------------------------------------------- /frost_sta_client/model/historical_location.py: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2021 Fraunhofer Institut IOSB, Fraunhoferstr. 1, D 76131 2 | # Karlsruhe, Germany. 3 | # 4 | # This program is free software: you can redistribute it and/or modify 5 | # it under the terms of the GNU Lesser General Public License as published by 6 | # the Free Software Foundation, either version 3 of the License, or 7 | # (at your option) any later version. 8 | # 9 | # This program is distributed in the hope that it will be useful, 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | # GNU Lesser General Public License for more details. 13 | # 14 | # You should have received a copy of the GNU Lesser General Public License 15 | # along with this program. If not, see . 16 | 17 | import frost_sta_client.model 18 | from . import entity 19 | from . import location 20 | from . import thing 21 | 22 | from frost_sta_client.dao.historical_location import HistoricalLocationDao 23 | 24 | from frost_sta_client import utils 25 | from .ext import entity_list 26 | from .ext import entity_type 27 | 28 | 29 | class HistoricalLocation(entity.Entity): 30 | 31 | def __init__(self, 32 | locations=None, 33 | time=None, 34 | thing=None, 35 | **kwargs): 36 | super().__init__(**kwargs) 37 | self.locations = locations 38 | self.time = time 39 | self.thing = thing 40 | 41 | def __new__(cls, *args, **kwargs): 42 | new_h_loc = super().__new__(cls) 43 | attributes = {'_id': None, '_location': None, '_time': None, '_thing': None, '_self_link': None, 44 | '_service': None} 45 | for key, value in attributes.items(): 46 | new_h_loc.__dict__[key] = value 47 | return new_h_loc 48 | 49 | @property 50 | def time(self): 51 | return self._time 52 | 53 | @time.setter 54 | def time(self, value): 55 | self._time = utils.check_datetime(value, 'time') 56 | 57 | @property 58 | def locations(self): 59 | return self._locations 60 | 61 | @locations.setter 62 | def locations(self, values): 63 | if values is None: 64 | self._locations = None 65 | return 66 | if isinstance(values, list) and all(isinstance(loc, location.Location) for loc in values): 67 | entity_class = entity_type.EntityTypes['Location']['class'] 68 | self._locations = entity_list.EntityList(entity_class=entity_class, entities=values) 69 | return 70 | if not isinstance(values, entity_list.EntityList) or \ 71 | any((not isinstance(loc, location.Location)) for loc in values.entities): 72 | raise ValueError('locations should be a list of locations') 73 | self._locations = values 74 | 75 | 76 | @property 77 | def thing(self): 78 | return self._thing 79 | 80 | @thing.setter 81 | def thing(self, value): 82 | if value is None or isinstance(value, thing.Thing): 83 | self._thing = value 84 | return 85 | raise ValueError('thing should be of type Thing!') 86 | 87 | def ensure_service_on_children(self, service): 88 | if self.locations is not None: 89 | self.locations.set_service(service) 90 | if self.thing is not None: 91 | self.thing.set_service(service) 92 | 93 | def __eq__(self, other): 94 | if not super().__eq__(other): 95 | return False 96 | if self.time != other.time: 97 | return False 98 | return True 99 | 100 | def __ne__(self, other): 101 | return not self == other 102 | 103 | def __getstate__(self): 104 | data = super().__getstate__() 105 | if self.time is not None: 106 | data['time'] = utils.parse_datetime(self.time) 107 | if self.thing is not None: 108 | data['Thing'] = self.thing.__getstate__() 109 | if self.locations is not None and len(self.locations.entities) > 0: 110 | data['Locations'] = self.locations.__getstate__() 111 | return data 112 | 113 | def __setstate__(self, state): 114 | super().__setstate__(state) 115 | self.time = state.get("time", None) 116 | if state.get("Thing", None) is not None: 117 | self.thing = frost_sta_client.model.thing.Thing() 118 | self.thing.__setstate__(state["Thing"]) 119 | if state.get("Locations", None) is not None and isinstance(state["Locations"], list): 120 | entity_class = entity_type.EntityTypes['Location']['class'] 121 | self.locations = utils.transform_json_to_entity_list(state['Locations'], entity_class) 122 | self.locations.next_link = state.get("Locations@iot.nextLink", None) 123 | self.locations.count = state.get("Locations@iot.count", None) 124 | 125 | 126 | def get_dao(self, service): 127 | return HistoricalLocationDao(service) 128 | -------------------------------------------------------------------------------- /frost_sta_client/service/sensorthingsservice.py: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2021 Fraunhofer Institut IOSB, Fraunhoferstr. 1, D 76131 2 | # Karlsruhe, Germany. 3 | # 4 | # This program is free software: you can redistribute it and/or modify 5 | # it under the terms of the GNU Lesser General Public License as published by 6 | # the Free Software Foundation, either version 3 of the License, or 7 | # (at your option) any later version. 8 | # 9 | # This program is distributed in the hope that it will be useful, 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | # GNU Lesser General Public License for more details. 13 | # 14 | # You should have received a copy of the GNU Lesser General Public License 15 | # along with this program. If not, see . 16 | 17 | import requests 18 | from furl import furl 19 | import logging 20 | 21 | from frost_sta_client.dao import * 22 | from frost_sta_client.service import auth_handler 23 | from frost_sta_client.model.ext import entity_type 24 | 25 | 26 | class SensorThingsService: 27 | 28 | def __init__(self, url, auth_handler=None, proxies=None): 29 | self.url = url 30 | self.auth_handler = auth_handler 31 | self.proxies = proxies 32 | 33 | @property 34 | def url(self): 35 | return self._url 36 | 37 | @url.setter 38 | def url(self, value): 39 | if value is None: 40 | self._url = value 41 | return 42 | try: 43 | self._url = furl(value) 44 | except ValueError as e: 45 | logging.error("received invalid url") 46 | raise e 47 | 48 | 49 | @property 50 | def auth_handler(self): 51 | return self._auth_handler 52 | 53 | @auth_handler.setter 54 | def auth_handler(self, value): 55 | if value is None: 56 | self._auth_handler = None 57 | return 58 | if not isinstance(value, auth_handler.AuthHandler): 59 | raise ValueError('auth should be of type AuthHandler!') 60 | self._auth_handler = value 61 | 62 | 63 | @property 64 | def proxies(self): 65 | return self._proxies 66 | 67 | @proxies.setter 68 | def proxies(self, value): 69 | if value is None: 70 | self._proxies = None 71 | return 72 | elif not isinstance(value, dict): 73 | raise ValueError('Proxies must be a Dictionary!') 74 | self._proxies = value 75 | 76 | 77 | def execute(self, method, url, **kwargs): 78 | if self.auth_handler is not None: 79 | response = requests.request(method, url, proxies=self.proxies, auth=self.auth_handler.add_auth_header(), **kwargs) 80 | else: 81 | response = requests.request(method, url, proxies=self.proxies, **kwargs) 82 | try: 83 | response.raise_for_status() 84 | except requests.exceptions.HTTPError as e: 85 | raise e 86 | 87 | return response 88 | 89 | def get_path(self, parent, relation): 90 | if parent is None: 91 | return relation 92 | this_entity_type = entity_type.get_list_for_class(type(parent)) 93 | _id = f"'{parent.id}'" if isinstance(parent.id, str) else parent.id 94 | return "{entity_type}({id})/{relation}".format(entity_type=this_entity_type, id=_id, relation=relation) 95 | 96 | def get_full_path(self, parent, relation): 97 | slash = "" if self.url.pathstr[-1] == '/' else "/" 98 | url = self.url.url + slash + self.get_path(parent, relation) 99 | return furl(url) 100 | 101 | def create(self, entity): 102 | entity.get_dao(self).create(entity) 103 | 104 | def update(self, entity): 105 | entity.get_dao(self).update(entity) 106 | 107 | def patch(self, entity, patches): 108 | entity.get_dao(self).patch(entity, patches) 109 | 110 | def delete(self, entity): 111 | entity.get_dao(self).delete(entity) 112 | 113 | def actuators(self): 114 | return actuator.ActuatorDao(self) 115 | 116 | def datastreams(self): 117 | return datastream.DatastreamDao(self) 118 | 119 | def features_of_interest(self): 120 | return features_of_interest.FeaturesOfInterestDao(self) 121 | 122 | def historical_locations(self): 123 | return historical_location.HistoricalLocationDao(self) 124 | 125 | def locations(self): 126 | return location.LocationDao(self) 127 | 128 | def multi_datastreams(self): 129 | return multi_datastream.MultiDatastreamDao(self) 130 | 131 | def observations(self): 132 | return observation.ObservationDao(self) 133 | 134 | def observed_properties(self): 135 | return observedproperty.ObservedPropertyDao(self) 136 | 137 | def sensors(self): 138 | return sensor.SensorDao(self) 139 | 140 | def tasks(self): 141 | return task.TaskDao(self) 142 | 143 | def tasking_capabilities(self): 144 | return tasking_capability.TaskingCapabilityDao(self) 145 | 146 | def things(self): 147 | return thing.ThingDao(self) 148 | -------------------------------------------------------------------------------- /frost_sta_client/query/query.py: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2021 Fraunhofer Institut IOSB, Fraunhoferstr. 1, D 76131 2 | # Karlsruhe, Germany. 3 | # 4 | # This program is free software: you can redistribute it and/or modify 5 | # it under the terms of the GNU Lesser General Public License as published by 6 | # the Free Software Foundation, either version 3 of the License, or 7 | # (at your option) any later version. 8 | # 9 | # This program is distributed in the hope that it will be useful, 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | # GNU Lesser General Public License for more details. 13 | # 14 | # You should have received a copy of the GNU Lesser General Public License 15 | # along with this program. If not, see . 16 | 17 | import frost_sta_client.utils 18 | import frost_sta_client.model.ext.entity_list 19 | 20 | 21 | import logging 22 | import requests 23 | from requests.exceptions import JSONDecodeError 24 | 25 | 26 | class Query: 27 | def __init__(self, service, entity, entitytype_plural, entity_class, parent): 28 | self.service = service 29 | self.entity = entity 30 | self.entitytype_plural = entitytype_plural 31 | self.entity_class = entity_class 32 | self.params = {} 33 | self.parent = parent 34 | 35 | @property 36 | def service(self): 37 | return self._service 38 | 39 | @service.setter 40 | def service(self, service): 41 | if service is None: 42 | self._service = service 43 | return 44 | if not isinstance(service, frost_sta_client.service.sensorthingsservice.SensorThingsService): 45 | raise ValueError('service should be of type SensorThingsService') 46 | self._service = service 47 | 48 | @property 49 | def entity(self): 50 | return self._entity 51 | 52 | @entity.setter 53 | def entity(self, value): 54 | if value is None or isinstance(value, str): 55 | self._entity = value 56 | return 57 | raise ValueError('entity should be of type String') 58 | 59 | @property 60 | def entitytype_plural(self): 61 | return self._entitytype_plural 62 | 63 | @entitytype_plural.setter 64 | def entitytype_plural(self, value): 65 | if value is None or isinstance(value, str): 66 | self._entitytype_plural = value 67 | return 68 | raise ValueError('entitytype_plural should be of type String') 69 | 70 | @property 71 | def entity_class(self): 72 | return self._entity_class 73 | 74 | @entity_class.setter 75 | def entity_class(self, value): 76 | if value is None or isinstance(value, str): 77 | self._entity_class = value 78 | return 79 | raise ValueError('entity_class should be of type string') 80 | 81 | @property 82 | def parent(self): 83 | return self._parent 84 | 85 | @parent.setter 86 | def parent(self, value): 87 | self._parent = value 88 | 89 | def remove_all_params(self, key): 90 | self.params.pop(key, None) 91 | 92 | def count(self): 93 | self.remove_all_params('$count') 94 | self.params['$count'] = 'true' 95 | return self 96 | 97 | def top(self, num): 98 | self.remove_all_params('$top') 99 | self.params['$top'] = num 100 | return self 101 | 102 | def skip(self, num): 103 | self.remove_all_params('$skip') 104 | self.params['$skip'] = num 105 | return self 106 | 107 | def select(self, *args): 108 | self.remove_all_params('$select') 109 | if args is None: 110 | return self 111 | values = '' 112 | for item in args: 113 | if not isinstance(item, str): 114 | return self 115 | values = values + item + ',' 116 | values = values[:-1] 117 | self.params['$select'] = values 118 | return self 119 | 120 | def filter(self, statement=None): 121 | self.remove_all_params('$filter') 122 | if statement is None: 123 | return self 124 | self.params['$filter'] = statement 125 | return self 126 | 127 | def orderby(self, criteria, order='DESC'): 128 | self.remove_all_params('$orderby') 129 | self.params['$orderby'] = criteria + ' ' + order 130 | return self 131 | 132 | def expand(self, expansion): 133 | self.remove_all_params('$expand') 134 | self.params['$expand'] = expansion 135 | return self 136 | 137 | # exception: similar functions in basedao 138 | def list(self, callback=None, step_size=None): 139 | """ 140 | Get an entity collection as a dictionary 141 | callbacks so far only work in combination with step_size. If step_size is set, then the callback function 142 | is called at every iteration of the step_size 143 | """ 144 | url = self.service.get_full_path(self.parent, self.entitytype_plural) 145 | url.args = self.params 146 | try: 147 | response = self.service.execute('get', url) 148 | except requests.exceptions.HTTPError as e: 149 | frost_sta_client.utils.handle_server_error(e, 'Query') 150 | logging.debug('Received response: {} from {}'.format(response.status_code, url)) 151 | try: 152 | json_response = response.json() 153 | except JSONDecodeError: 154 | raise ValueError('Cannot find json in http response') 155 | entity_list = frost_sta_client.utils.transform_json_to_entity_list(json_response, self.entity_class) 156 | entity_list.set_service(self.service) 157 | 158 | entity_list.callback = callback 159 | entity_list.step_size = step_size 160 | 161 | return entity_list 162 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Sensorthings API Python Client 2 | 3 | The **FR**aunhofer **O**pensource **S**ensor**T**hings API Python Client is a python package for the [SensorThingsAPI](https://github.com/opengeospatial/sensorthings) and aims to simplify development of SensorThings enabled client applications 4 | 5 | ## Features 6 | * CRUD operations 7 | * Queries on entity lists 8 | * MultiDatastreams 9 | 10 | ## API 11 | 12 | The `SensorThingsService` class is central to the library. An instance of it represents a SensorThings service and is 13 | identified by a URI. 14 | 15 | 16 | ### CRUD operations 17 | The source code below demonstrates the CRUD operations for Thing objects. Operations for other entities work similarly. 18 | ```python 19 | import frost_sta_client as fsc 20 | 21 | url = "exampleserver.com/FROST-Server/v1.1" 22 | auth_handler = fsc.AuthHandler(username="admin", password="admin") # if server is configured for basic auth, else None 23 | service = fsc.SensorThingsService(url, auth_handler=auth_handler) 24 | ``` 25 | #### Creating Entities 26 | ```python 27 | from geojson import Point 28 | 29 | point = Point((-115.81, 37.24)) 30 | location = fsc.Location(name="here", description="and there", location=point, encoding_type='application/geo+json') 31 | 32 | thing = fsc.Thing(name='new thing', 33 | description='I am a thing with a location', 34 | properties={'withLocation': True, 'owner': 'IOSB'}) 35 | thing.locations = [location] 36 | service.create(thing) 37 | ``` 38 | #### Querying Entities 39 | Queries to the FROST Server can be modified to include filters, selections or expansions. The return value is always 40 | an EntityList object, containing the parsed json response of the server. 41 | ```python 42 | things_list = service.things().query().filter('id eq 1').list() 43 | 44 | for thing in things_list: 45 | print("my name is: {}".format(thing.name)) 46 | ``` 47 | ### EntityLists 48 | 49 | When querying a list of entities that is particularly long, the FROST server divides the list into smaller chunks, 50 | replaying to the request with the first chunk accompanied by the link to the next one. 51 | 52 | The class `EntityList` implements the function `__iter__` and `__next__` which makes it capable of iterating 53 | through the entire list of entities, including the calls to all chunks. 54 | ```python 55 | things_list = service.things().query().list() 56 | 57 | for thing in things_list: 58 | print("my name is: {}".format(thing.name)) 59 | ``` 60 | 61 | In a case where only the current chunk is supposed to be iterated, the `entities` list can be used. 62 | 63 | ```python 64 | things_list = service.things().query().top(20).list() 65 | 66 | for thing in things_list.entities: 67 | print("my name is: {}".format(thing.name)) 68 | ``` 69 | 70 | ### Queries to related entity lists 71 | 72 | For example the Observations of a given Datastream can be queried via 73 | ```python 74 | datastream = service.datastreams().find(1) 75 | observations_list = datastream.get_observations().query().filter("result gt 10").list() 76 | ``` 77 | 78 | ### Callback function in `EntityList` 79 | 80 | The progress of the loading process can be tracked by supplying a callback function along with a step size. The callback 81 | function and the step size must both be provided to the `list` function (see example below). 82 | 83 | If a callback function and a step size are used, the callback function is called every time the step size is 84 | reached during the iteration within the for-loop. (Note that the callback function so far only works in 85 | combination with a for-loop). 86 | 87 | The callback function is called with one argument, which is the current index of the iteration. 88 | 89 | ```python 90 | def callback_func(loaded_entities): 91 | print("loaded {} entities!".format(loaded_entities)) 92 | 93 | service = fsc.SensorThingsService('example_url') 94 | 95 | things = service.things().query().list(callback=callback_func, step_size=5) 96 | for thing in things: 97 | print(thing.name) 98 | ``` 99 | 100 | ### DataArrays 101 | DataArrays can be used to make the creation of Observations easier, because with an DataArray only one HTTP Request 102 | has to be created. 103 | 104 | An example usage looks as follows: 105 | ```python 106 | import frost_sta_client as fsc 107 | 108 | service = fsc.SensorThingsService("exampleserver.com/FROST-Server/v1.1") 109 | dav = fsc.model.ext.data_array_value.DataArrayValue() 110 | datastream = service.datastreams().find(1) 111 | foi = service.features_of_interest().find(1) 112 | components = {dav.Property.PHENOMENON_TIME, dav.Property.RESULT, dav.Property.FEATURE_OF_INTEREST} 113 | dav.components = components 114 | dav.datastream = datastream 115 | obs1 = fsc.Observation(result=3, 116 | phenomenon_time='2022-12-19T10:00:00Z', 117 | datastream=datastream, 118 | feature_of_interest=foi) 119 | obs2 = fsc.Observation(result=5, 120 | phenomenon_time='2022-12-19T10:00:00Z/2022-12-19T11:00:00Z', 121 | datastream=datastream, 122 | feature_of_interest=foi) 123 | dav.add_observation(obs1) 124 | dav.add_observation(obs2) 125 | dad = fsc.model.ext.data_array_document.DataArrayDocument() 126 | dad.add_data_array_value(dav) 127 | result_list = service.observations().create(dad) 128 | ``` 129 | 130 | ### Json (De)Serialization 131 | Since not all possible backends that are configurable in jsonpickle handle long floats equally, the backend json 132 | module is set to demjson3 per default. The backend can be modified by calling 133 | `jsonpickle.set_preferred_backend('name_of_preferred_backend')` anywhere in the code that uses the client. -------------------------------------------------------------------------------- /frost_sta_client/utils.py: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2021 Fraunhofer Institut IOSB, Fraunhoferstr. 1, D 76131 2 | # Karlsruhe, Germany. 3 | # 4 | # This program is free software: you can redistribute it and/or modify 5 | # it under the terms of the GNU Lesser General Public License as published by 6 | # the Free Software Foundation, either version 3 of the License, or 7 | # (at your option) any later version. 8 | # 9 | # This program is distributed in the hope that it will be useful, 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | # GNU Lesser General Public License for more details. 13 | # 14 | # You should have received a copy of the GNU Lesser General Public License 15 | # along with this program. If not, see . 16 | 17 | import jsonpickle 18 | import datetime 19 | from dateutil.parser import isoparse 20 | import geojson 21 | import logging 22 | import sys 23 | import frost_sta_client.model.ext.entity_list 24 | 25 | 26 | def extract_value(location): 27 | try: 28 | value = int(location[location.find('(')+1: location.find(')')]) 29 | except ValueError: 30 | value = str(location[location.find('(')+2: location.find(')')-1]) 31 | return value 32 | 33 | def transform_entity_to_json_dict(entity): 34 | try: 35 | data = entity.__getstate__() 36 | except AttributeError: 37 | data = entity.__dict__ 38 | return data 39 | 40 | def class_from_string(string): 41 | module_name, class_name = string.rsplit(".", 1) 42 | return getattr(sys.modules[module_name], class_name) 43 | 44 | def transform_json_to_entity(json_response, entity_class): 45 | cl = class_from_string(entity_class) 46 | obj = cl() 47 | obj.__setstate__(json_response) 48 | return obj 49 | 50 | def transform_json_to_entity_list(json_response, entity_class): 51 | entity_list = frost_sta_client.model.ext.entity_list.EntityList(entity_class) 52 | result_list = [] 53 | if isinstance(json_response, dict): 54 | try: 55 | response_list = json_response['value'] 56 | entity_list.next_link = json_response.get("@iot.nextLink", None) 57 | entity_list.count = json_response.get("@iot.count", None) 58 | except AttributeError as e: 59 | raise e 60 | elif isinstance(json_response, list): 61 | response_list = json_response 62 | else: 63 | raise ValueError("expected json as a dict or list to transform into entity list") 64 | entity_list.entities = [transform_json_to_entity(item, entity_list.entity_class) for item in response_list] 65 | return entity_list 66 | 67 | 68 | def check_datetime(value, time_entity): 69 | try: 70 | parse_datetime(value) 71 | except ValueError as e: 72 | logging.error(f"error during {time_entity} check") 73 | raise e 74 | return value 75 | 76 | 77 | def parse_datetime(value) -> str: 78 | if value is None: 79 | return value 80 | if isinstance(value, str): 81 | if '/' in value: 82 | try: 83 | times = value.split('/') 84 | if len(times) != 2: 85 | raise ValueError("If the time interval is provided as a string," 86 | " it should be in isoformat") 87 | result = [isoparse(times[0]), 88 | isoparse(times[1])] 89 | except ValueError: 90 | raise ValueError("If the time entity interval is provided as a string," 91 | " it should be in isoformat") 92 | result = result[0].isoformat() + '/' + result[1].isoformat() 93 | return result 94 | else: 95 | try: 96 | result = isoparse(value) 97 | except ValueError: 98 | raise ValueError("If the phenomenon time is provided as string, it should be in isoformat") 99 | result = result.isoformat() 100 | return result 101 | if isinstance(value, datetime.datetime): 102 | return value.isoformat() 103 | if isinstance(value, list) and all(isinstance(v, datetime.datetime) for v in value): 104 | return value[0].isoformat() + value[1].isoformat() 105 | else: 106 | raise ValueError('time entities should consist of one or two datetimes') 107 | 108 | 109 | def process_area(value): 110 | if not isinstance(value, dict): 111 | raise ValueError("geojsons can only be handled as dictionaries!") 112 | if value.get("type", None) is None or value.get("coordinates", None) is None: 113 | raise ValueError("Both type and coordinates need to be specified in the dictionary") 114 | if value["type"] == "Point": 115 | return geojson.geometry.Point(value["coordinates"]) 116 | if value["type"] == "Polygon": 117 | return geojson.geometry.Polygon(value["coordinates"]) 118 | if value["type"] == "Geometry": 119 | return geojson.geometry.Geometry(value["coordinates"]) 120 | if value["type"] == "LineString": 121 | return geojson.geometry.LineString(value["coordinates"]) 122 | raise ValueError("can only handle geojson of type Point, Polygon, Geometry or LineString") 123 | 124 | def handle_server_error(error, failed_action): 125 | # Try to extract a meaningful error message even if the response is not JSON 126 | try: 127 | err = error.response.json() 128 | if isinstance(err, dict): 129 | error_message = err.get('message', err.get('error', str(err))) 130 | else: 131 | error_message = str(err) 132 | except Exception: 133 | try: 134 | error_message = getattr(error.response, 'text', str(error)) 135 | except Exception: 136 | error_message = str(error) 137 | logging.error("{} failed with status-code {}, {}".format(failed_action, getattr(error.response, 'status_code', 'unknown'), error_message)) 138 | raise error -------------------------------------------------------------------------------- /frost_sta_client/model/ext/entity_list.py: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2021 Fraunhofer Institut IOSB, Fraunhoferstr. 1, D 76131 2 | # Karlsruhe, Germany. 3 | # 4 | # This program is free software: you can redistribute it and/or modify 5 | # it under the terms of the GNU Lesser General Public License as published by 6 | # the Free Software Foundation, either version 3 of the License, or 7 | # (at your option) any later version. 8 | # 9 | # This program is distributed in the hope that it will be useful, 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | # GNU Lesser General Public License for more details. 13 | # 14 | # You should have received a copy of the GNU Lesser General Public License 15 | # along with this program. If not, see . 16 | 17 | import logging 18 | import requests 19 | import frost_sta_client 20 | 21 | 22 | class EntityList: 23 | def __init__(self, entity_class, entities=None): 24 | if entities is None: 25 | entities = [] 26 | self.entities = entities 27 | self.entity_class = entity_class 28 | self.next_link = None 29 | self.service = None 30 | self.iterable_entities = None 31 | self.count = None 32 | self.callback = None 33 | self.step_size = None 34 | 35 | def __new__(cls, *args, **kwargs): 36 | new_entity_list = super().__new__(cls) 37 | attributes = {'_entities': None, '_entity_class': '', '_next_link': '', '_service': {}, '_count': '', 38 | '_iterable_entities': None, '_callback': None, 39 | '_step_size': None} 40 | for key, value in attributes.items(): 41 | new_entity_list.__dict__[key] = value 42 | return new_entity_list 43 | 44 | def __iter__(self): 45 | self.iterable_entities = iter(enumerate(self.entities)) 46 | return self 47 | 48 | def __next__(self): 49 | idx, next_entity = next(self.iterable_entities, (None, None)) 50 | # Only trigger callback when returning a real entity, not on sentinel indices 51 | if next_entity is None: 52 | # If current page is exhausted, try to load the next page 53 | if self.next_link is None: 54 | raise StopIteration 55 | try: 56 | response = self.service.execute('get', self.next_link) 57 | except requests.exceptions.HTTPError as e: 58 | frost_sta_client.utils.handle_server_error(e, 'Query') 59 | logging.debug('Received response: {} from {}'.format(response.status_code, self.next_link)) 60 | try: 61 | json_response = response.json() 62 | except ValueError: 63 | raise ValueError('Cannot find json in http response') 64 | 65 | result_list = frost_sta_client.utils.transform_json_to_entity_list(json_response, self.entity_class) 66 | # Append new entities and reset iterator to iterate over the newly fetched page 67 | start_index = len(self.entities) 68 | self.entities += result_list.entities 69 | self.set_service(self.service) 70 | self.next_link = json_response.get("@iot.nextLink", None) 71 | self.iterable_entities = iter(enumerate(self.entities[start_index:], start=start_index)) 72 | idx, next_entity = next(self.iterable_entities, (None, None)) 73 | if next_entity is None: 74 | raise StopIteration 75 | if self.step_size is not None and self.callback is not None and idx % self.step_size == 0: 76 | self.callback(idx) 77 | return next_entity 78 | raise StopIteration 79 | 80 | def get(self, index): 81 | if not isinstance(index, int): 82 | raise IndexError('index must be an integer') 83 | if index >= len(self.entities): 84 | raise IndexError('index exceeds total number of entities') 85 | if index < 0: 86 | raise IndexError('negative indices cannot be accessed') 87 | return self.entities[index] 88 | 89 | @property 90 | def entity_class(self): 91 | return self._entity_class 92 | 93 | @entity_class.setter 94 | def entity_class(self, value): 95 | if isinstance(value, str): 96 | self._entity_class = value 97 | return 98 | raise ValueError('entity_class should be of type str') 99 | 100 | @property 101 | def entities(self): 102 | return self._entities 103 | 104 | @entities.setter 105 | def entities(self, values): 106 | if isinstance(values, list) and all(isinstance(v, frost_sta_client.model.entity.Entity) for v in values): 107 | self._entities = values 108 | return 109 | raise ValueError('entities should be a list of entities') 110 | 111 | @property 112 | def callback(self): 113 | return self._callback 114 | 115 | @callback.setter 116 | def callback(self, callback): 117 | if callable(callback) or callback is None: 118 | self._callback = callback 119 | 120 | @property 121 | def step_size(self): 122 | return self._step_size 123 | 124 | @step_size.setter 125 | def step_size(self, value): 126 | if isinstance(value, int) or value is None: 127 | self._step_size = value 128 | return 129 | raise ValueError('step_size should be of type int') 130 | 131 | @property 132 | def next_link(self): 133 | return self._next_link 134 | 135 | @next_link.setter 136 | def next_link(self, value): 137 | if value is None or isinstance(value, str): 138 | self._next_link = value 139 | return 140 | raise ValueError('next_link should be of type string') 141 | 142 | @property 143 | def service(self): 144 | return self._service 145 | 146 | @service.setter 147 | def service(self, value): 148 | if value is None or isinstance(value, frost_sta_client.service.sensorthingsservice.SensorThingsService): 149 | self._service = value 150 | return 151 | raise ValueError('service should be of type SensorThingsService') 152 | 153 | def set_service(self, service): 154 | self.service = service 155 | for entity in self.entities: 156 | entity.set_service(service) 157 | 158 | def __getstate__(self): 159 | data = [] 160 | for entity in self.entities: 161 | data.append(entity.__getstate__()) 162 | return data 163 | 164 | def __setstate__(self, state): 165 | self._next_link = state.get(self.entities + '@nextLink') 166 | pass 167 | -------------------------------------------------------------------------------- /tests/test_entity_formatter.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | from geojson import Point 3 | 4 | import frost_sta_client.model.location 5 | import frost_sta_client.model.thing 6 | import frost_sta_client.utils 7 | import frost_sta_client.model.ext.entity_list 8 | from frost_sta_client.model.ext import entity_type 9 | 10 | 11 | class TestEntityFormatter(unittest.TestCase): 12 | 13 | def test_create_thing_basic(self): 14 | exp_result = dict(name='nice thing', 15 | description='the description of the thing', 16 | properties=dict( 17 | nice=True, 18 | level_of_niceness=1000 19 | ) 20 | ) 21 | entity = frost_sta_client.model.thing.Thing() 22 | entity.name = 'nice thing' 23 | entity.description = 'the description of the thing' 24 | properties = {'nice': True, 'level_of_niceness': 1000} 25 | entity.properties = properties 26 | entity_json = frost_sta_client.utils.transform_entity_to_json_dict(entity) 27 | self.assertDictEqual(exp_result, entity_json) 28 | 29 | def test_write_thing_completely_empty(self): 30 | result = {} 31 | 32 | entity = frost_sta_client.model.thing.Thing() 33 | entity_json = frost_sta_client.utils.transform_entity_to_json_dict(entity) 34 | self.assertDictEqual(result, entity_json) 35 | 36 | def test_write_thing_with_location(self): 37 | result = {'name': 'another nice thing', 38 | 'description': 'This thing has also a nice location', 39 | 'Locations': [ 40 | { 41 | '@iot.id': 1, 42 | } 43 | ]} 44 | entity = frost_sta_client.model.thing.Thing() 45 | entity.name = 'another nice thing' 46 | entity.description = 'This thing has also a nice location' 47 | entity.properties = {} 48 | 49 | location = frost_sta_client.model.location.Location() 50 | location.id = 1 51 | entity.locations = frost_sta_client.model.ext.entity_list.EntityList(entities=[location], 52 | entity_class=entity_type.EntityTypes['Location']['class']) 53 | entity_json = frost_sta_client.utils.transform_entity_to_json_dict(entity) 54 | self.assertDictEqual(result, entity_json) 55 | 56 | 57 | def test_write_thing_with_specified_id_and_attributes(self): 58 | result = {'@iot.id': 123, 59 | 'name': 'another nice thing', 60 | 'Locations': [ 61 | { 62 | '@iot.id': 456, 63 | 'name': 'location with specified id' 64 | } 65 | ], 66 | 'HistoricalLocations': [ 67 | { 68 | '@iot.id': 789 69 | } 70 | ], 71 | 'Datastreams': [ 72 | { 73 | 'name': 'Datastream without specified id' 74 | } 75 | ], 76 | } 77 | entity = frost_sta_client.model.thing.Thing(id=123) 78 | entity.name = 'another nice thing' 79 | 80 | location = frost_sta_client.model.location.Location(name='location with specified id') 81 | location.id = 456 82 | historical_location = frost_sta_client.model.historical_location.HistoricalLocation(id=789) 83 | datastream = frost_sta_client.model.datastream.Datastream(name='Datastream without specified id') 84 | 85 | entity.locations = frost_sta_client.model.ext.entity_list.EntityList(entities=[location], 86 | entity_class=entity_type.EntityTypes['Location']['class']) 87 | entity.historical_locations = frost_sta_client.model.ext.entity_list.EntityList(entities=[historical_location], 88 | entity_class=entity_type.EntityTypes['HistoricalLocation']['class']) 89 | entity.datastreams = frost_sta_client.model.ext.entity_list.EntityList(entities=[datastream], entity_class=entity_type.EntityTypes['Datastream']['class']) 90 | entity_json = frost_sta_client.utils.transform_entity_to_json_dict(entity) 91 | self.assertDictEqual(result, entity_json) 92 | 93 | 94 | def test_incorrect_collection(self): 95 | exp_result = { 96 | 'name': 'test thing', 97 | 'description': 'incorrect thing for testing', 98 | 'Locations': [ 99 | { 100 | 'name': 'favorite place', 101 | 'description': 'this is my favorite place', 102 | 'encodingType': 'application/vnd.geo+json', 103 | 'location': { 104 | 'type': 'Point', 105 | 'coordinates': [-49.593, 85.23] 106 | } 107 | } 108 | ] 109 | } 110 | 111 | entity = frost_sta_client.model.thing.Thing() 112 | entity.name = 'test thing' 113 | entity.description = 'incorrect thing for testing' 114 | 115 | location = frost_sta_client.model.location.Location() 116 | location.name = 'favorite place' 117 | location.description = 'this is my favorite place' 118 | location.encoding_type = 'application/vnd.geo+json' 119 | location.location = Point((-49.593, 85.23)) 120 | 121 | entity.locations = frost_sta_client.model.ext.entity_list.EntityList(entities=[location], 122 | entity_class=entity_type.EntityTypes['Location']['class']) 123 | entity_json = frost_sta_client.utils.transform_entity_to_json_dict(entity) 124 | self.assertDictEqual(exp_result, entity_json) 125 | 126 | def test_write_location_geojson(self): 127 | input_json = { 128 | '@iot.id': 1, 129 | 'name': 'Treasure', 130 | 'description': 'location of the treasure', 131 | 'encodingType': 'application/vnd.geo+json', 132 | 'location': { 133 | 'type': 'Point', 134 | 'coordinates': [-49.593, 85.23] 135 | } 136 | } 137 | exp_result = frost_sta_client.model.location.Location() 138 | exp_result.id = 1 139 | exp_result.name = 'Treasure' 140 | exp_result.description = 'location of the treasure' 141 | exp_result.encoding_type = 'application/vnd.geo+json' 142 | exp_result.location = Point((-49.593, 85.23)) 143 | result = frost_sta_client.utils.transform_json_to_entity(input_json, 'frost_sta_client.model.location.Location') 144 | self.assertEqual(result, exp_result) 145 | 146 | 147 | if __name__ == '__main__': 148 | unittest.main() 149 | -------------------------------------------------------------------------------- /frost_sta_client/model/ext/data_array_value.py: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2021 Fraunhofer Institut IOSB, Fraunhoferstr. 1, D 76131 2 | # Karlsruhe, Germany. 3 | # 4 | # This program is free software: you can redistribute it and/or modify 5 | # it under the terms of the GNU Lesser General Public License as published by 6 | # the Free Software Foundation, either version 3 of the License, or 7 | # (at your option) any later version. 8 | # 9 | # This program is distributed in the hope that it will be useful, 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | # GNU Lesser General Public License for more details. 13 | # 14 | # You should have received a copy of the GNU Lesser General Public License 15 | # along with this program. If not, see . 16 | from enum import Enum 17 | 18 | import frost_sta_client 19 | import datetime 20 | 21 | 22 | class DataArrayValue: 23 | 24 | class Property(Enum): 25 | ID = 1 26 | PHENOMENON_TIME = 2 27 | RESULT = 3 28 | RESULT_TIME = 4 29 | RESULT_QUALITY = 5 30 | VALID_TIME = 6 31 | PARAMETERS = 7 32 | FEATURE_OF_INTEREST = 8 33 | 34 | def to_string(self): 35 | if self is DataArrayValue.Property.ID: 36 | return "id" 37 | if self is DataArrayValue.Property.PHENOMENON_TIME: 38 | return "phenomenonTime" 39 | if self is DataArrayValue.Property.RESULT: 40 | return "result" 41 | if self is DataArrayValue.Property.RESULT_TIME: 42 | return "resultTime" 43 | if self is DataArrayValue.Property.RESULT_QUALITY: 44 | return "resultQuality" 45 | if self is DataArrayValue.Property.VALID_TIME: 46 | return "validTime" 47 | if self is DataArrayValue.Property.PARAMETERS: 48 | return "parameters" 49 | if self is DataArrayValue.Property.FEATURE_OF_INTEREST: 50 | return "FeatureOfInterest/id" 51 | 52 | 53 | class VisibleProperties: 54 | def __init__(self, all_values: bool): 55 | self.id = all_values 56 | self.phenomenon_time = all_values 57 | self.result = all_values 58 | self.result_time = all_values 59 | self.result_quality = all_values 60 | self.valid_time = all_values 61 | self.parameters = all_values 62 | self.feature_of_interest = all_values 63 | 64 | def __init__(self): 65 | self = DataArrayValue.VisibleProperties(False) 66 | 67 | def __init__(self, select): 68 | self.id = DataArrayValue.Property.ID in select 69 | self.phenomenon_time = DataArrayValue.Property.PHENOMENON_TIME in select 70 | self.result = DataArrayValue.Property.RESULT in select 71 | self.result_time = DataArrayValue.Property.RESULT_TIME in select 72 | self.result_quality = DataArrayValue.Property.RESULT_QUALITY in select 73 | self.valid_time = DataArrayValue.Property.VALID_TIME in select 74 | self.parameters = DataArrayValue.Property.PARAMETERS in select 75 | self.feature_of_interest = DataArrayValue.Property.FEATURE_OF_INTEREST in select 76 | 77 | def get_components(self): 78 | components = [] 79 | if self.id: 80 | components.append(DataArrayValue.Property.ID.to_string()) 81 | if self.phenomenon_time: 82 | components.append(DataArrayValue.Property.PHENOMENON_TIME.to_string()) 83 | if self.result: 84 | components.append(DataArrayValue.Property.RESULT.to_string()) 85 | if self.result_time: 86 | components.append(DataArrayValue.Property.RESULT_TIME.to_string()) 87 | if self.result_quality: 88 | components.append(DataArrayValue.Property.RESULT_QUALITY.to_string()) 89 | if self.valid_time: 90 | components.append(DataArrayValue.Property.VALID_TIME.to_string()) 91 | if self.parameters: 92 | components.append(DataArrayValue.Property.PARAMETERS.to_string()) 93 | if self.feature_of_interest: 94 | components.append(DataArrayValue.Property.FEATURE_OF_INTEREST.to_string()) 95 | return components 96 | 97 | 98 | def from_observation(self, o: frost_sta_client.Observation): 99 | value = [] 100 | if self.id: 101 | value.append(o.id) 102 | if self.phenomenon_time: 103 | if type(o.phenomenon_time) == str: 104 | value.append(o.phenomenon_time) 105 | if type(o.phenomenon_time) == datetime.datetime: 106 | value.append(o.phenomenon_time.isoformat()) 107 | if self.result: 108 | value.append(o.result) 109 | if self.result_time: 110 | value.append(o.result_time) 111 | if self.result_quality: 112 | value.append(o.result_quality) 113 | if self.valid_time: 114 | value.append(o.valid_time) 115 | if self.parameters: 116 | value.append(o.parameters) 117 | if self.feature_of_interest: 118 | value.append(o.feature_of_interest.id) 119 | return value 120 | 121 | 122 | def __init__(self): 123 | self.datastream = None 124 | self.multi_datastream = None 125 | self.visible_properties = None 126 | self.data_array = [] 127 | self.components = None 128 | self.observations = [] 129 | 130 | 131 | @property 132 | def datastream(self): 133 | return self._datastream 134 | 135 | @datastream.setter 136 | def datastream(self, value): 137 | self._datastream = value 138 | 139 | @property 140 | def components(self): 141 | return self._components 142 | 143 | @components.setter 144 | def components(self, properties): 145 | if properties is None: 146 | self._components = None 147 | return 148 | if len(self.data_array) >= 1: 149 | raise ValueError("Can not change components after adding Observations") 150 | self.visible_properties = self.VisibleProperties(properties) 151 | self._components = self.visible_properties.get_components() 152 | 153 | @property 154 | def data_array(self): 155 | return self._data_array 156 | 157 | @data_array.setter 158 | def data_array(self, value): 159 | self._data_array = value 160 | 161 | def add_observation(self, o): 162 | self.data_array.append(self.visible_properties.from_observation(o)) 163 | self.observations.append(o) 164 | 165 | def __getstate__(self): 166 | data = {"Datastream": { 167 | "@iot.id": self.datastream.id 168 | }, 169 | "components": self.components, 170 | "dataArray": self.data_array} 171 | return data 172 | 173 | -------------------------------------------------------------------------------- /frost_sta_client/model/actuator.py: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2021 Fraunhofer Institut IOSB, Fraunhoferstr. 1, D 76131 2 | # Karlsruhe, Germany. 3 | # 4 | # This program is free software: you can redistribute it and/or modify 5 | # it under the terms of the GNU Lesser General Public License as published by 6 | # the Free Software Foundation, either version 3 of the License, or 7 | # (at your option) any later version. 8 | # 9 | # This program is distributed in the hope that it will be useful, 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | # GNU Lesser General Public License for more details. 13 | # 14 | # You should have received a copy of the GNU Lesser General Public License 15 | # along with this program. If not, see . 16 | import json 17 | 18 | from frost_sta_client.dao.actuator import ActuatorDao 19 | 20 | from . import entity 21 | from . import tasking_capability 22 | 23 | from .ext import entity_list 24 | from .ext import entity_type 25 | from frost_sta_client import utils 26 | 27 | 28 | class Actuator(entity.Entity): 29 | 30 | def __init__(self, 31 | name='', 32 | description='', 33 | encoding_type='', 34 | tasking_capabilities=None, 35 | metadata='', 36 | properties=None, 37 | **kwargs): 38 | super().__init__(**kwargs) 39 | if properties is None: 40 | properties = {} 41 | self.name = name 42 | self.description = description 43 | self.encoding_type = encoding_type 44 | self.tasking_capabilities = tasking_capabilities 45 | self.metadata = metadata 46 | self.properties = properties 47 | 48 | def __new__(cls, *args, **kwargs): 49 | new_actuator = super().__new__(cls) 50 | attributes = {'_id': None, '_name': '', '_description': '', '_properties': {}, '_encoding_type': '', 51 | '_metadata': '', '_self_link': '', '_service': None, '_tasking_capabilities': None} 52 | for key, value in attributes.items(): 53 | new_actuator.__dict__[key] = value 54 | return new_actuator 55 | 56 | @property 57 | def name(self): 58 | return self._name 59 | 60 | @name.setter 61 | def name(self, value): 62 | if not isinstance(value, str): 63 | raise ValueError('name should be of type str!') 64 | self._name = value 65 | 66 | @property 67 | def description(self): 68 | return self._description 69 | 70 | @description.setter 71 | def description(self, value): 72 | if not isinstance(value, str): 73 | raise ValueError('description should be of type str!') 74 | self._description = value 75 | 76 | @property 77 | def encoding_type(self): 78 | return self._encoding_type 79 | 80 | @encoding_type.setter 81 | def encoding_type(self, value): 82 | if not isinstance(value, str): 83 | raise ValueError('encodingtype should be of type str!') 84 | self._encoding_type = value 85 | 86 | @property 87 | def metadata(self): 88 | return self._metadata 89 | 90 | @metadata.setter 91 | def metadata(self, value): 92 | try: 93 | json.dumps(value) 94 | except TypeError: 95 | raise TypeError('result should be json serializable') 96 | self._metadata = value 97 | if self._metadata is None: 98 | raise Warning('metadata is a mandatory property') 99 | 100 | @property 101 | def properties(self): 102 | return self._properties 103 | 104 | @properties.setter 105 | def properties(self, value): 106 | if not isinstance(value, dict): 107 | raise ValueError('properties should be of type dict!') 108 | self._properties = value 109 | 110 | @property 111 | def tasking_capabilities(self): 112 | return self._tasking_capabilities 113 | 114 | @tasking_capabilities.setter 115 | def tasking_capabilities(self, values): 116 | if values is None: 117 | self._tasking_capabilities = None 118 | return 119 | if isinstance(values, list) and all(isinstance(tc, tasking_capability.TaskingCapability) for tc in values): 120 | entity_class = entity_type.EntityTypes['TaskingCapability']['class'] 121 | self._tasking_capabilities = entity_list.EntityList(entity_class=entity_class, entities=values) 122 | return 123 | if not isinstance(values, entity_list.EntityList) or \ 124 | any((not isinstance(tc, tasking_capability.TaskingCapability)) for tc in values.entities): 125 | raise ValueError('Tasking capabilities should be a list of TaskingCapabilities') 126 | self._tasking_capabilities = values 127 | 128 | def get_tasking_capabilities(self): 129 | result = self.service.tasking_capabilities() 130 | result.parent = self 131 | return result 132 | 133 | def ensure_service_on_children(self, service): 134 | if self.tasking_capabilities is not None: 135 | self.tasking_capabilities.set_service(service) 136 | 137 | def __eq__(self, other): 138 | if not super().__eq__(other): 139 | return False 140 | if self.name != other.name: 141 | return False 142 | if self.description != other.description: 143 | return False 144 | if self.encoding_type != other.encoding_type: 145 | return False 146 | if self.metadata != other.metadata: 147 | return False 148 | if self.properties != other.properties: 149 | return False 150 | return True 151 | 152 | def __ne__(self, other): 153 | return not self == other 154 | 155 | def __getstate__(self): 156 | data = super().__getstate__() 157 | if self.name is not None and self.name != '': 158 | data['name'] = self.name 159 | if self.description is not None and self.description != '': 160 | data['description'] = self.description 161 | if self.encoding_type is not None: 162 | data['encodingType'] = self.encoding_type 163 | if self.metadata is not None: 164 | data['metadata'] = self.metadata 165 | if self.properties is not None and self.properties != {}: 166 | data['properties'] = self.properties 167 | if self.tasking_capabilities is not None and len(self.tasking_capabilities.entities) > 0: 168 | data['TaskingCapabilities'] = self.tasking_capabilities.__getstate__() 169 | return data 170 | 171 | def __setstate__(self, state): 172 | super().__setstate__(state) 173 | self.name = state.get("name", None) 174 | self.description = state.get("description", None) 175 | self.encoding_type = state.get("encodingType", "") 176 | self.metadata = state.get("metadata", "") 177 | self.properties = state.get("properties", None) 178 | if state.get("TaskingCapabilities", None) is not None and isinstance(state["TaskingCapabilities"], list): 179 | entity_class = entity_type.EntityTypes['TaskingCapability']['class'] 180 | self.tasking_capabilities = utils.transform_json_to_entity_list(state['TaskingCapabilities'], entity_class) 181 | self.tasking_capabilities.next_link = state.get("TaskingCapabilities@iot.nextLink", None) 182 | self.tasking_capabilities.count = state.get("TaskingCapabilities@iot.count", None) 183 | 184 | def get_dao(self, service): 185 | return ActuatorDao(service) 186 | -------------------------------------------------------------------------------- /frost_sta_client/model/feature_of_interest.py: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2021 Fraunhofer Institut IOSB, Fraunhoferstr. 1, D 76131 2 | # Karlsruhe, Germany. 3 | # 4 | # This program is free software: you can redistribute it and/or modify 5 | # it under the terms of the GNU Lesser General Public License as published by 6 | # the Free Software Foundation, either version 3 of the License, or 7 | # (at your option) any later version. 8 | # 9 | # This program is distributed in the hope that it will be useful, 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | # GNU Lesser General Public License for more details. 13 | # 14 | # You should have received a copy of the GNU Lesser General Public License 15 | # along with this program. If not, see . 16 | 17 | from . import entity 18 | 19 | import frost_sta_client.dao.features_of_interest 20 | 21 | from .ext import entity_list, entity_type 22 | 23 | import json 24 | 25 | from .. import utils 26 | 27 | 28 | class FeatureOfInterest(entity.Entity): 29 | def __init__(self, 30 | name='', 31 | description='', 32 | encoding_type='', 33 | feature=None, 34 | properties=None, 35 | observations=None, 36 | **kwargs): 37 | super().__init__(**kwargs) 38 | if properties is None: 39 | properties = {} 40 | self.name = name 41 | self.description = description 42 | self.encoding_type = encoding_type 43 | self.feature = feature 44 | self.properties = properties 45 | self.observations = observations 46 | 47 | def __new__(cls, *args, **kwargs): 48 | new_foi = super().__new__(cls) 49 | attributes = {'_id': None, '_name': '', '_description': '', '_properties': {}, '_encoding_type': '', 50 | '_feature': '', '_observations': None, '_self_link': '', '_service': None} 51 | for key, value in attributes.items(): 52 | new_foi.__dict__[key] = value 53 | return new_foi 54 | 55 | @property 56 | def name(self): 57 | return self._name 58 | 59 | @name.setter 60 | def name(self, value): 61 | if value is None: 62 | self._name = None 63 | return 64 | if not isinstance(value, str): 65 | raise ValueError('name should be of type str!') 66 | self._name = value 67 | 68 | @property 69 | def description(self): 70 | return self._description 71 | 72 | @description.setter 73 | def description(self, value): 74 | if value is None: 75 | self._description = None 76 | return 77 | if not isinstance(value, str): 78 | raise ValueError('description should be of type str!') 79 | self._description = value 80 | 81 | @property 82 | def properties(self): 83 | return self._properties 84 | 85 | @properties.setter 86 | def properties(self, value): 87 | if value is None: 88 | self._properties = None 89 | return 90 | if not isinstance(value, dict): 91 | raise ValueError('properties should be of type dict!') 92 | self._properties = value 93 | 94 | @property 95 | def encoding_type(self): 96 | return self._encoding_type 97 | 98 | @encoding_type.setter 99 | def encoding_type(self, value): 100 | if value is None: 101 | self._encoding_type = None 102 | return 103 | if not isinstance(value, str): 104 | raise ValueError('encodingType should be of type str!') 105 | self._encoding_type = value 106 | 107 | @property 108 | def observations(self): 109 | return self._observations 110 | 111 | @observations.setter 112 | def observations(self, values): 113 | if values is None: 114 | self._observations = None 115 | return 116 | if isinstance(values, list) and \ 117 | all(isinstance(ob, frost_sta_client.model.observation.Observation) for ob in values): 118 | entity_class = entity_type.EntityTypes['Observation']['class'] 119 | self._observations = entity_list.EntityList(entity_class=entity_class, entities=values) 120 | return 121 | if isinstance(values, entity_list.EntityList) and \ 122 | all((isinstance(ob, frost_sta_client.model.observation.Observation)) for ob in values.entities): 123 | self._observations = values 124 | return 125 | raise ValueError('Observations should be a list of Observations') 126 | 127 | @property 128 | def feature(self): 129 | return self._feature 130 | 131 | @feature.setter 132 | def feature(self, value): 133 | if value is None: 134 | self._feature = None 135 | return 136 | try: 137 | json.dumps(value) 138 | except TypeError: 139 | raise TypeError('feature should be json serializable') 140 | self._feature = value 141 | 142 | def get_observations(self): 143 | result = self.service.observations() 144 | result.parent = self 145 | return result 146 | 147 | def ensure_service_on_children(self, service): 148 | if self.observations is not None: 149 | self.observations.set_service(service) 150 | 151 | def __eq__(self, other): 152 | if not super().__eq__(other): 153 | return False 154 | if self.name != other.name: 155 | return False 156 | if self.properties != other.properties: 157 | return False 158 | if self.description != other.description: 159 | return False 160 | if self.encoding_type != other.encoding_type: 161 | return False 162 | if self.feature != other.feature: 163 | return False 164 | return True 165 | 166 | def __ne__(self, other): 167 | return not self == other 168 | 169 | def __getstate__(self): 170 | data = super().__getstate__() 171 | if self.name is not None and self.name != '': 172 | data['name'] = self.name 173 | if self.description is not None and self.description != '': 174 | data['description'] = self.description 175 | if self.properties is not None and self.properties != {}: 176 | data['properties'] = self.properties 177 | if self.encoding_type is not None and self.encoding_type != '': 178 | data['encodingType'] = self.encoding_type 179 | if self.feature is not None: 180 | data['feature'] = self.feature 181 | if self.observations is not None and len(self.observations.entities) > 0: 182 | data['Observations'] = self.observations.__getstate__() 183 | return data 184 | 185 | def __setstate__(self, state): 186 | super().__setstate__(state) 187 | self.name = state.get("name", None) 188 | self.description = state.get("description", None) 189 | self.properties = state.get("properties", {}) 190 | self.encoding_type = state.get("encodingType", None) 191 | self.feature = state.get("feature", None) 192 | if state.get("Observations", None) is not None and isinstance(state["Observations"], list): 193 | entity_class = entity_type.EntityTypes['Observation']['class'] 194 | self.observations = utils.transform_json_to_entity_list(state['Observations'], entity_class) 195 | self.observations.next_link = state.get("Observations@iot.nextLink", None) 196 | self.observations.count = state.get("Observations@iot.count", None) 197 | 198 | def get_dao(self, service): 199 | return frost_sta_client.dao.features_of_interest.FeaturesOfInterestDao(service) 200 | -------------------------------------------------------------------------------- /frost_sta_client/dao/base.py: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2021 Fraunhofer Institut IOSB, Fraunhoferstr. 1, D 76131 2 | # Karlsruhe, Germany. 3 | # 4 | # This program is free software: you can redistribute it and/or modify 5 | # it under the terms of the GNU Lesser General Public License as published by 6 | # the Free Software Foundation, either version 3 of the License, or 7 | # (at your option) any later version. 8 | # 9 | # This program is distributed in the hope that it will be useful, 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | # GNU Lesser General Public License for more details. 13 | # 14 | # You should have received a copy of the GNU Lesser General Public License 15 | # along with this program. If not, see . 16 | 17 | import frost_sta_client.query.query 18 | import frost_sta_client.utils 19 | 20 | import logging 21 | import requests 22 | import jsonpatch 23 | import json 24 | from furl import furl 25 | 26 | 27 | class BaseDao: 28 | """ 29 | The entity independent implementation of a data access object. Specific entity Daos 30 | can be implemented by inheriting from this class. 31 | """ 32 | APPLICATION_JSON_PATCH = {'Content-type': 'application/json-patch+json'} 33 | 34 | def __init__(self, service, entitytype): 35 | """ 36 | Constructor. 37 | params: 38 | service: the service to operate on 39 | entitytype: a dictionary describing the type of the entity 40 | """ 41 | self.service = service 42 | self.entitytype = entitytype["singular"] 43 | self.entitytype_plural = entitytype["plural"] 44 | self.entity_class = entitytype["class"] 45 | self.parent = None 46 | 47 | @property 48 | def service(self): 49 | return self._service 50 | 51 | @service.setter 52 | def service(self, value): 53 | if value is None or isinstance(value, frost_sta_client.service.sensorthingsservice.SensorThingsService): 54 | self._service = value 55 | return 56 | raise ValueError('service should be of type SensorThingsService') 57 | 58 | @property 59 | def entitytype(self): 60 | return self._entitytype 61 | 62 | @entitytype.setter 63 | def entitytype(self, value): 64 | if value is None or isinstance(value, str): 65 | self._entitytype = value 66 | return 67 | raise ValueError('entitytype should be of type String') 68 | 69 | @property 70 | def entitytype_plural(self): 71 | return self._entitytype_plural 72 | 73 | @entitytype_plural.setter 74 | def entitytype_plural(self, value): 75 | if value is None or isinstance(value, str): 76 | self._entitytype_plural = value 77 | return 78 | raise ValueError('entitytype_plural should be of type String') 79 | 80 | @property 81 | def entity_class(self): 82 | return self._entity_class 83 | 84 | @entity_class.setter 85 | def entity_class(self, value): 86 | if value is None or isinstance(value, str): 87 | self._entity_class = value 88 | return 89 | raise ValueError('entity_class should be of type string') 90 | 91 | @property 92 | def parent(self): 93 | return self._parent 94 | 95 | @parent.setter 96 | def parent(self, value): 97 | self._parent = value 98 | 99 | def create(self, entity): 100 | url = furl(self.service.url) 101 | url.path.add(self.entitytype_plural) 102 | logging.debug('Posting to ' + str(url.url)) 103 | json_dict = frost_sta_client.utils.transform_entity_to_json_dict(entity) 104 | try: 105 | response = self.service.execute('post', url, json=json_dict) 106 | except requests.exceptions.HTTPError as e: 107 | frost_sta_client.utils.handle_server_error(e, 'Creating {}'.format(type(entity).__name__)) 108 | entity.id = frost_sta_client.utils.extract_value(response.headers['location']) 109 | entity.service = self.service 110 | logging.debug('Received response: ' + str(response.status_code)) 111 | 112 | def patch(self, entity, patches): 113 | """ 114 | method to patch STA entities 115 | param entity: entity, that the patches should be applied to 116 | param patches: either a JsonPatch object or list of dictionaries, containing jsonpatch commands 117 | """ 118 | url = furl(self.service.url) 119 | if entity.id is None or entity.id == '': 120 | raise AttributeError('please provide an entity with a valid id') 121 | url.path.add(self.entity_path(entity.id)) 122 | logging.debug(f'Patching to {url.url}') 123 | headers = self.APPLICATION_JSON_PATCH 124 | if patches is None: 125 | raise ValueError('please provide a list of patches, either as a jsonpatch object or a ' 126 | 'list of dictionaries') 127 | if not isinstance(patches, jsonpatch.JsonPatch) and \ 128 | not (isinstance(patches, list) and all(isinstance(x, dict) for x in patches)): 129 | raise ValueError('please provide a list of patches, either as a jsonpatch object or a ' 130 | 'list of dictionaries') 131 | if isinstance(patches, jsonpatch.JsonPatch): 132 | patches = patches.patch 133 | try: 134 | response = self.service.execute('patch', url, json=patches, headers=headers) 135 | except requests.exceptions.HTTPError as e: 136 | frost_sta_client.utils.handle_server_error(e, 'Patching {}'.format(type(entity).__name__)) 137 | logging.debug(f'Received response: {str(response.status_code)}') 138 | 139 | def update(self, entity): 140 | url = furl(self.service.url) 141 | if entity.id is None or entity.id == '': 142 | raise AttributeError('please provide an entity with a valid id') 143 | url.path.add(self.entity_path(entity.id)) 144 | logging.debug('Updating to {}'.format(url.url)) 145 | json_dict = frost_sta_client.utils.transform_entity_to_json_dict(entity) 146 | try: 147 | response = self.service.execute('put', url, json=json_dict) 148 | except requests.exceptions.HTTPError as e: 149 | frost_sta_client.utils.handle_server_error(e, 'Updating {}'.format(type(entity).__name__)) 150 | logging.debug('Received response: {}'.format(str(response.status_code))) 151 | 152 | def find(self, id): 153 | url = furl(self.service.url) 154 | url.path.add(self.entity_path(id)) 155 | logging.debug('Fetching: {}'.format(url.url)) 156 | try: 157 | response = self.service.execute('get', url) 158 | except requests.exceptions.HTTPError as e: 159 | frost_sta_client.utils.handle_server_error(e, 'Finding {}'.format(id)) 160 | logging.debug('Received response: {}'.format(response.status_code)) 161 | json_response = response.json() 162 | json_response['id'] = json_response['@iot.id'] 163 | entity = frost_sta_client.utils.transform_json_to_entity(json_response, self.entity_class) 164 | entity.service = self.service 165 | return entity 166 | 167 | def delete(self, entity): 168 | url = furl(self.service.url) 169 | url.path.add(self.entity_path(entity.id)) 170 | logging.debug('Deleting: {}'.format(url.url)) 171 | try: 172 | response = self.service.execute('delete', url) 173 | except requests.exceptions.HTTPError as e: 174 | frost_sta_client.utils.handle_server_error(e, 'Deleting {}'.format(type(entity).__name__)) 175 | logging.debug('Received response: {}'.format(response.status_code)) 176 | 177 | def entity_path(self, id): 178 | if isinstance(id, int): 179 | return "{}({})".format(self.entitytype_plural, id) 180 | return "{}('{}')".format(self.entitytype_plural, id) 181 | 182 | def query(self): 183 | return frost_sta_client.query.query.Query(self.service, self.entitytype, self.entitytype_plural, 184 | self.entity_class, self.parent) 185 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | GNU LESSER GENERAL PUBLIC LICENSE 2 | Version 3, 29 June 2007 3 | 4 | Copyright (C) 2007 Free Software Foundation, Inc. 5 | Everyone is permitted to copy and distribute verbatim copies 6 | of this license document, but changing it is not allowed. 7 | 8 | 9 | This version of the GNU Lesser General Public License incorporates 10 | the terms and conditions of version 3 of the GNU General Public 11 | License, supplemented by the additional permissions listed below. 12 | 13 | 0. Additional Definitions. 14 | 15 | As used herein, "this License" refers to version 3 of the GNU Lesser 16 | General Public License, and the "GNU GPL" refers to version 3 of the GNU 17 | General Public License. 18 | 19 | "The Library" refers to a covered work governed by this License, 20 | other than an Application or a Combined Work as defined below. 21 | 22 | An "Application" is any work that makes use of an interface provided 23 | by the Library, but which is not otherwise based on the Library. 24 | Defining a subclass of a class defined by the Library is deemed a mode 25 | of using an interface provided by the Library. 26 | 27 | A "Combined Work" is a work produced by combining or linking an 28 | Application with the Library. The particular version of the Library 29 | with which the Combined Work was made is also called the "Linked 30 | Version". 31 | 32 | The "Minimal Corresponding Source" for a Combined Work means the 33 | Corresponding Source for the Combined Work, excluding any source code 34 | for portions of the Combined Work that, considered in isolation, are 35 | based on the Application, and not on the Linked Version. 36 | 37 | The "Corresponding Application Code" for a Combined Work means the 38 | object code and/or source code for the Application, including any data 39 | and utility programs needed for reproducing the Combined Work from the 40 | Application, but excluding the System Libraries of the Combined Work. 41 | 42 | 1. Exception to Section 3 of the GNU GPL. 43 | 44 | You may convey a covered work under sections 3 and 4 of this License 45 | without being bound by section 3 of the GNU GPL. 46 | 47 | 2. Conveying Modified Versions. 48 | 49 | If you modify a copy of the Library, and, in your modifications, a 50 | facility refers to a function or data to be supplied by an Application 51 | that uses the facility (other than as an argument passed when the 52 | facility is invoked), then you may convey a copy of the modified 53 | version: 54 | 55 | a) under this License, provided that you make a good faith effort to 56 | ensure that, in the event an Application does not supply the 57 | function or data, the facility still operates, and performs 58 | whatever part of its purpose remains meaningful, or 59 | 60 | b) under the GNU GPL, with none of the additional permissions of 61 | this License applicable to that copy. 62 | 63 | 3. Object Code Incorporating Material from Library Header Files. 64 | 65 | The object code form of an Application may incorporate material from 66 | a header file that is part of the Library. You may convey such object 67 | code under terms of your choice, provided that, if the incorporated 68 | material is not limited to numerical parameters, data structure 69 | layouts and accessors, or small macros, inline functions and templates 70 | (ten or fewer lines in length), you do both of the following: 71 | 72 | a) Give prominent notice with each copy of the object code that the 73 | Library is used in it and that the Library and its use are 74 | covered by this License. 75 | 76 | b) Accompany the object code with a copy of the GNU GPL and this license 77 | document. 78 | 79 | 4. Combined Works. 80 | 81 | You may convey a Combined Work under terms of your choice that, 82 | taken together, effectively do not restrict modification of the 83 | portions of the Library contained in the Combined Work and reverse 84 | engineering for debugging such modifications, if you also do each of 85 | the following: 86 | 87 | a) Give prominent notice with each copy of the Combined Work that 88 | the Library is used in it and that the Library and its use are 89 | covered by this License. 90 | 91 | b) Accompany the Combined Work with a copy of the GNU GPL and this license 92 | document. 93 | 94 | c) For a Combined Work that displays copyright notices during 95 | execution, include the copyright notice for the Library among 96 | these notices, as well as a reference directing the user to the 97 | copies of the GNU GPL and this license document. 98 | 99 | d) Do one of the following: 100 | 101 | 0) Convey the Minimal Corresponding Source under the terms of this 102 | License, and the Corresponding Application Code in a form 103 | suitable for, and under terms that permit, the user to 104 | recombine or relink the Application with a modified version of 105 | the Linked Version to produce a modified Combined Work, in the 106 | manner specified by section 6 of the GNU GPL for conveying 107 | Corresponding Source. 108 | 109 | 1) Use a suitable shared library mechanism for linking with the 110 | Library. A suitable mechanism is one that (a) uses at run time 111 | a copy of the Library already present on the user's computer 112 | system, and (b) will operate properly with a modified version 113 | of the Library that is interface-compatible with the Linked 114 | Version. 115 | 116 | e) Provide Installation Information, but only if you would otherwise 117 | be required to provide such information under section 6 of the 118 | GNU GPL, and only to the extent that such information is 119 | necessary to install and execute a modified version of the 120 | Combined Work produced by recombining or relinking the 121 | Application with a modified version of the Linked Version. (If 122 | you use option 4d0, the Installation Information must accompany 123 | the Minimal Corresponding Source and Corresponding Application 124 | Code. If you use option 4d1, you must provide the Installation 125 | Information in the manner specified by section 6 of the GNU GPL 126 | for conveying Corresponding Source.) 127 | 128 | 5. Combined Libraries. 129 | 130 | You may place library facilities that are a work based on the 131 | Library side by side in a single library together with other library 132 | facilities that are not Applications and are not covered by this 133 | License, and convey such a combined library under terms of your 134 | choice, if you do both of the following: 135 | 136 | a) Accompany the combined library with a copy of the same work based 137 | on the Library, uncombined with any other library facilities, 138 | conveyed under the terms of this License. 139 | 140 | b) Give prominent notice with the combined library that part of it 141 | is a work based on the Library, and explaining where to find the 142 | accompanying uncombined form of the same work. 143 | 144 | 6. Revised Versions of the GNU Lesser General Public License. 145 | 146 | The Free Software Foundation may publish revised and/or new versions 147 | of the GNU Lesser General Public License from time to time. Such new 148 | versions will be similar in spirit to the present version, but may 149 | differ in detail to address new problems or concerns. 150 | 151 | Each version is given a distinguishing version number. If the 152 | Library as you received it specifies that a certain numbered version 153 | of the GNU Lesser General Public License "or any later version" 154 | applies to it, you have the option of following the terms and 155 | conditions either of that published version or of any later version 156 | published by the Free Software Foundation. If the Library as you 157 | received it does not specify a version number of the GNU Lesser 158 | General Public License, you may choose any version of the GNU Lesser 159 | General Public License ever published by the Free Software Foundation. 160 | 161 | If the Library as you received it specifies that a proxy can decide 162 | whether future versions of the GNU Lesser General Public License shall 163 | apply, that proxy's public statement of acceptance of any version is 164 | permanent authorization for you to choose that version for the 165 | Library. -------------------------------------------------------------------------------- /frost_sta_client/model/tasking_capability.py: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2021 Fraunhofer Institut IOSB, Fraunhoferstr. 1, D 76131 2 | # Karlsruhe, Germany. 3 | # 4 | # This program is free software: you can redistribute it and/or modify 5 | # it under the terms of the GNU Lesser General Public License as published by 6 | # the Free Software Foundation, either version 3 of the License, or 7 | # (at your option) any later version. 8 | # 9 | # This program is distributed in the hope that it will be useful, 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | # GNU Lesser General Public License for more details. 13 | # 14 | # You should have received a copy of the GNU Lesser General Public License 15 | # along with this program. If not, see . 16 | 17 | import frost_sta_client.model.task 18 | from . import entity 19 | from . import thing 20 | from . import actuator 21 | 22 | from frost_sta_client import utils 23 | from .ext import entity_type 24 | from .ext import entity_list 25 | 26 | from frost_sta_client.dao.tasking_capability import TaskingCapabilityDao 27 | 28 | 29 | class TaskingCapability(entity.Entity): 30 | 31 | def __init__(self, 32 | name='', 33 | description='', 34 | properties=None, 35 | tasking_parameters=None, 36 | tasks=None, 37 | thing=None, 38 | actuator=None, 39 | **kwargs): 40 | super().__init__(**kwargs) 41 | if tasking_parameters is None: 42 | tasking_parameters = {} 43 | if properties is None: 44 | properties = {} 45 | self.name = name 46 | self.description = description 47 | self.tasking_parameters = tasking_parameters 48 | self.properties = properties 49 | self.tasks = tasks 50 | self.thing = thing 51 | self.actuator = actuator 52 | 53 | def __new__(cls, *args, **kwargs): 54 | new_tc = super().__new__(cls) 55 | attributes = {'_id': None, '_name': '', '_description': '', '_properties': {}, '_tasking_parameters': {}, 56 | '_tasks': None, '_thing': None, '_actuator': None, '_self_link': '', '_service': None} 57 | for key, value in attributes.items(): 58 | new_tc.__dict__[key] = value 59 | return new_tc 60 | 61 | @property 62 | def name(self): 63 | return self._name 64 | 65 | @name.setter 66 | def name(self, value): 67 | if not isinstance(value, str): 68 | raise ValueError('name should be of type str!') 69 | self._name = value 70 | 71 | @property 72 | def description(self): 73 | return self._description 74 | 75 | @description.setter 76 | def description(self, value): 77 | if not isinstance(value, str): 78 | raise ValueError('description should be of type str!') 79 | self._description = value 80 | 81 | @property 82 | def properties(self): 83 | return self._properties 84 | 85 | @properties.setter 86 | def properties(self, value): 87 | if not isinstance(value, dict): 88 | raise ValueError('properties should be of type dict!') 89 | self._properties = value 90 | 91 | @property 92 | def tasking_parameters(self): 93 | return self._tasking_parameters 94 | 95 | @tasking_parameters.setter 96 | def tasking_parameters(self, value): 97 | if value is None: 98 | self._tasking_parameters = {} 99 | return 100 | if not isinstance(value, dict): 101 | raise ValueError('Tasking parameters should be of type dict!') 102 | self._tasking_parameters = value 103 | 104 | @property 105 | def tasks(self): 106 | return self._tasks 107 | 108 | @tasks.setter 109 | def tasks(self, value): 110 | if value is None: 111 | self._tasks = None 112 | return 113 | if not isinstance(value, list) and all(isinstance(t, frost_sta_client.model.task.Task) for t in value): 114 | entity_class = entity_type.EntityTypes['Task']['class'] 115 | self._tasks = entity_list.EntityList(entity_class=entity_class, entities=value) 116 | return 117 | if isinstance(value, entity_list.EntityList) \ 118 | and all(isinstance(t, frost_sta_client.model.task.Task) for t in value.entities): 119 | self._tasks = value 120 | return 121 | raise ValueError('tasks should be of type Task!') 122 | 123 | @property 124 | def thing(self): 125 | return self._thing 126 | 127 | @thing.setter 128 | def thing(self, value): 129 | if value is None: 130 | self._thing = None 131 | return 132 | if not isinstance(value, thing.Thing): 133 | raise ValueError('thing should be of type Thing!') 134 | self._thing = value 135 | 136 | @property 137 | def actuator(self): 138 | return self._actuator 139 | 140 | @actuator.setter 141 | def actuator(self, value): 142 | if value is None: 143 | self._actuator = None 144 | return 145 | if not isinstance(value, actuator.Actuator): 146 | raise ValueError('actuator should be of type Actuator!') 147 | self._actuator = value 148 | 149 | def get_tasks(self): 150 | result = self.service.tasks() 151 | result.parent = self 152 | return result 153 | 154 | def ensure_service_on_children(self, service): 155 | if self.actuator is not None: 156 | self.actuator.set_service(service) 157 | if self.thing is not None: 158 | self.thing.set_service(service) 159 | if self.tasks is not None: 160 | self.thing.set_service(service) 161 | 162 | def __eq__(self, other): 163 | if not super().__eq__(other): 164 | return False 165 | if self.name != other.name: 166 | return False 167 | if self.description != other.description: 168 | return False 169 | if self.tasking_parameters != other.tasking_parameters: 170 | return False 171 | if self.properties != other.properties: 172 | return False 173 | return True 174 | 175 | def __ne__(self, other): 176 | return not self == other 177 | 178 | def __getstate__(self): 179 | data = super().__getstate__() 180 | if self.name is not None and self.name != '': 181 | data['name'] = self.name 182 | if self.description is not None and self.description != '': 183 | data['description'] = self.description 184 | if self.tasking_parameters is not None and self.tasking_parameters != {}: 185 | data['taskingParameters'] = self.tasking_parameters 186 | if self.properties is not None and self.properties != {}: 187 | data['properties'] = self.properties 188 | if self.thing is not None: 189 | data['Thing'] = self.thing.__getstate__() 190 | if self.tasks is not None and len(self.tasks.entities) > 0: 191 | data['Tasks'] = self.tasks.__getstate__() 192 | if self.actuator is not None: 193 | data['Actuator'] = self.actuator.__getstate__() 194 | return data 195 | 196 | def __setstate__(self, state): 197 | super().__setstate__(state) 198 | self.name = state.get('name', '') 199 | self.description = state.get('description', '') 200 | self.tasking_parameters = state.get('taskingParameters', {}) 201 | self.properties = state.get('properties', {}) 202 | if state.get('Tasks', None) is not None: 203 | entity_class = entity_type.EntityTypes['Task']['class'] 204 | self.tasks = utils.transform_json_to_entity_list(state['Tasks'], entity_class) 205 | self.tasks.next_link = state.get('Tasks@iot.nextLink', None) 206 | self.tasks.count = state.get('Tasks@iot.count', None) 207 | if state.get('Actuator', None) is not None: 208 | self.actuator = frost_sta_client.model.actuator.Actuator() 209 | self.actuator.__setstate__(state['Actuator']) 210 | if state.get('Thing', None) is not None: 211 | self.thing = frost_sta_client.model.thing.Thing() 212 | self.thing.__setstate__(state['Thing']) 213 | 214 | def get_dao(self, service): 215 | return TaskingCapabilityDao(service) 216 | -------------------------------------------------------------------------------- /frost_sta_client/model/observedproperty.py: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2021 Fraunhofer Institut IOSB, Fraunhoferstr. 1, D 76131 2 | # Karlsruhe, Germany. 3 | # 4 | # This program is free software: you can redistribute it and/or modify 5 | # it under the terms of the GNU Lesser General Public License as published by 6 | # the Free Software Foundation, either version 3 of the License, or 7 | # (at your option) any later version. 8 | # 9 | # This program is distributed in the hope that it will be useful, 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | # GNU Lesser General Public License for more details. 13 | # 14 | # You should have received a copy of the GNU Lesser General Public License 15 | # along with this program. If not, see . 16 | 17 | from frost_sta_client.dao.observedproperty import ObservedPropertyDao 18 | 19 | from . import entity 20 | from . import datastream 21 | from . import multi_datastream 22 | 23 | from frost_sta_client import utils 24 | from .ext import entity_type 25 | from .ext import entity_list 26 | 27 | 28 | class ObservedProperty(entity.Entity): 29 | 30 | def __init__(self, 31 | name='', 32 | definition='', 33 | description='', 34 | datastreams=None, 35 | properties=None, 36 | multi_datastreams=None, 37 | **kwargs): 38 | super().__init__(**kwargs) 39 | if properties is None: 40 | properties = {} 41 | self.properties = properties 42 | self.name = name 43 | self.definition = definition 44 | self.description = description 45 | self.datastreams = datastreams 46 | self.multi_datastreams = multi_datastreams 47 | 48 | def __new__(cls, *args, **kwargs): 49 | new_observed_property = super().__new__(cls) 50 | attributes = {'_id': None, '_name': '', '_definition': '', '_description': '', 51 | '_datastreams': None, '_multi_datastreams': None, '_self_link': None, '_service': None} 52 | for key, value in attributes.items(): 53 | new_observed_property.__dict__[key] = value 54 | return new_observed_property 55 | 56 | @property 57 | def name(self): 58 | return self._name 59 | 60 | @name.setter 61 | def name(self, value): 62 | if value is None: 63 | self._name = None 64 | return 65 | if not isinstance(value, str): 66 | raise ValueError('name should be of type str!') 67 | self._name = value 68 | 69 | @property 70 | def description(self): 71 | return self._description 72 | 73 | @description.setter 74 | def description(self, value): 75 | if value is None: 76 | self._description = None 77 | return 78 | if not isinstance(value, str): 79 | raise ValueError('description should be of type str!') 80 | self._description = value 81 | 82 | @property 83 | def definition(self): 84 | return self._definition 85 | 86 | @definition.setter 87 | def definition(self, value): 88 | if value is None: 89 | self._definition = None 90 | return 91 | if not isinstance(value, str): 92 | raise ValueError('description should be of type str!') 93 | self._definition = value 94 | 95 | @property 96 | def properties(self): 97 | return self._properties 98 | 99 | @properties.setter 100 | def properties(self, value): 101 | if value is None: 102 | self._properties = {} 103 | return 104 | if not isinstance(value, dict): 105 | raise ValueError('properties should be of type dict!') 106 | self._properties = value 107 | 108 | @property 109 | def datastreams(self): 110 | return self._datastreams 111 | 112 | @datastreams.setter 113 | def datastreams(self, value): 114 | if value is None: 115 | self._datastreams = None 116 | return 117 | if isinstance(value, list) and all(isinstance(ds, datastream.Datastream) for ds in value): 118 | entity_class = entity_type.EntityTypes['Datastream']['class'] 119 | self._datastreams = entity_list.EntityList(entity_class=entity_class, entities=value) 120 | return 121 | if not isinstance(value, entity_list.EntityList) \ 122 | or any((not isinstance(ds, datastream.Datastream)) for ds in value.entities): 123 | raise ValueError('datastreams should be of list of type Datastream!') 124 | self._datastreams = value 125 | 126 | @property 127 | def multi_datastreams(self): 128 | return self._multi_datastreams 129 | 130 | @multi_datastreams.setter 131 | def multi_datastreams(self, values): 132 | if values is None: 133 | self._multi_datastreams = None 134 | return 135 | if isinstance(values, list) and all(isinstance(mds, multi_datastream.MultiDatastream) for mds in values): 136 | entity_class = entity_type.EntityTypes['MultiDatastream']['class'] 137 | self._multi_datastreams = entity_list.EntityList(entity_class=entity_class, entities=values) 138 | return 139 | if not isinstance(values, entity_list.EntityList) or\ 140 | any((not isinstance(mds, multi_datastream.MultiDatastream)) for mds in values.entities): 141 | raise ValueError('multi_datastreams should be a list of multi_datastreams!') 142 | self._multi_datastreams = values 143 | 144 | def get_datastreams(self): 145 | result = self.service.datastreams() 146 | result.parent = self 147 | return result 148 | 149 | def get_multi_datastreams(self): 150 | result = self.service.multi_datastreams() 151 | result.parent = self 152 | return result 153 | 154 | def ensure_service_on_children(self, service): 155 | if self.datastreams is not None: 156 | self.datastreams.set_service(service) 157 | if self.multi_datastreams is not None: 158 | self.multi_datastreams.set_service(service) 159 | 160 | def __eq__(self, other): 161 | if not super().__eq__(other): 162 | return False 163 | if self.name != other.name: 164 | return False 165 | if self.description != other.description: 166 | return False 167 | if self.definition != other.definition: 168 | return False 169 | if self.properties != other.properties: 170 | return False 171 | return True 172 | 173 | def __ne__(self, other): 174 | return not self == other 175 | 176 | def __getstate__(self): 177 | data = super().__getstate__() 178 | if self.name is not None and self.name != '': 179 | data['name'] = self.name 180 | if self.description is not None and self.description != '': 181 | data['description'] = self.description 182 | if self.definition is not None and self.definition != '': 183 | data['definition'] = self.definition 184 | if self.properties is not None and self.properties != {}: 185 | data['properties'] = self.properties 186 | if self.datastreams is not None and len(self.datastreams.entities) > 0: 187 | data['Datastreams'] = self.datastreams.__getstate__() 188 | if self.multi_datastreams is not None and len(self.multi_datastreams.entities) > 0: 189 | data['MultiDatastreams'] = self.multi_datastreams.__getstate__() 190 | return data 191 | 192 | def __setstate__(self, state): 193 | super().__setstate__(state) 194 | self.name = state.get("name", None) 195 | self.description = state.get("description", None) 196 | self.definition = state.get("definition", None) 197 | self.properties = state.get("properties", {}) 198 | if state.get("Datastreams", None) is not None and isinstance(state["Datastreams"], list): 199 | entity_class = entity_type.EntityTypes['Datastream']['class'] 200 | self.datastreams = utils.transform_json_to_entity_list(state['Datastreams'], entity_class) 201 | self.datastreams.next_link = state.get('Datastreams@iot.nextLink', None) 202 | self.datastreams.count = state.get('Datastreams@iot.count', None) 203 | if state.get("MultiDatastreams", None) is not None and isinstance(state["MultiDatastreams"], list): 204 | entity_class = entity_type.EntityTypes['MultiDatastream']['class'] 205 | self.multi_datastreams = utils.transform_json_to_entity_list(state['MultiDatastreams'], entity_class) 206 | self.multi_datastreams.next_link = state.get('MultiDatastreams@iot.nextLink', None) 207 | self.multi_datastreams.count = state.get('MultiDatastreams@iot.count', None) 208 | 209 | def get_dao(self, service): 210 | return ObservedPropertyDao(service) 211 | -------------------------------------------------------------------------------- /frost_sta_client/model/sensor.py: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2021 Fraunhofer Institut IOSB, Fraunhoferstr. 1, D 76131 2 | # Karlsruhe, Germany. 3 | # 4 | # This program is free software: you can redistribute it and/or modify 5 | # it under the terms of the GNU Lesser General Public License as published by 6 | # the Free Software Foundation, either version 3 of the License, or 7 | # (at your option) any later version. 8 | # 9 | # This program is distributed in the hope that it will be useful, 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | # GNU Lesser General Public License for more details. 13 | # 14 | # You should have received a copy of the GNU Lesser General Public License 15 | # along with this program. If not, see . 16 | import json 17 | 18 | from frost_sta_client.dao.sensor import SensorDao 19 | 20 | from . import entity 21 | from . import datastream 22 | from . import multi_datastream 23 | 24 | from frost_sta_client import utils 25 | from .ext import entity_type 26 | from .ext import entity_list 27 | 28 | 29 | class Sensor(entity.Entity): 30 | 31 | def __init__(self, 32 | name='', 33 | description='', 34 | encoding_type='', 35 | properties=None, 36 | metadata=None, 37 | datastreams=None, 38 | multi_datastreams=None, 39 | **kwargs): 40 | super().__init__(**kwargs) 41 | if properties is None: 42 | properties = {} 43 | self.name = name 44 | self.description = description 45 | self.properties = properties 46 | self.encoding_type = encoding_type 47 | self.metadata = metadata 48 | self.datastreams = datastreams 49 | self.multi_datastreams = multi_datastreams 50 | 51 | def __new__(cls, *args, **kwargs): 52 | new_sensor = super().__new__(cls) 53 | attributes = {'_id': None, '_name': '', '_description': '', '_properties': {}, '_encoding_type': '', 54 | '_metadata': '', '_datastreams': None, '_multi_datastreams': None, '_self_link': '', 55 | '_service': None} 56 | for key, value in attributes.items(): 57 | new_sensor.__dict__[key] = value 58 | return new_sensor 59 | 60 | @property 61 | def name(self): 62 | return self._name 63 | 64 | @name.setter 65 | def name(self, value): 66 | if not isinstance(value, str): 67 | raise ValueError('name should be of type str!') 68 | self._name = value 69 | 70 | @property 71 | def description(self): 72 | return self._description 73 | 74 | @description.setter 75 | def description(self, value): 76 | if not isinstance(value, str): 77 | raise ValueError('description should be of type str!') 78 | self._description = value 79 | 80 | @property 81 | def properties(self): 82 | return self._properties 83 | 84 | @properties.setter 85 | def properties(self, value): 86 | if not isinstance(value, dict): 87 | raise ValueError('properties should be of type dict!') 88 | self._properties = value 89 | 90 | @property 91 | def encoding_type(self): 92 | return self._encoding_type 93 | 94 | @encoding_type.setter 95 | def encoding_type(self, value): 96 | if not isinstance(value, str): 97 | raise ValueError('encoding_type should be of type str!') 98 | self._encoding_type = value 99 | 100 | @property 101 | def metadata(self): 102 | return self._metadata 103 | 104 | @metadata.setter 105 | def metadata(self, value): 106 | if value is None: 107 | self._metadata = None 108 | return 109 | try: 110 | json.dumps(value) 111 | except TypeError: 112 | raise TypeError('metadata should be json serializable') 113 | self._metadata = value 114 | 115 | @property 116 | def datastreams(self): 117 | return self._datastreams 118 | 119 | @datastreams.setter 120 | def datastreams(self, values): 121 | if values is None: 122 | self._datastreams = None 123 | return 124 | if isinstance(values, list) and all(isinstance(ds, datastream.Datastream) for ds in values): 125 | entity_class = entity_type.EntityTypes['Datastream']['class'] 126 | self._datastreams = entity_list.EntityList(entity_class=entity_class, entities=values) 127 | return 128 | if not isinstance(values, entity_list.EntityList) or\ 129 | any((not isinstance(ds, datastream.Datastream)) for ds in values.entities): 130 | raise ValueError('datastreams should be an entity list of datastreams!') 131 | self._datastreams = values 132 | 133 | @property 134 | def multi_datastreams(self): 135 | return self._multi_datastreams 136 | 137 | @multi_datastreams.setter 138 | def multi_datastreams(self, values): 139 | if values is None: 140 | self._multi_datastreams = None 141 | return 142 | if isinstance(values, list) and all(isinstance(mds, multi_datastream.MultiDatastream) for mds in values): 143 | entity_class = entity_type.EntityTypes['MultiDatastream']['class'] 144 | self._multi_datastreams = entity_list.EntityList(entity_class=entity_class, entities=values) 145 | return 146 | if not isinstance(values, entity_list.EntityList) or\ 147 | any((not isinstance(mds, multi_datastream.MultiDatastream)) for mds in values.entities): 148 | raise ValueError('multi_datastreams should be a list of multi_datastreams!') 149 | self._multi_datastreams = values 150 | 151 | def get_datastreams(self): 152 | result = self.service.datastreams() 153 | result.parent = self 154 | return result 155 | 156 | def get_multi_datastreams(self): 157 | result = self.service.multi_datastreams() 158 | result.parent = self 159 | return result 160 | 161 | def ensure_service_on_children(self, service): 162 | if self.datastreams is not None: 163 | self.datastreams.set_service(service) 164 | if self.multi_datastreams is not None: 165 | self.multi_datastreams.set_service(service) 166 | 167 | def __eq__(self, other): 168 | if not super().__eq__(other): 169 | return False 170 | if self.name != other.name: 171 | return False 172 | if self.description != other.description: 173 | return False 174 | if self.encoding_type != other.encoding_type: 175 | return False 176 | if self.properties != other.properties: 177 | return False 178 | if self.metadata != other.metadata: 179 | return False 180 | return True 181 | 182 | def __ne__(self, other): 183 | return not self == other 184 | 185 | def __getstate__(self): 186 | data = super().__getstate__() 187 | if self.name is not None and self.name != '': 188 | data['name'] = self._name 189 | if self.description is not None and self.description != '': 190 | data['description'] = self._description 191 | if self.properties is not None and self.properties != {}: 192 | data['properties'] = self._properties 193 | if self.encoding_type is not None and self.encoding_type != '': 194 | data['encodingType'] = self._encoding_type 195 | if self.metadata is not None: 196 | data['metadata'] = self._metadata 197 | if self.datastreams is not None and len(self._datastreams.entities) > 0: 198 | data['Datastreams'] = self._datastreams.__getstate__() 199 | if self.multi_datastreams is not None and len(self.multi_datastreams.entities) > 0: 200 | data['MultiDatastreams'] = self._multi_datastreams.__getstate__() 201 | return data 202 | 203 | def __setstate__(self, state): 204 | super().__setstate__(state) 205 | self.name = state.get('name', '') 206 | self.description = state.get('description', '') 207 | self.encoding_type = state.get('encodingType', '') 208 | self.metadata = state.get('metadata', '') 209 | self.properties = state.get('properties', {}) 210 | if state.get('Datastreams', None) is not None: 211 | entity_class = entity_type.EntityTypes['Datastream']['class'] 212 | self.datastreams = utils.transform_json_to_entity_list(state['Datastreams'], entity_class) 213 | self.datastreams.next_link = state.get('Datastreams@iot.nextLink', None) 214 | self.datastreams.count = state.get('Datastreams@iot.count', None) 215 | if state.get('MultiDatastreams', None) is not None: 216 | entity_class = entity_type.EntityTypes['MultiDatastream']['class'] 217 | self.multi_datastreams = utils.transform_json_to_entity_list(state['MultiDatastreams'], entity_class) 218 | self.multi_datastreams.next_link = state.get('MultiDatastreams@iot.nextLink', None) 219 | self.multi_datastreams.count = state.get('MultiDatastreams@iot.count', None) 220 | 221 | def get_dao(self, service): 222 | return SensorDao(service) 223 | -------------------------------------------------------------------------------- /frost_sta_client/model/observation.py: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2021 Fraunhofer Institut IOSB, Fraunhoferstr. 1, D 76131 2 | # Karlsruhe, Germany. 3 | # 4 | # This program is free software: you can redistribute it and/or modify 5 | # it under the terms of the GNU Lesser General Public License as published by 6 | # the Free Software Foundation, either version 3 of the License, or 7 | # (at your option) any later version. 8 | # 9 | # This program is distributed in the hope that it will be useful, 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | # GNU Lesser General Public License for more details. 13 | # 14 | # You should have received a copy of the GNU Lesser General Public License 15 | # along with this program. If not, see . 16 | 17 | import json 18 | import datetime 19 | 20 | import frost_sta_client.model 21 | from . import entity 22 | from . import multi_datastream 23 | from . import datastream 24 | from . import feature_of_interest 25 | 26 | from frost_sta_client.dao.observation import ObservationDao 27 | 28 | from frost_sta_client import utils 29 | 30 | 31 | class Observation(entity.Entity): 32 | 33 | def __init__(self, 34 | phenomenon_time=None, 35 | result=None, 36 | result_time=None, 37 | result_quality=None, 38 | valid_time=None, 39 | parameters=None, 40 | datastream=None, 41 | multi_datastream=None, 42 | feature_of_interest=None, 43 | **kwargs): 44 | super().__init__(**kwargs) 45 | if parameters is None: 46 | parameters = {} 47 | self.phenomenon_time = phenomenon_time 48 | self.result = result 49 | self.result_time = result_time 50 | self.result_quality = result_quality 51 | self.valid_time = valid_time 52 | self.parameters = parameters 53 | self.datastream = datastream 54 | self.multi_datastream = multi_datastream 55 | self.feature_of_interest = feature_of_interest 56 | 57 | def __new__(cls, *args, **kwargs): 58 | new_observation = super().__new__(cls) 59 | attributes = {'_id': None, '_phenomenon_time': None, '_result': None, '_result_time': None, 60 | '_result_quality': None, '_valid_time': None, '_parameters': {}, '_datastream': None, 61 | '_multi_datastream': None, '_feature_of_interest': None, '_self_link': '', '_service': None} 62 | for key, value in attributes.items(): 63 | new_observation.__dict__[key] = value 64 | return new_observation 65 | 66 | @property 67 | def phenomenon_time(self): 68 | return self._phenomenon_time 69 | 70 | @phenomenon_time.setter 71 | def phenomenon_time(self, value): 72 | self._phenomenon_time = utils.check_datetime(value, 'phenomenon_time') 73 | 74 | def phenomenon_time_as_str(self): 75 | if type(self._phenomenon_time) == str or self._phenomenon_time is None: 76 | return self._phenomenon_time 77 | if type(self._phenomenon_time) == datetime.datetime: 78 | return self._phenomenon_time.isoformat() 79 | return None 80 | 81 | @property 82 | def result(self): 83 | return self._result 84 | 85 | @result.setter 86 | def result(self, value): 87 | if value is None: 88 | self._result = None 89 | return 90 | try: 91 | json.dumps(value) 92 | except TypeError: 93 | raise TypeError('result should be json serializable') 94 | self._result = value 95 | 96 | @property 97 | def result_time(self): 98 | return self._result_time 99 | 100 | @result_time.setter 101 | def result_time(self, value): 102 | self._result_time = utils.check_datetime(value, 'result_time') 103 | 104 | @property 105 | def result_quality(self): 106 | return self._result_quality 107 | 108 | @result_quality.setter 109 | def result_quality(self, value): 110 | if value is None: 111 | self._result_quality = None 112 | return 113 | try: 114 | json.dumps(value) 115 | except TypeError: 116 | raise TypeError('result_quality should be json serializable') 117 | self._result_quality = value 118 | 119 | @property 120 | def valid_time(self): 121 | return self._valid_time 122 | 123 | @valid_time.setter 124 | def valid_time(self, value): 125 | self._valid_time = utils.check_datetime(value, 'valid_time') 126 | 127 | @property 128 | def parameters(self): 129 | return self._parameters 130 | 131 | @parameters.setter 132 | def parameters(self, values): 133 | if values is None: 134 | self._parameters = None 135 | return 136 | if not isinstance(values, dict): 137 | raise ValueError('parameters should be of type dict!') 138 | self._parameters = values 139 | 140 | @property 141 | def feature_of_interest(self): 142 | return self._feature_of_interest 143 | 144 | @feature_of_interest.setter 145 | def feature_of_interest(self, value): 146 | if value is None: 147 | self._feature_of_interest = None 148 | return 149 | if not isinstance(value, feature_of_interest.FeatureOfInterest): 150 | raise ValueError('feature_of_interest should be of type FeatureOfInterest!') 151 | self._feature_of_interest = value 152 | 153 | @property 154 | def datastream(self): 155 | return self._datastream 156 | 157 | @datastream.setter 158 | def datastream(self, value): 159 | if value is None: 160 | self._datastream = None 161 | return 162 | if not isinstance(value, datastream.Datastream): 163 | raise ValueError('datastream should be of type Datastream!') 164 | self._datastream = value 165 | 166 | @property 167 | def multi_datastream(self): 168 | return self._multi_datastream 169 | 170 | @multi_datastream.setter 171 | def multi_datastream(self, value): 172 | if value is None: 173 | self._multi_datastream = None 174 | return 175 | if isinstance(value, multi_datastream.MultiDatastream): 176 | self._multi_datastream = value 177 | return 178 | raise ValueError('multi_datastream should be of type MultiDatastream!') 179 | 180 | def ensure_service_on_children(self, service): 181 | if self.datastream is not None: 182 | self.datastream.set_service(service) 183 | if self.multi_datastream is not None: 184 | self.multi_datastream.set_service(service) 185 | if self.feature_of_interest is not None: 186 | self.feature_of_interest.set_service(service) 187 | 188 | def __eq__(self, other): 189 | if not super().__eq__(other): 190 | return False 191 | if self.result != other.result: 192 | return False 193 | if self.phenomenon_time != other.phenomenon_time: 194 | return False 195 | if self.result_time != other.result_time: 196 | return False 197 | if self.valid_time != other.valid_time: 198 | return False 199 | if self.parameters != other.parameters: 200 | return False 201 | if self.result_quality != other.result_quality: 202 | return False 203 | return True 204 | 205 | def __ne__(self, other): 206 | return not self == other 207 | 208 | def __getstate__(self): 209 | data = super().__getstate__() 210 | if self.parameters is not None and self.parameters != {}: 211 | data['parameters'] = self.parameters 212 | if self.result is not None: 213 | data['result'] = self.result 214 | if self.result_quality is not None: 215 | data['resultQuality'] = self.result_quality 216 | if self.phenomenon_time is not None: 217 | data['phenomenonTime'] = utils.parse_datetime(self.phenomenon_time) 218 | if self.result_time is not None: 219 | data['resultTime'] = utils.parse_datetime(self.result_time) 220 | if self.valid_time is not None: 221 | data['validTime'] = utils.parse_datetime(self.valid_time) 222 | if self.datastream is not None: 223 | data['Datastream'] = self.datastream.__getstate__() 224 | if self.multi_datastream is not None: 225 | data['MultiDatastream'] = self.multi_datastream.__getstate__() 226 | if self.feature_of_interest is not None: 227 | data['FeatureOfInterest'] = self.feature_of_interest.__getstate__() 228 | return data 229 | 230 | def __setstate__(self, state): 231 | super().__setstate__(state) 232 | self.parameters = state.get("parameters", {}) 233 | self.result = state.get("result", None) 234 | self.result_quality = state.get("resultQuality", None) 235 | self.phenomenon_time = state.get("phenomenonTime", None) 236 | self.result_time = state.get("resultTime", None) 237 | self.valid_time = state.get("validTime", None) 238 | if state.get('Datastream', None) is not None: 239 | self.datastream = frost_sta_client.model.datastream.Datastream() 240 | self.datastream.__setstate__(state['Datastream']) 241 | if state.get('MultiDatastream', None) is not None: 242 | self.multi_datastream = frost_sta_client.model.multi_datastream.MultiDatastream() 243 | self.multi_datastream.__setstate__(state['MultiDatastream']) 244 | if state.get("FeatureOfInterest", None) is not None: 245 | self.feature_of_interest = frost_sta_client.model.feature_of_interest.FeatureOfInterest() 246 | self.feature_of_interest.__setstate__(state['FeatureOfInterest']) 247 | 248 | def get_dao(self, service): 249 | return ObservationDao(service) 250 | -------------------------------------------------------------------------------- /frost_sta_client/model/location.py: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2021 Fraunhofer Institut IOSB, Fraunhoferstr. 1, D 76131 2 | # Karlsruhe, Germany. 3 | # 4 | # This program is free software: you can redistribute it and/or modify 5 | # it under the terms of the GNU Lesser General Public License as published by 6 | # the Free Software Foundation, either version 3 of the License, or 7 | # (at your option) any later version. 8 | # 9 | # This program is distributed in the hope that it will be useful, 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | # GNU Lesser General Public License for more details. 13 | # 14 | # You should have received a copy of the GNU Lesser General Public License 15 | # along with this program. If not, see . 16 | 17 | import frost_sta_client.model 18 | from frost_sta_client.dao.location import LocationDao 19 | 20 | from . import entity 21 | from . import thing 22 | from . import historical_location 23 | 24 | import inspect 25 | import json 26 | import geojson 27 | 28 | from frost_sta_client import utils 29 | from .ext import entity_type 30 | from .ext import entity_list 31 | 32 | 33 | class Location(entity.Entity): 34 | 35 | def __init__(self, 36 | name='', 37 | description='', 38 | encoding_type='', 39 | properties=None, 40 | location=None, 41 | things=None, 42 | historical_locations=None, 43 | **kwargs): 44 | super().__init__(**kwargs) 45 | if properties is None: 46 | properties = {} 47 | self.name = name 48 | self.description = description 49 | self.encoding_type = encoding_type 50 | self.properties = properties 51 | self.location = location 52 | self.things = things 53 | self.historical_locations = historical_locations 54 | 55 | def __new__(cls, *args, **kwargs): 56 | new_loc = super().__new__(cls) 57 | attributes = {'_id': None, '_name': '', '_description': '', '_properties': {}, '_encodingType': '', 58 | '_location': None, '_things': None, '_historical_locations': None, '_self_link': '', 59 | '_service': None} 60 | for key, value in attributes.items(): 61 | new_loc.__dict__[key] = value 62 | return new_loc 63 | 64 | @property 65 | def name(self): 66 | return self._name 67 | 68 | @name.setter 69 | def name(self, value): 70 | if value is None: 71 | self._name = None 72 | return 73 | if not isinstance(value, str): 74 | raise ValueError('name should be of type str!') 75 | self._name = value 76 | 77 | @property 78 | def description(self): 79 | return self._description 80 | 81 | @description.setter 82 | def description(self, value): 83 | if value is None: 84 | self._description = None 85 | return 86 | if not isinstance(value, str): 87 | raise ValueError('description should be of type str!') 88 | self._description = value 89 | 90 | @property 91 | def encoding_type(self): 92 | return self._encoding_type 93 | 94 | @encoding_type.setter 95 | def encoding_type(self, value): 96 | if value is None: 97 | self._encoding_type = None 98 | return 99 | if not isinstance(value, str): 100 | raise ValueError('encodingType should be of type str!') 101 | self._encoding_type = value 102 | 103 | @property 104 | def properties(self): 105 | return self._properties 106 | 107 | @properties.setter 108 | def properties(self, values): 109 | if values is None: 110 | self._properties = None 111 | return 112 | if not isinstance(values, dict): 113 | raise ValueError('properties should be of type dict!') 114 | self._properties = values 115 | 116 | @property 117 | def location(self): 118 | return self._location 119 | 120 | @location.setter 121 | def location(self, value): 122 | if value is None: 123 | self._location = None 124 | return 125 | geo_classes = [obj for _, obj in inspect.getmembers(geojson) if inspect.isclass(obj) and 126 | obj.__module__ == 'geojson.geometry'] 127 | if type(value) in geo_classes: 128 | self._location = value 129 | return 130 | else: 131 | try: 132 | json.dumps(value) 133 | except TypeError: 134 | raise ValueError('location should be json serializable!') 135 | self._location = value 136 | 137 | @property 138 | def things(self): 139 | return self._things 140 | 141 | @things.setter 142 | def things(self, values): 143 | if values is None: 144 | self._things = None 145 | return 146 | if isinstance(values, list) and all(isinstance(th, thing.Thing) for th in values): 147 | entity_class = entity_type.EntityTypes['Thing']['class'] 148 | self._things = entity_list.EntityList(entity_class=entity_class, entities=values) 149 | return 150 | if not isinstance(values, entity_list.EntityList) or \ 151 | any((not isinstance(th, thing.Thing)) for th in values.entities): 152 | raise ValueError('Things should be a list of things!') 153 | self._things = values 154 | 155 | @property 156 | def historical_locations(self): 157 | return self._historical_locations 158 | 159 | @historical_locations.setter 160 | def historical_locations(self, values): 161 | if values is None: 162 | self._historical_locations = None 163 | return 164 | if isinstance(values, list) and all(isinstance(hl, historical_location.HistoricalLocation) for hl in values): 165 | entity_class = entity_type.EntityTypes['HistoricalLocation']['class'] 166 | self._historical_locations = entity_list.EntityList(entity_class=entity_class, entities=values) 167 | return 168 | if isinstance(values, entity_list.EntityList) and \ 169 | all(isinstance(hl, historical_location.HistoricalLocation) for hl in values.entities): 170 | self._historical_locations = values 171 | return 172 | raise ValueError('historical_location should be of type HistoricalLocation!') 173 | 174 | def get_things(self): 175 | result = self.service.things() 176 | result.parent = self 177 | return result 178 | 179 | def get_historical_locations(self): 180 | result = self.service.historical_locations() 181 | result.parent = self 182 | return result 183 | 184 | def ensure_service_on_children(self, service): 185 | if self.things is not None: 186 | self.things.set_service(service) 187 | if self.historical_locations is not None: 188 | self.historical_locations.set_service(service) 189 | 190 | def __eq__(self, other): 191 | if not super().__eq__(other): 192 | return False 193 | if self.name != other.name: 194 | return False 195 | if self.description != other.description: 196 | return False 197 | if self.encoding_type != other.encoding_type: 198 | return False 199 | if self.location != other.location: 200 | return False 201 | if self.properties != other.properties: 202 | return False 203 | return True 204 | 205 | def __ne__(self, other): 206 | return not self == other 207 | 208 | def __getstate__(self): 209 | data = super().__getstate__() 210 | if self.name is not None and self.name != '': 211 | data['name'] = self.name 212 | if self.description is not None and self.description != '': 213 | data['description'] = self.description 214 | if self.encoding_type is not None and self.encoding_type != '': 215 | data['encodingType'] = self.encoding_type 216 | if self.properties is not None and self.properties != {}: 217 | data['properties'] = self.properties 218 | if self.location is not None: 219 | data['location'] = self.location 220 | if self.things is not None: 221 | data['Things'] = self.things.__getstate__() 222 | if self.historical_locations is not None and len(self.historical_locations.entities) > 0: 223 | data['HistoricalLocations'] = self.historical_locations.__getstate__() 224 | return data 225 | 226 | def __setstate__(self, state): 227 | super().__setstate__(state) 228 | self.name = state.get("name", None) 229 | self.description = state.get("description", None) 230 | self.encoding_type = state.get("encodingType", None) 231 | self.properties = state.get("properties", {}) 232 | if state.get("Things", None) is not None: 233 | entity_class = entity_type.EntityTypes['Thing']['class'] 234 | self.things = utils.transform_json_to_entity_list(state['Things'], entity_class) 235 | self.things.next_link = state.get('Things@iot.nextLink', None) 236 | self.things.count = state.get('Things@iot.count', None) 237 | if state.get("location", None) is not None: 238 | self.location = state["location"] 239 | if state.get("HistoricalLocations", None) is not None: 240 | entity_class = entity_type.EntityTypes['HistoricalLocation']['class'] 241 | self.historical_locations = utils.transform_json_to_entity_list(state['HistoricalLocations'], entity_class) 242 | self.historical_locations.next_link = state.get('HistoricalLocations@iot.nextLink', None) 243 | self.historical_locations.count = state.get('HistoricalLocations@iot.count', None) 244 | 245 | def get_dao(self, service): 246 | return LocationDao(service) 247 | -------------------------------------------------------------------------------- /frost_sta_client/model/datastream.py: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2021 Fraunhofer Institut IOSB, Fraunhoferstr. 1, D 76131 2 | # Karlsruhe, Germany. 3 | # 4 | # This program is free software: you can redistribute it and/or modify 5 | # it under the terms of the GNU Lesser General Public License as published by 6 | # the Free Software Foundation, either version 3 of the License, or 7 | # (at your option) any later version. 8 | # 9 | # This program is distributed in the hope that it will be useful, 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | # GNU Lesser General Public License for more details. 13 | # 14 | # You should have received a copy of the GNU Lesser General Public License 15 | # along with this program. If not, see . 16 | import inspect 17 | import json 18 | 19 | import frost_sta_client.model.ext.unitofmeasurement 20 | from frost_sta_client.dao.datastream import DatastreamDao 21 | 22 | from . import thing 23 | from . import sensor 24 | from . import observedproperty 25 | from . import observation 26 | from . import entity 27 | from .ext import unitofmeasurement 28 | from .ext import entity_list 29 | from .ext import entity_type 30 | 31 | from frost_sta_client import utils 32 | 33 | import geojson.geometry 34 | 35 | 36 | class Datastream(entity.Entity): 37 | 38 | def __init__(self, 39 | name='', 40 | description='', 41 | observation_type='', 42 | unit_of_measurement=None, 43 | observed_area=None, 44 | properties=None, 45 | phenomenon_time=None, 46 | result_time=None, 47 | thing=None, 48 | sensor=None, 49 | observed_property=None, 50 | observations=None, 51 | **kwargs): 52 | """ 53 | This class handles Datastreams assigned to a Thing. Before you create a Datastreams, you firstly have to 54 | create a Thing, a Sensor and an observedProperty to which you have to refer by specifying its ids. 55 | 56 | Parameters 57 | ---------- 58 | name: str 59 | description: str 60 | observation_type: str 61 | unit_of_measurement: dict 62 | Should be a dict of keys 'name', 'symbol', 'definition' with values of str. 63 | 64 | """ 65 | super().__init__(**kwargs) 66 | if properties is None: 67 | properties = {} 68 | self.name = name 69 | self.description = description 70 | self.observation_type = observation_type 71 | self.unit_of_measurement = unit_of_measurement 72 | self.observed_area = observed_area 73 | self.properties = properties 74 | self.phenomenon_time = phenomenon_time 75 | self.result_time = result_time 76 | self.thing = thing 77 | self.sensor = sensor 78 | self.observations = observations 79 | self.observed_property = observed_property 80 | 81 | 82 | def __new__(cls, *args, **kwargs): 83 | new_datastream = super().__new__(cls) 84 | attributes = dict(_id=None, _name='', _description='', _properties={}, _observation_type='', 85 | _unit_of_measurement=None, _observed_area=None, _phenomenon_time=None, _result_time=None, 86 | _thing=None, _sensor=None, _observed_property=None, _observations=None, _self_link='', 87 | _service=None) 88 | for key, value in attributes.items(): 89 | new_datastream.__dict__[key] = value 90 | return new_datastream 91 | 92 | @property 93 | def name(self): 94 | return self._name 95 | 96 | @name.setter 97 | def name(self, value): 98 | if value is None: 99 | self._name = None 100 | return 101 | if not isinstance(value, str): 102 | raise ValueError('name should be of type str!') 103 | self._name = value 104 | 105 | @property 106 | def description(self): 107 | return self._description 108 | 109 | @description.setter 110 | def description(self, value): 111 | if value is None: 112 | self._description = None 113 | return 114 | if not isinstance(value, str): 115 | raise ValueError('description should be of type str!') 116 | self._description = value 117 | 118 | @property 119 | def observation_type(self): 120 | return self._observation_type 121 | 122 | @observation_type.setter 123 | def observation_type(self, value): 124 | if value is None: 125 | self._observation_type = None 126 | return 127 | if not isinstance(value, str): 128 | raise ValueError('observation_type should be of type str!') 129 | self._observation_type = value 130 | 131 | @property 132 | def unit_of_measurement(self): 133 | return self._unit_of_measurement 134 | 135 | @unit_of_measurement.setter 136 | def unit_of_measurement(self, value): 137 | if value is None or isinstance(value, unitofmeasurement.UnitOfMeasurement): 138 | self._unit_of_measurement = value 139 | return 140 | raise ValueError('unitOfMeasurement should be of type UnitOfMeasurement!') 141 | 142 | @property 143 | def observed_area(self): 144 | return self._observed_area 145 | 146 | @observed_area.setter 147 | def observed_area(self, value): 148 | if value is None: 149 | self._observed_area = None 150 | return 151 | geo_classes = [obj for _, obj in inspect.getmembers(geojson) if inspect.isclass(obj) and 152 | obj.__module__ == 'geojson.geometry'] 153 | if type(value) in geo_classes: 154 | self._observed_area = value 155 | return 156 | else: 157 | try: 158 | json.dumps(value) 159 | except TypeError: 160 | raise ValueError('observedArea should be of json_serializable!') 161 | self._observed_area = value 162 | 163 | @property 164 | def properties(self): 165 | return self._properties 166 | 167 | @properties.setter 168 | def properties(self, value): 169 | if value is None: 170 | self._properties = None 171 | return 172 | if not isinstance(value, dict): 173 | raise ValueError('properties should be of type dict') 174 | self._properties = value 175 | 176 | @property 177 | def phenomenon_time(self): 178 | return self._phenomenon_time 179 | 180 | @phenomenon_time.setter 181 | def phenomenon_time(self, value): 182 | self._phenomenon_time = utils.check_datetime(value, 'phenomenon_time') 183 | 184 | @property 185 | def result_time(self): 186 | return self._result_time 187 | 188 | @result_time.setter 189 | def result_time(self, value): 190 | self._result_time = utils.check_datetime(value, 'result_time') 191 | 192 | @property 193 | def thing(self): 194 | return self._thing 195 | 196 | @thing.setter 197 | def thing(self, value): 198 | if value is None or isinstance(value, thing.Thing): 199 | self._thing = value 200 | return 201 | raise ValueError('thing should be of type Thing!') 202 | 203 | @property 204 | def sensor(self): 205 | return self._sensor 206 | 207 | @sensor.setter 208 | def sensor(self, value): 209 | if value is None or isinstance(value, sensor.Sensor): 210 | self._sensor = value 211 | return 212 | raise ValueError('sensor should be of type Sensor!') 213 | 214 | @property 215 | def observed_property(self): 216 | return self._observed_property 217 | 218 | @observed_property.setter 219 | def observed_property(self, value): 220 | if isinstance(value, observedproperty.ObservedProperty) or value is None: 221 | self._observed_property = value 222 | return 223 | raise ValueError('observed property should by of type ObservedProperty!') 224 | 225 | @property 226 | def observations(self): 227 | return self._observations 228 | 229 | @observations.setter 230 | def observations(self, values): 231 | if values is None: 232 | self._observations = None 233 | return 234 | if isinstance(values, list) and all(isinstance(ob, observation.Observation) for ob in values): 235 | entity_class = entity_type.EntityTypes['Observation']['class'] 236 | self._observations = entity_list.EntityList(entity_class=entity_class, entities=values) 237 | return 238 | if isinstance(values, entity_list.EntityList) and \ 239 | all(isinstance(ob, observation.Observation) for ob in values.entities): 240 | self._observations = values 241 | return 242 | raise ValueError('Observations should be a list of Observations') 243 | 244 | def get_observations(self): 245 | result = self.service.observations() 246 | result.parent = self 247 | return result 248 | 249 | def ensure_service_on_children(self, service): 250 | if self.thing is not None: 251 | self.thing.set_service(service) 252 | if self.sensor is not None: 253 | self.sensor.set_service(service) 254 | if self.observed_property is not None: 255 | self.observed_property.set_service(service) 256 | if self.observations is not None: 257 | self.observations.set_service(service) 258 | 259 | def __eq__(self, other): 260 | if not super().__eq__(other): 261 | return False 262 | if self.name != other.name: 263 | return False 264 | if self.description != other.description: 265 | return False 266 | if self.observation_type != other.observation_type: 267 | return False 268 | if self.unit_of_measurement != other.unit_of_measurement: 269 | return False 270 | if self.properties != other.properties: 271 | return False 272 | if self.result_time != other.result_time: 273 | return False 274 | return True 275 | 276 | def __ne__(self, other): 277 | return not self == other 278 | 279 | def __getstate__(self): 280 | data = super().__getstate__() 281 | if self.name is not None and self.name != '': 282 | data['name'] = self.name 283 | if self.description is not None and self.description != '': 284 | data['description'] = self.description 285 | if self.observation_type is not None and self.observation_type != '': 286 | data['observationType'] = self.observation_type 287 | if self.properties is not None and self.properties != {}: 288 | data['properties'] = self.properties 289 | if self.unit_of_measurement is not None: 290 | data['unitOfMeasurement'] = self.unit_of_measurement.__getstate__() 291 | if self.observed_area is not None: 292 | data['observedArea'] = self.observed_area 293 | if self.phenomenon_time is not None: 294 | data['phenomenonTime'] = utils.parse_datetime(self.phenomenon_time) 295 | if self.result_time is not None: 296 | data['resultTime'] = utils.parse_datetime(self.result_time) 297 | if self.thing is not None: 298 | data['Thing'] = self.thing.__getstate__() 299 | if self.sensor is not None: 300 | data['Sensor'] = self.sensor.__getstate__() 301 | if self.observed_property is not None: 302 | data['ObservedProperty'] = self.observed_property.__getstate__() 303 | if self.observations is not None and len(self.observations.entities) > 0: 304 | data['Observations'] = self.observations.__getstate__() 305 | return data 306 | 307 | def __setstate__(self, state): 308 | super().__setstate__(state) 309 | self.name = state.get("name", None) 310 | self.description = state.get("description", None) 311 | self.observation_type = state.get("observationType", None) 312 | self.properties = state.get("properties", {}) 313 | if state.get("unitOfMeasurement", None) is not None: 314 | self.unit_of_measurement = frost_sta_client.model.ext.unitofmeasurement.UnitOfMeasurement() 315 | self.unit_of_measurement.__setstate__(state["unitOfMeasurement"]) 316 | if state.get("observedArea", None) is not None: 317 | self.observed_area = frost_sta_client.utils.process_area(state["observedArea"]) 318 | if state.get("phenomenonTime", None) is not None: 319 | self.phenomenon_time = state["phenomenonTime"] 320 | if state.get("resultTime", None) is not None: 321 | self.result_time = state["resultTime"] 322 | if state.get("Thing", None) is not None: 323 | self.thing = frost_sta_client.model.thing.Thing() 324 | self.thing.__setstate__(state["Thing"]) 325 | if state.get("ObservedProperty", None) is not None: 326 | self.observed_property = frost_sta_client.model.observedproperty.ObservedProperty() 327 | self.observed_property.__setstate__(state["ObservedProperty"]) 328 | if state.get("Sensor", None) is not None: 329 | self.sensor = frost_sta_client.model.sensor.Sensor() 330 | self.sensor.__setstate__(state["Sensor"]) 331 | if state.get("Observations", None) is not None and isinstance(state["Observations"], list): 332 | entity_class = entity_type.EntityTypes['Observation']['class'] 333 | self.observations = utils.transform_json_to_entity_list(state['Observations'], entity_class) 334 | self.observations.next_link = state.get("Observations@iot.nextLink", None) 335 | self.observations.count = state.get("Observations@iot.count", None) 336 | 337 | def get_dao(self, service): 338 | return DatastreamDao(service) 339 | -------------------------------------------------------------------------------- /frost_sta_client/model/thing.py: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2021 Fraunhofer Institut IOSB, Fraunhoferstr. 1, D 76131 2 | # Karlsruhe, Germany. 3 | # 4 | # This program is free software: you can redistribute it and/or modify 5 | # it under the terms of the GNU Lesser General Public License as published by 6 | # the Free Software Foundation, either version 3 of the License, or 7 | # (at your option) any later version. 8 | # 9 | # This program is distributed in the hope that it will be useful, 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | # GNU Lesser General Public License for more details. 13 | # 14 | # You should have received a copy of the GNU Lesser General Public License 15 | # along with this program. If not, see . 16 | 17 | from . import entity 18 | from . import location 19 | from . import datastream 20 | from . import multi_datastream 21 | from . import historical_location 22 | from . import tasking_capability 23 | 24 | from frost_sta_client.dao.thing import ThingDao 25 | from frost_sta_client import utils 26 | from .ext import entity_list 27 | from .ext import entity_type 28 | 29 | 30 | class Thing(entity.Entity): 31 | 32 | def __init__(self, 33 | name='', 34 | description='', 35 | properties=None, 36 | locations=None, 37 | historical_locations=None, 38 | datastreams=None, 39 | multi_datastreams=None, 40 | tasking_capabilities=None, 41 | **kwargs): 42 | super().__init__(**kwargs) 43 | if properties is None: 44 | properties = {} 45 | self.name = name 46 | self.description = description 47 | self.properties = properties 48 | self.locations = locations 49 | self.historical_locations = historical_locations 50 | self.datastreams = datastreams 51 | self.multi_datastreams = multi_datastreams 52 | self.tasking_capabilities = tasking_capabilities 53 | 54 | def __new__(cls, *args, **kwargs): 55 | new_thing = super().__new__(cls) 56 | attributes = {'_id': None, '_name': '', '_description': '', '_properties': {}, '_locations': None, 57 | '_historical_locations': None, '_datastreams': None, '_multi_datastreams': None, 58 | '_tasking_capabilities': None, '_self_link': '', '_service': None} 59 | for key, value in attributes.items(): 60 | new_thing.__dict__[key] = value 61 | return new_thing 62 | 63 | @property 64 | def name(self): 65 | return self._name 66 | 67 | @name.setter 68 | def name(self, value): 69 | if value is None: 70 | self._name = None 71 | return 72 | if not isinstance(value, str): 73 | raise ValueError('name should be of type str!') 74 | self._name = value 75 | 76 | @property 77 | def description(self): 78 | return self._description 79 | 80 | @description.setter 81 | def description(self, value): 82 | if value is None: 83 | self._description = None 84 | return 85 | if not isinstance(value, str): 86 | raise ValueError('description should be of type str!') 87 | self._description = value 88 | 89 | @property 90 | def properties(self): 91 | return self._properties 92 | 93 | @properties.setter 94 | def properties(self, value): 95 | if value is None: 96 | self._properties = None 97 | return 98 | if not isinstance(value, dict): 99 | raise ValueError('properties should be of type dict!') 100 | self._properties = value 101 | 102 | @property 103 | def locations(self): 104 | return self._locations 105 | 106 | @locations.setter 107 | def locations(self, values): 108 | if values is None: 109 | self._locations = None 110 | return 111 | if isinstance(values, list) and all(isinstance(loc, location.Location) for loc in values): 112 | entity_class = entity_type.EntityTypes['Location']['class'] 113 | self._locations = entity_list.EntityList(entity_class=entity_class, entities=values) 114 | return 115 | if not isinstance(values, entity_list.EntityList) or \ 116 | any((not isinstance(loc, location.Location)) for loc in values.entities): 117 | raise ValueError('locations should be a list of locations') 118 | self._locations = values 119 | 120 | @property 121 | def historical_locations(self): 122 | return self._historical_locations 123 | 124 | @historical_locations.setter 125 | def historical_locations(self, values): 126 | if values is None: 127 | self._historical_locations = None 128 | return 129 | if isinstance(values, list) and all(isinstance(loc, historical_location.HistoricalLocation) for loc in values): 130 | entity_class = entity_type.EntityTypes['HistoricalLocation']['class'] 131 | self._historical_locations = entity_list.EntityList(entity_class=entity_class, entities=values) 132 | return 133 | if not isinstance(values, entity_list.EntityList) or \ 134 | any((not isinstance(loc, historical_location.HistoricalLocation)) for loc in values.entities): 135 | raise ValueError('historical_locations should be a list of historical locations') 136 | self._historical_locations = values 137 | 138 | @property 139 | def datastreams(self): 140 | return self._datastreams 141 | 142 | @datastreams.setter 143 | def datastreams(self, values): 144 | if values is None: 145 | self._datastreams = None 146 | return 147 | if isinstance(values, list) and all(isinstance(ds, datastream.Datastream) for ds in values): 148 | entity_class = entity_type.EntityTypes['Datastream']['class'] 149 | self._datastreams = entity_list.EntityList(entity_class=entity_class, entities=values) 150 | return 151 | if not isinstance(values, entity_list.EntityList) or \ 152 | any((not isinstance(ds, datastream.Datastream)) for ds in values.entities): 153 | raise ValueError('datastreams should be a list of datastreams') 154 | self._datastreams = values 155 | 156 | @property 157 | def multi_datastreams(self): 158 | return self._multi_datastreams 159 | 160 | @multi_datastreams.setter 161 | def multi_datastreams(self, values): 162 | if values is None: 163 | self._multi_datastreams = None 164 | return 165 | if isinstance(values, list) and all(isinstance(ds, multi_datastream.MultiDatastream) for ds in values): 166 | entity_class = entity_type.EntityTypes['MultiDatastream']['class'] 167 | self._multi_datastreams = entity_list.EntityList(entity_class=entity_class, entities=values) 168 | return 169 | if not isinstance(values, entity_list.EntityList) or \ 170 | any((not isinstance(ds, multi_datastream.MultiDatastream)) for ds in values.entities): 171 | raise ValueError('Multidatastreams should be a list of MultiDatastreams') 172 | self._multi_datastreams = values 173 | 174 | @property 175 | def tasking_capabilities(self): 176 | return self._tasking_capabilities 177 | 178 | @tasking_capabilities.setter 179 | def tasking_capabilities(self, values): 180 | if values is None: 181 | self._tasking_capabilities = None 182 | return 183 | if isinstance(values, list) and all(isinstance(tc, tasking_capability.TaskingCapability) for tc in values): 184 | entity_class = entity_type.EntityTypes['TaskingCapability']['class'] 185 | self._tasking_capabilities = entity_list.EntityList(entity_class=entity_class, entities=values) 186 | return 187 | if not isinstance(values, entity_list.EntityList) or \ 188 | any((not isinstance(tc, tasking_capability.TaskingCapability)) for tc in values.entities): 189 | raise ValueError('Tasking capabilities should be a list of TaskingCapabilities') 190 | self._tasking_capabilities = values 191 | 192 | def get_datastreams(self): 193 | result = self.service.datastreams() 194 | result.parent = self 195 | return result 196 | 197 | def get_multi_datastreams(self): 198 | result = self.service.multi_datastreams() 199 | result.parent = self 200 | return result 201 | 202 | def get_locations(self): 203 | result = self.service.locations() 204 | result.parent = self 205 | return result 206 | 207 | def get_historical_locations(self): 208 | result = self.service.historical_locations() 209 | result.parent = self 210 | return result 211 | 212 | def get_tasking_capabilities(self): 213 | result = self.service.tasking_capabilities() 214 | result.parent = self 215 | return result 216 | 217 | def ensure_service_on_children(self, service): 218 | if self.locations is not None: 219 | self.locations.set_service(service) 220 | if self.datastreams is not None: 221 | self.datastreams.set_service(service) 222 | if self.multi_datastreams is not None: 223 | self.multi_datastreams.set_service(service) 224 | if self.tasking_capabilities is not None: 225 | self.tasking_capabilities.set_service(service) 226 | 227 | def __eq__(self, other): 228 | if not super().__eq__(other): 229 | return False 230 | if self.name != other.name: 231 | return False 232 | if self.description != other.description: 233 | return False 234 | if self.properties != other.properties: 235 | return False 236 | return True 237 | 238 | def __ne__(self, other): 239 | return not self == other 240 | 241 | def __getstate__(self): 242 | data = super().__getstate__() 243 | if self.name is not None and self.name != '': 244 | data['name'] = self.name 245 | if self.description is not None and self.description != '': 246 | data['description'] = self.description 247 | if self.properties is not None and self.properties != {}: 248 | data['properties'] = self.properties 249 | if self._locations is not None and len(self.locations.entities) > 0: 250 | data['Locations'] = self.locations.__getstate__() 251 | if self._historical_locations is not None and len(self.historical_locations.entities) > 0: 252 | data['HistoricalLocations'] = self.historical_locations.__getstate__() 253 | if self._datastreams is not None and len(self.datastreams.entities) > 0: 254 | data['Datastreams'] = self.datastreams.__getstate__() 255 | if self._multi_datastreams is not None and len(self.multi_datastreams.entities) > 0: 256 | data['MultiDatastreams'] = self.multi_datastreams.__getstate__() 257 | if self._tasking_capabilities is not None and len(self.tasking_capabilities.entities) > 0: 258 | data['TaskingCapabilities'] = self.tasking_capabilities.__getstate__() 259 | return data 260 | 261 | def __setstate__(self, state): 262 | super().__setstate__(state) 263 | self.name = state.get("name", None) 264 | self.description = state.get("description", None) 265 | self.properties = state.get("properties", {}) 266 | 267 | if state.get("Locations", None) is not None and isinstance(state["Locations"], list): 268 | entity_class = entity_type.EntityTypes['Location']['class'] 269 | self.locations = utils.transform_json_to_entity_list(state['Locations'], entity_class) 270 | self.locations.next_link = state.get("Locations@iot.nextLink", None) 271 | self.locations.count = state.get("Locations@iot.count", None) 272 | if state.get("HistoricalLocations", None) is not None and isinstance(state["HistoricalLocations"], list): 273 | entity_class = entity_type.EntityTypes['HistoricalLocation']['class'] 274 | self.historical_locations = utils.transform_json_to_entity_list(state['HistoricalLocations'], entity_class) 275 | self.historical_locations.next_link = state.get("HistoricalLocations@iot.nextLink", None) 276 | self.historical_locations.count = state.get("HistoricalLocations@iot.count", None) 277 | if state.get("Datastreams", None) is not None and isinstance(state["Datastreams"], list): 278 | entity_class = entity_type.EntityTypes['Datastream']['class'] 279 | self.datastreams = utils.transform_json_to_entity_list(state['Datastreams'], entity_class) 280 | self.datastreams.next_link = state.get("Datastreams@iot.nextLink", None) 281 | self.datastreams.count = state.get("Datastreams@iot.count", None) 282 | if state.get("MultiDatastreams", None) is not None and isinstance(state["MultiDatastreams"], list): 283 | entity_class = entity_type.EntityTypes['MultiDatastream']['class'] 284 | self.multi_datastreams = utils.transform_json_to_entity_list(state['MultiDatastreams'], entity_class) 285 | self.multi_datastreams.next_link = state.get("MultiDatastreams@iot.nextLink", None) 286 | self.multi_datastreams.count = state.get("MultiDatastreams@iot.count", None) 287 | if state.get("TaskingCapabilities", None) is not None and isinstance(state["TaskingCapabilities"], list): 288 | entity_class = entity_type.EntityTypes['TaskingCapability']['class'] 289 | self.tasking_capabilities = utils.transform_json_to_entity_list(state['TaskingCapabilities'], entity_class) 290 | self.tasking_capabilities.next_link = state.get("TaskingCapabilities@iot.nextLink", None) 291 | self.tasking_capabilities.count = state.get("TaskingCapabilities@iot.count", None) 292 | 293 | def get_dao(self, service): 294 | return ThingDao(service) 295 | --------------------------------------------------------------------------------