From 517f41e39ee00048fd14aceeb94647f7637196c5 Mon Sep 17 00:00:00 2001 From: Thibaut Vermeulen Date: Thu, 6 Apr 2023 17:44:38 +0200 Subject: [PATCH 1/7] fix: fix, rename and improve method get_int_addr(node) -> node.get_path_from_ld() --- src/scl_loader/scl_loader.py | 29 +++++++++++++++-------------- tests/test_scd_manager.py | 15 +++++++++++++-- 2 files changed, 28 insertions(+), 16 deletions(-) diff --git a/src/scl_loader/scl_loader.py b/src/scl_loader/scl_loader.py index ec14099..b7f575b 100644 --- a/src/scl_loader/scl_loader.py +++ b/src/scl_loader/scl_loader.py @@ -414,7 +414,7 @@ def get_DO_nodes(self) -> dict: node = self while node.parent is not None: if isinstance(node.parent(), LN): - do_nodes[self.get_int_addr(node)] = node + do_nodes[node.get_path_from_ld()] = node node = node.parent() else: self._collect_DO_nodes(self, do_nodes) @@ -466,7 +466,7 @@ def get_name_subtree(self, fc_filter: str = None): Tree of 2-tuple elements (name, [subelem]) """ tree = {} - node_depth = len(self.get_int_addr(self).split(".")) + node_depth = len(self.get_path_from_ld().split(".")) for leaf_path, leaf_node in self.get_DA_leaf_nodes().items(): if fc_filter is None or leaf_node.get_associated_fc() == fc_filter: @@ -584,7 +584,7 @@ def _collect_DA_leaf_nodes(self, node, leaves: dict) -> dict: """ if node is not None: if self._is_leaf(node): - leaves[self.get_int_addr(node)] = node + leaves[node.get_path_from_ld()] = node else: children = node.get_children() @@ -612,15 +612,14 @@ def _collect_DO_nodes(self, node, do_nodes: dict) -> dict: """ if node is not None: if isinstance(node, DO): - do_nodes[self.get_int_addr(node)] = node + do_nodes[node.get_path_from_ld()] = node else: children = node.get_children() for child in children: self._collect_DO_nodes(child, do_nodes) - - def get_int_addr(self, node) -> str: + def get_path_from_ld(self) -> str: """ Get int adr of SCDNode @@ -632,21 +631,23 @@ def get_int_addr(self, node) -> str: Returns ------- `str` - Return the internal address of the node + Return the path of the node from LD (format LD.LN.DO.DA) """ - assert isinstance(node, (LD, LN, DO, DA)), "Invalid SCDNode level, expect LD, LN, DO or DA" - int_addr = node.name - if node.parent is None: - logging.debug(f'SCDNode::get_int_addr: parent node of node: {self.name} is None, cannot build int_addr') + assert isinstance(self, (LD, LN, DO, DA)), "Invalid SCDNode level, expect LD, LN, DO or DA" + path = self.name + if isinstance(self, LD): + return path + if self.parent is None: + logging.debug(f'SCDNode::get_path_from_ld: parent node of node: {self.name} is None, cannot build path') return None - ancestor = node.parent() + ancestor = self.parent() while ancestor is not None: - int_addr = ancestor.name + '.' + int_addr + path = ancestor.name + '.' + path if ancestor.parent is not None and ancestor.tag != 'LDevice': ancestor = ancestor.parent() else: ancestor = None - return int_addr + return path def get_object_reference(self) -> str: """ diff --git a/tests/test_scd_manager.py b/tests/test_scd_manager.py index 814aa09..2bbe95a 100644 --- a/tests/test_scd_manager.py +++ b/tests/test_scd_manager.py @@ -309,7 +309,7 @@ def test_get_DA_leaf_nodes(self): assert len(da_list) == 54166 for da in da_list.values(): assert da.tag == 'DA' or da.tag == 'BDA' - assert ied.get_int_addr(da) is not None + assert da.get_path_from_ld() is not None assert da_list['LD_all.LLN0.OpTmh.blkEna'].name == 'blkEna' self._end_perfo_stats() @@ -586,13 +586,24 @@ def test_get_name_subtree(self): ln = ied.PROCESS_AP.Server.LDASLD.PTRC2 with pytest.raises(AssertionError): ied.get_name_subtree() - assert(ied.PROCESS_AP.Server.LDASLD.get_name_subtree() == ('blkEna', [])) + assert(ied.PROCESS_AP.Server.LDASLD.get_name_subtree() == ('LDASLD', [('LLN0', [('Beh', [('blkEna', []), ('d', []), ('q', []), ('stVal', []), ('subEna', []), ('subID', []), ('subQ', []), ('subVal', []), ('t', [])]), ('Health', [('blkEna', []), ('d', []), ('q', []), ('stVal', []), ('subEna', []), ('subID', []), ('subQ', []), ('subVal', []), ('t', [])]), ('InRef1', [('d', []), ('intAddr', []), ('purpose', []), ('setSrcCB', []), ('setSrcRef', []), ('setTstCB', []), ('setTstRef', []), ('tstEna', [])]), ('InRef2', [('d', []), ('intAddr', []), ('purpose', []), ('setSrcCB', []), ('setSrcRef', []), ('setTstCB', []), ('setTstRef', []), ('tstEna', [])]), ('InRef3', [('d', []), ('intAddr', []), ('purpose', []), ('setSrcCB', []), ('setSrcRef', []), ('setTstCB', []), ('setTstRef', []), ('tstEna', [])]), ('InRef4', [('d', []), ('intAddr', []), ('purpose', []), ('setSrcCB', []), ('setSrcRef', []), ('setTstCB', []), ('setTstRef', []), ('tstEna', [])]), ('InRef5', [('d', []), ('intAddr', []), ('purpose', []), ('setSrcCB', []), ('setSrcRef', []), ('setTstCB', []), ('setTstRef', []), ('tstEna', [])]), ('InRef6', [('d', []), ('intAddr', []), ('purpose', []), ('setSrcCB', []), ('setSrcRef', []), ('setTstCB', []), ('setTstRef', []), ('tstEna', [])]), ('InRef7', [('d', []), ('intAddr', []), ('purpose', []), ('setSrcCB', []), ('setSrcRef', []), ('setTstCB', []), ('setTstRef', []), ('tstEna', [])]), ('Mod', [('ctlModel', []), ('blkEna', []), ('d', []), ('q', []), ('stVal', []), ('subEna', []), ('subID', []), ('subQ', []), ('subVal', []), ('t', [])]), ('NamPlt', [('ldNs', []), ('d', []), ('paramRev', []), ('valRev', []), ('vendor', [])])]), ('RBRF2', [('Beh', [('blkEna', []), ('d', []), ('q', []), ('stVal', []), ('subEna', []), ('subID', []), ('subQ', []), ('subVal', []), ('t', [])]), ('OpEx', [('d', []), ('general', []), ('neut', []), ('phsA', []), ('phsB', []), ('phsC', []), ('q', []), ('t', []), ('originSrc', [('orCat', []), ('orIdent', [])])])]), ('RBRF1', [('Beh', [('blkEna', []), ('d', []), ('q', []), ('stVal', []), ('subEna', []), ('subID', []), ('subQ', []), ('subVal', []), ('t', [])]), ('OpEx', [('d', []), ('general', []), ('neut', []), ('phsA', []), ('phsB', []), ('phsC', []), ('q', []), ('t', []), ('originSrc', [('orCat', []), ('orIdent', [])])])]), ('PTRC2', [('Beh', [('blkEna', []), ('d', []), ('q', []), ('stVal', []), ('subEna', []), ('subID', []), ('subQ', []), ('subVal', []), ('t', [])]), ('Tr', [('d', []), ('general', []), ('neut', []), ('phsA', []), ('phsB', []), ('phsC', []), ('q', []), ('t', []), ('originSrc', [('orCat', []), ('orIdent', [])])])]), ('PTRC1', [('Beh', [('blkEna', []), ('d', []), ('q', []), ('stVal', []), ('subEna', []), ('subID', []), ('subQ', []), ('subVal', []), ('t', [])]), ('Tr', [('d', []), ('general', []), ('neut', []), ('phsA', []), ('phsB', []), ('phsC', []), ('q', []), ('t', []), ('originSrc', [('orCat', []), ('orIdent', [])])])]), ('RBRF3', [('Beh', [('blkEna', []), ('d', []), ('q', []), ('stVal', []), ('subEna', []), ('subID', []), ('subQ', []), ('subVal', []), ('t', [])]), ('OpEx', [('d', []), ('general', []), ('neut', []), ('phsA', []), ('phsB', []), ('phsC', []), ('q', []), ('t', []), ('originSrc', [('orCat', []), ('orIdent', [])])])]), ('LPHD0', [('NamPlt', [('configRev', []), ('d', []), ('paramRev', []), ('swRev', []), ('valRev', []), ('vendor', [])]), ('PhyHealth', [('blkEna', []), ('d', []), ('q', []), ('stVal', []), ('subEna', []), ('subID', []), ('subQ', []), ('subVal', []), ('t', [])]), ('PhyNam', [('d', []), ('hwRev', []), ('location', []), ('mRID', []), ('model', []), ('serNum', []), ('swRev', []), ('vendor', [])]), ('Proxy', [('blkEna', []), ('d', []), ('q', []), ('stVal', []), ('subEna', []), ('subID', []), ('subQ', []), ('subVal', []), ('t', [])])]), ('PTRC3', [('Beh', [('blkEna', []), ('d', []), ('q', []), ('stVal', []), ('subEna', []), ('subID', []), ('subQ', []), ('subVal', []), ('t', [])]), ('Tr', [('d', []), ('general', []), ('neut', []), ('phsA', []), ('phsB', []), ('phsC', []), ('q', []), ('t', []), ('originSrc', [('orCat', []), ('orIdent', [])])])])])) assert(ln.get_name_subtree() == ('PTRC2', [('Beh', [('blkEna', []), ('d', []), ('q', []), ('stVal', []), ('subEna', []), ('subID', []), ('subQ', []), ('subVal', []), ('t', [])]), ('Tr', [('d', []), ('general', []), ('neut', []), ('phsA', []), ('phsB', []), ('phsC', []), ('q', []), ('t', []), ('originSrc', [('orCat', []), ('orIdent', [])])])])) assert(ln.Tr.d.get_name_subtree() == ('d', []) ) assert(ln.Tr.get_name_subtree() == ('Tr', [('d', []), ('general', []), ('neut', []), ('phsA', []), ('phsB', []), ('phsC', []), ('q', []), ('t', []), ('originSrc', [('orCat', []), ('orIdent', [])])])) assert(ln.Tr.originSrc.get_name_subtree() == ('originSrc', [('orCat', []), ('orIdent', [])])) assert(ln.get_name_subtree("ST") == ('PTRC2', [('Beh', [('q', []), ('stVal', []), ('t', [])]), ('Tr', [('general', []), ('neut', []), ('phsA', []), ('phsB', []), ('phsC', []), ('q', []), ('t', []), ('originSrc', [('orCat', []), ('orIdent', [])])])])) + def test_get_path_from_ld(self): + ied = self.SCD_HANDLER.get_IED_by_name('AUT1A_SITE_1') + ln = ied.PROCESS_AP.Server.LDASLD.PTRC2 + with pytest.raises(AssertionError): + ied.get_path_from_ld() + assert ied.PROCESS_AP.Server.LDASLD.get_path_from_ld() == "LDASLD" + assert ln.get_path_from_ld() == 'LDASLD.PTRC2' + assert ln.Tr.d.get_path_from_ld() =='LDASLD.PTRC2.Tr.d' + assert ln.Tr.get_path_from_ld() == 'LDASLD.PTRC2.Tr' + assert ln.Tr.originSrc.orCat.get_path_from_ld() == 'LDASLD.PTRC2.Tr.originSrc.orCat' + def test_get_object_reference(self): ied = self.SCD_HANDLER.get_IED_by_name('AUT1A_SITE_1') ln = ied.PROCESS_AP.Server.LDASLD.PTRC2 From f0a472546badcd6b852ffdd1b3cdc8256852e5d0 Mon Sep 17 00:00:00 2001 From: Thibaut Vermeulen Date: Wed, 19 Apr 2023 19:11:30 +0200 Subject: [PATCH 2/7] fix: get_IED_by_type was returning different object every time --- src/scl_loader/scl_loader.py | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/src/scl_loader/scl_loader.py b/src/scl_loader/scl_loader.py index b7f575b..e0d3916 100644 --- a/src/scl_loader/scl_loader.py +++ b/src/scl_loader/scl_loader.py @@ -1529,16 +1529,12 @@ def get_IED_by_type(self, ied_type: str) -> list: The loaded IED object """ - for ied1 in self._IEDs: - if hasattr(self._IEDs, 'type') and ied1['type'] == ied_type: - return ied1 - ied_elems = self._get_IED_elems_by_types([ied_type]) result = [] - for ied2 in ied_elems: - ied_name = ied2.get('name') - if not hasattr(self._IEDs, ied_name): - self._IEDs[ied_name] = IED(self.datatypes, ied2, self._fullattrs) + for ied_elem in ied_elems: + ied_name = ied_elem.get('name') + if ied_name not in self._IEDs: + self._IEDs[ied_name] = IED(self.datatypes, ied_elem, self._fullattrs) result.append(self._IEDs[ied_name]) From 05bc9f8233f32dd926def0fc44283e12ca68276d Mon Sep 17 00:00:00 2001 From: Thibaut Vermeulen Date: Thu, 27 Apr 2023 16:38:59 +0200 Subject: [PATCH 3/7] improve get_path_from_ld with recursive and by keeping value in object --- src/scl_loader/scl_loader.py | 21 +++++++++------------ 1 file changed, 9 insertions(+), 12 deletions(-) diff --git a/src/scl_loader/scl_loader.py b/src/scl_loader/scl_loader.py index e0d3916..20fe109 100644 --- a/src/scl_loader/scl_loader.py +++ b/src/scl_loader/scl_loader.py @@ -329,6 +329,7 @@ def __init__(self, datatypes: DataTypeTemplates, node_elem: etree.Element = None if len(self.name) == 0: raise AttributeError('Name cannot be set') + self._path_from_ld = None def add_subnode_by_elem(self, elem: etree.Element): """ @@ -633,20 +634,16 @@ def get_path_from_ld(self) -> str: `str` Return the path of the node from LD (format LD.LN.DO.DA) """ + if self._path_from_ld is not None: + return self._path_from_ld assert isinstance(self, (LD, LN, DO, DA)), "Invalid SCDNode level, expect LD, LN, DO or DA" path = self.name - if isinstance(self, LD): - return path - if self.parent is None: - logging.debug(f'SCDNode::get_path_from_ld: parent node of node: {self.name} is None, cannot build path') - return None - ancestor = self.parent() - while ancestor is not None: - path = ancestor.name + '.' + path - if ancestor.parent is not None and ancestor.tag != 'LDevice': - ancestor = ancestor.parent() - else: - ancestor = None + if not isinstance(self, LD): + if self.parent is None: + logging.debug(f'SCDNode::get_path_from_ld: parent node of node: {self.name} is None, cannot build path') + return None + path = self.parent().get_path_from_ld() + '.' + path + self._path_from_ld = path return path def get_object_reference(self) -> str: From 8d5ec5d110eaac37233c41e5dc67646e6d2fe400 Mon Sep 17 00:00:00 2001 From: Thibaut Vermeulen Date: Thu, 4 May 2023 17:25:16 +0200 Subject: [PATCH 4/7] feat: add specific AttributeException when SCDNode is missing --- src/scl_loader/scl_loader.py | 35 ++++++++++++++++++++++++++++++++++- 1 file changed, 34 insertions(+), 1 deletion(-) diff --git a/src/scl_loader/scl_loader.py b/src/scl_loader/scl_loader.py index 20fe109..8228938 100644 --- a/src/scl_loader/scl_loader.py +++ b/src/scl_loader/scl_loader.py @@ -123,6 +123,10 @@ class ServiceType(str, Enum): SMV = 'SMV' +class SCLLoaderError(AttributeError): + pass + + def _safe_convert_value(value: str) -> any: """ Convert a string value in typed value une valeur string en valeur typée. @@ -259,7 +263,6 @@ def get_Data_Type_Definitions(self) -> dict: return tags - class SCDNode: """ Basic class to compute SCD nodes @@ -823,10 +826,18 @@ def _manage_SDI(self, inst_node: etree.Element, current_node: bool = None): upd_node._set_instances(inst_node) + + class DA(SCDNode): """ Class to manage a DA / SDA / DAI """ + def __getattr__(self, item): + raise SCLLoaderError("'{}' {} has no attribute '{}'" + .format(type(self).__name__, + self.get_parent_with_class(IED).name + '.' + self.get_path_from_ld(), + item)) + def __init__(self, datatypes: DataTypeTemplates, node_elem: etree.Element = None, fullattrs: bool = False, **kwargs: dict): """ Constructor @@ -891,6 +902,12 @@ class DO(SCDNode): """ Class to manage a DO / SDO / DOI """ + def __getattr__(self, item): + raise SCLLoaderError("'{}' {} has no attribute '{}'" + .format(type(self).__name__, + self.get_parent_with_class(IED).name + '.' + self.get_path_from_ld(), + item)) + def __init__(self, datatypes: DataTypeTemplates, node_elem: etree.Element = None, fullattrs: bool = False, **kwargs: dict): """ Constructor @@ -940,6 +957,13 @@ class LN(SCDNode): """ Class to manage a LN """ + + def __getattr__(self, item): + raise SCLLoaderError("'{}' {} has no attribute '{}'" + .format(type(self).__name__, + self.get_parent_with_class(IED).name + '.' + self.get_path_from_ld(), + item)) + def __init__(self, datatypes: DataTypeTemplates, node_elem: etree.Element = None, fullattrs: bool = False, **kwargs: dict): """ Constructor @@ -1133,6 +1157,12 @@ class LD(SCDNode): """ Class to manage a LD """ + def __getattr__(self, item): + raise SCLLoaderError("'{}' {} has no attribute '{}'" + .format(type(self).__name__, + self.get_parent_with_class(IED).name + '.' + self.get_path_from_ld(), + item)) + def __init__(self, datatypes: DataTypeTemplates, node_elem: etree.Element = None, fullattrs: bool = False, **kwargs: dict): """ Constructor @@ -1278,6 +1308,9 @@ class IED(SCDNode): """ DEFAULT_AP = 'PROCESS_AP' + def __getattr__(self, item): + raise SCLLoaderError("'{}' {} has no attribute '{}'".format(type(self).__name__, self.name, item)) + def __init__(self, datatypes: DataTypeTemplates, node_elem: etree.Element = None, fullattrs: bool = False, **kwargs: dict): """ Constructor From 296e2e98af01babcdac131498d98559552c2e3e3 Mon Sep 17 00:00:00 2001 From: Thibaut Vermeulen Date: Tue, 9 May 2023 15:30:12 +0200 Subject: [PATCH 5/7] no mandatory version for required packages --- requirements.txt | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/requirements.txt b/requirements.txt index 8adcc47..52e4bf8 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,3 +1,3 @@ -lxml~=4.9.1 -pytest~=7.2.1 -setuptools~=65.5.1 \ No newline at end of file +lxml +pytest +setuptools From 00d3f39bd39fb6112f683e63b52cc501ee09e9b8 Mon Sep 17 00:00:00 2001 From: Thibaut Vermeulen Date: Mon, 18 Sep 2023 10:28:37 +0200 Subject: [PATCH 6/7] update version --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index e909531..f49f7d6 100644 --- a/setup.py +++ b/setup.py @@ -25,7 +25,7 @@ # Fields marked as "Optional" may be commented out. setup( name='scl_loader', # Required - version='1.11.1', # Required + version='1.11.2', # Required description='Outil de manipulation de SCD', # Required long_description=LONG_DESCRIPTION, # Optional long_description_content_type='text/markdown', # Optional (see note above) From 78c5283534a36d6a82abaed3a936ccc7cd86e007 Mon Sep 17 00:00:00 2001 From: Thibaut Vermeulen Date: Mon, 18 Sep 2023 10:48:54 +0200 Subject: [PATCH 7/7] update code owners --- .github/CODEOWNERS | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 80e4045..d841e47 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -1 +1 @@ -* @sylmoha @vermeulenthi +* @syllamoh @vermeulenthi