diff --git a/requirements.txt b/requirements.txt index 2d3ba7d83f2fa31bf34728818813e7f23166b3dd..d504927dc9f35d68b9ff8aa18b28dc626181c025 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,16 +1,11 @@ -aiohttp @ file:///D:/bld/aiohttp_1605734736785/work +aiohttp==3.7.3 async-timeout==3.0.1 attrs @ file:///home/conda/feedstock_root/build_artifacts/attrs_1605083924122/work certifi==2020.11.8 chardet @ file:///D:/bld/chardet_1602255574834/work docstring-parser==0.7.3 -idna @ file:///home/conda/feedstock_root/build_artifacts/idna_1593328102638/work -multidict @ file:///D:/bld/multidict_1602413330828/work -nest-asyncio @ file:///home/conda/feedstock_root/build_artifacts/nest-asyncio_1605195931949/work -paho-mqtt==1.5.1 +nest-asyncio==1.4.3 strict-rfc3339==0.7 -typing-extensions @ file:///home/conda/feedstock_root/build_artifacts/typing_extensions_1602702424206/work -wincertstore==0.2 -wzl-mqtt==1.3.1 +wzl-mqtt==2.3.0 wzl-utilities==1.2.2 yarl @ file:///D:/bld/yarl_1605429655746/work diff --git a/src/soil/component.py b/src/soil/component.py index ab830e744d290bea80c532f746f3a2f3ef66a5b3..dc6418df7015f34e3006e32fe3cda7256e030a33 100644 --- a/src/soil/component.py +++ b/src/soil/component.py @@ -1,3 +1,11 @@ +'''Provides the Component class being the structuring element of the UDI, i.e. the underlying SOIL Model. + +Components are structural elements of a SOIL-model. +A component contains an arbitrary number of children elements such that the overall model has tree-like shape. +Children elements can be components, functions, parameters or measurements. + +''' + # from __future__ import annotations import json import os @@ -20,7 +28,41 @@ logger = root_logger.get(__name__) class Component(Element): def __init__(self, uuid: str, name: str, description: str, functions: List[Function], measurements: List[Measurement], - parameters: List[Parameter], components: List['Component'], implementation: Dict, ontology: str = None): + parameters: List[Parameter], components: List['Component'], mapping: Dict, ontology: str = None): + """ + + Args: + uuid: Locally unique identifier of the component. Must start with 'COM-'. + For the sake if simplicity, it is suggested to use the name and simply prepend 'COM-' to obtain the UUID: + name: Human readable name of the component. + description: Human readable description of the purpose of the component. + functions: List of all children functions. + measurements: List of all children measurements. + parameters: List of all children parameters. + components: List of all children components. Might contain dynamic-components. + mapping: Dictionary containing a mapping of the underlying device implementation to the HTTP-endpoints. + The mapping of a component looks as follows : + + { + 'getter': com_implementation.get, + 'setter': com_implementation.set, + 'MEA-Temperature': com_implementation.get_mea_temperature, + 'PAR-State': {...}, + 'FUN-Reset: {...}, + 'COM-Part': {...}, + } + + If the component does not have dynamic children components, 'getter' and 'setter' are set to None. + For all children there is a key-value pair where the UUID of the child is the key and the mapping of the child is the value. + For the structure of the childrens' mappings please refer to the respective documentation. + ontology: Optional field containing the reference to a semantic definition of the components name or purpose. + + Raises: + ValueError: The UUID does not start with 'COM'. + AmbiguousUUIDException: There are at least two children having the same UUID. + InvalidModelException: One of the lists containing the components' children is not a list or contains elements which are not of the correct type. + InvalidMappingException: If something is wrong with the provided mapping. + """ Element.__init__(self, uuid, name, description, ontology) if uuid[:3] != 'COM': raise Exception('{}: The UUID must start with COM!'.format(uuid)) @@ -53,18 +95,35 @@ class Component(Element): self._implementation_remove = implementation['remove'] if 'remove' in implementation else None def __getitem__(self, item: Union[str, List[str]], method: int = HTTP_GET) -> Any: - attribute = False - if isinstance(item, str): - attribute = hasattr(self, item) - if item == "functions": + """Returns the value of the specified item. + + Args: + item: Either a string or a list of uuids. Possible values string + values are 'functions', 'measurements', 'parameters', 'components' + and 'children'. The returned value is the list of the specified + elements. + If a list of UUIDs is given, the __getitem__ method of the child of + which its UUID is equal to first UUID in the list is called with + item[1:0]. + + method: + + Returns: Either a list of (specific) children or, if a list of UUIDs is + given, the method returns an Element. + + Raises + + """ + + if item == 'functions': return self._functions - if item == "measurements": + if item == 'measurements': return self._measurements - if item == "parameters": + if item == 'parameters': return self._measurements - if item == "components": + if item == 'components': return self._components - if item == "children": + if item == 'children': ret = [] everything = self._components + self._measurements + self._parameters + self._functions for o in everything: @@ -82,33 +141,33 @@ class Component(Element): if len(item) == 1: return o else: - return o.__getitem__(item[1:], method) - raise Exception("{}: Given uuid {} is not the id of a child of the current component!".format(self.uuid, item)) + return child.__getitem__(item[1:], method) + raise ChildNotFoundException(f'{self.uuid}: Given uuid {item} is not the id of a child of the current component!') return super().__getitem__(item, method) def __setitem__(self, key: str, value: Any): - if key == "functions": + if key == 'functions': if not isinstance(value, list): raise Exception('{}: Given functions are not a list!'.format(self.uuid)) for f in value: if not isinstance(f, Function): raise Exception('{}: Given function is not of type Function!'.format(self.uuid)) self._functions = value - elif key == "measurements": + elif key == 'measurements': if not isinstance(value, list): raise Exception('{}: Given measurements are not a list!'.format(self.uuid)) for v in value: if not isinstance(v, Measurement): raise Exception('{}: Given measurement is not of type Variable!'.format(self.uuid)) self._measurements = value - elif key == "parameters": + elif key == 'parameters': if not isinstance(value, list): raise Exception('{}: Given parameters are not a list!'.format(self.uuid)) for v in value: if not isinstance(v, Parameter): raise Exception('{}: Given parameter is not of type Parameter!'.format(self.uuid)) self._measurements = value - elif key == "components": + elif key == 'components': if not isinstance(value, list): raise Exception('{}: Given components are not a list!'.format(self.uuid)) for o in value: @@ -174,7 +233,7 @@ class Component(Element): component_list[idx]['returns'] = merge_measurements(function['returns'], component_list[idx]['returns']) return component_list - # merge components, i.e. overwrite fields of "static" children dictionary with the "dynamic" fields of the parents dictionary + # merge components, i.e. overwrite fields of 'static' children dictionary with the 'dynamic' fields of the parents dictionary uuid = parent_dict['uuid'] component_dict['uuid'] = uuid if 'name' in parent_dict: @@ -228,7 +287,7 @@ class Component(Element): measurements = [] for var in dictionary['measurements']: if mapping is not None: - submapping = mapping[var["uuid"]] if var['uuid'] in mapping else None + submapping = mapping[var['uuid']] if var['uuid'] in mapping else None measurements += [Measurement.deserialize(var, submapping)] else: measurements += [Measurement.deserialize(var)] @@ -238,7 +297,7 @@ class Component(Element): parameters = [] for par in dictionary['parameters']: if mapping is not None: - submapping = mapping[par["uuid"]] if par['uuid'] in mapping else None + submapping = mapping[par['uuid']] if par['uuid'] in mapping else None parameters += [Parameter.deserialize(par, submapping)] else: parameters += [Parameter.deserialize(par)] @@ -248,7 +307,7 @@ class Component(Element): functions = [] for func in dictionary['functions']: if mapping is not None: - submapping = mapping[func["uuid"]] if func['uuid'] in mapping else None + submapping = mapping[func['uuid']] if func['uuid'] in mapping else None functions += [Function.deserialize(func, submapping)] else: functions += [Function.deserialize(func)] @@ -258,7 +317,7 @@ class Component(Element): components = [] for obj in dictionary['components']: if mapping is not None: - submapping = mapping[obj["uuid"]] if obj['uuid'] in mapping else None + submapping = mapping[obj['uuid']] if obj['uuid'] in mapping else None components += [Component.deserialize(obj, submapping, filepath)] else: components += [Component.deserialize(obj, filepath=filepath)] @@ -272,12 +331,12 @@ class Component(Element): raise SerialisationException('{}: The component can not be deserialized. {}'.format(uuid, e)) def write(self, filename: str): - if filename[-5:] != ".json": + if filename[-5:] != '.json': raise Exception('{} is not a json file!'.format(filename)) model_dict = self.serialize(['all']) - f = open(filename, "w") + f = open(filename, 'w') f.write(json.dumps(model_dict)) f.close() @@ -289,7 +348,7 @@ class Component(Element): return # self._components.append(element) else: - raise Exception("Wrong type updating element on existing model!") + raise Exception('Wrong type updating element on existing model!') def add(self, uuid: str, class_name: str, json_file: str, *args, **kwargs): if uuid[:3] == 'COM': @@ -325,7 +384,7 @@ class Component(Element): if isinstance(file, str): if not os.path.isfile(file): raise Exception('There is no file named {}!'.format(file)) - if file[-5:] != ".json": + if file[-5:] != '.json': raise Exception('{} is not a json file!'.format(file)) with open(file, 'r') as f: model_dict = json.load(f) diff --git a/src/soil/error.py b/src/soil/error.py index 26f9f30ff5dc2901336110f9d0a5c7386f9da13c..173189b1f71787daf140547a5a9c0d48981b417a 100644 --- a/src/soil/error.py +++ b/src/soil/error.py @@ -18,6 +18,11 @@ class DimensionException(DeviceException): def __init__(self, description): BasicException.__init__(self, description) +class ChildNotFound(UserException): + + def __init__(self, description): + UserException.__init__(self, description) + class ChildNotFoundException(DeviceException):