├── 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 |
--------------------------------------------------------------------------------