Descriptor Registries keep track of the available interaction affordances for a given class or instance, allowing for dynamic introspection with the Thing's capabilities.
The purpose can be summarized as:
add and remove affordances in runtime
find by name, check existence, iterate and introspect affordances
implement group operations on affordances (readmultipleproperties, writemultipleproperties, readallproperties, writeallproperties)
PropertyRegistry for managing property descriptors
ActionRegistry for managing action descriptors
EventRegistry for managing event descriptors
Find by Name/Check Existence
Lets say a serial device supports an optional list of supported commands. The presence or absence of this property could indicate whether such a list is available or not.
classSerialUtility(Thing):@action()defexecute(self,command:str,expected_return_data_size:int=0)->Any:"""Execute a command on the serial device."""if('instructions'inself.propertiesand \
commandnotinself.properties["instructions"]):raiseRuntimeError(f"command {command} not a valid command.")# Implementation of command executionreturnresult@action()defadd_command(self,command:str)->None:"""Add a command to the list of supported commands."""if'instructions'notinself.properties:self.properties.add('instructions',List(default=None,item_type=str,doc="List of supported commands"))self.instructions=[]self.properties['instructions'].append(command)# or self.instructions.append(command)
Of course, one could use an empty list instead of having a dynamic property.
This is a contrived example.
Iterate and Introspect Affordances
Lets say you have overloaded a getter of a composite property that returns a group of properties' values:
classSpectrometer(Thing):measurement_settings=Property(default=None,readonly=True,model=...,# please use a decent JSON schema or pydantic model heredoc="Settings of the spectrometer, including integration time, trigger mode etc.")@measurement_settings.getterdefread_settings(self,**kwargs)->None:setting_props=dict()fornamein["integration_time","trigger_mode","pixel_count","nonlinearity_correction","background_subtraction"]:ifnameinself.properties:setting_props[name]=dict(current_value=self.properties[name].__get__(),set_value=self.properties[name].metadata["set_value"])returnsetting_props
One could also alter the metadata of interaction affordances as an administrative task. For example, one could make an action inaccessible or make a property read-only
even if the setter is defined.
classSpectrometer(Thing):@action()deffreeze_measurement_settings(self)->None:"""freeze important measurement settings to prevent accidental changes"""self.properties['background_correction'].readonly=Trueself.properties['nonlinearity_correction'].readonly=Trueself.properties['trigger_mode'].readonly=Trueself.properties['integration_time'].readonly=Trueself._inaccessible_actions["send_raw_command"]=self.actions.pop("send_raw_command")# remove from descriptor registry to make actions inaccessibleintegration_time=Number(default=1000,bounds=(0.001,None),crop_to_bounds=True,doc="integration time of measurement in milliseconds")# type: float# can be set to readonly even if not originally defined as suchtrigger_mode=Selector(objects=[0,1,2,3,4],default=0,observable=True,doc="""0 = normal/free running, 1 = Software trigger, 2 = Ext. Trigger Level, 3 = Ext. Trigger Synchro/ Shutter mode, 4 = Ext. Trigger Edge""")# type: int@trigger_mode.setterdefapply_trigger_mode(self,value:int):self.device.trigger_mode(value)@trigger_mode.getterdefget_trigger_mode(self):# can be set to readonly even if the setter is definedreturnself.device.trigger_mode()@action()defunfreeze_measurement_settings(self)->None:""" unfreeze measurement settings to allow changes again after measurement is complete """...
Of course, such administrative tasks needs to be wrapped in a security definition to prevent unauthorized execution.
Implement Group Operations
One could iterate through all the available interactions to perform group operations. WoT operations on multiple properties are implemented as follows:
classPropertyRegistry(DescriptorRegistry):defget(self,**kwargs:typing.Dict[str,typing.Any])->typing.Dict[str,typing.Any]:""" read properties from the object, implements WoT operations `readAllProperties` and `readMultipleProperties` Parameters ---------- **kwargs: typing.Dict[str, typing.Any] - names: `List[str]` list of property names to be fetched - name: `str` name of the property to be fetched, along with a 'rename' for the property in the response. For example { 'foo_prop' : 'fooProp' } will return the property 'foo_prop' as 'fooProp' in the response. """data={}iflen(kwargs)==0:# read all propertiesforname,propinself.remote_objects.items():ifself.owner_instisNoneandnotprop.class_member:continuedata[name]=prop.__get__(self.owner_inst,self.owner_cls)returndataelif'names'inkwargs:# read multiple properties whose names are specifiednames=kwargs.get('names')ifnotisinstance(names,(list,tuple,str)):raiseTypeError("Specify properties to be fetched as a list, tuple or comma separated names. "+f"Given type {type(names)}")ifisinstance(names,str):names=names.split(',')kwargs={name:namefornameinnames}forrequested_prop,renameinkwargs.items():ifnotisinstance(requested_prop,str):raiseTypeError(f"property name must be a string. Given type {type(requested_prop)}")ifnotisinstance(rename,str):raiseTypeError(f"requested new name must be a string. Given type {type(rename)}")ifrequested_propnotinself.descriptors:raiseAttributeError(f"property {requested_prop} does not exist")ifrequested_propnotinself.remote_objects:raiseAttributeError(f"property {requested_prop} is not remote accessible")prop=self.descriptors[requested_prop]ifself.owner_instisNoneandnotprop.class_member:continuedata[rename]=prop.__get__(self.owner_inst,self.owner_cls)returndata
classPropertyRegistry(DescriptorRegistry):defset(self,**values:typing.Dict[str,typing.Any])->None:""" set properties whose name is specified by keys of a dictionary; implements WoT operations `writeMultipleProperties` or `writeAllProperties`. Parameters ---------- values: typing.Dict[str, typing.Any] dictionary of property names and its new values Raises ------ AttributeError if property does not exist or is not remote accessible RuntimeError if some properties could not be set due to errors, check exception notes or server logs for more information """errors=''forname,valueinvalues.items():try:ifnamenotinself.descriptors:raiseAttributeError(f"property {name} does not exist")ifnamenotinself.remote_objects:raiseAttributeError(f"property {name} is not remote accessible")prop=self.descriptors[name]ifself.owner_instisNoneandnotprop.class_member:raiseAttributeError(f"property {name} is not a class member and cannot be set at class level")setattr(self.owner,name,value)exceptExceptionasex:errors+=f'{name}: {str(ex)}\n'iferrors:ex=RuntimeError("Some properties could not be set due to errors. "+"Check exception notes or server logs for more information.")ex.__notes__=errorsraiseexfromNone