From 5078db8b262e4d148827125d62f00f2c3392d1d5 Mon Sep 17 00:00:00 2001 From: atom3 Date: Wed, 28 Apr 2021 00:10:13 +0200 Subject: [PATCH 01/34] fixed typos changed all 'applicabiliy' to 'applicability' --- src/ifcopenshell-python/ifcopenshell/ids.py | 38 +++++++++++++++------ 1 file changed, 28 insertions(+), 10 deletions(-) diff --git a/src/ifcopenshell-python/ifcopenshell/ids.py b/src/ifcopenshell-python/ifcopenshell/ids.py index 542793a664..4473e181f6 100644 --- a/src/ifcopenshell-python/ifcopenshell/ids.py +++ b/src/ifcopenshell-python/ifcopenshell/ids.py @@ -79,6 +79,8 @@ class entity(facet): parameters = ["name"] message = "an entity name '%(name)s'" + # @todo predefinedtype + def __call__(self, inst, logger): logger.debug("Testing %s == %s", inst.is_a(), self.name) # @nb with inheritance @@ -134,7 +136,7 @@ class property(facet): msg = self.message % di else: if pset: - msg = "a set '%(propertyset)s', but no property '%(name)'" % di + msg = "a set '%(propertyset)s', but no property '%(name)s'" % di else: msg = "no set '%(propertyset)s'" % di @@ -152,6 +154,7 @@ class material(facet): material_relations = [rel for rel in inst.HasAssociations if rel.is_a("IfcRelAssociatesMaterial")] names = [] for rel in material_relations: + # @todo not all subtypes of IfcMaterial handled if rel.RelatingMaterial.is_a() == "IfcMaterialLayerSetUsage": layers = rel.RelatingMaterial.ForLayerSet.MaterialLayers names = [layer.Material.Name for layer in layers] @@ -214,9 +217,10 @@ class restriction: self.type = "length" elif n.nodeType == n.ELEMENT_NODE and n.tagName.endswith("pattern"): self.options.append(n.getAttribute("value")) - self.type = "pattern" - - # "Given an instance with %(applicabiliy)s\nWe expect %(requirements)s" % self.__dict__ + self.type = "pattern" + + # "Given an instance with %(applicability)s\nWe expect %(requirements)s" % self.__dict__ + def __eq__(self, other): return other in self.options @@ -250,10 +254,10 @@ class specification: phrases[0].tagName == "applicability" or error("expected ") phrases[1].tagName == "requirements" or error("expected ") - self.applicabiliy, self.requirements = (boolean_and(parse_rules(phrase)) for phrase in phrases) + self.applicability, self.requirements = (boolean_and(parse_rules(phrase)) for phrase in phrases) def __call__(self, inst, logger): - if self.applicabiliy(inst, logger): + if self.applicability(inst, logger): valid = self.requirements(inst, logger) if valid: @@ -262,7 +266,7 @@ class specification: logger.error({'guid':inst.GlobalId, 'result':valid.success, 'sentence':str(self) + "\n%s has" % inst + " " + str(valid) + " so is not compliant"}) def __str__(self): - return "Given an instance with %(applicabiliy)s\nWe expect %(requirements)s" % self.__dict__ + return "Given an instance with %(applicability)s\nWe expect %(requirements)s" % self.__dict__ class ids: @@ -292,9 +296,23 @@ if __name__ == "__main__": filename = os.path.join(os.getcwd(), "ids.txt") logger = logging.getLogger("IDS") - logging.basicConfig(filename=filename, level=logging.INFO, format="%(message)s") + #TEMP logging.basicConfig(filename=filename, level=logging.INFO, format="%(message)s") + logging.basicConfig(filename=filename, level=logging.INFO, format="%(levelname)s - %(message)s") + #TEMP logging.basicConfig(level=logging.INFO, format="%(message)s") + logging.basicConfig(level=logging.DEBUG, format="%(message)s") logging.FileHandler(filename, mode='w') - ids_file = ids(sys.argv[1]) - ifc_file = ifcopenshell.open(sys.argv[2]) + # ids_file = ids(sys.argv[1]) + ids_file = ids(r"C:\Users\artom\Desktop\Code\IFC sandbox\IDS, MVDxml samples\IDS_test_1.xml") + # ifc_file = ifcopenshell.open(sys.argv[2]) + ifc_file = ifcopenshell.open(r"C:\Users\artom\Desktop\Code\IFC sandbox\IFC samples\IFC Artur.ifc") + # ifc_file = ifcopenshell.open(r"C:\Users\artom\Desktop\Code\IFC sandbox\IFC samples\IFC Schependomlaan.ifc") + + #applicability = ids_file.specifications[0].applicability + requirements = ids_file.specifications[0].requirements + # print(len(applicability)) + # print(requirements) + products = ifc_file.by_type('IfcProduct') + print(f"IFC file contains {len(products)} elements") + ids_file.validate(ifc_file, logger) From ca34c9505738e061e4c0403fab9554cb1d24c94e Mon Sep 17 00:00:00 2001 From: atom3 Date: Wed, 28 Apr 2021 00:25:08 +0200 Subject: [PATCH 02/34] added boolean restriction --- src/ifcopenshell-python/ifcopenshell/ids.py | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/src/ifcopenshell-python/ifcopenshell/ids.py b/src/ifcopenshell-python/ifcopenshell/ids.py index 4473e181f6..0f70a4e2ae 100644 --- a/src/ifcopenshell-python/ifcopenshell/ids.py +++ b/src/ifcopenshell-python/ifcopenshell/ids.py @@ -217,7 +217,10 @@ class restriction: self.type = "length" elif n.nodeType == n.ELEMENT_NODE and n.tagName.endswith("pattern"): self.options.append(n.getAttribute("value")) - self.type = "pattern" + self.type = "pattern" + elif n.nodeType == n.ELEMENT_NODE and n.tagName.endswith("boolean"): + self.options.append(n.getAttribute("value").capitalize()) + self.type = "boolean" # "Given an instance with %(applicability)s\nWe expect %(requirements)s" % self.__dict__ @@ -234,7 +237,8 @@ class restriction: return "of type %s with a length of %s" % (self.restriction_on, self.options[0]) elif self.type == "pattern": return "of type %s respecting pattern %s" % (self.restriction_on, self.options[0]) - + elif self.type == "boolean": + return "of type %s set to %s" % (self.restriction_on, self.options[0]) class specification: """ @@ -303,7 +307,7 @@ if __name__ == "__main__": logging.FileHandler(filename, mode='w') # ids_file = ids(sys.argv[1]) - ids_file = ids(r"C:\Users\artom\Desktop\Code\IFC sandbox\IDS, MVDxml samples\IDS_test_1.xml") + ids_file = ids(r"C:\Users\artom\Desktop\Code\IFC sandbox\IDS, MVDxml samples\IDS_test_2.xml") # ifc_file = ifcopenshell.open(sys.argv[2]) ifc_file = ifcopenshell.open(r"C:\Users\artom\Desktop\Code\IFC sandbox\IFC samples\IFC Artur.ifc") # ifc_file = ifcopenshell.open(r"C:\Users\artom\Desktop\Code\IFC sandbox\IFC samples\IFC Schependomlaan.ifc") From 90ece8a0588fc13e16957fb38f367667e7375f38 Mon Sep 17 00:00:00 2001 From: atom3 Date: Thu, 29 Apr 2021 20:46:55 +0200 Subject: [PATCH 03/34] Add PredefinedType support --- src/ifcopenshell-python/ifcopenshell/ids.py | 78 +++++++++------------ 1 file changed, 33 insertions(+), 45 deletions(-) diff --git a/src/ifcopenshell-python/ifcopenshell/ids.py b/src/ifcopenshell-python/ifcopenshell/ids.py index 0f70a4e2ae..3334b0816d 100644 --- a/src/ifcopenshell-python/ifcopenshell/ids.py +++ b/src/ifcopenshell-python/ifcopenshell/ids.py @@ -52,12 +52,16 @@ class facet(metaclass=meta_facet): self.node = node def __getattr__(self, k): - v = self.node.getElementsByTagName(k)[0] - elems = [n for n in v.childNodes if n.nodeType == n.ELEMENT_NODE] - if elems: - return restriction(elems[0]) - else: - return v.firstChild.nodeValue.strip() + try: + v = self.node.getElementsByTagName(k)[0] + elems = [n for n in v.childNodes if n.nodeType == n.ELEMENT_NODE] + + if elems: + return restriction(elems[0]) + else: + return v.firstChild.nodeValue.strip() + except IndexError: + return None def __iter__(self): for k in self.parameters: @@ -75,18 +79,18 @@ class entity(facet): """ The IDS entity facet currently *with* inheritance """ - - parameters = ["name"] - message = "an entity name '%(name)s'" - - # @todo predefinedtype - + parameters = ["name", "predefinedtype"] + def __call__(self, inst, logger): - logger.debug("Testing %s == %s", inst.is_a(), self.name) # @nb with inheritance - # return inst.is_a() == self.name - return facet_evaluation(inst.is_a(self.name), self.message % {"name": inst.is_a()}) - + if self.predefinedtype: + # logger.debug("Testing if entity predefinedtype '%s' == '%s'", inst.PredefinedType, self.predefinedtype) + self.message = "an entity name '%(name)s' of predefined type '%(predefinedtype)s'" + return facet_evaluation(inst.is_a(self.name) and inst.PredefinedType == self.predefinedtype, self.message % {"name": inst.is_a(), "predefinedtype": inst.PredefinedType}) + else: + self.message = "an entity name '%(name)s'" + return facet_evaluation(inst.is_a(self.name), self.message % {"name": inst.is_a()}) + class classification(facet): """ @@ -124,7 +128,7 @@ class property(facet): props = ifcopenshell.util.element.get_psets(inst) pset = props.get(self.propertyset) val = pset.get(self.name) if pset else None - logger.debug("Testing %s == %s", val, self.value) + logger.debug("Testing if property %s == %s", val, self.value) di = { "name": self.name, @@ -161,7 +165,6 @@ class material(facet): elif rel.RelatingMaterial.is_a() == "IfcMaterial": names.append(rel.RelatingMaterial.Name) - return facet_evaluation( 0, # @todo @@ -217,10 +220,7 @@ class restriction: self.type = "length" elif n.nodeType == n.ELEMENT_NODE and n.tagName.endswith("pattern"): self.options.append(n.getAttribute("value")) - self.type = "pattern" - elif n.nodeType == n.ELEMENT_NODE and n.tagName.endswith("boolean"): - self.options.append(n.getAttribute("value").capitalize()) - self.type = "boolean" + self.type = "pattern" # "Given an instance with %(applicability)s\nWe expect %(requirements)s" % self.__dict__ @@ -237,8 +237,7 @@ class restriction: return "of type %s with a length of %s" % (self.restriction_on, self.options[0]) elif self.type == "pattern": return "of type %s respecting pattern %s" % (self.restriction_on, self.options[0]) - elif self.type == "boolean": - return "of type %s set to %s" % (self.restriction_on, self.options[0]) + class specification: """ @@ -253,7 +252,7 @@ class specification: return [cls(n) for cls, n in zip(classes, children)] phrases = [n for n in node.childNodes if n.nodeType == n.ELEMENT_NODE] - + len(phrases) == 2 or error("expected two child nodes for ") phrases[0].tagName == "applicability" or error("expected ") phrases[1].tagName == "requirements" or error("expected ") @@ -265,9 +264,9 @@ class specification: valid = self.requirements(inst, logger) if valid: - logger.info({'guid':inst.GlobalId, 'result':valid.success,'sentence':str(self) + "\n%s has" % inst + " " + str(valid) + " so is compliant"}) + logger.info({'guid':inst.GlobalId, 'result':valid.success,'sentence':str(self) + "\n'" + inst.Name + "' (id:" + inst.GlobalId + ") has " + str(valid) + " so is compliant"}) else: - logger.error({'guid':inst.GlobalId, 'result':valid.success, 'sentence':str(self) + "\n%s has" % inst + " " + str(valid) + " so is not compliant"}) + logger.error({'guid':inst.GlobalId, 'result':valid.success, 'sentence':str(self) + "\n'" + inst.Name + "' (id:" + inst.GlobalId + ") has " + str(valid) + " so is not compliant"}) def __str__(self): return "Given an instance with %(applicability)s\nWe expect %(requirements)s" % self.__dict__ @@ -290,7 +289,7 @@ class ids: def validate(self, ifc_file, logger): for spec in self.specifications: for elem in ifc_file.by_type("IfcObject"): - spec(elem, logger) + spec(elem, logger) if __name__ == "__main__": import sys, os @@ -300,23 +299,12 @@ if __name__ == "__main__": filename = os.path.join(os.getcwd(), "ids.txt") logger = logging.getLogger("IDS") - #TEMP logging.basicConfig(filename=filename, level=logging.INFO, format="%(message)s") - logging.basicConfig(filename=filename, level=logging.INFO, format="%(levelname)s - %(message)s") - #TEMP logging.basicConfig(level=logging.INFO, format="%(message)s") - logging.basicConfig(level=logging.DEBUG, format="%(message)s") + logging.basicConfig(filename=filename, level=logging.INFO, format="%(message)s") logging.FileHandler(filename, mode='w') - # ids_file = ids(sys.argv[1]) - ids_file = ids(r"C:\Users\artom\Desktop\Code\IFC sandbox\IDS, MVDxml samples\IDS_test_2.xml") - # ifc_file = ifcopenshell.open(sys.argv[2]) - ifc_file = ifcopenshell.open(r"C:\Users\artom\Desktop\Code\IFC sandbox\IFC samples\IFC Artur.ifc") - # ifc_file = ifcopenshell.open(r"C:\Users\artom\Desktop\Code\IFC sandbox\IFC samples\IFC Schependomlaan.ifc") - - #applicability = ids_file.specifications[0].applicability - requirements = ids_file.specifications[0].requirements - # print(len(applicability)) - # print(requirements) - products = ifc_file.by_type('IfcProduct') - print(f"IFC file contains {len(products)} elements") - + ids_file = ids(sys.argv[1]) + ifc_file = ifcopenshell.open(sys.argv[2]) + ids_file.validate(ifc_file, logger) + + print(f"Validated {len(ids_file.specifications[0].requirements.terms)} IDS requirements on {len(ifc_file.by_type('IfcProduct'))} IFC elements. Results saved to {filename}") From 29fa5bdffaadcda8a71cf76e6a57138aaab6d1a8 Mon Sep 17 00:00:00 2001 From: atom3 Date: Thu, 29 Apr 2021 21:33:07 +0200 Subject: [PATCH 04/34] replace f-string with % --- src/ifcopenshell-python/ifcopenshell/ids.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/ifcopenshell-python/ifcopenshell/ids.py b/src/ifcopenshell-python/ifcopenshell/ids.py index 3334b0816d..6b3d1cb08f 100644 --- a/src/ifcopenshell-python/ifcopenshell/ids.py +++ b/src/ifcopenshell-python/ifcopenshell/ids.py @@ -304,7 +304,7 @@ if __name__ == "__main__": ids_file = ids(sys.argv[1]) ifc_file = ifcopenshell.open(sys.argv[2]) - + ids_file.validate(ifc_file, logger) - print(f"Validated {len(ids_file.specifications[0].requirements.terms)} IDS requirements on {len(ifc_file.by_type('IfcProduct'))} IFC elements. Results saved to {filename}") + print("Validated %s IDS requirements on %s IFC elements. Results saved to %s" % (len(ids_file.specifications[0].requirements.terms), len(ifc_file.by_type('IfcProduct')), filename)) \ No newline at end of file From 2e0d5fa707392441d340de5f02cb9e10d5903418 Mon Sep 17 00:00:00 2001 From: atom3 Date: Thu, 29 Apr 2021 23:41:22 +0200 Subject: [PATCH 05/34] skip PredefinedType if not IFC4 --- src/ifcopenshell-python/ifcopenshell/ids.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/ifcopenshell-python/ifcopenshell/ids.py b/src/ifcopenshell-python/ifcopenshell/ids.py index 2185151236..eaa4537808 100644 --- a/src/ifcopenshell-python/ifcopenshell/ids.py +++ b/src/ifcopenshell-python/ifcopenshell/ids.py @@ -84,7 +84,7 @@ class entity(facet): def __call__(self, inst, logger): # @nb with inheritance - if self.predefinedtype: + if self.predefinedtype and "4" in ifc_file.schema: # logger.debug("Testing if entity predefinedtype '%s' == '%s'", inst.PredefinedType, self.predefinedtype) self.message = "an entity name '%(name)s' of predefined type '%(predefinedtype)s'" return facet_evaluation(inst.is_a(self.name) and inst.PredefinedType == self.predefinedtype, self.message % {"name": inst.is_a(), "predefinedtype": inst.PredefinedType}) From a65a34cfe7de9de5dae591018b57571de4ce03e1 Mon Sep 17 00:00:00 2001 From: atom3 Date: Fri, 30 Apr 2021 00:09:04 +0200 Subject: [PATCH 06/34] code formatting --- src/ifcopenshell-python/ifcopenshell/ids.py | 13 +++++-------- 1 file changed, 5 insertions(+), 8 deletions(-) diff --git a/src/ifcopenshell-python/ifcopenshell/ids.py b/src/ifcopenshell-python/ifcopenshell/ids.py index eaa4537808..d3340cf720 100644 --- a/src/ifcopenshell-python/ifcopenshell/ids.py +++ b/src/ifcopenshell-python/ifcopenshell/ids.py @@ -55,7 +55,6 @@ class facet(metaclass=meta_facet): try: v = self.node.getElementsByTagName(k)[0] elems = [n for n in v.childNodes if n.nodeType == n.ELEMENT_NODE] - if elems: return restriction(elems[0]) else: @@ -91,7 +90,7 @@ class entity(facet): else: self.message = "an entity name '%(name)s'" return facet_evaluation(inst.is_a(self.name), self.message % {"name": inst.is_a()}) - + class classification(facet): """ @@ -221,10 +220,8 @@ class restriction: self.type = "length" elif n.nodeType == n.ELEMENT_NODE and n.tagName.endswith("pattern"): self.options.append(n.getAttribute("value")) - self.type = "pattern" + self.type = "pattern" - # "Given an instance with %(applicability)s\nWe expect %(requirements)s" % self.__dict__ - def __eq__(self, other): return other in self.options @@ -253,7 +250,7 @@ class specification: return [cls(n) for cls, n in zip(classes, children)] phrases = [n for n in node.childNodes if n.nodeType == n.ELEMENT_NODE] - + len(phrases) == 2 or error("expected two child nodes for ") phrases[0].tagName == "applicability" or error("expected ") phrases[1].tagName == "requirements" or error("expected ") @@ -290,8 +287,8 @@ class ids: def validate(self, ifc_file, logger): for spec in self.specifications: for elem in ifc_file.by_type("IfcObject"): - spec(elem, logger) - + spec(elem, logger) + if __name__ == "__main__": import sys, os import logging From 3b806a5466a23c05ea8a5480fb591ac655b4d91c Mon Sep 17 00:00:00 2001 From: atom3 Date: Fri, 30 Apr 2021 08:41:33 +0200 Subject: [PATCH 07/34] check PredefinedType if exists not based on schema --- src/ifcopenshell-python/ifcopenshell/ids.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/ifcopenshell-python/ifcopenshell/ids.py b/src/ifcopenshell-python/ifcopenshell/ids.py index d3340cf720..a2c407c8f9 100644 --- a/src/ifcopenshell-python/ifcopenshell/ids.py +++ b/src/ifcopenshell-python/ifcopenshell/ids.py @@ -83,7 +83,7 @@ class entity(facet): def __call__(self, inst, logger): # @nb with inheritance - if self.predefinedtype and "4" in ifc_file.schema: + if self.predefinedtype and hasattr(inst, "PredefinedType"): # logger.debug("Testing if entity predefinedtype '%s' == '%s'", inst.PredefinedType, self.predefinedtype) self.message = "an entity name '%(name)s' of predefined type '%(predefinedtype)s'" return facet_evaluation(inst.is_a(self.name) and inst.PredefinedType == self.predefinedtype, self.message % {"name": inst.is_a(), "predefinedtype": inst.PredefinedType}) From 9ca1e011e5e3b3321d0c9901acd4d6890d828369 Mon Sep 17 00:00:00 2001 From: atom3 Date: Fri, 30 Apr 2021 11:06:36 +0200 Subject: [PATCH 08/34] narrow try-except --- src/ifcopenshell-python/ifcopenshell/ids.py | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/src/ifcopenshell-python/ifcopenshell/ids.py b/src/ifcopenshell-python/ifcopenshell/ids.py index a2c407c8f9..e94fb6d614 100644 --- a/src/ifcopenshell-python/ifcopenshell/ids.py +++ b/src/ifcopenshell-python/ifcopenshell/ids.py @@ -54,12 +54,15 @@ class facet(metaclass=meta_facet): def __getattr__(self, k): try: v = self.node.getElementsByTagName(k)[0] + except IndexError: + v = None + if v: elems = [n for n in v.childNodes if n.nodeType == n.ELEMENT_NODE] if elems: return restriction(elems[0]) else: return v.firstChild.nodeValue.strip() - except IndexError: + else: return None def __iter__(self): @@ -300,8 +303,10 @@ if __name__ == "__main__": logging.basicConfig(filename=filename, level=logging.INFO, format="%(message)s") logging.FileHandler(filename, mode='w') - ids_file = ids(sys.argv[1]) - ifc_file = ifcopenshell.open(sys.argv[2]) + # ids_file = ids(sys.argv[1]) + # ifc_file = ifcopenshell.open(sys.argv[2]) + ids_file = ids(r"C:\Users\artom\Desktop\Code\IFC sandbox\IDS, MVDxml samples\IDS_test_3.xml") + ifc_file = ifcopenshell.open(r"C:\Users\artom\Desktop\Code\IFC sandbox\IFC samples\IFC Artur.ifc") ids_file.validate(ifc_file, logger) From 5419d72a6a9e052bf27f7f6643218f8b6fbaaf9f Mon Sep 17 00:00:00 2001 From: atom3 Date: Fri, 30 Apr 2021 11:31:23 +0200 Subject: [PATCH 09/34] remove debug data --- src/ifcopenshell-python/ifcopenshell/ids.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/src/ifcopenshell-python/ifcopenshell/ids.py b/src/ifcopenshell-python/ifcopenshell/ids.py index e94fb6d614..1654b9179b 100644 --- a/src/ifcopenshell-python/ifcopenshell/ids.py +++ b/src/ifcopenshell-python/ifcopenshell/ids.py @@ -303,10 +303,8 @@ if __name__ == "__main__": logging.basicConfig(filename=filename, level=logging.INFO, format="%(message)s") logging.FileHandler(filename, mode='w') - # ids_file = ids(sys.argv[1]) - # ifc_file = ifcopenshell.open(sys.argv[2]) - ids_file = ids(r"C:\Users\artom\Desktop\Code\IFC sandbox\IDS, MVDxml samples\IDS_test_3.xml") - ifc_file = ifcopenshell.open(r"C:\Users\artom\Desktop\Code\IFC sandbox\IFC samples\IFC Artur.ifc") + ids_file = ids(sys.argv[1]) + ifc_file = ifcopenshell.open(sys.argv[2]) ids_file.validate(ifc_file, logger) From 4017097f1981e36a4bfff89c8068d455d74cf55d Mon Sep 17 00:00:00 2001 From: ArturTomczak Date: Tue, 8 Jun 2021 10:34:02 +0200 Subject: [PATCH 10/34] Add xmlschema validation --- src/ifcopenshell-python/ifcopenshell/ids.py | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/src/ifcopenshell-python/ifcopenshell/ids.py b/src/ifcopenshell-python/ifcopenshell/ids.py index 1654b9179b..31a10f4ecc 100644 --- a/src/ifcopenshell-python/ifcopenshell/ids.py +++ b/src/ifcopenshell-python/ifcopenshell/ids.py @@ -2,6 +2,7 @@ import operator import ifcopenshell.util.element from xml.dom.minidom import parse +from xmlschema import XMLSchema class exception(Exception): @@ -279,10 +280,13 @@ class ids: """ def __init__(self, fn): + ids_schema = XMLSchema("http://standards.buildingsmart.org/IDS/ids.xsd") + ids_schema.validate(fn) + dom = parse(fn) ids = dom.childNodes[0] ids.tagName == "ids" or error("expected ") - + self.specifications = [ specification(n) for n in ids.childNodes if n.nodeType == n.ELEMENT_NODE and n.tagName == "specification" ] @@ -293,11 +297,14 @@ class ids: spec(elem, logger) if __name__ == "__main__": + import time + start_time = time.time() import sys, os import logging import ifcopenshell + from datetime import date - filename = os.path.join(os.getcwd(), "ids.txt") + filename = os.path.join(os.getcwd(), str(date.today())+"_ids_result.txt") logger = logging.getLogger("IDS") logging.basicConfig(filename=filename, level=logging.INFO, format="%(message)s") @@ -308,4 +315,4 @@ if __name__ == "__main__": ids_file.validate(ifc_file, logger) - print("Validated %s IDS requirements on %s IFC elements. Results saved to %s" % (len(ids_file.specifications[0].requirements.terms), len(ifc_file.by_type('IfcProduct')), filename)) \ No newline at end of file + print("Validated %s IDS requirements on %s IFC elements in %ss. Results saved to %s" % (len(ids_file.specifications[0].requirements.terms), len(ifc_file.by_type('IfcProduct')), round(time.time() - start_time, 2), filename)) \ No newline at end of file From 8c9ab9869729c76d28fdd51053a6e6f1e9aa1611 Mon Sep 17 00:00:00 2001 From: ArturTomczak Date: Thu, 10 Jun 2021 15:19:23 +0200 Subject: [PATCH 11/34] switch from dom to xmlschema as suggested by @Moult, xmlschema better fits the purpose than xml.dom.minidom --- src/ifcopenshell-python/ifcopenshell/ids.py | 89 +++++++++++---------- 1 file changed, 47 insertions(+), 42 deletions(-) diff --git a/src/ifcopenshell-python/ifcopenshell/ids.py b/src/ifcopenshell-python/ifcopenshell/ids.py index 31a10f4ecc..eedaff4d70 100644 --- a/src/ifcopenshell-python/ifcopenshell/ids.py +++ b/src/ifcopenshell-python/ifcopenshell/ids.py @@ -1,7 +1,6 @@ import operator import ifcopenshell.util.element -from xml.dom.minidom import parse from xmlschema import XMLSchema @@ -47,16 +46,22 @@ class facet(metaclass=meta_facet): The base class for IDS facets. IDS facets are functors constructed from XML nodes that return True or False. A getattr method is provided for conveniently extracting XML child node text content. - """ + """ def __init__(self, node): self.node = node def __getattr__(self, k): - try: - v = self.node.getElementsByTagName(k)[0] - except IndexError: + + if k in self.node: + v = self.node[k] + else: v = None + + return v + + #TODO implement restrictons + """ if v: elems = [n for n in v.childNodes if n.nodeType == n.ELEMENT_NODE] if elems: @@ -65,6 +70,7 @@ class facet(metaclass=meta_facet): return v.firstChild.nodeValue.strip() else: return None + """ def __iter__(self): for k in self.parameters: @@ -90,10 +96,16 @@ class entity(facet): if self.predefinedtype and hasattr(inst, "PredefinedType"): # logger.debug("Testing if entity predefinedtype '%s' == '%s'", inst.PredefinedType, self.predefinedtype) self.message = "an entity name '%(name)s' of predefined type '%(predefinedtype)s'" - return facet_evaluation(inst.is_a(self.name) and inst.PredefinedType == self.predefinedtype, self.message % {"name": inst.is_a(), "predefinedtype": inst.PredefinedType}) + return facet_evaluation( + inst.is_a(self.name) and inst.PredefinedType == self.predefinedtype, + self.message % {"name": inst.is_a(), "predefinedtype": inst.PredefinedType} + ) else: self.message = "an entity name '%(name)s'" - return facet_evaluation(inst.is_a(self.name), self.message % {"name": inst.is_a()}) + return facet_evaluation( + inst.is_a(self.name), + self.message % {"name": inst.is_a()} + ) class classification(facet): @@ -124,8 +136,6 @@ class property(facet): """ parameters = ["name", "propertyset", "value"] - - # import pdb;pdb.set_trace() message = "a property '%(name)s' in '%(propertyset)s' with value '%(value)s'" def __call__(self, inst, logger): @@ -211,20 +221,25 @@ class restriction: self.restriction_on = node.getAttribute("base") self.options = [] self.type = [] - - for n in node.childNodes: - if n.nodeType == n.ELEMENT_NODE and n.tagName.endswith("enumeration"): - self.options.append(n.getAttribute("value")) - self.type = "enumeration" - elif n.nodeType == n.ELEMENT_NODE and (n.tagName.endswith("Inclusive") or n.tagName.endswith("Exclusive")): - self.options.append(n.getAttribute("value")) - self.type = "bounds" - elif n.nodeType == n.ELEMENT_NODE and n.tagName.endswith("length"): - self.options.append(n.getAttribute("value")) - self.type = "length" - elif n.nodeType == n.ELEMENT_NODE and n.tagName.endswith("pattern"): - self.options.append(n.getAttribute("value")) - self.type = "pattern" + + #TODO implement restrictions + # for n in node.childNodes: + # if n.nodeType == n.ELEMENT_NODE and n.tagName.endswith("enumeration"): + # self.options.append(n.getAttribute("value")) + # self.type = "enumeration" + # elif n.nodeType == n.ELEMENT_NODE and (n.tagName.endswith("Inclusive") or n.tagName.endswith("Exclusive")): + # self.options.append(n.getAttribute("value")) + # self.type = "bounds" + # elif n.nodeType == n.ELEMENT_NODE and n.tagName.endswith("length"): + # self.options.append(n.getAttribute("value")) + # self.type = "length" + # elif n.nodeType == n.ELEMENT_NODE and n.tagName.endswith("pattern"): + # self.options.append(n.getAttribute("value")) + # self.type = "pattern" + #TODO add min/maxLength + #TODO add fractionDigits + #TODO add totalDigits + #TODO add whiteSpace def __eq__(self, other): return other in self.options @@ -248,27 +263,22 @@ class specification: def __init__(self, node): def parse_rules(node): - children = [n for n in node.childNodes if n.nodeType == n.ELEMENT_NODE] - names = map(operator.attrgetter("tagName"), children) + children = [node[n][0] for n in node] + names = [n for n in node] classes = map(meta_facet.facets.__getitem__, names) return [cls(n) for cls, n in zip(classes, children)] - phrases = [n for n in node.childNodes if n.nodeType == n.ELEMENT_NODE] - - len(phrases) == 2 or error("expected two child nodes for ") - phrases[0].tagName == "applicability" or error("expected ") - phrases[1].tagName == "requirements" or error("expected ") - - self.applicability, self.requirements = (boolean_and(parse_rules(phrase)) for phrase in phrases) + self.applicability = boolean_and(parse_rules(node['applicability'])) + self.requirements = boolean_and(parse_rules(node['requirements'])) def __call__(self, inst, logger): if self.applicability(inst, logger): valid = self.requirements(inst, logger) if valid: - logger.info({'guid':inst.GlobalId, 'result':valid.success,'sentence':str(self) + "\n'" + inst.Name + "' (id:" + inst.GlobalId + ") has " + str(valid) + " so is compliant"}) + logger.info({'guid':inst.GlobalId, 'result':valid.success,'sentence':str(self) + "\n" + inst.is_a() + " '" + str(inst.Name) + "' (#" + str(inst.id()) + ") has " + str(valid) + " so is compliant"}) else: - logger.error({'guid':inst.GlobalId, 'result':valid.success, 'sentence':str(self) + "\n'" + inst.Name + "' (id:" + inst.GlobalId + ") has " + str(valid) + " so is not compliant"}) + logger.error({'guid':inst.GlobalId, 'result':valid.success, 'sentence':str(self) + "\n" + inst.is_a() + " '" + str(inst.Name) + "' (#" + str(inst.id()) + ") has " + str(valid) + " so is not compliant"}) def __str__(self): return "Given an instance with %(applicability)s\nWe expect %(requirements)s" % self.__dict__ @@ -282,14 +292,9 @@ class ids: def __init__(self, fn): ids_schema = XMLSchema("http://standards.buildingsmart.org/IDS/ids.xsd") ids_schema.validate(fn) - - dom = parse(fn) - ids = dom.childNodes[0] - ids.tagName == "ids" or error("expected ") - - self.specifications = [ - specification(n) for n in ids.childNodes if n.nodeType == n.ELEMENT_NODE and n.tagName == "specification" - ] + + ids = ids_schema.to_dict(fn) + self.specifications = [specification(s) for s in ids['specification']] def validate(self, ifc_file, logger): for spec in self.specifications: From 4e60ca01c312324fd923529c3ca735feba2e7f6f Mon Sep 17 00:00:00 2001 From: ArturTomczak Date: Thu, 10 Jun 2021 15:21:07 +0200 Subject: [PATCH 12/34] improve feedback message added counters and runtime info --- src/ifcopenshell-python/ifcopenshell/ids.py | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/src/ifcopenshell-python/ifcopenshell/ids.py b/src/ifcopenshell-python/ifcopenshell/ids.py index eedaff4d70..ed6cb94a28 100644 --- a/src/ifcopenshell-python/ifcopenshell/ids.py +++ b/src/ifcopenshell-python/ifcopenshell/ids.py @@ -273,9 +273,13 @@ class specification: def __call__(self, inst, logger): if self.applicability(inst, logger): + global ifc_checked + ifc_checked += 1 valid = self.requirements(inst, logger) if valid: + global ifc_passed + ifc_passed += 1 logger.info({'guid':inst.GlobalId, 'result':valid.success,'sentence':str(self) + "\n" + inst.is_a() + " '" + str(inst.Name) + "' (#" + str(inst.id()) + ") has " + str(valid) + " so is compliant"}) else: logger.error({'guid':inst.GlobalId, 'result':valid.success, 'sentence':str(self) + "\n" + inst.is_a() + " '" + str(inst.Name) + "' (#" + str(inst.id()) + ") has " + str(valid) + " so is not compliant"}) @@ -301,6 +305,9 @@ class ids: for elem in ifc_file.by_type("IfcObject"): spec(elem, logger) + +ifc_checked = 0 +ifc_passed = 0 if __name__ == "__main__": import time start_time = time.time() @@ -320,4 +327,6 @@ if __name__ == "__main__": ids_file.validate(ifc_file, logger) - print("Validated %s IDS requirements on %s IFC elements in %ss. Results saved to %s" % (len(ids_file.specifications[0].requirements.terms), len(ifc_file.by_type('IfcProduct')), round(time.time() - start_time, 2), filename)) \ No newline at end of file + print("Out of %s IFC elements, %s were checked against %s IDS requirements and %s of them passed (%s).\nRuntime=%ss. Results saved to %s" + % (len(ifc_file.by_type('IfcProduct')), ifc_checked, len(ids_file.specifications[0].requirements.terms), ifc_passed, str(ifc_passed/ifc_checked*100)+'%', round(time.time() - start_time, 2), filename)) + From 9db01bc6cc0fa474b8b6b50a863621265d711fbf Mon Sep 17 00:00:00 2001 From: ArturTomczak Date: Thu, 10 Jun 2021 15:21:51 +0200 Subject: [PATCH 13/34] implement classification facet --- src/ifcopenshell-python/ifcopenshell/ids.py | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/src/ifcopenshell-python/ifcopenshell/ids.py b/src/ifcopenshell-python/ifcopenshell/ids.py index ed6cb94a28..b787047c16 100644 --- a/src/ifcopenshell-python/ifcopenshell/ids.py +++ b/src/ifcopenshell-python/ifcopenshell/ids.py @@ -121,14 +121,18 @@ class classification(facet): for association in inst.HasAssociations: if association.is_a("IfcRelAssociatesClassification"): cref = association.RelatingClassification - refs.append((cref.ReferencedSource.Name, cref.ItemReference)) - - return facet_evaluation( - (self.system, self.value) in refs, - # @todo - "[classification_eval_todo]", - ) + refs.append((cref.ReferencedSource.Name, cref.Identification)) # before was .ItemReference instead of .Identification + if refs: + return facet_evaluation( + (self.system, self.value) in refs, + self.message % {"system": refs[0][0], "value": refs[0][1]} + ) + else: + return facet_evaluation( + False, + "has no classification" + ) class property(facet): """ From 65fa5768f250ee05ac156667af79e08deb9fcb8b Mon Sep 17 00:00:00 2001 From: ArturTomczak Date: Fri, 11 Jun 2021 17:10:54 +0200 Subject: [PATCH 14/34] implement material facet added actual evaluation and extended current implementation with all possible ifc relations for materials. --- src/ifcopenshell-python/ifcopenshell/ids.py | 56 ++++++++++++++------- 1 file changed, 37 insertions(+), 19 deletions(-) diff --git a/src/ifcopenshell-python/ifcopenshell/ids.py b/src/ifcopenshell-python/ifcopenshell/ids.py index b787047c16..d78b51ee6e 100644 --- a/src/ifcopenshell-python/ifcopenshell/ids.py +++ b/src/ifcopenshell-python/ifcopenshell/ids.py @@ -94,7 +94,6 @@ class entity(facet): def __call__(self, inst, logger): # @nb with inheritance if self.predefinedtype and hasattr(inst, "PredefinedType"): - # logger.debug("Testing if entity predefinedtype '%s' == '%s'", inst.PredefinedType, self.predefinedtype) self.message = "an entity name '%(name)s' of predefined type '%(predefinedtype)s'" return facet_evaluation( inst.is_a(self.name) and inst.PredefinedType == self.predefinedtype, @@ -130,10 +129,11 @@ class classification(facet): ) else: return facet_evaluation( - False, + False, "has no classification" ) + class property(facet): """ The IDS property facet implenented using `ifcopenshell.util.element` @@ -146,7 +146,6 @@ class property(facet): props = ifcopenshell.util.element.get_psets(inst) pset = props.get(self.propertyset) val = pset.get(self.name) if pset else None - logger.debug("Testing if property %s == %s", val, self.value) di = { "name": self.name, @@ -158,35 +157,50 @@ class property(facet): msg = self.message % di else: if pset: - msg = "a set '%(propertyset)s', but no property '%(name)s'" % di + msg = "no property '%(name)s' in a set '%(propertyset)s'" % di else: msg = "no set '%(propertyset)s'" % di - return facet_evaluation(val == self.value, msg) + return facet_evaluation( + str(val) == str(self.value), + msg + ) class material(facet): """ - The IDS material facet + The IDS material facet by traversing the HasAssociations inverse attribute """ - parameters = ["name", "value"] - message = "a material '%(name)s with value '%(value)s'" + parameters = ["value"] + message = "a material '%(value)s'" def __call__(self, inst, logger): material_relations = [rel for rel in inst.HasAssociations if rel.is_a("IfcRelAssociatesMaterial")] - names = [] + materials = [] for rel in material_relations: - # @todo not all subtypes of IfcMaterial handled - if rel.RelatingMaterial.is_a() == "IfcMaterialLayerSetUsage": + #TODO test all subtypes of material definitions + if rel.RelatingMaterial.is_a() == "IfcMaterial": + materials.append(rel.RelatingMaterial.Name) + elif rel.RelatingMaterial.is_a() == "IfcMaterialMaterialList": #DEPRECATED in IFC4 + [materials.append(mat.Name) for mat in rel.RelatingMaterial] + elif rel.RelatingMaterial.is_a() == "IfcMaterialConstituentSet": + [materials.append(mat.Material.Name) for mat in rel.RelatingMaterial.MaterialConstituents] + elif rel.RelatingMaterial.is_a() == "IfcMaterialLayerSet": + [materials.append(mat.Name) for mat in rel.RelatingMaterial.MaterialLayers] + elif rel.RelatingMaterial.is_a() == "IfcMaterialLayerSetUsage": layers = rel.RelatingMaterial.ForLayerSet.MaterialLayers - names = [layer.Material.Name for layer in layers] - elif rel.RelatingMaterial.is_a() == "IfcMaterial": - names.append(rel.RelatingMaterial.Name) - + [materials.append(layer.Material.Name) for layer in layers] + elif rel.RelatingMaterial.is_a() == "IfcMaterialProfileSet": + [materials.append(mat.Material.Name) for mat in rel.RelatingMaterial.MaterialProfiles] + elif rel.RelatingMaterial.is_a() == "IfcMaterialProfileSetUsage": + profileSets = rel.RelatingMaterial.ForProfileSet.MaterialProfiles + [materials.append(pset.Material.Name) for pset in profileSets] + else: + logger.error({'guid':inst.GlobalId, 'result':'ERROR', 'sentence':'IfcRelAssociatesMaterial not implemented'}) + return facet_evaluation( - 0, - # @todo - "[material_eval_todo]", + self.value in materials, + self.message % {"value": self.value}, ) @@ -199,9 +213,13 @@ class boolean_logic: self.terms = terms def __call__(self, *args): + #TODO only takes one requirement of a type, should take all eval = [t(*args) for t in self.terms] join = [" and ", " or "][self.fold == any] - return facet_evaluation(self.fold(eval), join.join(map(str, eval))) + return facet_evaluation( + self.fold(eval), + join.join(map(str, eval)) + ) def __str__(self): return [" and ", " or "][self.fold == any].join(map(str, self.terms)) From 768606571ee81607c7c3a3d39e26be87c40a6be0 Mon Sep 17 00:00:00 2001 From: ArturTomczak Date: Fri, 11 Jun 2021 19:03:03 +0200 Subject: [PATCH 15/34] fix multiple requirements at a time --- src/ifcopenshell-python/ifcopenshell/ids.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/ifcopenshell-python/ifcopenshell/ids.py b/src/ifcopenshell-python/ifcopenshell/ids.py index d78b51ee6e..f504b92e4c 100644 --- a/src/ifcopenshell-python/ifcopenshell/ids.py +++ b/src/ifcopenshell-python/ifcopenshell/ids.py @@ -213,7 +213,6 @@ class boolean_logic: self.terms = terms def __call__(self, *args): - #TODO only takes one requirement of a type, should take all eval = [t(*args) for t in self.terms] join = [" and ", " or "][self.fold == any] return facet_evaluation( @@ -285,8 +284,8 @@ class specification: def __init__(self, node): def parse_rules(node): - children = [node[n][0] for n in node] - names = [n for n in node] + names = [req for req in node for n in node[req]] + children = [child for req in node for child in node[req]] classes = map(meta_facet.facets.__getitem__, names) return [cls(n) for cls, n in zip(classes, children)] From 459149e4180e8c7b60ac9aeab14f2ec6b1432ba0 Mon Sep 17 00:00:00 2001 From: ArturTomczak Date: Sat, 12 Jun 2021 18:08:13 +0200 Subject: [PATCH 16/34] add enumeration restriction --- src/ifcopenshell-python/ifcopenshell/ids.py | 93 +++++++++++---------- 1 file changed, 50 insertions(+), 43 deletions(-) diff --git a/src/ifcopenshell-python/ifcopenshell/ids.py b/src/ifcopenshell-python/ifcopenshell/ids.py index f504b92e4c..81905608d8 100644 --- a/src/ifcopenshell-python/ifcopenshell/ids.py +++ b/src/ifcopenshell-python/ifcopenshell/ids.py @@ -52,25 +52,14 @@ class facet(metaclass=meta_facet): self.node = node def __getattr__(self, k): - if k in self.node: v = self.node[k] - else: - v = None - - return v - - #TODO implement restrictons - """ - if v: - elems = [n for n in v.childNodes if n.nodeType == n.ELEMENT_NODE] - if elems: - return restriction(elems[0]) + if isinstance(v, dict): #is restriction? + return restriction(v['xs:restriction'][0]) else: - return v.firstChild.nodeValue.strip() + return v else: return None - """ def __iter__(self): for k in self.parameters: @@ -140,7 +129,7 @@ class property(facet): """ parameters = ["name", "propertyset", "value"] - message = "a property '%(name)s' in '%(propertyset)s' with value '%(value)s'" + message = "a property '%(name)s' in '%(propertyset)s' with a value '%(value)s'" def __call__(self, inst, logger): props = ifcopenshell.util.element.get_psets(inst) @@ -162,7 +151,7 @@ class property(facet): msg = "no set '%(propertyset)s'" % di return facet_evaluation( - str(val) == str(self.value), + val == self.value, msg ) @@ -239,43 +228,61 @@ class restriction: def __init__(self, node): - self.restriction_on = node.getAttribute("base") + self.restriction_on = node['@base'][3:] + self.type = "" self.options = [] - self.type = [] - - #TODO implement restrictions - # for n in node.childNodes: - # if n.nodeType == n.ELEMENT_NODE and n.tagName.endswith("enumeration"): - # self.options.append(n.getAttribute("value")) - # self.type = "enumeration" - # elif n.nodeType == n.ELEMENT_NODE and (n.tagName.endswith("Inclusive") or n.tagName.endswith("Exclusive")): - # self.options.append(n.getAttribute("value")) - # self.type = "bounds" - # elif n.nodeType == n.ELEMENT_NODE and n.tagName.endswith("length"): - # self.options.append(n.getAttribute("value")) - # self.type = "length" - # elif n.nodeType == n.ELEMENT_NODE and n.tagName.endswith("pattern"): - # self.options.append(n.getAttribute("value")) - # self.type = "pattern" + + for n in node: + if n[0:3] == "xs:": + if n[3:] == "enumeration": + self.type = "enumeration" + [self.options.append(x["@value"]) for x in node[n]] + #TODO implement other restrictions + # elif n.nodeType == n.ELEMENT_NODE and (n.tagName.endswith("Inclusive") or n.tagName.endswith("Exclusive")): + # self.type = "bounds" + # self.options.append(n.getAttribute("value")) + # elif n.nodeType == n.ELEMENT_NODE and n.tagName.endswith("length"): + # self.type = "length" + # self.options.append(n.getAttribute("value")) + # elif n.nodeType == n.ELEMENT_NODE and n.tagName.endswith("pattern"): + # self.type = "pattern" + # self.options.append(n.getAttribute("value")) #TODO add min/maxLength #TODO add fractionDigits #TODO add totalDigits #TODO add whiteSpace + else: + logger.error({'guid':inst.GlobalId, 'result':'ERROR', 'sentence':'Restriction not implemented'}) def __eq__(self, other): - return other in self.options - - def __repr__(self): if self.type == "enumeration": - return " or ".join(self.options) + return other in self.options elif self.type == "bounds": self.options.sort() - return "of type %s, having a value between %s and %s" % (self.restriction_on, self.options[0], self.options[1]) + return False #TODO elif self.type == "length": - return "of type %s with a length of %s" % (self.restriction_on, self.options[0]) + return False #TODO elif self.type == "pattern": - return "of type %s respecting pattern %s" % (self.restriction_on, self.options[0]) - + return False #TODO + #TODO add min/maxLength + #TODO add fractionDigits + #TODO add totalDigits + #TODO add whiteSpace + + def __repr__(self): + if self.type == "enumeration": + return "' or '".join(self.options) + elif self.type == "bounds": + self.options.sort() + return "of type '%s', having a value between %s and %s" % (self.restriction_on, str(self.options[0]), str(self.options[1])) + elif self.type == "length": + return "of type '%s' with a length of %s" % (self.restriction_on, str(self.options[0])) + elif self.type == "pattern": + return "of type '%s' respecting pattern '%s'" % (self.restriction_on, self.options[0]) + #TODO add min/maxLength + #TODO add fractionDigits + #TODO add totalDigits + #TODO add whiteSpace class specification: """ @@ -348,6 +355,6 @@ if __name__ == "__main__": ids_file.validate(ifc_file, logger) - print("Out of %s IFC elements, %s were checked against %s IDS requirements and %s of them passed (%s).\nRuntime=%ss. Results saved to %s" - % (len(ifc_file.by_type('IfcProduct')), ifc_checked, len(ids_file.specifications[0].requirements.terms), ifc_passed, str(ifc_passed/ifc_checked*100)+'%', round(time.time() - start_time, 2), filename)) + print("Out of %s IFC elements, %s were checked against %s requirements in %s specification(s) and %s of them passed (%s).\nRuntime=%ss. Results saved to %s" + % (len(ifc_file.by_type('IfcProduct')), ifc_checked, len(ids_file.specifications[0].requirements.terms), len(ids_file.specifications), ifc_passed, str(ifc_passed/ifc_checked*100)+'%', round(time.time() - start_time, 2), filename)) From 7e02eb923df36b1ab9aac2a9cd3026daa958ded0 Mon Sep 17 00:00:00 2001 From: ArturTomczak Date: Sat, 12 Jun 2021 21:10:09 +0200 Subject: [PATCH 17/34] add bounds restriction --- src/ifcopenshell-python/ifcopenshell/ids.py | 29 +++++++++++++++------ 1 file changed, 21 insertions(+), 8 deletions(-) diff --git a/src/ifcopenshell-python/ifcopenshell/ids.py b/src/ifcopenshell-python/ifcopenshell/ids.py index 81905608d8..3553753701 100644 --- a/src/ifcopenshell-python/ifcopenshell/ids.py +++ b/src/ifcopenshell-python/ifcopenshell/ids.py @@ -129,7 +129,7 @@ class property(facet): """ parameters = ["name", "propertyset", "value"] - message = "a property '%(name)s' in '%(propertyset)s' with a value '%(value)s'" + message = "a property '%(name)s' in '%(propertyset)s' with a value %(value)s" def __call__(self, inst, logger): props = ifcopenshell.util.element.get_psets(inst) @@ -139,7 +139,7 @@ class property(facet): di = { "name": self.name, "propertyset": self.propertyset, - "value": val, + "value": "'%s'" % val, } if val is not None: @@ -236,7 +236,17 @@ class restriction: if n[0:3] == "xs:": if n[3:] == "enumeration": self.type = "enumeration" - [self.options.append(x["@value"]) for x in node[n]] + for x in node[n]: + self.options.append(x["@value"]) + elif n[8:] == "clusive": + self.type = "bounds" + if n[3:6] == 'min': + self.options.insert(0,'>') + else: + self.options.insert(0,'<') + if n[6:9] == 'Inc': + self.options[0] += '=' + self.options[0] += node[n]['@value'] #TODO implement other restrictions # elif n.nodeType == n.ELEMENT_NODE and (n.tagName.endswith("Inclusive") or n.tagName.endswith("Exclusive")): # self.type = "bounds" @@ -252,14 +262,17 @@ class restriction: #TODO add totalDigits #TODO add whiteSpace else: - logger.error({'guid':inst.GlobalId, 'result':'ERROR', 'sentence':'Restriction not implemented'}) + logger.error({'result':'ERROR', 'sentence':'Restriction not implemented'}) def __eq__(self, other): if self.type == "enumeration": return other in self.options elif self.type == "bounds": - self.options.sort() - return False #TODO + result = True + for op in self.options: + if not(eval(str(other)+op)): + result = False + return result elif self.type == "length": return False #TODO elif self.type == "pattern": @@ -271,10 +284,10 @@ class restriction: def __repr__(self): if self.type == "enumeration": - return "' or '".join(self.options) + return "'%s'" % "' or '".join(self.options) elif self.type == "bounds": self.options.sort() - return "of type '%s', having a value between %s and %s" % (self.restriction_on, str(self.options[0]), str(self.options[1])) + return "of type '%s', having a value %s" % (self.restriction_on, ' and '.join(self.options)) elif self.type == "length": return "of type '%s' with a length of %s" % (self.restriction_on, str(self.options[0])) elif self.type == "pattern": From ba26772e464d5fa938e8d1f5a36b251dfa346b2e Mon Sep 17 00:00:00 2001 From: ArturTomczak Date: Sat, 12 Jun 2021 21:10:50 +0200 Subject: [PATCH 18/34] add bounds restriction --- src/ifcopenshell-python/ifcopenshell/ids.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/ifcopenshell-python/ifcopenshell/ids.py b/src/ifcopenshell-python/ifcopenshell/ids.py index 3553753701..aea3102757 100644 --- a/src/ifcopenshell-python/ifcopenshell/ids.py +++ b/src/ifcopenshell-python/ifcopenshell/ids.py @@ -248,9 +248,6 @@ class restriction: self.options[0] += '=' self.options[0] += node[n]['@value'] #TODO implement other restrictions - # elif n.nodeType == n.ELEMENT_NODE and (n.tagName.endswith("Inclusive") or n.tagName.endswith("Exclusive")): - # self.type = "bounds" - # self.options.append(n.getAttribute("value")) # elif n.nodeType == n.ELEMENT_NODE and n.tagName.endswith("length"): # self.type = "length" # self.options.append(n.getAttribute("value")) From 8fd49651068246f55f091447bb5b984d08942168 Mon Sep 17 00:00:00 2001 From: ArturTomczak Date: Sat, 12 Jun 2021 22:35:37 +0200 Subject: [PATCH 19/34] add length restriction both length and min/maxlength added --- src/ifcopenshell-python/ifcopenshell/ids.py | 41 ++++++++++++--------- 1 file changed, 24 insertions(+), 17 deletions(-) diff --git a/src/ifcopenshell-python/ifcopenshell/ids.py b/src/ifcopenshell-python/ifcopenshell/ids.py index aea3102757..f633ec8969 100644 --- a/src/ifcopenshell-python/ifcopenshell/ids.py +++ b/src/ifcopenshell-python/ifcopenshell/ids.py @@ -247,17 +247,21 @@ class restriction: if n[6:9] == 'Inc': self.options[0] += '=' self.options[0] += node[n]['@value'] - #TODO implement other restrictions - # elif n.nodeType == n.ELEMENT_NODE and n.tagName.endswith("length"): - # self.type = "length" - # self.options.append(n.getAttribute("value")) - # elif n.nodeType == n.ELEMENT_NODE and n.tagName.endswith("pattern"): - # self.type = "pattern" - # self.options.append(n.getAttribute("value")) - #TODO add min/maxLength - #TODO add fractionDigits - #TODO add totalDigits - #TODO add whiteSpace + elif n[-5:] == "ength": + self.type = "length" + if n[3:6] == "min": + self.options.append('>=') + elif n[3:6] == "max": + self.options.append('<=') + else: + self.options.append('==') + self.options[-1] += str(node[n]['@value']) + elif n[3:] == "pattern": + self.type = "pattern" + self.options.append(node[n]['@value']) + #TODO add fractionDigits + #TODO add totalDigits + #TODO add whiteSpace else: logger.error({'result':'ERROR', 'sentence':'Restriction not implemented'}) @@ -271,10 +275,14 @@ class restriction: result = False return result elif self.type == "length": - return False #TODO + result = True + for op in self.options: + if not(eval(str(len(other))+op)): + result = False + return result elif self.type == "pattern": - return False #TODO - #TODO add min/maxLength + #TODO verify XML pattern + return False #TODO add fractionDigits #TODO add totalDigits #TODO add whiteSpace @@ -286,10 +294,9 @@ class restriction: self.options.sort() return "of type '%s', having a value %s" % (self.restriction_on, ' and '.join(self.options)) elif self.type == "length": - return "of type '%s' with a length of %s" % (self.restriction_on, str(self.options[0])) + return "of type '%s' with %s letters" % (self.restriction_on, ' and '.join(self.options)) elif self.type == "pattern": - return "of type '%s' respecting pattern '%s'" % (self.restriction_on, self.options[0]) - #TODO add min/maxLength + return "of type '%s' respecting pattern '%s'" % (self.restriction_on, ' and '.join(self.options)) #TODO add fractionDigits #TODO add totalDigits #TODO add whiteSpace From 5b94043cdc1db26f3809064eee483ace55005c2e Mon Sep 17 00:00:00 2001 From: ArturTomczak Date: Fri, 18 Jun 2021 16:50:26 +0200 Subject: [PATCH 20/34] ids parse, restrictions change __init__ to parse for separating those two --- src/ifcopenshell-python/ifcopenshell/ids.py | 88 +++++++++++++-------- 1 file changed, 53 insertions(+), 35 deletions(-) diff --git a/src/ifcopenshell-python/ifcopenshell/ids.py b/src/ifcopenshell-python/ifcopenshell/ids.py index f633ec8969..c4d89e257a 100644 --- a/src/ifcopenshell-python/ifcopenshell/ids.py +++ b/src/ifcopenshell-python/ifcopenshell/ids.py @@ -105,6 +105,7 @@ class classification(facet): message = "a classification reference '%(value)s' from '%(system)s'" def __call__(self, inst, logger): + #TODO Location: 'type'/'instance'/'any' refs = [] for association in inst.HasAssociations: if association.is_a("IfcRelAssociatesClassification"): @@ -132,6 +133,7 @@ class property(facet): message = "a property '%(name)s' in '%(propertyset)s' with a value %(value)s" def __call__(self, inst, logger): + #TODO Location: 'type'/'instance'/'any' props = ifcopenshell.util.element.get_psets(inst) pset = props.get(self.propertyset) val = pset.get(self.name) if pset else None @@ -150,6 +152,7 @@ class property(facet): else: msg = "no set '%(propertyset)s'" % di + #TODO implement data type comparison return facet_evaluation( val == self.value, msg @@ -165,6 +168,8 @@ class material(facet): def __call__(self, inst, logger): material_relations = [rel for rel in inst.HasAssociations if rel.is_a("IfcRelAssociatesMaterial")] + #TODO Location: 'type'/'instance'/'any'. Handle type... https://github.com/IfcOpenShell/IfcOpenShell/blob/257997c2cb8d382a7f3026f9a33fed6ccbe31282/src/ifcopenshell-python/ifcopenshell/util/element.py#L54 + # [material_relations.append(rel) for rel in ifcopenshell.util.element.get_type(inst).HasAssociations if rel.is_a("IfcRelAssociatesMaterial")] materials = [] for rel in material_relations: #TODO test all subtypes of material definitions @@ -187,9 +192,12 @@ class material(facet): else: logger.error({'guid':inst.GlobalId, 'result':'ERROR', 'sentence':'IfcRelAssociatesMaterial not implemented'}) + if not materials: + materials.append('UNDEFINED') + return facet_evaluation( self.value in materials, - self.message % {"value": self.value}, + self.message % {"value": "'/'".join(materials)}, ) @@ -266,27 +274,30 @@ class restriction: logger.error({'result':'ERROR', 'sentence':'Restriction not implemented'}) def __eq__(self, other): - if self.type == "enumeration": - return other in self.options - elif self.type == "bounds": - result = True - for op in self.options: - if not(eval(str(other)+op)): - result = False - return result - elif self.type == "length": - result = True - for op in self.options: - if not(eval(str(len(other))+op)): - result = False - return result - elif self.type == "pattern": - #TODO verify XML pattern - return False - #TODO add fractionDigits - #TODO add totalDigits - #TODO add whiteSpace - + result=False + #TODO implement data type comparison + if self and other: + if self.type == "enumeration" and self.restriction_on == 'bool': + self.options = [x.lower() for x in self.options] + result = str(other).lower() in self.options + elif self.type == "enumeration": + result = other in self.options + elif self.type == "bounds": + for op in self.options: + if eval(str(other)+op): #TODO eval not safe? + result = True + elif self.type == "length": + for op in self.options: + if eval(str(len(other))+op): #TODO eval not safe? + result = True + elif self.type == "pattern": + #TODO verify XML pattern + pass + #TODO add fractionDigits + #TODO add totalDigits + #TODO add whiteSpace + return result + def __repr__(self): if self.type == "enumeration": return "'%s'" % "' or '".join(self.options) @@ -301,6 +312,7 @@ class restriction: #TODO add totalDigits #TODO add whiteSpace + class specification: """ Represents the XML node and its two children and @@ -318,16 +330,17 @@ class specification: def __call__(self, inst, logger): if self.applicability(inst, logger): - global ifc_checked - ifc_checked += 1 + valid = self.requirements(inst, logger) if valid: - global ifc_passed - ifc_passed += 1 logger.info({'guid':inst.GlobalId, 'result':valid.success,'sentence':str(self) + "\n" + inst.is_a() + " '" + str(inst.Name) + "' (#" + str(inst.id()) + ") has " + str(valid) + " so is compliant"}) + return True, True else: logger.error({'guid':inst.GlobalId, 'result':valid.success, 'sentence':str(self) + "\n" + inst.is_a() + " '" + str(inst.Name) + "' (#" + str(inst.id()) + ") has " + str(valid) + " so is not compliant"}) + return True, False + else: + return False, False def __str__(self): return "Given an instance with %(applicability)s\nWe expect %(requirements)s" % self.__dict__ @@ -338,21 +351,27 @@ class ids: Represents the XML root node and its childNodes. """ - def __init__(self, fn): + @staticmethod + def parse(fn): ids_schema = XMLSchema("http://standards.buildingsmart.org/IDS/ids.xsd") ids_schema.validate(fn) + + ids_content = ids_schema.to_dict(fn) + new_ids = ids() + new_ids.specifications = [specification(s) for s in ids_content['specification']] + return new_ids - ids = ids_schema.to_dict(fn) - self.specifications = [specification(s) for s in ids['specification']] def validate(self, ifc_file, logger): + self.ifc_checked = 0 + self.ifc_passed = 0 for spec in self.specifications: for elem in ifc_file.by_type("IfcObject"): - spec(elem, logger) + apply, comply = spec(elem, logger) + if apply: self.ifc_checked += 1 + if comply: self.ifc_passed += 1 -ifc_checked = 0 -ifc_passed = 0 if __name__ == "__main__": import time start_time = time.time() @@ -367,11 +386,10 @@ if __name__ == "__main__": logging.basicConfig(filename=filename, level=logging.INFO, format="%(message)s") logging.FileHandler(filename, mode='w') - ids_file = ids(sys.argv[1]) ifc_file = ifcopenshell.open(sys.argv[2]) + ids_file = ids.parse(sys.argv[1]) ids_file.validate(ifc_file, logger) print("Out of %s IFC elements, %s were checked against %s requirements in %s specification(s) and %s of them passed (%s).\nRuntime=%ss. Results saved to %s" - % (len(ifc_file.by_type('IfcProduct')), ifc_checked, len(ids_file.specifications[0].requirements.terms), len(ids_file.specifications), ifc_passed, str(ifc_passed/ifc_checked*100)+'%', round(time.time() - start_time, 2), filename)) - + % (len(ifc_file.by_type('IfcProduct')), ids_file.ifc_checked, len(ids_file.specifications[0].requirements.terms), len(ids_file.specifications), ids_file.ifc_passed, str(ids_file.ifc_passed/ids_file.ifc_checked*100)+'%', round(time.time() - start_time, 2), filename)) From 3b270a61dfd93732895c6321901bdeb8ecbf7a88 Mon Sep 17 00:00:00 2001 From: ArturTomczak Date: Sat, 19 Jun 2021 11:44:33 +0200 Subject: [PATCH 21/34] handle ifc2x3 and 4 handles .ItemReference or .Identification --- src/ifcopenshell-python/ifcopenshell/ids.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/ifcopenshell-python/ifcopenshell/ids.py b/src/ifcopenshell-python/ifcopenshell/ids.py index c4d89e257a..389faa4068 100644 --- a/src/ifcopenshell-python/ifcopenshell/ids.py +++ b/src/ifcopenshell-python/ifcopenshell/ids.py @@ -110,8 +110,10 @@ class classification(facet): for association in inst.HasAssociations: if association.is_a("IfcRelAssociatesClassification"): cref = association.RelatingClassification - refs.append((cref.ReferencedSource.Name, cref.Identification)) # before was .ItemReference instead of .Identification - + if hasattr(cref, 'ItemReference'): #IFC2x3 + refs.append((cref.ReferencedSource.Name, cref.ItemReference)) + elif hasattr(cref, 'Identification'): # IFC4 + refs.append((cref.ReferencedSource.Name, cref.Identification)) if refs: return facet_evaluation( (self.system, self.value) in refs, From 99f0e6c93eea1e7fee452f1f572bce41b8d65d92 Mon Sep 17 00:00:00 2001 From: ArturTomczak Date: Mon, 21 Jun 2021 15:23:03 +0200 Subject: [PATCH 22/34] add regex pattern validation --- src/ifcopenshell-python/ifcopenshell/ids.py | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/src/ifcopenshell-python/ifcopenshell/ids.py b/src/ifcopenshell-python/ifcopenshell/ids.py index 123bcaa565..ca30522d46 100644 --- a/src/ifcopenshell-python/ifcopenshell/ids.py +++ b/src/ifcopenshell-python/ifcopenshell/ids.py @@ -1,8 +1,10 @@ import operator import ifcopenshell.util.element +import re from xmlschema import XMLSchema +from xmlschema.validators import facets, identities class exception(Exception): pass @@ -128,7 +130,7 @@ class classification(facet): class property(facet): """ - The IDS property facet implenented using `ifcopenshell.util.element` + The IDS property facet implemented using `ifcopenshell.util.element` """ parameters = ["name", "propertyset", "value"] @@ -308,8 +310,11 @@ class restriction: if eval(str(len(other))+op): #TODO eval not safe? result = True elif self.type == "pattern": - #TODO verify XML pattern - pass + self.options + translated_pattern = identities.translate_pattern(r'[A-Z]{1,3}') # Between one and three capital letters + regex_pattern = re.compile(translated_pattern) + if regex_pattern.fullmatch(other) is not None: + result = True #TODO add fractionDigits #TODO add totalDigits #TODO add whiteSpace From eaa527f512da824d7ec33c801ee4c402688d5f4c Mon Sep 17 00:00:00 2001 From: ArturTomczak Date: Mon, 21 Jun 2021 17:48:27 +0200 Subject: [PATCH 23/34] include location (type/inst/any) --- src/ifcopenshell-python/ifcopenshell/ids.py | 120 ++++++++++++++------ 1 file changed, 83 insertions(+), 37 deletions(-) diff --git a/src/ifcopenshell-python/ifcopenshell/ids.py b/src/ifcopenshell-python/ifcopenshell/ids.py index ca30522d46..9057c10bbf 100644 --- a/src/ifcopenshell-python/ifcopenshell/ids.py +++ b/src/ifcopenshell-python/ifcopenshell/ids.py @@ -3,7 +3,6 @@ import ifcopenshell.util.element import re from xmlschema import XMLSchema - from xmlschema.validators import facets, identities class exception(Exception): @@ -103,28 +102,48 @@ class classification(facet): The IDS classification facet by traversing the HasAssociations inverse attribute """ - parameters = ["system", "value"] - message = "a classification reference '%(value)s' from '%(system)s'" + parameters = ["system", "value", "location"] + message = "%(location)sclassification reference %(value)s from '%(system)s'" def __call__(self, inst, logger): - #TODO Location: 'type'/'instance'/'any' + + self.location = self.node['@location'] + + instance_classiciations = inst.HasAssociations + if ifcopenshell.util.element.get_type(inst): + type_classifications = ifcopenshell.util.element.get_type(inst).HasAssociations + else: + type_classifications = () + + if self.location == 'instance' and instance_classiciations: + associations = instance_classiciations + elif self.location == 'type' and type_classifications: + associations = type_classifications + elif self.location == 'any' and (instance_classiciations or type_classifications): + associations = instance_classiciations + type_classifications + else: + associations = () + refs = [] - for association in inst.HasAssociations: + for association in associations: if association.is_a("IfcRelAssociatesClassification"): cref = association.RelatingClassification if hasattr(cref, 'ItemReference'): #IFC2x3 refs.append((cref.ReferencedSource.Name, cref.ItemReference)) elif hasattr(cref, 'Identification'): # IFC4 refs.append((cref.ReferencedSource.Name, cref.Identification)) + + self.location = location[self.location] + if refs: return facet_evaluation( (self.system, self.value) in refs, - self.message % {"system": refs[0][0], "value": refs[0][1]} + self.message % {"system": refs[0][0], "value": "'"+refs[0][1]+"'", "location": self.location} # what if not first item of refs? ) - else: + else: return facet_evaluation( False, - "has no classification" + "does not have %sclassification reference" % self.location ) @@ -133,28 +152,46 @@ class property(facet): The IDS property facet implemented using `ifcopenshell.util.element` """ - parameters = ["name", "propertyset", "value"] - message = "a property '%(name)s' in '%(propertyset)s' with a value %(value)s" + parameters = ["name", "propertyset", "value", "location"] + message = "%(location)sproperty '%(name)s' in '%(propertyset)s' with a value %(value)s" def __call__(self, inst, logger): - #TODO Location: 'type'/'instance'/'any' - props = ifcopenshell.util.element.get_psets(inst) + + self.location = self.node['@location'] + + instance_props = ifcopenshell.util.element.get_psets(inst) + if ifcopenshell.util.element.get_type(inst): + type_props = ifcopenshell.util.element.get_psets( ifcopenshell.util.element.get_type(inst) ) + else: + type_props = {} + + if self.location == 'instance': + props = instance_props + elif self.location == 'type' and type_props: + props = type_props + elif self.location == 'any' and (instance_props or type_props): + props = {**instance_props , **type_props} + else: + props = {} + pset = props.get(self.propertyset) val = pset.get(self.name) if pset else None - + + self.location = location[self.location] di = { "name": self.name, "propertyset": self.propertyset, "value": "'%s'" % val, + "location": self.location } if val is not None: msg = self.message % di else: if pset: - msg = "no property '%(name)s' in a set '%(propertyset)s'" % di + msg = "does not have %(location)sproperty '%(name)s' in a set '%(propertyset)s'" % di else: - msg = "no set '%(propertyset)s'" % di + msg = "does not have %(location)sset '%(propertyset)s'" % di #TODO implement data type comparison return facet_evaluation( @@ -167,16 +204,30 @@ class material(facet): """ The IDS material facet by traversing the HasAssociations inverse attribute """ - parameters = ["value"] - message = "a material '%(value)s'" + parameters = ["value", "location"] + message = "%(location)smaterial '%(value)s'" def __call__(self, inst, logger): - material_relations = [rel for rel in inst.HasAssociations if rel.is_a("IfcRelAssociatesMaterial")] - #TODO Location: 'type'/'instance'/'any'. Handle type... https://github.com/IfcOpenShell/IfcOpenShell/blob/257997c2cb8d382a7f3026f9a33fed6ccbe31282/src/ifcopenshell-python/ifcopenshell/util/element.py#L54 - # [material_relations.append(rel) for rel in ifcopenshell.util.element.get_type(inst).HasAssociations if rel.is_a("IfcRelAssociatesMaterial")] + + self.location = self.node['@location'] + + instance_material_rel = [rel for rel in inst.HasAssociations if rel.is_a("IfcRelAssociatesMaterial")] + if ifcopenshell.util.element.get_type(inst): + type_material_rel = [rel for rel in ifcopenshell.util.element.get_type(inst).HasAssociations if rel.is_a("IfcRelAssociatesMaterial")] + else: + type_material_rel = [] + + if self.location == 'instance': + material_relations = list(instance_material_rel) + elif self.location == 'type' and type_material_rel: + material_relations = list(type_material_rel) + elif self.location == 'any' and (instance_material_rel or type_material_rel): + material_relations = instance_material_rel + type_material_rel + else: + material_relations = [] + materials = [] for rel in material_relations: - #TODO test all subtypes of material definitions if rel.RelatingMaterial.is_a() == "IfcMaterial": materials.append(rel.RelatingMaterial.Name) elif rel.RelatingMaterial.is_a() == "IfcMaterialMaterialList": #DEPRECATED in IFC4 @@ -199,9 +250,11 @@ class material(facet): if not materials: materials.append('UNDEFINED') + self.location = location[self.location] + return facet_evaluation( self.value in materials, - self.message % {"value": "'/'".join(materials)}, + self.message % {"value": "'/'".join(materials), "location": self.location}, ) @@ -276,21 +329,6 @@ class restriction: #TODO add whiteSpace else: logger.error({'result':'ERROR', 'sentence':'Restriction not implemented'}) - self.type = [] - - for n in node.childNodes: - if n.nodeType == n.ELEMENT_NODE and n.tagName.endswith("enumeration"): - self.options.append(n.getAttribute("value")) - self.type = "enumeration" - elif n.nodeType == n.ELEMENT_NODE and (n.tagName.endswith("Inclusive") or n.tagName.endswith("Exclusive")): - self.options.append(n.getAttribute("value")) - self.type = "bounds" - elif n.nodeType == n.ELEMENT_NODE and n.tagName.endswith("length"): - self.options.append(n.getAttribute("value")) - self.type = "length" - elif n.nodeType == n.ELEMENT_NODE and n.tagName.endswith("pattern"): - self.options.append(n.getAttribute("value")) - self.type = "pattern" def __eq__(self, other): result=False @@ -394,6 +432,14 @@ class ids: if comply: self.ifc_passed += 1 + +location = { + 'instance': 'an instance ', + 'type': 'a type ', + 'any': 'a ' +} + + if __name__ == "__main__": import time start_time = time.time() From ad8542f2446d0f9766cd0e7a7d58a9b88e12aad2 Mon Sep 17 00:00:00 2001 From: ArturTomczak Date: Fri, 2 Jul 2021 17:27:09 +0200 Subject: [PATCH 24/34] add unittest init --- .../ifcopenshell/test_ids.py | 22 +++++++++++++++++++ 1 file changed, 22 insertions(+) create mode 100644 src/ifcopenshell-python/ifcopenshell/test_ids.py diff --git a/src/ifcopenshell-python/ifcopenshell/test_ids.py b/src/ifcopenshell-python/ifcopenshell/test_ids.py new file mode 100644 index 0000000000..23bd98e43d --- /dev/null +++ b/src/ifcopenshell-python/ifcopenshell/test_ids.py @@ -0,0 +1,22 @@ +import unittest +from ids import ids, specification, entity, classification, property, material + + +class TestIds(unittest.TestCase): + + """ Parsing IDS.xml """ + + def test_parse(self): + ids_file = ids.parse(sys.argv[1]) + self.assertEqual( type(ids_file).__name__, "ids" ) + + """ IDS authoring """ + + """ IFC validation with IDS """ + + """ IDS validation results """ + + +if __name__ == '__main__': + import sys + unittest.main() \ No newline at end of file From 41a47f14ae311b54c31fc8f0200531b2d8292e48 Mon Sep 17 00:00:00 2001 From: ArturTomczak Date: Mon, 5 Jul 2021 13:21:36 +0200 Subject: [PATCH 25/34] add basic parsing tests --- .../ifcopenshell/test_ids.py | 46 ++++++++++++++++++- 1 file changed, 44 insertions(+), 2 deletions(-) diff --git a/src/ifcopenshell-python/ifcopenshell/test_ids.py b/src/ifcopenshell-python/ifcopenshell/test_ids.py index 23bd98e43d..73dec07978 100644 --- a/src/ifcopenshell-python/ifcopenshell/test_ids.py +++ b/src/ifcopenshell-python/ifcopenshell/test_ids.py @@ -1,20 +1,62 @@ import unittest from ids import ids, specification, entity, classification, property, material +import requests +def read_web_file(URL): + return requests.get(URL).text + class TestIds(unittest.TestCase): """ Parsing IDS.xml """ - def test_parse(self): - ids_file = ids.parse(sys.argv[1]) + def test_basic_ids_parse(self): + IDS_URL = "https://raw.githubusercontent.com/atomczak/Sample-BIM-Files/main/IDS/IDS_Wall_needs_all_fields.xml" + ids_file = ids.parse(read_web_file(IDS_URL)) self.assertEqual( type(ids_file).__name__, "ids" ) + + def test_entity_facet(self): + IDS_URL = "https://raw.githubusercontent.com/atomczak/Sample-BIM-Files/main/IDS/IDS_Wall_needs_entity.xml" + ids_file = ids.parse(read_web_file(IDS_URL)) + self.assertEqual( ids_file.specifications[0].requirements.terms[0].node['name'] , "IfcWall" ) + def test_predefinedtype_facet(self): + IDS_URL = "https://raw.githubusercontent.com/atomczak/Sample-BIM-Files/main/IDS/IDS_Wall_needs_predefinedtype.xml" + ids_file = ids.parse(read_web_file(IDS_URL)) + self.assertEqual( ids_file.specifications[0].requirements.terms[0].node['predefinedtype'] , "CLADDING" ) + + def test_property_facet(self): + IDS_URL = "https://raw.githubusercontent.com/atomczak/Sample-BIM-Files/main/IDS/IDS_Wall_needs_property.xml" + ids_file = ids.parse(read_web_file(IDS_URL)) + self.assertEqual( ids_file.specifications[0].requirements.terms[0].node['propertyset'] , "Test_PropertySet" ) + self.assertEqual( ids_file.specifications[0].requirements.terms[0].node['name'] , "Test_Parameter" ) + self.assertEqual( ids_file.specifications[0].requirements.terms[0].node['value'] , "Test_Value" ) + + def test_material_facet(self): + IDS_URL = "https://raw.githubusercontent.com/atomczak/Sample-BIM-Files/main/IDS/IDS_Wall_needs_material.xml" + ids_file = ids.parse(read_web_file(IDS_URL)) + self.assertEqual( ids_file.specifications[0].requirements.terms[0].node['value'] , "Test_Material" ) + + def test_classification_facet(self): + IDS_URL = "https://raw.githubusercontent.com/atomczak/Sample-BIM-Files/main/IDS/IDS_Wall_needs_classification.xml" + ids_file = ids.parse(read_web_file(IDS_URL)) + self.assertEqual( ids_file.specifications[0].requirements.terms[0].node['value'] , "Test_Classification" ) + self.assertEqual( ids_file.specifications[0].requirements.terms[0].node['system'] , "Test_System" ) + + """ Parsing invalid IDS.xml """ + # TODO + # def test_invalid_classification_facet(self): + # IDS_URL = "https://raw.githubusercontent.com/atomczak/Sample-BIM-Files/main/IDS/Invalid_IDS_Wall_needs_classification.xml" + # self.assertRaises( XMLSchemaChildrenValidationError, ids.parse(read_web_file(IDS_URL)) ) + """ IDS authoring """ + # TODO """ IFC validation with IDS """ + # TODO """ IDS validation results """ + # TODO if __name__ == '__main__': From c2bdfa939420dae2484b5fb79dbfe191c39326c1 Mon Sep 17 00:00:00 2001 From: ArturTomczak Date: Thu, 8 Jul 2021 18:00:33 +0200 Subject: [PATCH 26/34] fix imports and move ids_schema outside --- src/ifcopenshell-python/ifcopenshell/ids.py | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/src/ifcopenshell-python/ifcopenshell/ids.py b/src/ifcopenshell-python/ifcopenshell/ids.py index 9057c10bbf..7e2dc07a65 100644 --- a/src/ifcopenshell-python/ifcopenshell/ids.py +++ b/src/ifcopenshell-python/ifcopenshell/ids.py @@ -1,9 +1,13 @@ import operator import ifcopenshell.util.element import re - from xmlschema import XMLSchema -from xmlschema.validators import facets, identities +from xmlschema import etree_tostring +from xmlschema.validators import facets +from xmlschema.validators import identities + +ids_schema = XMLSchema("http://standards.buildingsmart.org/IDS/ids.xsd") + class exception(Exception): pass @@ -412,8 +416,7 @@ class ids: """ @staticmethod - def parse(fn): - ids_schema = XMLSchema("http://standards.buildingsmart.org/IDS/ids.xsd") + def parse(fn, ids_schema=ids_schema): ids_schema.validate(fn) ids_content = ids_schema.to_dict(fn) From 6dba96e78027d92a656882598ed46dba22e64d2b Mon Sep 17 00:00:00 2001 From: ArturTomczak Date: Thu, 8 Jul 2021 18:04:50 +0200 Subject: [PATCH 27/34] add location_msg as separate param --- src/ifcopenshell-python/ifcopenshell/ids.py | 30 +++++++++++++-------- 1 file changed, 19 insertions(+), 11 deletions(-) diff --git a/src/ifcopenshell-python/ifcopenshell/ids.py b/src/ifcopenshell-python/ifcopenshell/ids.py index 7e2dc07a65..5aaef19c68 100644 --- a/src/ifcopenshell-python/ifcopenshell/ids.py +++ b/src/ifcopenshell-python/ifcopenshell/ids.py @@ -53,8 +53,17 @@ class facet(metaclass=meta_facet): conveniently extracting XML child node text content. """ - def __init__(self, node): - self.node = node + def __init__(self, node=None, location=None): + if node: + self.node = node + if '@location' in self: + self.location = self.node['@location'] + else: + self.location = 'any' + if location: + self.location = location + else: + self.location = 'any' def __getattr__(self, k): if k in self.node: @@ -109,9 +118,8 @@ class classification(facet): parameters = ["system", "value", "location"] message = "%(location)sclassification reference %(value)s from '%(system)s'" - def __call__(self, inst, logger): - self.location = self.node['@location'] + def __call__(self, inst, logger): instance_classiciations = inst.HasAssociations if ifcopenshell.util.element.get_type(inst): @@ -137,17 +145,17 @@ class classification(facet): elif hasattr(cref, 'Identification'): # IFC4 refs.append((cref.ReferencedSource.Name, cref.Identification)) - self.location = location[self.location] + self.location_msg = location[self.location] if refs: return facet_evaluation( (self.system, self.value) in refs, - self.message % {"system": refs[0][0], "value": "'"+refs[0][1]+"'", "location": self.location} # what if not first item of refs? + self.message % {"system": refs[0][0], "value": "'"+refs[0][1]+"'", "location": self.location_msg} # what if not first item of refs? ) else: return facet_evaluation( False, - "does not have %sclassification reference" % self.location + "does not have %sclassification reference" % self.location_msg ) @@ -181,12 +189,12 @@ class property(facet): pset = props.get(self.propertyset) val = pset.get(self.name) if pset else None - self.location = location[self.location] + self.location_msg = location[self.location] di = { "name": self.name, "propertyset": self.propertyset, "value": "'%s'" % val, - "location": self.location + "location": self.location_msg } if val is not None: @@ -254,11 +262,11 @@ class material(facet): if not materials: materials.append('UNDEFINED') - self.location = location[self.location] + self.location_msg = location[self.location] return facet_evaluation( self.value in materials, - self.message % {"value": "'/'".join(materials), "location": self.location}, + self.message % {"value": "'/'".join(materials), "location": self.location_msg}, ) From d189113b98f3ab6b7aac07cb784845186bb86b27 Mon Sep 17 00:00:00 2001 From: ArturTomczak Date: Thu, 8 Jul 2021 18:06:07 +0200 Subject: [PATCH 28/34] add asdict() to all ids classes --- src/ifcopenshell-python/ifcopenshell/ids.py | 66 +++++++++++++++++++++ 1 file changed, 66 insertions(+) diff --git a/src/ifcopenshell-python/ifcopenshell/ids.py b/src/ifcopenshell-python/ifcopenshell/ids.py index 5aaef19c68..ee5175e11b 100644 --- a/src/ifcopenshell-python/ifcopenshell/ids.py +++ b/src/ifcopenshell-python/ifcopenshell/ids.py @@ -94,6 +94,13 @@ class entity(facet): parameters = ["name", "predefinedtype"] + + def asdict(self): + fac_dict = {'name': self.name} + if 'predefinedtype' in self: + fac_dict['predefinedtype'] = self.predefinedtype + return fac_dict + def __call__(self, inst, logger): # @nb with inheritance if self.predefinedtype and hasattr(inst, "PredefinedType"): @@ -118,6 +125,13 @@ class classification(facet): parameters = ["system", "value", "location"] message = "%(location)sclassification reference %(value)s from '%(system)s'" + def asdict(self): + fac_dict = { + '@location': self.location, + 'value': self.value, + 'system': self.system + } + return fac_dict def __call__(self, inst, logger): @@ -166,6 +180,17 @@ class property(facet): parameters = ["name", "propertyset", "value", "location"] message = "%(location)sproperty '%(name)s' in '%(propertyset)s' with a value %(value)s" + def asdict(self): + fac_dict = { + '@location': self.location, + 'propertyset': self.propertyset, + 'name': self.name, + 'value': self.value, + # TODO '@href': 'http://identifier.buildingsmart.org/uri/buildingsmart/ifc-4.3/prop/FireRating', #https://identifier.buildingsmart.org/uri/something + # TODO 'instructions': 'Please add the desired rating.' + } + return fac_dict + def __call__(self, inst, logger): @@ -219,6 +244,16 @@ class material(facet): parameters = ["value", "location"] message = "%(location)smaterial '%(value)s'" + def asdict(self): + fac_dict = { + '@location': self.location, + 'value': self.value, + # TODO '@href': 'http://identifier.buildingsmart.org/uri/buildingsmart/ifc-4.3/prop/FireRating', #https://identifier.buildingsmart.org/uri/something + # TODO 'instructions': 'Please add the desired rating.' + # TODO '@use': 'optional' + } + return fac_dict + def __call__(self, inst, logger): self.location = self.node['@location'] @@ -391,6 +426,25 @@ class specification: """ def __init__(self, node): + def asdict(self): + spec_dict = { + '@name': self.name, + 'applicability': {}, + 'requirements': {} + } + for fac in self.applicability.terms: + fclass = type(fac).__name__ + if fclass in spec_dict['applicability']: + spec_dict['applicability'][fclass].append(fac.asdict()) + else: + spec_dict['applicability'][fclass] = [fac.asdict()] + for fac in self.requirements.terms: + fclass = type(fac).__name__ + if fclass in spec_dict['requirements']: + spec_dict['requirements'][fclass].append(fac.asdict()) + else: + spec_dict['requirements'][fclass] = [fac.asdict()] + return spec_dict def parse_rules(node): names = [req for req in node for n in node[req]] children = [child for req in node for child in node[req]] @@ -423,6 +477,18 @@ class ids: Represents the XML root node and its childNodes. """ + def asdict(self): + ids_dict = {'@xmlns': 'http://standards.buildingsmart.org/IDS', + '@xmlns:xs': 'http://www.w3.org/2001/XMLSchema', + '@xmlns:xsi': 'http://www.w3.org/2001/XMLSchema-instance', + '@xsi:schemaLocation': 'http://standards.buildingsmart.org/IDS ' + 'http://standards.buildingsmart.org/IDS/ids.xsd', + 'specification': [], + 'info': self.info, + } + for spec in self.specifications: + ids_dict['specification'].append(spec.asdict()) + return ids_dict @staticmethod def parse(fn, ids_schema=ids_schema): ids_schema.validate(fn) From 3837c9d6ff2b58a6db5b9820fab2f5a548f46512 Mon Sep 17 00:00:00 2001 From: ArturTomczak Date: Thu, 8 Jul 2021 18:09:02 +0200 Subject: [PATCH 29/34] add methods to specification class --- src/ifcopenshell-python/ifcopenshell/ids.py | 45 ++++++++++++++++++--- 1 file changed, 39 insertions(+), 6 deletions(-) diff --git a/src/ifcopenshell-python/ifcopenshell/ids.py b/src/ifcopenshell-python/ifcopenshell/ids.py index ee5175e11b..44d5e069ae 100644 --- a/src/ifcopenshell-python/ifcopenshell/ids.py +++ b/src/ifcopenshell-python/ifcopenshell/ids.py @@ -425,7 +425,11 @@ class specification: Represents the XML node and its two children and """ - def __init__(self, node): + def __init__(self, name='Specification'): + self.name = name + self.applicability = None + self.requirements = None + def asdict(self): spec_dict = { '@name': self.name, @@ -445,14 +449,42 @@ class specification: else: spec_dict['requirements'][fclass] = [fac.asdict()] return spec_dict + + @staticmethod + def parse(node): def parse_rules(node): names = [req for req in node for n in node[req]] children = [child for req in node for child in node[req]] classes = map(meta_facet.facets.__getitem__, names) - return [cls(n) for cls, n in zip(classes, children)] + # return [cls.parse(n) for cls, n in zip(classes, children)] + return [cls(n) for cls, n in zip(classes, children)] # list of facet objects + + spec = specification() + spec.name = node['@name'] + spec.applicability = boolean_and(parse_rules(node['applicability'])) + spec.requirements = boolean_and(parse_rules(node['requirements'])) + return spec - self.applicability = boolean_and(parse_rules(node['applicability'])) - self.requirements = boolean_and(parse_rules(node['requirements'])) + # TODO adding applicability/requirements to specification. How to avoid repetitions? + def add_applicability(self, facet): + """ + Applicability specifies what conditions must be meet for an IFC object to be used for validation. + Takes: entity, classification, property or material objects as an input (at least one entity is required). + """ + if self.applicability: + self.applicability = boolean_and( self.applicability.terms + [facet] ) + else: + self.applicability = boolean_and([facet]) + + def add_requirement(self, facet): + """ + Requirement is validated on all applicable IFC elements. + Takes: entity, classification, property or material objects as an input (at least one of them is required). + """ + if self.requirements: + self.requirements = boolean_and( self.requirements.terms + [facet] ) + else: + self.requirements = boolean_and([facet]) def __call__(self, inst, logger): if self.applicability(inst, logger): @@ -460,10 +492,11 @@ class specification: valid = self.requirements(inst, logger) if valid: - logger.info({'guid':inst.GlobalId, 'result':valid.success,'sentence':str(self) + "\n" + inst.is_a() + " '" + str(inst.Name) + "' (#" + str(inst.id()) + ") has " + str(valid) + " so is compliant"}) + logger.info({'guid':inst.GlobalId, 'result':valid.success,'sentence':str(self) + ".\n" + inst.is_a() + " '" + str(inst.Name) + "' (#" + str(inst.id()) + ") has " + str(valid) + " so is compliant"}) return True, True else: - logger.error({'guid':inst.GlobalId, 'result':valid.success, 'sentence':str(self) + "\n" + inst.is_a() + " '" + str(inst.Name) + "' (#" + str(inst.id()) + ") has " + str(valid) + " so is not compliant"}) + # BUG "has does not have" + logger.error({'guid':inst.GlobalId, 'result':valid.success, 'sentence':str(self) + ".\n" + inst.is_a() + " '" + str(inst.Name) + "' (#" + str(inst.id()) + ") has " + str(valid) + " so is not compliant"}) return True, False else: return False, False From b0bb2cb58de756dd084cb71c7cd0896101c892a3 Mon Sep 17 00:00:00 2001 From: ArturTomczak Date: Thu, 8 Jul 2021 18:09:32 +0200 Subject: [PATCH 30/34] add ids.to_xml() --- src/ifcopenshell-python/ifcopenshell/ids.py | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/src/ifcopenshell-python/ifcopenshell/ids.py b/src/ifcopenshell-python/ifcopenshell/ids.py index 44d5e069ae..7d4e9d2c0b 100644 --- a/src/ifcopenshell-python/ifcopenshell/ids.py +++ b/src/ifcopenshell-python/ifcopenshell/ids.py @@ -522,6 +522,27 @@ class ids: for spec in self.specifications: ids_dict['specification'].append(spec.asdict()) return ids_dict + + def to_xml(self, fn='./', ids_schema=ids_schema): + if fn.endswith('/'): + fn = fn + 'IDS' + if not fn.endswith('.xml'): + fn = fn + '.xml' + + ids_dict = self.asdict() + + ids_xml = ids_schema.encode(ids_dict) #, namespaces='http://standards.buildingsmart.org/IDS') + ids_str = etree_tostring(ids_xml, namespaces={'': 'http://standards.buildingsmart.org/IDS'}) # if restrictions, add also: 'xs': 'http://www.w3.org/2001/XMLSchema' + ids_schema.validate(ids_str) + + with open(fn, 'w') as f: + f.write('\n') + f.write('\n') + f.write(ids_str) + + ids_schema.validate(fn) + return ids_schema.is_valid(fn) + @staticmethod def parse(fn, ids_schema=ids_schema): ids_schema.validate(fn) From 30a03b0c5179259a525cc5cf5f787e60641bae8d Mon Sep 17 00:00:00 2001 From: ArturTomczak Date: Thu, 8 Jul 2021 18:12:55 +0200 Subject: [PATCH 31/34] add facet creation methods --- src/ifcopenshell-python/ifcopenshell/ids.py | 31 +++++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/src/ifcopenshell-python/ifcopenshell/ids.py b/src/ifcopenshell-python/ifcopenshell/ids.py index 7d4e9d2c0b..f3a305e10c 100644 --- a/src/ifcopenshell-python/ifcopenshell/ids.py +++ b/src/ifcopenshell-python/ifcopenshell/ids.py @@ -94,6 +94,10 @@ class entity(facet): parameters = ["name", "predefinedtype"] + def create(name=None, predefinedtype=None): + inst.name = name + inst.predefinedtype = predefinedtype + return inst def asdict(self): fac_dict = {'name': self.name} @@ -125,6 +129,12 @@ class classification(facet): parameters = ["system", "value", "location"] message = "%(location)sclassification reference %(value)s from '%(system)s'" + def create(location='any', value=None, system=None): + inst.location = location + inst.value = value + inst.system = system + return inst + def asdict(self): fac_dict = { '@location': self.location, @@ -180,6 +190,18 @@ class property(facet): parameters = ["name", "propertyset", "value", "location"] message = "%(location)sproperty '%(name)s' in '%(propertyset)s' with a value %(value)s" + + def create(location='any', propertyset=None, name=None, value=None): + inst = property() + inst.location = location + inst.propertyset = propertyset + inst.name = name + inst.value = value + # cls.attributes = {'@location': location} # 'type', 'instance', 'any' + # BUG '@href': 'http://identifier.buildingsmart.org/uri/buildingsmart/ifc-4.3/prop/FireRating', #https://identifier.buildingsmart.org/uri/something + # BUG 'instructions': 'Please add the desired rating.', + return inst + def asdict(self): fac_dict = { '@location': self.location, @@ -243,6 +265,15 @@ class material(facet): """ parameters = ["value", "location"] message = "%(location)smaterial '%(value)s'" + + def create(location='any', value=None): + inst.location = location + inst.value = value + # self.attributes = {'@location': location} # 'type', 'instance', 'any' + # # BUG '@use': 'optional' + # # BUG '@href': 'https://identifier.buildingsmart.org/uri/something', + # # BUG 'instructions': 'Please add the desired...', + return inst def asdict(self): fac_dict = { From 93fad2a5c37cb747b60395e2b3847948abb8aa93 Mon Sep 17 00:00:00 2001 From: ArturTomczak Date: Thu, 8 Jul 2021 18:14:49 +0200 Subject: [PATCH 32/34] add ids constructor, fix ids bug --- src/ifcopenshell-python/ifcopenshell/ids.py | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/src/ifcopenshell-python/ifcopenshell/ids.py b/src/ifcopenshell-python/ifcopenshell/ids.py index f3a305e10c..cc26d06ff7 100644 --- a/src/ifcopenshell-python/ifcopenshell/ids.py +++ b/src/ifcopenshell-python/ifcopenshell/ids.py @@ -541,6 +541,16 @@ class ids: Represents the XML root node and its childNodes. """ + def __init__(self): + self.specifications = [] + self.info = None + #self.attributes = { + # '@xmlns:xs': 'http://www.w3.org/2001/XMLSchema', + # '@xmlns': 'http://standards.buildingsmart.org/IDS', + # '@xmlns:xsi': 'http://www.w3.org/2001/XMLSchema-instance', + # '@xsi:schemaLocation': 'http://standards.buildingsmart.org/IDS http://standards.buildingsmart.org/IDS/ids.xsd', + # } + def asdict(self): ids_dict = {'@xmlns': 'http://standards.buildingsmart.org/IDS', '@xmlns:xs': 'http://www.w3.org/2001/XMLSchema', @@ -577,10 +587,9 @@ class ids: @staticmethod def parse(fn, ids_schema=ids_schema): ids_schema.validate(fn) - - ids_content = ids_schema.to_dict(fn) + ids_content = ids_schema.decode(fn) new_ids = ids() - new_ids.specifications = [specification(s) for s in ids_content['specification']] + new_ids.specifications = [specification.parse(s) for s in ids_content['specification']] return new_ids From 1ea4f50d30097da2e4a9404c89bf971ab42ff7f7 Mon Sep 17 00:00:00 2001 From: ArturTomczak Date: Fri, 9 Jul 2021 08:35:04 +0200 Subject: [PATCH 33/34] fix facet constructors --- src/ifcopenshell-python/ifcopenshell/ids.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/ifcopenshell-python/ifcopenshell/ids.py b/src/ifcopenshell-python/ifcopenshell/ids.py index cc26d06ff7..4b0f9f762c 100644 --- a/src/ifcopenshell-python/ifcopenshell/ids.py +++ b/src/ifcopenshell-python/ifcopenshell/ids.py @@ -95,6 +95,7 @@ class entity(facet): parameters = ["name", "predefinedtype"] def create(name=None, predefinedtype=None): + inst = entity() inst.name = name inst.predefinedtype = predefinedtype return inst @@ -130,6 +131,7 @@ class classification(facet): message = "%(location)sclassification reference %(value)s from '%(system)s'" def create(location='any', value=None, system=None): + inst = classification() inst.location = location inst.value = value inst.system = system @@ -267,6 +269,7 @@ class material(facet): message = "%(location)smaterial '%(value)s'" def create(location='any', value=None): + inst = material() inst.location = location inst.value = value # self.attributes = {'@location': location} # 'type', 'instance', 'any' @@ -580,6 +583,7 @@ class ids: f.write('\n') f.write('\n') f.write(ids_str) + f.close() ids_schema.validate(fn) return ids_schema.is_valid(fn) From 5d8f71128e760d87eff4001eaf2c105bedd2b36c Mon Sep 17 00:00:00 2001 From: ArturTomczak Date: Fri, 9 Jul 2021 08:44:36 +0200 Subject: [PATCH 34/34] add tests for parsing and authoring --- .../ifcopenshell/test_ids.py | 146 ++++++++++++++---- 1 file changed, 115 insertions(+), 31 deletions(-) diff --git a/src/ifcopenshell-python/ifcopenshell/test_ids.py b/src/ifcopenshell-python/ifcopenshell/test_ids.py index 73dec07978..bec6587600 100644 --- a/src/ifcopenshell-python/ifcopenshell/test_ids.py +++ b/src/ifcopenshell-python/ifcopenshell/test_ids.py @@ -1,64 +1,148 @@ import unittest -from ids import ids, specification, entity, classification, property, material +import ids import requests +import os +# from xmlschema.validators.exceptions import XMLSchemaChildrenValidationError + def read_web_file(URL): return requests.get(URL).text - -class TestIds(unittest.TestCase): - """ Parsing IDS.xml """ +class TestIdsParsing(unittest.TestCase): def test_basic_ids_parse(self): IDS_URL = "https://raw.githubusercontent.com/atomczak/Sample-BIM-Files/main/IDS/IDS_Wall_needs_all_fields.xml" - ids_file = ids.parse(read_web_file(IDS_URL)) - self.assertEqual( type(ids_file).__name__, "ids" ) - + ids_file = ids.ids.parse(read_web_file(IDS_URL)) + self.assertEqual(type(ids_file).__name__, "ids") + def test_entity_facet(self): IDS_URL = "https://raw.githubusercontent.com/atomczak/Sample-BIM-Files/main/IDS/IDS_Wall_needs_entity.xml" - ids_file = ids.parse(read_web_file(IDS_URL)) - self.assertEqual( ids_file.specifications[0].requirements.terms[0].node['name'] , "IfcWall" ) + ids_file = ids.ids.parse(read_web_file(IDS_URL)) + self.assertEqual(ids_file.specifications[0].requirements.terms[0].node["name"], "IfcWall") def test_predefinedtype_facet(self): - IDS_URL = "https://raw.githubusercontent.com/atomczak/Sample-BIM-Files/main/IDS/IDS_Wall_needs_predefinedtype.xml" - ids_file = ids.parse(read_web_file(IDS_URL)) - self.assertEqual( ids_file.specifications[0].requirements.terms[0].node['predefinedtype'] , "CLADDING" ) + IDS_URL = ( + "https://raw.githubusercontent.com/atomczak/Sample-BIM-Files/main/IDS/IDS_Wall_needs_predefinedtype.xml" + ) + ids_file = ids.ids.parse(read_web_file(IDS_URL)) + self.assertEqual(ids_file.specifications[0].requirements.terms[0].node["predefinedtype"], "CLADDING") def test_property_facet(self): IDS_URL = "https://raw.githubusercontent.com/atomczak/Sample-BIM-Files/main/IDS/IDS_Wall_needs_property.xml" - ids_file = ids.parse(read_web_file(IDS_URL)) - self.assertEqual( ids_file.specifications[0].requirements.terms[0].node['propertyset'] , "Test_PropertySet" ) - self.assertEqual( ids_file.specifications[0].requirements.terms[0].node['name'] , "Test_Parameter" ) - self.assertEqual( ids_file.specifications[0].requirements.terms[0].node['value'] , "Test_Value" ) + ids_file = ids.ids.parse(read_web_file(IDS_URL)) + self.assertEqual(ids_file.specifications[0].requirements.terms[0].node["propertyset"], "Test_PropertySet") + self.assertEqual(ids_file.specifications[0].requirements.terms[0].node["name"], "Test_Parameter") + self.assertEqual(ids_file.specifications[0].requirements.terms[0].node["value"], "Test_Value") def test_material_facet(self): IDS_URL = "https://raw.githubusercontent.com/atomczak/Sample-BIM-Files/main/IDS/IDS_Wall_needs_material.xml" - ids_file = ids.parse(read_web_file(IDS_URL)) - self.assertEqual( ids_file.specifications[0].requirements.terms[0].node['value'] , "Test_Material" ) + ids_file = ids.ids.parse(read_web_file(IDS_URL)) + self.assertEqual(ids_file.specifications[0].requirements.terms[0].node["value"], "Test_Material") def test_classification_facet(self): - IDS_URL = "https://raw.githubusercontent.com/atomczak/Sample-BIM-Files/main/IDS/IDS_Wall_needs_classification.xml" - ids_file = ids.parse(read_web_file(IDS_URL)) - self.assertEqual( ids_file.specifications[0].requirements.terms[0].node['value'] , "Test_Classification" ) - self.assertEqual( ids_file.specifications[0].requirements.terms[0].node['system'] , "Test_System" ) + IDS_URL = ( + "https://raw.githubusercontent.com/atomczak/Sample-BIM-Files/main/IDS/IDS_Wall_needs_classification.xml" + ) + ids_file = ids.ids.parse(read_web_file(IDS_URL)) + self.assertEqual(ids_file.specifications[0].requirements.terms[0].node["value"], "Test_Classification") + self.assertEqual(ids_file.specifications[0].requirements.terms[0].node["system"], "Test_System") """ Parsing invalid IDS.xml """ # TODO # def test_invalid_classification_facet(self): # IDS_URL = "https://raw.githubusercontent.com/atomczak/Sample-BIM-Files/main/IDS/Invalid_IDS_Wall_needs_classification.xml" # self.assertRaises( XMLSchemaChildrenValidationError, ids.parse(read_web_file(IDS_URL)) ) - - """ IDS authoring """ - # TODO - """ IFC validation with IDS """ - # TODO + """ Saving parsed IDS to IDS.xml """ - """ IDS validation results """ + def test_parsed_ids_to_xml(self): + IDS_URL = "https://raw.githubusercontent.com/atomczak/Sample-BIM-Files/main/IDS/IDS_Wall_needs_all_fields.xml" + ids_file = ids.ids.parse(read_web_file(IDS_URL)) + fn = "TEST_FILE.xml" + result = ids_file.to_xml(fn) + os.remove(fn) + self.assertTrue(result) + + +class TestIdsAuthoring(unittest.TestCase): + + def test_entity_create(self): + e = ids.entity.create(name="Test_Name", predefinedtype="Test_PredefinedType") + self.assertEqual(e.name, "Test_Name") + self.assertEqual(e.predefinedtype, "Test_PredefinedType") + + def test_classification_create(self): + c = ids.classification.create(location="any", value="Test_Value", system="Test_System") + self.assertEqual(c.location, "any") + self.assertEqual(c.value, "Test_Value") + self.assertEqual(c.system, "Test_System") + + def test_property_create(self): + p = ids.property.create( + location="any", propertyset="Test_PropertySet", name="Test_Parameter", value="Test_Value" + ) + self.assertEqual(p.location, "any") + self.assertEqual(p.propertyset, "Test_PropertySet") + self.assertEqual(p.name, "Test_Parameter") + self.assertEqual(p.value, "Test_Value") + + def test_material_create(self): + m = ids.material.create(location="any", value="Test_Value") + self.assertEqual(m.location, "any") + self.assertEqual(m.value, "Test_Value") + + def test_specification_create(self): + s = ids.specification(name="Test_Specification") + self.assertEqual(s.name, "Test_Specification") + + def test_ids_create(self): + i = ids.ids() + self.assertEqual(i.specifications, []) + self.assertEqual(i.info, None) + + def test_ids_add_content(self): + i = ids.ids() + i.specifications.append(ids.specification(name="Test_Specification")) + self.assertEqual(i.specifications[0].name, "Test_Specification") + m = ids.material.create(location="any", value="Test_Value") + i.specifications[0].add_applicability(m) + self.assertEqual(i.specifications[0].applicability.terms[0].value, "Test_Value") + i.specifications[0].add_applicability(m) + self.assertEqual(i.specifications[0].applicability.terms[1].value, "Test_Value") + i.specifications[0].add_requirement(m) + self.assertEqual(i.specifications[0].requirements.terms[0].value, "Test_Value") + i.specifications[0].add_requirement(m) + self.assertEqual(i.specifications[0].requirements.terms[1].value, "Test_Value") + + """ Saving created IDS to IDS.xml """ + + def test_created_ids_to_xml(self): + i = ids.ids() + i.specifications.append(ids.specification(name="Test_Specification")) + e = ids.entity.create(name="Test_Name", predefinedtype="Test_PredefinedType") + c = ids.classification.create(location="any", value="Test_Value", system="Test_System") + m = ids.material.create(location="any", value="Test_Value") + p = ids.property.create(location="any", propertyset="Test_PropertySet", name="Test_Parameter", value="Test_Value") + i.specifications[0].add_applicability(e) + i.specifications[0].add_applicability(m) + i.specifications[0].add_requirement(c) + i.specifications[0].add_requirement(p) + fn = "TEST_FILE.xml" + result = i.to_xml(fn) + os.remove(fn) + self.assertTrue(result) + + +class TestIfcValidation(unittest.TestCase): + pass # TODO -if __name__ == '__main__': - import sys - unittest.main() \ No newline at end of file +class TestIdsResults(unittest.TestCase): + pass + # TODO + + +if __name__ == "__main__": + unittest.main()