From 4de99cc9b17b4bacbafe6a59c5232576adab6a67 Mon Sep 17 00:00:00 2001 From: stedif <stefano.difrischia@inaf.it> Date: Wed, 26 Oct 2022 15:17:08 +0200 Subject: [PATCH] L2SS-1030: rewrite configuration file --- .../common/configuration.py | 394 +++++------------- 1 file changed, 101 insertions(+), 293 deletions(-) diff --git a/tangostationcontrol/tangostationcontrol/common/configuration.py b/tangostationcontrol/tangostationcontrol/common/configuration.py index 58de15909..ad3752528 100644 --- a/tangostationcontrol/tangostationcontrol/common/configuration.py +++ b/tangostationcontrol/tangostationcontrol/common/configuration.py @@ -1,300 +1,108 @@ -# -# Code re-adapted from dsconfig python package in DSConfig container -# See: https://gitlab.com/MaxIV/lib-maxiv-dsconfig -# License: GPLv3 -# - from tango import DeviceProxy -from collections import defaultdict -import six -try: - from collections.abc import MutableMapping -except ImportError: - from collections import MutableMapping -try: - from collections.abc import Mapping -except ImportError: - from collections import Mapping from itertools import islice -def get_db_data(db): - # dump TANGO database into JSON. Optionally filter which things to include - # (currently only "positive" filters are possible; you can say which - # servers/classes/devices to include, but you can't exclude selectively) - # By default, dserver devices aren't included! - dbproxy = DeviceProxy(db.dev_name()) - data = SetterDict() - # the user did not specify a pattern, so we will dump *everything* - servers = get_servers_with_filters(dbproxy) - data.servers.update(servers) - return data.to_dict() - -def get_servers_with_filters(dbproxy, server="*", clss="*", device="*", - properties=True, attribute_properties=True, - aliases=True, dservers=False, - subdevices=False, uppercase_devices=False, - timeout=10): - """ - A performant way to get servers and devices in bulk from the DB - by direct SQL statements and joins, instead of e.g. using one - query to get the properties of each device. - - TODO: are there any length restrictions on the query results? In - that case, use limit and offset to get page by page. - """ - - server = server.replace("*", "%") # mysql wildcards - clss = clss.replace("*", "%") - device = device.replace("*", "%") - - # Collect all info about each device in this dict - devices = CaselessDictionary() - - # Queries can sometimes take more than de default 3 s, so it's - # good to increase the timeout a bit. - dbproxy.set_timeout_millis(timeout * 1000) - - if properties: - # Get all relevant device properties - query = ( - "SELECT device, property_device.name, property_device.value" - " FROM property_device" - " INNER JOIN device ON property_device.device = device.name" - " WHERE server LIKE '%s' AND class LIKE '%s' AND device LIKE '%s'") - if not dservers: - query += " AND class != 'DServer'" - if not subdevices: - query += " AND property_device.name != '__SubDevices'" - query += " ORDER BY property_device.count ASC" - _, result = dbproxy.command_inout("DbMySqlSelect", - query % (server, clss, device)) - for d, p, v in nwise(result, 3): - dev = devices.setdefault(d, {}) - # Make sure to use caseless dicts, since in principle property names - # should be caseles too. - props = dev.setdefault("properties", CaselessDictionary()) - value = props.setdefault(p, []) - value.append(v) - - if attribute_properties: - # Get all relevant attribute properties - query = ( - "SELECT device, attribute, property_attribute_device.name," - " property_attribute_device.value" - " FROM property_attribute_device" - " INNER JOIN device ON property_attribute_device.device =" - " device.name" - " WHERE server LIKE '%s' AND class LIKE '%s' AND device LIKE '%s'") - if not dservers: - query += " AND class != 'DServer'" - query += " ORDER BY property_attribute_device.count ASC" - _, result = dbproxy.command_inout("DbMySqlSelect", query % (server, clss, device)) - for d, a, p, v in nwise(result, 4): - dev = devices.setdefault(d, {}) - props = dev.setdefault("attribute_properties", CaselessDictionary()) - attr = props.setdefault(a, CaselessDictionary()) - value = attr.setdefault(p, []) - value.append(v) - - # dump relevant servers - query = ( - "SELECT server, class, name, alias FROM device" - " WHERE server LIKE '%s' AND class LIKE '%s' AND name LIKE '%s'") - - if not dservers: - query += " AND class != 'DServer'" - _, result = dbproxy.command_inout("DbMySqlSelect", query % (server, clss, device)) - - # combine all the information we have - servers = SetterDict() - for s, c, d, a in nwise(result, 4): - try: - srv, inst = s.split("/") - except ValueError: - # Malformed server name? It can happen! - continue - device = devices.get(d, {}) - if a and aliases: - device["alias"] = a - devname = maybe_upper(d, uppercase_devices) - servers[srv][inst][c][devname] = device - - return servers - -def nwise(it, n): - # [s_0, s_1, ...] => [(s_0, ..., s_(n-1)), (s_n, ... s_(2n-1)), ...] - return list(zip(*[islice(it, i, None, n) for i in range(n)])) - -def maybe_upper(s, upper=False): - if upper: - return s.upper() - return s - -class CaselessString(object): +DEVICE_PROPERTIES_QUERY = "SELECT device, property_device.name, property_device.value FROM property_device \ + INNER JOIN device ON property_device.device = device.name \ + WHERE server LIKE '%' AND class LIKE '%' AND device LIKE '%' \ + AND class != 'DServer' \ + AND property_device.name != '__SubDevices' \ + ORDER BY device, property_device.name, property_device.count ASC" + +ATTRS_PROPERTIES_QUERY = "SELECT device, attribute, property_attribute_device.name, \ + property_attribute_device.value \ + FROM property_attribute_device \ + INNER JOIN device ON property_attribute_device.device = device.name \ + WHERE server LIKE '%' AND class LIKE '%' AND device LIKE '%' \ + AND class != 'DServer' \ + ORDER BY device, property_attribute_device.name, property_attribute_device.count ASC" + +SERVER_QUERY = "SELECT server, class, name FROM device \ + WHERE server LIKE '%' AND class LIKE '%' AND name LIKE '%' \ + AND class != 'DServer' \ + ORDER BY server ASC" + +def get_db_data(db, tangodb_timeout:int = 10000): + """ Dump TANGO database into dictionary """ + dbproxy = DeviceProxy(db.dev_name()) # TangoDB + dbproxy.set_timeout_millis(tangodb_timeout) # Set a security timeout (default is 3000ms) + # Create empty dictionaries to be populated + devices_dict = {} + server_dict = {} + # Query TangoDb with built-in function for devices data + _, raw_result = dbproxy.command_inout("DbMySqlSelect", DEVICE_PROPERTIES_QUERY) + # Remodel the query result + device_property_result = query_to_tuples(raw_result, 3) + # Populate devices dictionary from query data + devices_dict = model_devices_dict(devices_dict, device_property_result) + # Query TangoDb with built-in function for attributes data + _, raw_result = dbproxy.command_inout("DbMySqlSelect", ATTRS_PROPERTIES_QUERY) + # Remodel the query result + attrs_property_result = query_to_tuples(raw_result, 4) + # Populate devices dictionary from query data + devices_dict = model_attrs_dict(devices_dict, attrs_property_result) + # Query TangoDb with built-in function for server data + _, raw_result = dbproxy.command_inout("DbMySqlSelect", SERVER_QUERY) + # Remodel the query result + server_result = query_to_tuples(raw_result, 3) + # Populate server dictionary from query data and merge it with devices dict + server_dict = model_server_dict(server_dict, devices_dict, server_result) + return server_dict + +def model_devices_dict(devices_dict:dict, result:list): + """ Model a devices dictionary with the following structure: + 'device_name': { 'properties' : { 'property_name': ['property_value'] } } """ - A mixin to make a string subclass case-insensitive in dict lookups. + for device, property, value in result: + # lowercase data + device = device.lower() + property = property.lower() + # model dictionary + device_data = devices_dict.setdefault(device, {}) + property_data = device_data.setdefault("properties", {}) + value_data = property_data.setdefault(property, []) + value_data.append(value) + return devices_dict + +def model_attrs_dict(devices_dict:dict, result:list): + """ Model a device dictionary with the following structure : + 'device_name': { 'attribute_properties' : { 'attribute_name': {'property_name' : ['property_value'] } } } """ - - def __hash__(self): - return hash(self.lower()) - - def __eq__(self, other): - return self.lower() == other.lower() - - def __cmp__(self, other): - return self.lower().__cmp__(other.lower()) - - @classmethod - def make_caseless(cls, string): - if isinstance(string, six.text_type): - return CaselessUnicode(string) - return CaselessStr(string) - -class CaselessStr(CaselessString, str): - pass - - -class CaselessUnicode(CaselessString, six.text_type): - pass - -class CaselessDictionary(MutableMapping): + for device, attribute, property, value in result: + # lowercase data + device = device.lower() + attribute = attribute.lower() + property = property.lower() + # model dictionary + device_data = devices_dict.setdefault(device, {}) + property_data = device_data.setdefault("attribute_properties", {}) + attr_data = property_data.setdefault(attribute, {}) + value_data = attr_data.setdefault(property, []) + value_data.append(value) + return devices_dict + +def model_server_dict(server_dict:dict, devices_dict:dict, result:list): + """ Model the server dictionary and merge it with the devices dictionary. + At the end of the process, the dictionary will have the following structure : + 'server_name' : { 'server_instance' : { 'server_class' : + 'device_name': { 'properties' : { 'property_name': ['property_value'] } }, + { 'attribute_properties' : { 'attribute_name': {'property_name' : ['property_value'] } } } } } """ - A dictionary-like object which ignores but preserves the case of strings. - - Example:: - - >>> cdict = CaselessDictionary() - - Access is case-insensitive:: - - >>> cdict['a'] = 1 - >>> cdict['A'] - 1 - - As is writing:: - - >>> cdict['key'] = 123 - >>> cdict['KeY'] = 456 - >>> cdict['key'] - 456 - - And deletion:: - - >>> del cdict['A'] - >>> 'a' in cdict - False - >>> 'A' in cdict - False - - However, the case of keys is preserved (the case of overridden keys will be - the first one which was set):: - - >>> cdict['aBcDeF'] = 1 - >>> sorted(list(cdict)) - ['aBcDeF', 'key'] - """ - - def __init__(self, *args, **kwargs): - self.__dict__["_dict"] = {} - temp_dict = dict(*args, **kwargs) - for key, value in list(temp_dict.items()): - if isinstance(key, six.string_types): - key = CaselessString.make_caseless(key) - - if isinstance(value, dict): - self._dict[key] = CaselessDictionary(value) - else: - self._dict[key] = value - - def __getitem__(self, key): - return self._dict[CaselessString.make_caseless(key)] - - def __setitem__(self, key, value): - self._dict[CaselessString.make_caseless(key)] = value - - def __delitem__(self, key): - del self._dict[CaselessString.make_caseless(key)] - - def __contains__(self, key): - return CaselessString.make_caseless(key) in self._dict - - def __iter__(self): - return iter(self._dict) - - def __len__(self): - return len(self._dict) - - def keys(self): - # convert back to ordinary strings - return [str(k) for k in self._dict] - - def items(self): - return list(zip(list(self.keys()), list(self.values()))) - -class SetterDict(CaselessDictionary, defaultdict): - """ - A recursive defaultdict with extra bells & whistles - - It enables you to set keys at any depth without first creating the - intermediate levels, e.g. - - d = SetterDict() - d["a"]["b"]["c"] = 1 - - It also allows access using normal getattr syntax, interchangeably: - - d.a["b"].c == d.a.b.c == d["a"]["b"]["c"] - - Note: only allows string keys for now. - - Keys are caseless, meaning that the key "MyKey" is the same as - "mykey", "MYKEY", etc. The case - - Note: this thing is dangerous! Accessing a non-existing key will - result in creating it, which means that confusing behavior is - likely. Please use it carefully and convert to an ordinary dict - (using to_dict()) when you're done creating it. - """ - - def __init__(self, value=None, factory=None): - factory = factory or SetterDict - value = value or {} - self.__dict__["_factory"] = factory - CaselessDictionary.__init__(self) - defaultdict.__init__(self, factory) - for k, v in list(value.items()): - self[k] = v - - def __getitem__(self, key): - try: - return CaselessDictionary.__getitem__(self, key) - except KeyError: - return self.__missing__(key) - - def __setitem__(self, key, value): - if isinstance(value, SetterDict): - CaselessDictionary.__setitem__(self, key, value) - elif isinstance(value, Mapping): - CaselessDictionary.__setitem__(self, key, self._factory(value)) - else: - CaselessDictionary.__setitem__(self, key, value) - - def __getattr__(self, name): - return self.__getitem__(name) - - def __setattr__(self, key, value): - return self.__setitem__(key, value) - - def to_dict(self): - """ - Returns a ordinary dict version of itself - """ - result = {} - for key, value in list(self.items()): - if isinstance(value, SetterDict): - result[key] = value.to_dict() - else: - result[key] = value - return result - + for server, sclass, device in result: + # lowercase data + device = device.lower() + server = server.lower() + sclass = sclass.lower() + # model dictionary + sname, instance = server.split('/') + device_data = devices_dict.get(device, {}) + server_data = server_dict.setdefault(sname, {}) + instance_data = server_data.setdefault(instance, {}) + class_data = instance_data.setdefault(sclass, {}) + # merge the two dictionaries + server_dict[sname][instance][sclass][device] = device_data + return server_dict + +def query_to_tuples(result, num_cols): + """ Given a query result and its number of columns, transforms the raw result in a list of tuples """ + return list(zip(*[islice(result, i, None, num_cols) for i in range(num_cols)])) -- GitLab